<# .SYNOPSIS Windows Print Server Prometheus Metrics Exporter .DESCRIPTION Prometheus exporter for Windows Print Server. Monitors print queues, job counts, job status, printer errors, spooler health, and driver information. Exports metrics as Prometheus-compatible text format for windows_exporter textfile collector or standalone HTTP listener. .PARAMETER Mode Output mode: 'stdout' (default), 'textfile', or 'http' .PARAMETER Port HTTP port for http mode (default: 9197) .PARAMETER TextfileDir Directory for textfile collector output (default: C:\ProgramData\node_exporter) .PARAMETER OutputFile Custom output file path .PARAMETER InstallScheduledTask Switch to create a scheduled task for automatic collection .PARAMETER TaskIntervalMinutes Interval in minutes for the scheduled task (default: 2) .NOTES Author: Phil Connor Contact: contact@mylinux.work Website: https://mylinux.work License: MIT Version: 1.0 #> param( [switch]$TextFile, [string]$OutFile = "", [switch]$Install, [string]$Listen = "", [int]$Interval = 120 ) $ErrorActionPreference = "SilentlyContinue" $Version = "1.0" $TextfileDir = "C:\ProgramData\node_exporter" $MetricPrefix = "windows_print" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ function Get-PrometheusEscape { param([string]$Value) $Value -replace '\\', '\\' -replace '"', '\"' -replace "`n", '' } function Write-MetricHeader { param([string]$Name, [string]$Help, [string]$Type) "# HELP $Name $Help" "# TYPE $Name $Type" } # ============================================================================ # METRIC GENERATION # ============================================================================ function Get-PrintMetrics { $startTime = Get-Date $metrics = [System.Collections.ArrayList]::new() # Check if Print Spooler is running $spooler = Get-Service -Name "Spooler" -ErrorAction SilentlyContinue $spoolerUp = if ($spooler -and $spooler.Status -eq "Running") { 1 } else { 0 } [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_up" "Print server exporter status (1=up, 0=down)" "gauge")) [void]$metrics.Add("${MetricPrefix}_up $spoolerUp") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_info" "Exporter version information" "gauge")) [void]$metrics.Add("${MetricPrefix}_exporter_info{version=`"$Version`"} 1") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_spooler_up" "Print Spooler service status" "gauge")) [void]$metrics.Add("${MetricPrefix}_spooler_up $spoolerUp") [void]$metrics.Add("") if ($spoolerUp -eq 0) { return ($metrics -join "`n") } # ======================================================================== # PRINTER METRICS # ======================================================================== $printers = Get-Printer -ErrorAction SilentlyContinue $totalPrinters = 0 $onlinePrinters = 0 $offlinePrinters = 0 $errorPrinters = 0 if ($printers) { $totalPrinters = @($printers).Count [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_printers_total" "Total configured printers" "gauge")) [void]$metrics.Add("${MetricPrefix}_printers_total $totalPrinters") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_printer_status" "Printer status (1=current state)" "gauge")) foreach ($printer in $printers) { $name = Get-PrometheusEscape $printer.Name $status = $printer.PrinterStatus.ToString() $shared = if ($printer.Shared) { "true" } else { "false" } [void]$metrics.Add("${MetricPrefix}_printer_status{printer=`"$name`",status=`"$status`",shared=`"$shared`"} 1") switch ($status) { "Normal" { $onlinePrinters++ } "Offline" { $offlinePrinters++ } default { $errorPrinters++ } } } [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_printers_online" "Online printers" "gauge")) [void]$metrics.Add("${MetricPrefix}_printers_online $onlinePrinters") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_printers_offline" "Offline printers" "gauge")) [void]$metrics.Add("${MetricPrefix}_printers_offline $offlinePrinters") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_printers_error" "Printers in error state" "gauge")) [void]$metrics.Add("${MetricPrefix}_printers_error $errorPrinters") [void]$metrics.Add("") } # ======================================================================== # PRINT JOB METRICS # ======================================================================== $jobs = Get-PrintJob -PrinterName * -ErrorAction SilentlyContinue $totalJobs = 0 $printingJobs = 0 $queuedJobs = 0 $errorJobs = 0 $totalPages = 0 $totalBytes = 0 if ($jobs) { $totalJobs = @($jobs).Count foreach ($job in $jobs) { $totalPages += $job.TotalPages $totalBytes += $job.Size switch ($job.JobStatus) { { $_ -match "Printing" } { $printingJobs++ } { $_ -match "Error" } { $errorJobs++ } default { $queuedJobs++ } } } } [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_jobs_total" "Total print jobs in queue" "gauge")) [void]$metrics.Add("${MetricPrefix}_jobs_total $totalJobs") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_jobs_printing" "Jobs currently printing" "gauge")) [void]$metrics.Add("${MetricPrefix}_jobs_printing $printingJobs") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_jobs_queued" "Jobs waiting in queue" "gauge")) [void]$metrics.Add("${MetricPrefix}_jobs_queued $queuedJobs") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_jobs_error" "Jobs in error state" "gauge")) [void]$metrics.Add("${MetricPrefix}_jobs_error $errorJobs") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_jobs_pages_total" "Total pages across all queued jobs" "gauge")) [void]$metrics.Add("${MetricPrefix}_jobs_pages_total $totalPages") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_jobs_bytes_total" "Total bytes across all queued jobs" "gauge")) [void]$metrics.Add("${MetricPrefix}_jobs_bytes_total $totalBytes") [void]$metrics.Add("") # Per-printer job counts if ($printers -and $jobs) { [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_queue_jobs" "Jobs per printer queue" "gauge")) foreach ($printer in $printers) { $name = Get-PrometheusEscape $printer.Name $pJobs = @($jobs | Where-Object { $_.PrinterName -eq $printer.Name }).Count [void]$metrics.Add("${MetricPrefix}_queue_jobs{printer=`"$name`"} $pJobs") } [void]$metrics.Add("") } # ======================================================================== # PRINT PORT METRICS # ======================================================================== $ports = Get-PrinterPort -ErrorAction SilentlyContinue if ($ports) { [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_ports_total" "Total configured printer ports" "gauge")) [void]$metrics.Add("${MetricPrefix}_ports_total $(@($ports).Count)") [void]$metrics.Add("") } # ======================================================================== # DRIVER METRICS # ======================================================================== $drivers = Get-PrinterDriver -ErrorAction SilentlyContinue if ($drivers) { [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_drivers_total" "Total installed printer drivers" "gauge")) [void]$metrics.Add("${MetricPrefix}_drivers_total $(@($drivers).Count)") [void]$metrics.Add("") } # ======================================================================== # PERFORMANCE COUNTERS # ======================================================================== $perfCounter = Get-Counter -Counter "\Print Queue(_Total)\Total Jobs Printed" -ErrorAction SilentlyContinue if ($perfCounter) { $totalPrinted = [math]::Round($perfCounter.CounterSamples[0].CookedValue) [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_jobs_printed_total" "Total jobs printed (lifetime)" "counter")) [void]$metrics.Add("${MetricPrefix}_jobs_printed_total $totalPrinted") [void]$metrics.Add("") } # ======================================================================== # EXPORTER RUNTIME # ======================================================================== $endTime = Get-Date $duration = [math]::Round(($endTime - $startTime).TotalSeconds, 2) $timestamp = [math]::Round((Get-Date -UFormat %s), 0) [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_duration_seconds" "Script execution time" "gauge")) [void]$metrics.Add("${MetricPrefix}_exporter_duration_seconds $duration") [void]$metrics.Add("") [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_last_run_timestamp" "Unix timestamp of last run" "gauge")) [void]$metrics.Add("${MetricPrefix}_exporter_last_run_timestamp $timestamp") return ($metrics -join "`n") } # ============================================================================ # OUTPUT MODES # ============================================================================ if ($Install) { $scriptPath = $MyInvocation.MyCommand.Path $action = New-ScheduledTaskAction -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -TextFile" $trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Seconds $Interval) -Once -At (Get-Date) $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable Register-ScheduledTask -TaskName "Print Server Metrics Exporter" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force Write-Host "Scheduled task 'Print Server Metrics Exporter' created (interval: ${Interval}s)" exit 0 } if ($Listen -ne "") { $port = $Listen -replace '.*:', '' if (-not $port) { $port = "9197" } $listener = [System.Net.HttpListener]::new() $listener.Prefixes.Add("http://+:$port/") $listener.Start() Write-Host "Print Server exporter listening on port $port..." while ($listener.IsListening) { $context = $listener.GetContext() $response = $context.Response $output = Get-PrintMetrics $buffer = [System.Text.Encoding]::UTF8.GetBytes($output) $response.ContentType = "text/plain; version=0.0.4" $response.ContentLength64 = $buffer.Length $response.OutputStream.Write($buffer, 0, $buffer.Length) $response.Close() } } elseif ($TextFile -or $OutFile -ne "") { $outputPath = if ($OutFile -ne "") { $OutFile } else { Join-Path $TextfileDir "print-server.prom" } $outputDir = Split-Path $outputPath -Parent if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir -Force | Out-Null } $tempFile = Join-Path $outputDir ".print-server-metrics.tmp" $metricsOutput = Get-PrintMetrics $metricsOutput | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline Move-Item -Path $tempFile -Destination $outputPath -Force Write-Host "Metrics written to $outputPath" } else { Get-PrintMetrics }