<# .SYNOPSIS Windows Memory Pressure Prometheus Metrics Exporter .DESCRIPTION Prometheus exporter for Windows memory pressure metrics. Exports committed memory usage, page file usage, paging activity rates, pool memory (paged/nonpaged), cache pressure, resource exhaustion events from the Windows Event Log, and NUMA memory balance. Each section auto-detects whether the relevant data source is available and skips gracefully if not. .PARAMETER Mode Output mode: 'stdout' (default), 'textfile', or 'http' .PARAMETER Port HTTP port for http mode (default: 9198) .PARAMETER TextfileDir Directory for textfile collector output (default: C:\ProgramData\node_exporter) .PARAMETER OutputFile Custom output file path .NOTES Author: Phil Connor Contact: contact@mylinux.work Website: https://mylinux.work License: MIT Version: 1.0 Metrics Exported: Core Status: - windows_memory_pressure_up - windows_memory_pressure_exporter_info{version} Committed Memory: - windows_memory_pressure_committed_bytes - windows_memory_pressure_commit_limit_bytes - windows_memory_pressure_committed_percent - windows_memory_pressure_available_bytes Page File: - windows_memory_pressure_pagefile_usage_percent{pagefile} - windows_memory_pressure_pagefile_peak_usage_percent{pagefile} - windows_memory_pressure_pagefile_allocated_bytes{pagefile} - windows_memory_pressure_pagefile_current_bytes{pagefile} Paging Activity: - windows_memory_pressure_pages_input_per_sec - windows_memory_pressure_pages_output_per_sec - windows_memory_pressure_page_faults_per_sec - windows_memory_pressure_hard_page_faults_per_sec Pool Memory: - windows_memory_pressure_pool_paged_bytes - windows_memory_pressure_pool_nonpaged_bytes - windows_memory_pressure_pool_paged_allocs - windows_memory_pressure_pool_nonpaged_allocs Cache: - windows_memory_pressure_cache_bytes - windows_memory_pressure_cache_faults_per_sec - windows_memory_pressure_standby_cache_bytes - windows_memory_pressure_modified_page_list_bytes Resource Exhaustion (requires admin): - windows_memory_pressure_resource_exhaustion_events_24h - windows_memory_pressure_resource_exhaustion_last_timestamp NUMA (multi-node only): - windows_memory_pressure_numa_total_bytes{node} - windows_memory_pressure_numa_free_bytes{node} - windows_memory_pressure_numa_used_percent{node} Exporter: - windows_memory_pressure_exporter_duration_seconds - windows_memory_pressure_exporter_last_run_timestamp #> param( [ValidateSet('stdout', 'textfile', 'http')] [string]$Mode = 'stdout', [int]$Port = 9198, [string]$TextfileDir = 'C:\ProgramData\node_exporter', [string]$OutputFile, [switch]$InstallScheduledTask, [int]$TaskIntervalMinutes = 5 ) if ($InstallScheduledTask) { $taskName = "WindowsMemoryPressureExporter" $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 Windows memory pressure 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 auto-start 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) } # ============================================================================ # COMMITTED MEMORY # ============================================================================ function Get-CommittedMemoryMetrics { $sb = [System.Text.StringBuilder]::new() try { $counters = Get-Counter -Counter @( '\Memory\Committed Bytes', '\Memory\Commit Limit', '\Memory\Available Bytes' ) -ErrorAction Stop $samples = $counters.CounterSamples $committed = ($samples | Where-Object { $_.Path -like '*committed bytes' -and $_.Path -notlike '*% committed*' }).CookedValue $limit = ($samples | Where-Object { $_.Path -like '*commit limit' }).CookedValue $available = ($samples | Where-Object { $_.Path -like '*available bytes' }).CookedValue $pct = if ($limit -gt 0) { Format-MetricValue (($committed / $limit) * 100) } else { 0 } [void]$sb.AppendLine('# HELP windows_memory_pressure_committed_bytes Total committed memory in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_committed_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_committed_bytes $([math]::Round($committed))") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_commit_limit_bytes Commit limit in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_commit_limit_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_commit_limit_bytes $([math]::Round($limit))") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_committed_percent Percentage of commit limit used') [void]$sb.AppendLine('# TYPE windows_memory_pressure_committed_percent gauge') [void]$sb.AppendLine("windows_memory_pressure_committed_percent $pct") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_available_bytes Available memory in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_available_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_available_bytes $([math]::Round($available))") [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect committed memory metrics: $_" } $sb.ToString() } # ============================================================================ # PAGE FILE USAGE # ============================================================================ function Get-PageFileMetrics { $sb = [System.Text.StringBuilder]::new() try { $pagefiles = Get-CimInstance -ClassName Win32_PageFileUsage -ErrorAction Stop if ($pagefiles) { foreach ($pf in $pagefiles) { $name = $pf.Name -replace '\\', '\\\\' $allocMB = $pf.AllocatedBaseSize $usedMB = $pf.CurrentUsage $peakMB = $pf.PeakUsage $usagePct = if ($allocMB -gt 0) { Format-MetricValue (($usedMB / $allocMB) * 100) } else { 0 } $peakPct = if ($allocMB -gt 0) { Format-MetricValue (($peakMB / $allocMB) * 100) } else { 0 } [void]$sb.AppendLine('# HELP windows_memory_pressure_pagefile_usage_percent Current page file usage percentage') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pagefile_usage_percent gauge') [void]$sb.AppendLine("windows_memory_pressure_pagefile_usage_percent{pagefile=`"$name`"} $usagePct") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_pagefile_peak_usage_percent Peak page file usage percentage') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pagefile_peak_usage_percent gauge') [void]$sb.AppendLine("windows_memory_pressure_pagefile_peak_usage_percent{pagefile=`"$name`"} $peakPct") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_pagefile_allocated_bytes Page file allocated size in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pagefile_allocated_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_pagefile_allocated_bytes{pagefile=`"$name`"} $($allocMB * 1MB)") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_pagefile_current_bytes Page file current usage in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pagefile_current_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_pagefile_current_bytes{pagefile=`"$name`"} $($usedMB * 1MB)") [void]$sb.AppendLine('') } } } catch { Write-Warning "Failed to collect page file metrics: $_" } $sb.ToString() } # ============================================================================ # PAGING ACTIVITY # ============================================================================ function Get-PagingActivityMetrics { $sb = [System.Text.StringBuilder]::new() try { $counters = Get-Counter -Counter @( '\Memory\Pages Input/sec', '\Memory\Pages Output/sec', '\Memory\Page Faults/sec' ) -SampleInterval 1 -ErrorAction Stop $samples = $counters.CounterSamples $pagesIn = Format-MetricValue ($samples | Where-Object { $_.Path -like '*pages input*' }).CookedValue $pagesOut = Format-MetricValue ($samples | Where-Object { $_.Path -like '*pages output*' }).CookedValue $faults = Format-MetricValue ($samples | Where-Object { $_.Path -like '*page faults*' }).CookedValue [void]$sb.AppendLine('# HELP windows_memory_pressure_pages_input_per_sec Pages read from disk per second') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pages_input_per_sec gauge') [void]$sb.AppendLine("windows_memory_pressure_pages_input_per_sec $pagesIn") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_pages_output_per_sec Pages written to disk per second') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pages_output_per_sec gauge') [void]$sb.AppendLine("windows_memory_pressure_pages_output_per_sec $pagesOut") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_page_faults_per_sec Page faults per second (soft and hard)') [void]$sb.AppendLine('# TYPE windows_memory_pressure_page_faults_per_sec gauge') [void]$sb.AppendLine("windows_memory_pressure_page_faults_per_sec $faults") [void]$sb.AppendLine('') # Hard page faults - Pages Input/sec is the best proxy for hard faults [void]$sb.AppendLine('# HELP windows_memory_pressure_hard_page_faults_per_sec Hard page faults per second (disk reads)') [void]$sb.AppendLine('# TYPE windows_memory_pressure_hard_page_faults_per_sec gauge') [void]$sb.AppendLine("windows_memory_pressure_hard_page_faults_per_sec $pagesIn") [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect paging activity metrics: $_" } $sb.ToString() } # ============================================================================ # POOL MEMORY # ============================================================================ function Get-PoolMemoryMetrics { $sb = [System.Text.StringBuilder]::new() try { $counters = Get-Counter -Counter @( '\Memory\Pool Paged Bytes', '\Memory\Pool Nonpaged Bytes', '\Memory\Pool Paged Allocs', '\Memory\Pool Nonpaged Allocs' ) -ErrorAction Stop $samples = $counters.CounterSamples $pagedBytes = [math]::Round(($samples | Where-Object { $_.Path -like '*pool paged bytes' }).CookedValue) $nonpagedBytes = [math]::Round(($samples | Where-Object { $_.Path -like '*pool nonpaged bytes' }).CookedValue) $pagedAllocs = [math]::Round(($samples | Where-Object { $_.Path -like '*pool paged allocs' }).CookedValue) $nonpagedAllocs = [math]::Round(($samples | Where-Object { $_.Path -like '*pool nonpaged allocs' }).CookedValue) [void]$sb.AppendLine('# HELP windows_memory_pressure_pool_paged_bytes Paged pool memory in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pool_paged_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_pool_paged_bytes $pagedBytes") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_pool_nonpaged_bytes Nonpaged pool memory in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pool_nonpaged_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_pool_nonpaged_bytes $nonpagedBytes") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_pool_paged_allocs Paged pool allocation count') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pool_paged_allocs counter') [void]$sb.AppendLine("windows_memory_pressure_pool_paged_allocs $pagedAllocs") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_pool_nonpaged_allocs Nonpaged pool allocation count') [void]$sb.AppendLine('# TYPE windows_memory_pressure_pool_nonpaged_allocs counter') [void]$sb.AppendLine("windows_memory_pressure_pool_nonpaged_allocs $nonpagedAllocs") [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect pool memory metrics: $_" } $sb.ToString() } # ============================================================================ # CACHE PRESSURE # ============================================================================ function Get-CachePressureMetrics { $sb = [System.Text.StringBuilder]::new() try { $counters = Get-Counter -Counter @( '\Memory\Cache Bytes', '\Memory\Cache Faults/sec', '\Memory\Standby Cache Normal Priority Bytes', '\Memory\Standby Cache Reserve Bytes', '\Memory\Standby Cache Core Bytes', '\Memory\Modified Page List Bytes' ) -ErrorAction Stop $samples = $counters.CounterSamples $cacheBytes = [math]::Round(($samples | Where-Object { $_.Path -like '*\cache bytes' }).CookedValue) $cacheFaults = Format-MetricValue ($samples | Where-Object { $_.Path -like '*cache faults*' }).CookedValue $standbyNormal = ($samples | Where-Object { $_.Path -like '*standby*normal*' }).CookedValue $standbyRes = ($samples | Where-Object { $_.Path -like '*standby*reserve*' }).CookedValue $standbyCore = ($samples | Where-Object { $_.Path -like '*standby*core*' }).CookedValue $standbyTotal = [math]::Round($standbyNormal + $standbyRes + $standbyCore) $modified = [math]::Round(($samples | Where-Object { $_.Path -like '*modified page*' }).CookedValue) [void]$sb.AppendLine('# HELP windows_memory_pressure_cache_bytes File system cache size in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_cache_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_cache_bytes $cacheBytes") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_cache_faults_per_sec Cache faults per second') [void]$sb.AppendLine('# TYPE windows_memory_pressure_cache_faults_per_sec gauge') [void]$sb.AppendLine("windows_memory_pressure_cache_faults_per_sec $cacheFaults") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_standby_cache_bytes Total standby cache size in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_standby_cache_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_standby_cache_bytes $standbyTotal") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_modified_page_list_bytes Modified page list size in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_modified_page_list_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_modified_page_list_bytes $modified") [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect cache metrics: $_" } $sb.ToString() } # ============================================================================ # RESOURCE EXHAUSTION EVENTS # ============================================================================ function Get-ResourceExhaustionMetrics { $sb = [System.Text.StringBuilder]::new() try { $since = (Get-Date).AddHours(-24) $events = Get-WinEvent -FilterHashtable @{ LogName = 'System' ProviderName = 'Microsoft-Windows-Resource-Exhaustion-Detector' Id = 2004 StartTime = $since } -ErrorAction Stop $count = @($events).Count $lastTimestamp = 0 if ($count -gt 0) { $lastTimestamp = [int][double]::Parse(($events[0].TimeCreated | Get-Date -UFormat '%s')) } } catch { # No events found or access denied - both are fine $count = 0 $lastTimestamp = 0 } [void]$sb.AppendLine('# HELP windows_memory_pressure_resource_exhaustion_events_24h Resource exhaustion events in last 24 hours') [void]$sb.AppendLine('# TYPE windows_memory_pressure_resource_exhaustion_events_24h gauge') [void]$sb.AppendLine("windows_memory_pressure_resource_exhaustion_events_24h $count") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_resource_exhaustion_last_timestamp Unix timestamp of last resource exhaustion event') [void]$sb.AppendLine('# TYPE windows_memory_pressure_resource_exhaustion_last_timestamp gauge') [void]$sb.AppendLine("windows_memory_pressure_resource_exhaustion_last_timestamp $lastTimestamp") [void]$sb.AppendLine('') $sb.ToString() } # ============================================================================ # NUMA MEMORY BALANCE # ============================================================================ function Get-NumaMemoryMetrics { $sb = [System.Text.StringBuilder]::new() try { $nodes = Get-CimInstance -ClassName Win32_NumaNode -ErrorAction Stop # Skip on single-node systems if (@($nodes).Count -le 1) { return $sb.ToString() } # Use performance counters for per-node data foreach ($node in $nodes) { $nodeId = $node.NodeId try { $numaCounters = Get-Counter -Counter @( "\NUMA Node Memory($nodeId)\Total MBytes", "\NUMA Node Memory($nodeId)\Available MBytes" ) -ErrorAction Stop $totalMB = ($numaCounters.CounterSamples | Where-Object { $_.Path -like '*total*' }).CookedValue $availMB = ($numaCounters.CounterSamples | Where-Object { $_.Path -like '*available*' }).CookedValue $totalBytes = [math]::Round($totalMB * 1MB) $freeBytes = [math]::Round($availMB * 1MB) $usedPct = if ($totalMB -gt 0) { Format-MetricValue ((($totalMB - $availMB) / $totalMB) * 100) } else { 0 } [void]$sb.AppendLine('# HELP windows_memory_pressure_numa_total_bytes Total memory per NUMA node in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_numa_total_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_numa_total_bytes{node=`"$nodeId`"} $totalBytes") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_numa_free_bytes Free memory per NUMA node in bytes') [void]$sb.AppendLine('# TYPE windows_memory_pressure_numa_free_bytes gauge') [void]$sb.AppendLine("windows_memory_pressure_numa_free_bytes{node=`"$nodeId`"} $freeBytes") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_numa_used_percent Memory usage percentage per NUMA node') [void]$sb.AppendLine('# TYPE windows_memory_pressure_numa_used_percent gauge') [void]$sb.AppendLine("windows_memory_pressure_numa_used_percent{node=`"$nodeId`"} $usedPct") [void]$sb.AppendLine('') } catch { # Counter not available for this node } } } catch { # NUMA not available } $sb.ToString() } # ============================================================================ # METRICS GENERATION # ============================================================================ function Get-AllMetrics { $scriptStart = Get-Date $sb = [System.Text.StringBuilder]::new() # Exporter status [void]$sb.AppendLine('# HELP windows_memory_pressure_up Exporter status (1=up)') [void]$sb.AppendLine('# TYPE windows_memory_pressure_up gauge') [void]$sb.AppendLine('windows_memory_pressure_up 1') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_exporter_info Exporter version information') [void]$sb.AppendLine('# TYPE windows_memory_pressure_exporter_info gauge') [void]$sb.AppendLine('windows_memory_pressure_exporter_info{version="1.0"} 1') [void]$sb.AppendLine('') # Collect all sections [void]$sb.Append((Get-CommittedMemoryMetrics)) [void]$sb.Append((Get-PageFileMetrics)) [void]$sb.Append((Get-PagingActivityMetrics)) [void]$sb.Append((Get-PoolMemoryMetrics)) [void]$sb.Append((Get-CachePressureMetrics)) [void]$sb.Append((Get-ResourceExhaustionMetrics)) [void]$sb.Append((Get-NumaMemoryMetrics)) # Exporter runtime $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds $timestamp = Get-UnixTimestamp [void]$sb.AppendLine('# HELP windows_memory_pressure_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE windows_memory_pressure_exporter_duration_seconds gauge') [void]$sb.AppendLine("windows_memory_pressure_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP windows_memory_pressure_exporter_last_run_timestamp Unix timestamp of last successful run') [void]$sb.AppendLine('# TYPE windows_memory_pressure_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("windows_memory_pressure_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 Windows memory pressure 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 = @" Windows Memory Pressure Exporter v1.0

Windows Memory Pressure Exporter v1.0

Metrics

Sections

"@ $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' { if (-not $OutputFile) { $OutputFile = Join-Path $TextfileDir 'memory_pressure.prom' } $outputDir = Split-Path $OutputFile -Parent if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null } $tempFile = Join-Path $outputDir ".memory_pressure_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 } }