<# .SYNOPSIS SQL Server Prometheus Metrics Exporter .DESCRIPTION Prometheus exporter for SQL Server - buffer cache hit ratio, page life expectancy, batch requests, SQL compilations, lock waits, tempdb usage, database sizes, log space, backup age, connections, memory, CPU, and wait stats. Exports metrics as Prometheus-compatible text format. .PARAMETER Mode Output mode: 'stdout' (default), 'textfile', or 'http' .PARAMETER Port HTTP port for http mode (default: 9399) .PARAMETER ServerInstance SQL Server instance (default: localhost) .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: - mssql_up - mssql_exporter_info{version} Performance: - mssql_buffer_cache_hit_ratio - mssql_page_life_expectancy_seconds - mssql_batch_requests_total - mssql_sql_compilations_total - mssql_sql_recompilations_total - mssql_lock_waits_total - mssql_deadlocks_total Tempdb: - mssql_tempdb_data_size_bytes - mssql_tempdb_log_size_bytes Databases: - mssql_database_size_bytes{database} - mssql_database_log_used_percent{database} - mssql_database_backup_age_hours{database} Connections: - mssql_connections_total - mssql_connections_by_database{database} Memory: - mssql_memory_buffer_pool_bytes - mssql_memory_plan_cache_bytes CPU: - mssql_cpu_usage_percent Wait Stats: - mssql_wait_stats{wait_type} Exporter: - mssql_exporter_duration_seconds - mssql_exporter_last_run_timestamp #> param( [ValidateSet('stdout', 'textfile', 'http')] [string]$Mode = 'stdout', [int]$Port = 9399, [string]$ServerInstance = 'localhost', [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 = "MssqlMetricsExporter" $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 -ServerInstance `"$ServerInstance`"" 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 SQL Server 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 Invoke-SqlQuery { param( [string]$Query, [string]$Database = 'master' ) # Try Invoke-Sqlcmd first (SqlServer module) try { $result = Invoke-Sqlcmd -ServerInstance $ServerInstance -Database $Database -Query $Query -ErrorAction Stop -QueryTimeout 10 return $result } catch { } # Fallback to sqlcmd.exe try { $output = & sqlcmd.exe -S $ServerInstance -d $Database -Q $Query -h -1 -W -s "|" 2>$null if ($LASTEXITCODE -ne 0) { return $null } return $output | Where-Object { $_ -and $_.Trim() -ne '' -and $_ -notmatch '^\-+' -and $_ -notmatch 'rows affected' } } catch { return $null } } # ============================================================================ # SQL SERVER METRICS # ============================================================================ function Get-MssqlMetrics { $sb = [System.Text.StringBuilder]::new() # --- Buffer Cache Hit Ratio --- [void]$sb.AppendLine('# HELP mssql_buffer_cache_hit_ratio Buffer cache hit ratio percentage') [void]$sb.AppendLine('# TYPE mssql_buffer_cache_hit_ratio gauge') try { $bcQuery = @" SELECT MAX(CASE WHEN counter_name = 'Buffer cache hit ratio' THEN cntr_value END) AS ratio, MAX(CASE WHEN counter_name = 'Buffer cache hit ratio base' THEN cntr_value END) AS base FROM sys.dm_os_performance_counters WHERE object_name LIKE '%Buffer Manager%' AND counter_name IN ('Buffer cache hit ratio', 'Buffer cache hit ratio base') "@ $bc = Invoke-SqlQuery -Query $bcQuery if ($bc) { $ratio = if ($bc.base -gt 0) { Format-MetricValue (($bc.ratio / $bc.base) * 100) } else { 0 } [void]$sb.AppendLine("mssql_buffer_cache_hit_ratio $ratio") } else { [void]$sb.AppendLine("mssql_buffer_cache_hit_ratio 0") } } catch { [void]$sb.AppendLine("mssql_buffer_cache_hit_ratio 0") } [void]$sb.AppendLine('') # --- Page Life Expectancy --- [void]$sb.AppendLine('# HELP mssql_page_life_expectancy_seconds Page life expectancy in seconds') [void]$sb.AppendLine('# TYPE mssql_page_life_expectancy_seconds gauge') try { $pleQuery = "SELECT cntr_value FROM sys.dm_os_performance_counters WHERE object_name LIKE '%Buffer Manager%' AND counter_name = 'Page life expectancy'" $ple = Invoke-SqlQuery -Query $pleQuery $pleVal = if ($ple) { $ple.cntr_value } else { 0 } [void]$sb.AppendLine("mssql_page_life_expectancy_seconds $pleVal") } catch { [void]$sb.AppendLine("mssql_page_life_expectancy_seconds 0") } [void]$sb.AppendLine('') # --- Batch Requests/sec --- [void]$sb.AppendLine('# HELP mssql_batch_requests_total Batch requests per second') [void]$sb.AppendLine('# TYPE mssql_batch_requests_total gauge') try { $brQuery = "SELECT cntr_value FROM sys.dm_os_performance_counters WHERE object_name LIKE '%SQL Statistics%' AND counter_name = 'Batch Requests/sec'" $br = Invoke-SqlQuery -Query $brQuery $brVal = if ($br) { $br.cntr_value } else { 0 } [void]$sb.AppendLine("mssql_batch_requests_total $brVal") } catch { [void]$sb.AppendLine("mssql_batch_requests_total 0") } [void]$sb.AppendLine('') # --- SQL Compilations/sec --- [void]$sb.AppendLine('# HELP mssql_sql_compilations_total SQL compilations per second') [void]$sb.AppendLine('# TYPE mssql_sql_compilations_total gauge') try { $scQuery = "SELECT cntr_value FROM sys.dm_os_performance_counters WHERE object_name LIKE '%SQL Statistics%' AND counter_name = 'SQL Compilations/sec'" $sc = Invoke-SqlQuery -Query $scQuery $scVal = if ($sc) { $sc.cntr_value } else { 0 } [void]$sb.AppendLine("mssql_sql_compilations_total $scVal") } catch { [void]$sb.AppendLine("mssql_sql_compilations_total 0") } [void]$sb.AppendLine('') # --- SQL Recompilations/sec --- [void]$sb.AppendLine('# HELP mssql_sql_recompilations_total SQL recompilations per second') [void]$sb.AppendLine('# TYPE mssql_sql_recompilations_total gauge') try { $srQuery = "SELECT cntr_value FROM sys.dm_os_performance_counters WHERE object_name LIKE '%SQL Statistics%' AND counter_name = 'SQL Re-Compilations/sec'" $sr = Invoke-SqlQuery -Query $srQuery $srVal = if ($sr) { $sr.cntr_value } else { 0 } [void]$sb.AppendLine("mssql_sql_recompilations_total $srVal") } catch { [void]$sb.AppendLine("mssql_sql_recompilations_total 0") } [void]$sb.AppendLine('') # --- Lock Waits/sec --- [void]$sb.AppendLine('# HELP mssql_lock_waits_total Lock waits per second') [void]$sb.AppendLine('# TYPE mssql_lock_waits_total gauge') try { $lwQuery = "SELECT cntr_value FROM sys.dm_os_performance_counters WHERE object_name LIKE '%Locks%' AND counter_name = 'Lock Waits/sec' AND instance_name = '_Total'" $lw = Invoke-SqlQuery -Query $lwQuery $lwVal = if ($lw) { $lw.cntr_value } else { 0 } [void]$sb.AppendLine("mssql_lock_waits_total $lwVal") } catch { [void]$sb.AppendLine("mssql_lock_waits_total 0") } [void]$sb.AppendLine('') # --- Deadlocks --- [void]$sb.AppendLine('# HELP mssql_deadlocks_total Number of deadlocks detected') [void]$sb.AppendLine('# TYPE mssql_deadlocks_total gauge') try { $dlQuery = "SELECT cntr_value FROM sys.dm_os_performance_counters WHERE object_name LIKE '%Locks%' AND counter_name = 'Number of Deadlocks/sec' AND instance_name = '_Total'" $dl = Invoke-SqlQuery -Query $dlQuery $dlVal = if ($dl) { $dl.cntr_value } else { 0 } [void]$sb.AppendLine("mssql_deadlocks_total $dlVal") } catch { [void]$sb.AppendLine("mssql_deadlocks_total 0") } [void]$sb.AppendLine('') # --- Tempdb Sizes --- [void]$sb.AppendLine('# HELP mssql_tempdb_data_size_bytes Tempdb data file size in bytes') [void]$sb.AppendLine('# TYPE mssql_tempdb_data_size_bytes gauge') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP mssql_tempdb_log_size_bytes Tempdb log file size in bytes') [void]$sb.AppendLine('# TYPE mssql_tempdb_log_size_bytes gauge') try { $tdQuery = "SELECT type, SUM(size) * 8192 AS size_bytes FROM sys.master_files WHERE database_id = 2 GROUP BY type" $td = Invoke-SqlQuery -Query $tdQuery $dataSize = 0 $logSize = 0 if ($td) { foreach ($row in $td) { if ($row.type -eq 0) { $dataSize = $row.size_bytes } if ($row.type -eq 1) { $logSize = $row.size_bytes } } } [void]$sb.AppendLine("mssql_tempdb_data_size_bytes $dataSize") [void]$sb.AppendLine("mssql_tempdb_log_size_bytes $logSize") } catch { [void]$sb.AppendLine("mssql_tempdb_data_size_bytes 0") [void]$sb.AppendLine("mssql_tempdb_log_size_bytes 0") } [void]$sb.AppendLine('') # --- Per-Database Size --- [void]$sb.AppendLine('# HELP mssql_database_size_bytes Database size in bytes') [void]$sb.AppendLine('# TYPE mssql_database_size_bytes gauge') try { $dbSizeQuery = "SELECT DB_NAME(database_id) AS db_name, SUM(size) * 8192 AS size_bytes FROM sys.master_files GROUP BY database_id" $dbSizes = Invoke-SqlQuery -Query $dbSizeQuery if ($dbSizes) { foreach ($row in $dbSizes) { $dbName = $row.db_name -replace '["]', '' [void]$sb.AppendLine("mssql_database_size_bytes{database=`"$dbName`"} $($row.size_bytes)") } } } catch { } [void]$sb.AppendLine('') # --- Per-Database Log Used % --- [void]$sb.AppendLine('# HELP mssql_database_log_used_percent Log space used percentage per database') [void]$sb.AppendLine('# TYPE mssql_database_log_used_percent gauge') try { $logQuery = @" SELECT DB_NAME(database_id) AS db_name, CAST(used_log_space_in_percent AS DECIMAL(5,2)) AS log_pct FROM sys.dm_db_log_space_usage "@ # dm_db_log_space_usage is per-database, query from each $dbListQuery = "SELECT name FROM sys.databases WHERE state_desc = 'ONLINE' AND database_id > 4" $dbList = Invoke-SqlQuery -Query $dbListQuery if ($dbList) { foreach ($db in $dbList) { $dbName = $db.name -replace '["]', '' try { $logPctQuery = "SELECT CAST(used_log_space_in_percent AS DECIMAL(5,2)) AS log_pct FROM sys.dm_db_log_space_usage" $logPct = Invoke-SqlQuery -Query $logPctQuery -Database $dbName if ($logPct) { $pct = Format-MetricValue $logPct.log_pct [void]$sb.AppendLine("mssql_database_log_used_percent{database=`"$dbName`"} $pct") } } catch { } } } } catch { } [void]$sb.AppendLine('') # --- Backup Age --- [void]$sb.AppendLine('# HELP mssql_database_backup_age_hours Hours since last full backup per database') [void]$sb.AppendLine('# TYPE mssql_database_backup_age_hours gauge') try { $bkQuery = @" SELECT d.name AS db_name, ISNULL(DATEDIFF(HOUR, MAX(b.backup_finish_date), GETDATE()), -1) AS age_hours FROM sys.databases d LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D' WHERE d.database_id > 4 AND d.state_desc = 'ONLINE' GROUP BY d.name "@ $bk = Invoke-SqlQuery -Query $bkQuery if ($bk) { foreach ($row in $bk) { $dbName = $row.db_name -replace '["]', '' [void]$sb.AppendLine("mssql_database_backup_age_hours{database=`"$dbName`"} $($row.age_hours)") } } } catch { } [void]$sb.AppendLine('') # --- Connections Total --- [void]$sb.AppendLine('# HELP mssql_connections_total Total user connections') [void]$sb.AppendLine('# TYPE mssql_connections_total gauge') try { $connQuery = "SELECT COUNT(*) AS cnt FROM sys.dm_exec_sessions WHERE is_user_process = 1" $conn = Invoke-SqlQuery -Query $connQuery $connVal = if ($conn) { $conn.cnt } else { 0 } [void]$sb.AppendLine("mssql_connections_total $connVal") } catch { [void]$sb.AppendLine("mssql_connections_total 0") } [void]$sb.AppendLine('') # --- Connections Per Database --- [void]$sb.AppendLine('# HELP mssql_connections_by_database User connections per database') [void]$sb.AppendLine('# TYPE mssql_connections_by_database gauge') try { $connDbQuery = "SELECT DB_NAME(database_id) AS db_name, COUNT(*) AS cnt FROM sys.dm_exec_sessions WHERE is_user_process = 1 AND database_id > 0 GROUP BY database_id" $connDb = Invoke-SqlQuery -Query $connDbQuery if ($connDb) { foreach ($row in $connDb) { $dbName = $row.db_name -replace '["]', '' [void]$sb.AppendLine("mssql_connections_by_database{database=`"$dbName`"} $($row.cnt)") } } } catch { } [void]$sb.AppendLine('') # --- Memory --- [void]$sb.AppendLine('# HELP mssql_memory_buffer_pool_bytes Buffer pool memory in bytes') [void]$sb.AppendLine('# TYPE mssql_memory_buffer_pool_bytes gauge') try { $bpQuery = "SELECT SUM(pages_kb) * 1024 AS size_bytes FROM sys.dm_os_memory_clerks WHERE type = 'MEMORYCLERK_SQLBUFFERPOOL'" $bp = Invoke-SqlQuery -Query $bpQuery $bpVal = if ($bp -and $bp.size_bytes) { $bp.size_bytes } else { 0 } [void]$sb.AppendLine("mssql_memory_buffer_pool_bytes $bpVal") } catch { [void]$sb.AppendLine("mssql_memory_buffer_pool_bytes 0") } [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP mssql_memory_plan_cache_bytes Plan cache memory in bytes') [void]$sb.AppendLine('# TYPE mssql_memory_plan_cache_bytes gauge') try { $pcQuery = "SELECT SUM(pages_kb) * 1024 AS size_bytes FROM sys.dm_os_memory_clerks WHERE type IN ('CACHESTORE_SQLCP', 'CACHESTORE_OBJCP')" $pc = Invoke-SqlQuery -Query $pcQuery $pcVal = if ($pc -and $pc.size_bytes) { $pc.size_bytes } else { 0 } [void]$sb.AppendLine("mssql_memory_plan_cache_bytes $pcVal") } catch { [void]$sb.AppendLine("mssql_memory_plan_cache_bytes 0") } [void]$sb.AppendLine('') # --- CPU Usage --- [void]$sb.AppendLine('# HELP mssql_cpu_usage_percent SQL Server CPU usage percentage') [void]$sb.AppendLine('# TYPE mssql_cpu_usage_percent gauge') try { $cpuQuery = @" SELECT TOP 1 SQLProcessUtilization AS cpu_pct FROM ( SELECT record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS SQLProcessUtilization, TIMESTAMP FROM ( SELECT TIMESTAMP, CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ) AS x ) AS y ORDER BY TIMESTAMP DESC "@ $cpu = Invoke-SqlQuery -Query $cpuQuery $cpuVal = if ($cpu) { $cpu.cpu_pct } else { 0 } [void]$sb.AppendLine("mssql_cpu_usage_percent $cpuVal") } catch { [void]$sb.AppendLine("mssql_cpu_usage_percent 0") } [void]$sb.AppendLine('') # --- Wait Stats --- [void]$sb.AppendLine('# HELP mssql_wait_stats Cumulative wait time in milliseconds by wait type') [void]$sb.AppendLine('# TYPE mssql_wait_stats gauge') try { $waitQuery = @" SELECT TOP 10 wait_type, wait_time_ms FROM sys.dm_os_wait_stats WHERE wait_type NOT IN ( 'WAITFOR', 'SLEEP_TASK', 'BROKER_RECEIVE_WAITFOR', 'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT', 'LAZYWRITER_SLEEP', 'RESOURCE_QUEUE', 'SQLTRACE_BUFFER_FLUSH', 'XE_TIMER_EVENT', 'XE_DISPATCHER_WAIT', 'FT_IFTS_SCHEDULER_IDLE_WAIT', 'BROKER_EVENTHANDLER', 'CHECKPOINT_QUEUE', 'BROKER_TO_FLUSH', 'BROKER_TASK_STOP', 'SP_SERVER_DIAGNOSTICS_SLEEP', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'DIRTY_PAGE_POLL', 'DISPATCHER_QUEUE_SEMAPHORE', 'REQUEST_FOR_DEADLOCK_SEARCH', 'LOGMGR_QUEUE', 'ONDEMAND_TASK_QUEUE', 'BROKER_TRANSMITTER' ) ORDER BY wait_time_ms DESC "@ $waits = Invoke-SqlQuery -Query $waitQuery if ($waits) { foreach ($row in $waits) { $wt = $row.wait_type -replace '["]', '' [void]$sb.AppendLine("mssql_wait_stats{wait_type=`"$wt`"} $($row.wait_time_ms)") } } } catch { } [void]$sb.AppendLine('') $sb.ToString() } # ============================================================================ # COLLECT ALL METRICS # ============================================================================ function Get-AllMetrics { $scriptStart = Get-Date $sb = [System.Text.StringBuilder]::new() # Exporter up - test SQL connectivity [void]$sb.AppendLine('# HELP mssql_up SQL Server reachability (1=up, 0=down)') [void]$sb.AppendLine('# TYPE mssql_up gauge') try { $test = Invoke-SqlQuery -Query "SELECT 1 AS test" $upVal = if ($test) { 1 } else { 0 } [void]$sb.AppendLine("mssql_up $upVal") } catch { [void]$sb.AppendLine("mssql_up 0") # Still emit duration and timestamp $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP mssql_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE mssql_exporter_duration_seconds gauge') [void]$sb.AppendLine("mssql_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP mssql_exporter_last_run_timestamp Unix timestamp of last run') [void]$sb.AppendLine('# TYPE mssql_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("mssql_exporter_last_run_timestamp $(Get-UnixTimestamp)") return $sb.ToString() } [void]$sb.AppendLine('') # Exporter info [void]$sb.AppendLine('# HELP mssql_exporter_info Exporter version information') [void]$sb.AppendLine('# TYPE mssql_exporter_info gauge') [void]$sb.AppendLine('mssql_exporter_info{version="1.0"} 1') [void]$sb.AppendLine('') # Collect SQL Server metrics [void]$sb.Append((Get-MssqlMetrics)) # Exporter runtime $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds $timestamp = Get-UnixTimestamp [void]$sb.AppendLine('# HELP mssql_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE mssql_exporter_duration_seconds gauge') [void]$sb.AppendLine("mssql_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP mssql_exporter_last_run_timestamp Unix timestamp of last successful run') [void]$sb.AppendLine('# TYPE mssql_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("mssql_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 SQL Server 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 = @" SQL Server Metrics Exporter v1.0

SQL Server 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 'mssql_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 ".mssql_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 } }