<# .SYNOPSIS SCOM Prometheus Metrics Exporter .DESCRIPTION Prometheus exporter for System Center Operations Manager - management server health, agent status, open alerts by severity, resolution state counts, management pack and monitor counts, override counts, pending actions, database sizes, stale agents, and alert age statistics. Exports metrics as Prometheus-compatible text format. .PARAMETER Mode Output mode: 'stdout' (default), 'textfile', or 'http' .PARAMETER Port HTTP port for http mode (default: 9650) .PARAMETER TextfileDir Directory for textfile collector output (default: C:\ProgramData\node_exporter) .PARAMETER InstallScheduledTask Switch to create a scheduled task for auto-start on system boot .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 Metrics Exported: Core Status: - scom_up - scom_exporter_info{version} Management Servers: - scom_management_server_health{server,health_state} Agents: - scom_agent_total - scom_agent_healthy - scom_agent_warning - scom_agent_critical - scom_agent_unmonitored - scom_agent_pending_actions Alerts: - scom_alerts_open_total - scom_alerts_by_severity{severity} - scom_alerts_by_resolution_state{resolution_state} - scom_alert_age_oldest_hours - scom_alert_age_average_hours Stale Agents: - scom_agent_stale_total Management Packs: - scom_management_pack_total Monitors: - scom_monitor_total - scom_monitor_healthy - scom_monitor_error - scom_monitor_warning Management Group: - scom_management_group_total Overrides: - scom_override_total Databases: - scom_database_size_bytes{database} Exporter: - scom_exporter_duration_seconds - scom_exporter_last_run_timestamp #> param( [ValidateSet('stdout', 'textfile', 'http')] [string]$Mode = 'stdout', [int]$Port = 9650, [string]$TextfileDir = 'C:\ProgramData\node_exporter', [switch]$InstallScheduledTask, [int]$TaskIntervalMinutes = 2 ) # Create a scheduled task to run this script every $TaskIntervalMinutes minutes if ($InstallScheduledTask) { $taskName = "ScomMetricsExporter" $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue if (-not $existingTask) { $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -Mode textfile" if (-not $TaskIntervalMinutes -or $TaskIntervalMinutes -le 0) { throw "TaskIntervalMinutes must be a positive integer" } $taskTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes $TaskIntervalMinutes) -RepetitionDuration (New-TimeSpan -Days 365) $taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest try { Write-Host "Creating scheduled task: $taskName" Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $taskTrigger -Principal $taskPrincipal -Description "Exports SCOM metrics for Prometheus every $TaskIntervalMinutes minutes" $createdTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue if (-not $createdTask) { throw "Failed to verify scheduled task creation" } Write-Host "Successfully created scheduled task: $taskName" -ForegroundColor Green } catch { Write-Error "Failed to create scheduled task: $($_.Exception.Message)" throw } } else { Write-Host "Scheduled task '$taskName' already exists, skipping creation" } } $ErrorActionPreference = 'SilentlyContinue' # ============================================================================ # HELPER FUNCTIONS # ============================================================================ function Get-UnixTimestamp { [int][double]::Parse((Get-Date -UFormat '%s')) } function Format-MetricValue { param([double]$Value, [int]$Decimals = 2) [math]::Round($Value, $Decimals) } function Test-ScomModule { try { Import-Module OperationsManager -ErrorAction Stop return $true } catch { return $false } } # ============================================================================ # SCOM METRICS # ============================================================================ function Get-ScomMetrics { $sb = [System.Text.StringBuilder]::new() # --- Management Server Health --- [void]$sb.AppendLine('# HELP scom_management_server_health Management server health (1=healthy, 0=unhealthy)') [void]$sb.AppendLine('# TYPE scom_management_server_health gauge') try { $mgmtServers = Get-SCOMManagementServer if ($mgmtServers) { foreach ($ms in $mgmtServers) { $serverName = $ms.Name -replace '["]', '' $healthVal = switch ($ms.HealthState) { 'Success' { 1 } default { 0 } } $stateLabel = "$($ms.HealthState)" [void]$sb.AppendLine("scom_management_server_health{server=`"$serverName`",health_state=`"$stateLabel`"} $healthVal") } } } catch { } [void]$sb.AppendLine('') # --- Agent Counts --- [void]$sb.AppendLine('# HELP scom_agent_total Total number of SCOM agents') [void]$sb.AppendLine('# TYPE scom_agent_total gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_agent_healthy Number of healthy agents') [void]$sb.AppendLine('# TYPE scom_agent_healthy gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_agent_warning Number of agents in warning state') [void]$sb.AppendLine('# TYPE scom_agent_warning gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_agent_critical Number of agents in critical state') [void]$sb.AppendLine('# TYPE scom_agent_critical gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_agent_unmonitored Number of unmonitored agents') [void]$sb.AppendLine('# TYPE scom_agent_unmonitored gauge') try { $agents = Get-SCOMAgent $agentTotal = if ($agents) { @($agents).Count } else { 0 } $agentHealthy = if ($agents) { @($agents | Where-Object { $_.HealthState -eq 'Success' }).Count } else { 0 } $agentWarning = if ($agents) { @($agents | Where-Object { $_.HealthState -eq 'Warning' }).Count } else { 0 } $agentCritical = if ($agents) { @($agents | Where-Object { $_.HealthState -eq 'Error' }).Count } else { 0 } $agentUnmonitored = if ($agents) { @($agents | Where-Object { $_.HealthState -eq 'Uninitialized' -or $_.HealthState -eq 'NotMonitored' }).Count } else { 0 } [void]$sb.AppendLine("scom_agent_total $agentTotal") [void]$sb.AppendLine("scom_agent_healthy $agentHealthy") [void]$sb.AppendLine("scom_agent_warning $agentWarning") [void]$sb.AppendLine("scom_agent_critical $agentCritical") [void]$sb.AppendLine("scom_agent_unmonitored $agentUnmonitored") } catch { [void]$sb.AppendLine("scom_agent_total 0") [void]$sb.AppendLine("scom_agent_healthy 0") [void]$sb.AppendLine("scom_agent_warning 0") [void]$sb.AppendLine("scom_agent_critical 0") [void]$sb.AppendLine("scom_agent_unmonitored 0") } [void]$sb.AppendLine('') # --- Stale / Grayed-Out Agents --- [void]$sb.AppendLine('# HELP scom_agent_stale_total Number of stale or grayed-out agents') [void]$sb.AppendLine('# TYPE scom_agent_stale_total gauge') try { $staleAgents = @(Get-SCOMAgent | Where-Object { $_.HealthState -eq 'Uninitialized' }).Count [void]$sb.AppendLine("scom_agent_stale_total $staleAgents") } catch { [void]$sb.AppendLine("scom_agent_stale_total 0") } [void]$sb.AppendLine('') # --- Agent Pending Actions --- [void]$sb.AppendLine('# HELP scom_agent_pending_actions Number of agents with pending actions') [void]$sb.AppendLine('# TYPE scom_agent_pending_actions gauge') try { $pendingAgents = @(Get-SCOMPendingManagement).Count [void]$sb.AppendLine("scom_agent_pending_actions $pendingAgents") } catch { [void]$sb.AppendLine("scom_agent_pending_actions 0") } [void]$sb.AppendLine('') # --- Open Alerts Total --- [void]$sb.AppendLine('# HELP scom_alerts_open_total Total number of open alerts') [void]$sb.AppendLine('# TYPE scom_alerts_open_total gauge') try { $alerts = Get-SCOMAlert -ResolutionState 0 $alertCount = if ($alerts) { @($alerts).Count } else { 0 } [void]$sb.AppendLine("scom_alerts_open_total $alertCount") } catch { [void]$sb.AppendLine("scom_alerts_open_total 0") } [void]$sb.AppendLine('') # --- Alerts by Severity --- [void]$sb.AppendLine('# HELP scom_alerts_by_severity Open alerts grouped by severity') [void]$sb.AppendLine('# TYPE scom_alerts_by_severity gauge') try { $allAlerts = Get-SCOMAlert -ResolutionState 0 if ($allAlerts) { $critAlerts = @($allAlerts | Where-Object { $_.Severity -eq 'Error' }).Count $warnAlerts = @($allAlerts | Where-Object { $_.Severity -eq 'Warning' }).Count $infoAlerts = @($allAlerts | Where-Object { $_.Severity -eq 'Information' }).Count [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"critical`"} $critAlerts") [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"warning`"} $warnAlerts") [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"information`"} $infoAlerts") } else { [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"critical`"} 0") [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"warning`"} 0") [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"information`"} 0") } } catch { [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"critical`"} 0") [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"warning`"} 0") [void]$sb.AppendLine("scom_alerts_by_severity{severity=`"information`"} 0") } [void]$sb.AppendLine('') # --- Alerts by Resolution State --- [void]$sb.AppendLine('# HELP scom_alerts_by_resolution_state Alert count grouped by resolution state') [void]$sb.AppendLine('# TYPE scom_alerts_by_resolution_state gauge') try { $allResAlerts = Get-SCOMAlert if ($allResAlerts) { $grouped = $allResAlerts | Group-Object ResolutionState foreach ($group in $grouped) { $state = $group.Name [void]$sb.AppendLine("scom_alerts_by_resolution_state{resolution_state=`"$state`"} $($group.Count)") } } } catch { } [void]$sb.AppendLine('') # --- Alert Age Stats --- [void]$sb.AppendLine('# HELP scom_alert_age_oldest_hours Age of the oldest open alert in hours') [void]$sb.AppendLine('# TYPE scom_alert_age_oldest_hours gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_alert_age_average_hours Average age of open alerts in hours') [void]$sb.AppendLine('# TYPE scom_alert_age_average_hours gauge') try { $openAlerts = Get-SCOMAlert -ResolutionState 0 if ($openAlerts -and @($openAlerts).Count -gt 0) { $now = Get-Date $ages = @($openAlerts | ForEach-Object { ($now - $_.TimeRaised).TotalHours }) $oldest = Format-MetricValue ($ages | Measure-Object -Maximum).Maximum $average = Format-MetricValue ($ages | Measure-Object -Average).Average [void]$sb.AppendLine("scom_alert_age_oldest_hours $oldest") [void]$sb.AppendLine("scom_alert_age_average_hours $average") } else { [void]$sb.AppendLine("scom_alert_age_oldest_hours 0") [void]$sb.AppendLine("scom_alert_age_average_hours 0") } } catch { [void]$sb.AppendLine("scom_alert_age_oldest_hours 0") [void]$sb.AppendLine("scom_alert_age_average_hours 0") } [void]$sb.AppendLine('') # --- Management Pack Count --- [void]$sb.AppendLine('# HELP scom_management_pack_total Total number of management packs') [void]$sb.AppendLine('# TYPE scom_management_pack_total gauge') try { $mpCount = @(Get-SCOMManagementPack).Count [void]$sb.AppendLine("scom_management_pack_total $mpCount") } catch { [void]$sb.AppendLine("scom_management_pack_total 0") } [void]$sb.AppendLine('') # --- Monitor Counts --- [void]$sb.AppendLine('# HELP scom_monitor_total Total number of monitors') [void]$sb.AppendLine('# TYPE scom_monitor_total gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_monitor_healthy Number of monitors in healthy state') [void]$sb.AppendLine('# TYPE scom_monitor_healthy gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_monitor_error Number of monitors in error state') [void]$sb.AppendLine('# TYPE scom_monitor_error gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_monitor_warning Number of monitors in warning state') [void]$sb.AppendLine('# TYPE scom_monitor_warning gauge') try { $monitors = Get-SCOMMonitor $monTotal = if ($monitors) { @($monitors).Count } else { 0 } [void]$sb.AppendLine("scom_monitor_total $monTotal") $monitoringObjects = Get-SCOMClassInstance | Where-Object { $_.HealthState } if ($monitoringObjects) { $monHealthy = @($monitoringObjects | Where-Object { $_.HealthState -eq 'Success' }).Count $monError = @($monitoringObjects | Where-Object { $_.HealthState -eq 'Error' }).Count $monWarning = @($monitoringObjects | Where-Object { $_.HealthState -eq 'Warning' }).Count [void]$sb.AppendLine("scom_monitor_healthy $monHealthy") [void]$sb.AppendLine("scom_monitor_error $monError") [void]$sb.AppendLine("scom_monitor_warning $monWarning") } else { [void]$sb.AppendLine("scom_monitor_healthy 0") [void]$sb.AppendLine("scom_monitor_error 0") [void]$sb.AppendLine("scom_monitor_warning 0") } } catch { [void]$sb.AppendLine("scom_monitor_total 0") [void]$sb.AppendLine("scom_monitor_healthy 0") [void]$sb.AppendLine("scom_monitor_error 0") [void]$sb.AppendLine("scom_monitor_warning 0") } [void]$sb.AppendLine('') # --- Management Group Count --- [void]$sb.AppendLine('# HELP scom_management_group_total Number of management groups') [void]$sb.AppendLine('# TYPE scom_management_group_total gauge') try { $mgCount = @(Get-SCOMManagementGroup).Count [void]$sb.AppendLine("scom_management_group_total $mgCount") } catch { [void]$sb.AppendLine("scom_management_group_total 0") } [void]$sb.AppendLine('') # --- Override Count --- [void]$sb.AppendLine('# HELP scom_override_total Total number of overrides') [void]$sb.AppendLine('# TYPE scom_override_total gauge') try { $overrides = Get-SCOMOverride $overrideCount = if ($overrides) { @($overrides).Count } else { 0 } [void]$sb.AppendLine("scom_override_total $overrideCount") } catch { [void]$sb.AppendLine("scom_override_total 0") } [void]$sb.AppendLine('') # --- Database Sizes --- [void]$sb.AppendLine('# HELP scom_database_size_bytes SCOM database size in bytes') [void]$sb.AppendLine('# TYPE scom_database_size_bytes gauge') try { $dbQuery = @" SELECT DB_NAME(database_id) AS db_name, SUM(size) * 8192 AS size_bytes FROM sys.master_files WHERE DB_NAME(database_id) IN ('OperationsManager', 'OperationsManagerDW') GROUP BY database_id "@ $sqlModule = Get-Module -Name SqlServer -ListAvailable -ErrorAction SilentlyContinue $scomMgmt = Get-SCOMManagementGroup if ($sqlModule) { Import-Module SqlServer -ErrorAction SilentlyContinue $dbSettings = $scomMgmt | ForEach-Object { try { $_.GetSettings() } catch { $null } } $sqlInstance = if ($dbSettings) { $dbSettings.DefaultDataWarehouseServer } else { 'localhost' } $dbSizes = Invoke-Sqlcmd -ServerInstance $sqlInstance -Database 'master' -Query $dbQuery -ErrorAction Stop -QueryTimeout 10 if ($dbSizes) { foreach ($row in $dbSizes) { $dbName = $row.db_name -replace '["]', '' [void]$sb.AppendLine("scom_database_size_bytes{database=`"$dbName`"} $($row.size_bytes)") } } } } catch { } [void]$sb.AppendLine('') $sb.ToString() } # ============================================================================ # COLLECT ALL METRICS # ============================================================================ function Get-AllMetrics { $scriptStart = Get-Date $sb = [System.Text.StringBuilder]::new() # Exporter up - test OperationsManager module availability [void]$sb.AppendLine('# HELP scom_up SCOM reachability (1=up, 0=down)') [void]$sb.AppendLine('# TYPE scom_up gauge') try { $moduleAvailable = Test-ScomModule if (-not $moduleAvailable) { [void]$sb.AppendLine("scom_up 0") $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE scom_exporter_duration_seconds gauge') [void]$sb.AppendLine("scom_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_exporter_last_run_timestamp Unix timestamp of last run') [void]$sb.AppendLine('# TYPE scom_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("scom_exporter_last_run_timestamp $(Get-UnixTimestamp)") return $sb.ToString() } $testMs = Get-SCOMManagementServer -ErrorAction Stop $upVal = if ($testMs) { 1 } else { 0 } [void]$sb.AppendLine("scom_up $upVal") } catch { [void]$sb.AppendLine("scom_up 0") $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE scom_exporter_duration_seconds gauge') [void]$sb.AppendLine("scom_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_exporter_last_run_timestamp Unix timestamp of last run') [void]$sb.AppendLine('# TYPE scom_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("scom_exporter_last_run_timestamp $(Get-UnixTimestamp)") return $sb.ToString() } [void]$sb.AppendLine('') # Exporter info [void]$sb.AppendLine('# HELP scom_exporter_info Exporter version information') [void]$sb.AppendLine('# TYPE scom_exporter_info gauge') [void]$sb.AppendLine('scom_exporter_info{version="1.0"} 1') [void]$sb.AppendLine('') # Collect SCOM metrics [void]$sb.Append((Get-ScomMetrics)) # Exporter runtime $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds $timestamp = Get-UnixTimestamp [void]$sb.AppendLine('# HELP scom_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE scom_exporter_duration_seconds gauge') [void]$sb.AppendLine("scom_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP scom_exporter_last_run_timestamp Unix timestamp of last successful run') [void]$sb.AppendLine('# TYPE scom_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("scom_exporter_last_run_timestamp $timestamp") [void]$sb.AppendLine('') $sb.ToString() } # ============================================================================ # HTTP SERVER MODE # ============================================================================ function Start-HttpServer { param([int]$ListenPort) $prefix = "http://+:$ListenPort/" $listener = [System.Net.HttpListener]::new() $listener.Prefixes.Add($prefix) try { $listener.Start() Write-Host "Starting SCOM metrics exporter on port $ListenPort..." -ForegroundColor Green Write-Host "Metrics available at http://localhost:$ListenPort/metrics" while ($listener.IsListening) { $context = $listener.GetContext() $request = $context.Request $response = $context.Response if ($request.Url.AbsolutePath -eq '/metrics') { $metrics = Get-AllMetrics $buffer = [System.Text.Encoding]::UTF8.GetBytes($metrics) $response.ContentType = 'text/plain; version=0.0.4; charset=utf-8' } else { $html = @" SCOM Metrics Exporter v1.0

SCOM Metrics Exporter v1.0

Metrics

Metrics

"@ $buffer = [System.Text.Encoding]::UTF8.GetBytes($html) $response.ContentType = 'text/html; charset=utf-8' } $response.ContentLength64 = $buffer.Length $response.OutputStream.Write($buffer, 0, $buffer.Length) $response.OutputStream.Close() } } catch { Write-Error "HTTP server error: $_" Write-Error "If access denied, run: netsh http add urlacl url=http://+:$ListenPort/ user=Everyone" } finally { if ($listener.IsListening) { $listener.Stop() } } } # ============================================================================ # MAIN EXECUTION # ============================================================================ switch ($Mode) { 'http' { Start-HttpServer -ListenPort $Port } 'textfile' { $OutputFile = Join-Path $TextfileDir 'scom_metrics.prom' $outputDir = Split-Path $OutputFile -Parent if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null } $tempFile = Join-Path $outputDir ".scom_metrics.$PID.tmp" try { $metrics = Get-AllMetrics $metrics | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline $lineCount = ($metrics -split "`n").Count if ($lineCount -lt 10) { Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue Write-Error "Metrics file too small ($lineCount lines), keeping previous" exit 1 } Move-Item -Path $tempFile -Destination $OutputFile -Force Write-Host "Metrics written to $OutputFile ($lineCount lines)" -ForegroundColor Green } catch { Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue Write-Error "Failed to generate metrics: $_" exit 1 } } default { Get-AllMetrics | Write-Output } }