<# .SYNOPSIS IIS Prometheus Metrics Exporter .DESCRIPTION Prometheus exporter for IIS (Internet Information Services). Collects metrics via PowerShell WebAdministration module, Windows performance counters, and IIS log analysis. Outputs Prometheus-compatible text format for consumption by windows_exporter textfile collector. .PARAMETER Mode Output mode: 'stdout' (default), 'textfile', or 'http' .PARAMETER Port HTTP port for http mode (default: 9210) .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 auto-start on system boot .PARAMETER TaskIntervalMinutes Interval in minutes for the scheduled task (default: 5) .NOTES Author: Phil Connor Contact: contact@mylinux.work Website: https://mylinux.work License: MIT Version: 1.0 Metrics Exported: Core Status: - iis_up - iis_exporter_info{version} Site Status: - iis_site_state{site} - iis_site_binding_info{site,protocol,address,port,hostname} Application Pools: - iis_apppool_state{apppool} - iis_apppool_worker_process_count{apppool} - iis_apppool_recycle_count{apppool} Request Throughput: - iis_current_connections{site} - iis_requests_per_second{site} - iis_bytes_sent_per_second{site} - iis_bytes_received_per_second{site} - iis_total_bytes_sent{site} - iis_total_bytes_received{site} - iis_requests_total{site} HTTP Methods: - iis_get_requests_total{site} - iis_post_requests_total{site} - iis_put_requests_total{site} - iis_delete_requests_total{site} HTTP Status Codes: - iis_status_2xx_total{site} - iis_status_3xx_total{site} - iis_status_4xx_total{site} - iis_status_5xx_total{site} SSL/TLS: - iis_ssl_connections_current - iis_ssl_connections_per_second - iis_ssl_handshake_failures Worker Processes: - iis_worker_cpu_percent{apppool} - iis_worker_memory_bytes{apppool} - iis_worker_active_requests{apppool} - iis_worker_uptime_seconds{apppool} Cache: - iis_cache_output_entries - iis_cache_output_hits - iis_cache_output_misses - iis_cache_uri_hits - iis_cache_uri_misses - iis_cache_kernel_hits Failed Requests: - iis_failed_requests_500_last_hour - iis_failed_requests_total{site} Exporter: - iis_exporter_duration_seconds - iis_exporter_last_run_timestamp #> param( [ValidateSet('stdout', 'textfile', 'http')] [string]$Mode = 'stdout', [int]$Port = 9210, [string]$TextfileDir = 'C:\ProgramData\node_exporter', [string]$OutputFile, [switch]$InstallScheduledTask, [int]$TaskIntervalMinutes = 5 ) # Create a scheduled task to run this script every $TaskIntervalMinutes minutes # The task will run as SYSTEM and will be set to run at startup if ($InstallScheduledTask) { $taskName = "IISMetricsExporter" $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 IIS 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) } function Get-SafeCounter { param([string]$CounterPath) try { $counter = Get-Counter -Counter $CounterPath -ErrorAction Stop if ($counter -and $counter.CounterSamples) { return [math]::Max(0, [math]::Round($counter.CounterSamples[0].CookedValue, 2)) } } catch {} return 0 } function Get-SafeCounterMulti { param([string]$CounterPath) try { $counter = Get-Counter -Counter $CounterPath -ErrorAction Stop if ($counter -and $counter.CounterSamples) { return $counter.CounterSamples } } catch {} return @() } # ============================================================================ # SITE STATUS # ============================================================================ function Get-SiteStatusMetrics { $sb = [System.Text.StringBuilder]::new() try { Import-Module WebAdministration -ErrorAction Stop $sites = Get-Website -ErrorAction Stop [void]$sb.AppendLine('# HELP iis_site_state Site state (1=Started, 0=Stopped)') [void]$sb.AppendLine('# TYPE iis_site_state gauge') foreach ($site in $sites) { $name = $site.Name $state = if ($site.State -eq 'Started') { 1 } else { 0 } [void]$sb.AppendLine("iis_site_state{site=`"$name`"} $state") } [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_site_binding_info Site binding information (always 1)') [void]$sb.AppendLine('# TYPE iis_site_binding_info gauge') foreach ($site in $sites) { $name = $site.Name foreach ($binding in $site.Bindings.Collection) { $proto = $binding.protocol $info = $binding.bindingInformation -split ':' $addr = if ($info[0]) { $info[0] } else { '*' } $port = if ($info[1]) { $info[1] } else { '80' } $host = if ($info[2]) { $info[2] } else { '' } [void]$sb.AppendLine("iis_site_binding_info{site=`"$name`",protocol=`"$proto`",address=`"$addr`",port=`"$port`",hostname=`"$host`"} 1") } } [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect site status metrics: $_" } $sb.ToString() } # ============================================================================ # APPLICATION POOLS # ============================================================================ function Get-AppPoolMetrics { $sb = [System.Text.StringBuilder]::new() try { Import-Module WebAdministration -ErrorAction Stop $pools = Get-ChildItem IIS:\AppPools -ErrorAction Stop [void]$sb.AppendLine('# HELP iis_apppool_state Application pool state (1=Started, 0=Stopped)') [void]$sb.AppendLine('# TYPE iis_apppool_state gauge') foreach ($pool in $pools) { $name = $pool.Name $state = if ($pool.State -eq 'Started') { 1 } else { 0 } [void]$sb.AppendLine("iis_apppool_state{apppool=`"$name`"} $state") } [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_apppool_worker_process_count Number of worker processes per pool') [void]$sb.AppendLine('# TYPE iis_apppool_worker_process_count gauge') foreach ($pool in $pools) { $name = $pool.Name $wpCount = 0 try { $workers = Get-ChildItem "IIS:\AppPools\$name\WorkerProcesses" -ErrorAction Stop $wpCount = @($workers).Count } catch {} [void]$sb.AppendLine("iis_apppool_worker_process_count{apppool=`"$name`"} $wpCount") } [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_apppool_recycle_count Application pool recycle count') [void]$sb.AppendLine('# TYPE iis_apppool_recycle_count gauge') foreach ($pool in $pools) { $name = $pool.Name $recycleCount = Get-SafeCounter "\W3SVC_W3WP(*$name*)\Total Threads" $recycleVal = 0 try { $recycleEvents = Get-WinEvent -FilterHashtable @{ LogName = 'System' ProviderName = 'WAS' Id = 5074, 5075, 5076, 5077, 5078, 5079, 5080, 5186 StartTime = (Get-Date).AddHours(-24) } -ErrorAction Stop | Where-Object { $_.Message -like "*$name*" } $recycleVal = @($recycleEvents).Count } catch {} [void]$sb.AppendLine("iis_apppool_recycle_count{apppool=`"$name`"} $recycleVal") } [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect application pool metrics: $_" } $sb.ToString() } # ============================================================================ # REQUEST THROUGHPUT # ============================================================================ function Get-RequestThroughputMetrics { $sb = [System.Text.StringBuilder]::new() try { $connSamples = Get-SafeCounterMulti '\Web Service(*)\Current Connections' if ($connSamples.Count -gt 0) { [void]$sb.AppendLine('# HELP iis_current_connections Current active connections per site') [void]$sb.AppendLine('# TYPE iis_current_connections gauge') foreach ($sample in $connSamples) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = [math]::Max(0, [math]::Round($sample.CookedValue)) [void]$sb.AppendLine("iis_current_connections{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } $reqSamples = Get-SafeCounterMulti '\Web Service(*)\Total Method Requests/sec' if ($reqSamples.Count -gt 0) { [void]$sb.AppendLine('# HELP iis_requests_per_second HTTP requests per second per site') [void]$sb.AppendLine('# TYPE iis_requests_per_second gauge') foreach ($sample in $reqSamples) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = Format-MetricValue $sample.CookedValue [void]$sb.AppendLine("iis_requests_per_second{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } $bytesSentSamples = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Sent/sec' if ($bytesSentSamples.Count -gt 0) { [void]$sb.AppendLine('# HELP iis_bytes_sent_per_second Bytes sent per second per site') [void]$sb.AppendLine('# TYPE iis_bytes_sent_per_second gauge') foreach ($sample in $bytesSentSamples) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = Format-MetricValue $sample.CookedValue [void]$sb.AppendLine("iis_bytes_sent_per_second{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } $bytesRecvSamples = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Received/sec' if ($bytesRecvSamples.Count -gt 0) { [void]$sb.AppendLine('# HELP iis_bytes_received_per_second Bytes received per second per site') [void]$sb.AppendLine('# TYPE iis_bytes_received_per_second gauge') foreach ($sample in $bytesRecvSamples) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = Format-MetricValue $sample.CookedValue [void]$sb.AppendLine("iis_bytes_received_per_second{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } $totalBytesSent = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Sent' if ($totalBytesSent.Count -gt 0) { [void]$sb.AppendLine('# HELP iis_total_bytes_sent Total bytes sent per site') [void]$sb.AppendLine('# TYPE iis_total_bytes_sent counter') foreach ($sample in $totalBytesSent) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = [math]::Max(0, [math]::Round($sample.CookedValue)) [void]$sb.AppendLine("iis_total_bytes_sent{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } $totalBytesRecv = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Received' if ($totalBytesRecv.Count -gt 0) { [void]$sb.AppendLine('# HELP iis_total_bytes_received Total bytes received per site') [void]$sb.AppendLine('# TYPE iis_total_bytes_received counter') foreach ($sample in $totalBytesRecv) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = [math]::Max(0, [math]::Round($sample.CookedValue)) [void]$sb.AppendLine("iis_total_bytes_received{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } $totalReqs = Get-SafeCounterMulti '\Web Service(*)\Total Method Requests' if ($totalReqs.Count -gt 0) { [void]$sb.AppendLine('# HELP iis_requests_total Total requests per site') [void]$sb.AppendLine('# TYPE iis_requests_total counter') foreach ($sample in $totalReqs) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = [math]::Max(0, [math]::Round($sample.CookedValue)) [void]$sb.AppendLine("iis_requests_total{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } } catch { Write-Warning "Failed to collect request throughput metrics: $_" } $sb.ToString() } # ============================================================================ # HTTP METHODS # ============================================================================ function Get-HttpMethodMetrics { $sb = [System.Text.StringBuilder]::new() try { $methods = @{ 'get' = '\Web Service(*)\Total Get Requests' 'post' = '\Web Service(*)\Total Post Requests' 'put' = '\Web Service(*)\Total Put Requests' 'delete' = '\Web Service(*)\Total Delete Requests' } foreach ($method in $methods.GetEnumerator()) { $samples = Get-SafeCounterMulti $method.Value if ($samples.Count -gt 0) { [void]$sb.AppendLine("# HELP iis_$($method.Key)_requests_total Total $($method.Key.ToUpper()) requests per site") [void]$sb.AppendLine("# TYPE iis_$($method.Key)_requests_total counter") foreach ($sample in $samples) { $instance = ($sample.Path -split '\\')[-2] -replace '[()]', '' if ($instance -eq '_total') { continue } $val = [math]::Max(0, [math]::Round($sample.CookedValue)) [void]$sb.AppendLine("iis_$($method.Key)_requests_total{site=`"$instance`"} $val") } [void]$sb.AppendLine('') } } } catch { Write-Warning "Failed to collect HTTP method metrics: $_" } $sb.ToString() } # ============================================================================ # HTTP STATUS CODES (from IIS log analysis) # ============================================================================ function Get-HttpStatusMetrics { $sb = [System.Text.StringBuilder]::new() try { Import-Module WebAdministration -ErrorAction Stop $sites = Get-Website -ErrorAction Stop $oneHourAgo = (Get-Date).AddHours(-1) $siteStatusCounts = @{} foreach ($site in $sites) { $logDir = "$($site.logFile.directory)\W3SVC$($site.id)" -replace '%SystemDrive%', $env:SystemDrive $siteStatusCounts[$site.Name] = @{ '2xx' = 0; '3xx' = 0; '4xx' = 0; '5xx' = 0 } if (-not (Test-Path $logDir)) { continue } $logFile = Get-ChildItem $logDir -Filter '*.log' -ErrorAction Stop | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $logFile) { continue } $reader = [System.IO.StreamReader]::new($logFile.FullName) try { while ($null -ne ($line = $reader.ReadLine())) { if ($line.StartsWith('#')) { continue } $fields = $line -split '\s+' if ($fields.Count -lt 4) { continue } try { $logDate = [datetime]::ParseExact("$($fields[0]) $($fields[1])", 'yyyy-MM-dd HH:mm:ss', $null) if ($logDate -lt $oneHourAgo) { continue } } catch { continue } $statusIdx = if ($fields.Count -ge 12) { 11 } elseif ($fields.Count -ge 10) { 9 } else { continue } $statusCode = 0 if ([int]::TryParse($fields[$statusIdx], [ref]$statusCode)) { if ($statusCode -ge 200 -and $statusCode -lt 300) { $siteStatusCounts[$site.Name]['2xx']++ } elseif ($statusCode -ge 300 -and $statusCode -lt 400) { $siteStatusCounts[$site.Name]['3xx']++ } elseif ($statusCode -ge 400 -and $statusCode -lt 500) { $siteStatusCounts[$site.Name]['4xx']++ } elseif ($statusCode -ge 500 -and $statusCode -lt 600) { $siteStatusCounts[$site.Name]['5xx']++ } } } } finally { $reader.Close() } } foreach ($code in @('2xx', '3xx', '4xx', '5xx')) { [void]$sb.AppendLine("# HELP iis_status_${code}_total Total ${code} responses per site (last hour)") [void]$sb.AppendLine("# TYPE iis_status_${code}_total gauge") foreach ($siteName in $siteStatusCounts.Keys) { [void]$sb.AppendLine("iis_status_${code}_total{site=`"$siteName`"} $($siteStatusCounts[$siteName][$code])") } [void]$sb.AppendLine('') } } catch { Write-Warning "Failed to collect HTTP status code metrics: $_" } $sb.ToString() } # ============================================================================ # SSL/TLS # ============================================================================ function Get-SslMetrics { $sb = [System.Text.StringBuilder]::new() try { $sslCurrent = Get-SafeCounter '\Web Service(_Total)\Current ISAPI Extension Requests' $sslTotal = Get-SafeCounter '\Web Service(_Total)\Total Connection Attempts (all instances)' [void]$sb.AppendLine('# HELP iis_ssl_connections_current Current SSL connections') [void]$sb.AppendLine('# TYPE iis_ssl_connections_current gauge') [void]$sb.AppendLine("iis_ssl_connections_current $sslCurrent") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_ssl_connections_per_second SSL connections per second') [void]$sb.AppendLine('# TYPE iis_ssl_connections_per_second gauge') $sslPerSec = Get-SafeCounter '\Web Service(_Total)\Connection Attempts/sec' [void]$sb.AppendLine("iis_ssl_connections_per_second $sslPerSec") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_ssl_handshake_failures SSL/TLS handshake failures') [void]$sb.AppendLine('# TYPE iis_ssl_handshake_failures counter') $sslFailures = 0 try { $sslEvents = Get-WinEvent -FilterHashtable @{ LogName = 'System' ProviderName = 'Schannel' Level = 2, 3 StartTime = (Get-Date).AddHours(-24) } -ErrorAction Stop $sslFailures = @($sslEvents).Count } catch {} [void]$sb.AppendLine("iis_ssl_handshake_failures $sslFailures") [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect SSL metrics: $_" } $sb.ToString() } # ============================================================================ # WORKER PROCESSES # ============================================================================ function Get-WorkerProcessMetrics { $sb = [System.Text.StringBuilder]::new() try { Import-Module WebAdministration -ErrorAction Stop $pools = Get-ChildItem IIS:\AppPools -ErrorAction Stop [void]$sb.AppendLine('# HELP iis_worker_cpu_percent Worker process CPU usage percentage per pool') [void]$sb.AppendLine('# TYPE iis_worker_cpu_percent gauge') $cpuLines = [System.Text.StringBuilder]::new() $memLines = [System.Text.StringBuilder]::new() $reqLines = [System.Text.StringBuilder]::new() $upLines = [System.Text.StringBuilder]::new() foreach ($pool in $pools) { $name = $pool.Name try { $workers = Get-ChildItem "IIS:\AppPools\$name\WorkerProcesses" -ErrorAction Stop foreach ($worker in $workers) { $pid = $worker.processId if ($pid -and $pid -gt 0) { $proc = Get-Process -Id $pid -ErrorAction Stop $cpuPercent = Format-MetricValue $proc.CPU $memBytes = $proc.WorkingSet64 $uptimeSeconds = [math]::Round(((Get-Date) - $proc.StartTime).TotalSeconds) [void]$cpuLines.AppendLine("iis_worker_cpu_percent{apppool=`"$name`"} $cpuPercent") [void]$memLines.AppendLine("iis_worker_memory_bytes{apppool=`"$name`"} $memBytes") [void]$upLines.AppendLine("iis_worker_uptime_seconds{apppool=`"$name`"} $uptimeSeconds") } } $activeReqs = Get-SafeCounter "\W3SVC_W3WP(*$name*)\Active Requests" [void]$reqLines.AppendLine("iis_worker_active_requests{apppool=`"$name`"} $activeReqs") } catch {} } [void]$sb.AppendLine('# HELP iis_worker_cpu_percent Worker process CPU time per pool') [void]$sb.AppendLine('# TYPE iis_worker_cpu_percent gauge') [void]$sb.Append($cpuLines.ToString()) [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_worker_memory_bytes Worker process working set in bytes per pool') [void]$sb.AppendLine('# TYPE iis_worker_memory_bytes gauge') [void]$sb.Append($memLines.ToString()) [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_worker_active_requests Active requests per worker process per pool') [void]$sb.AppendLine('# TYPE iis_worker_active_requests gauge') [void]$sb.Append($reqLines.ToString()) [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_worker_uptime_seconds Worker process uptime in seconds per pool') [void]$sb.AppendLine('# TYPE iis_worker_uptime_seconds gauge') [void]$sb.Append($upLines.ToString()) [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect worker process metrics: $_" } $sb.ToString() } # ============================================================================ # CACHE # ============================================================================ function Get-CacheMetrics { $sb = [System.Text.StringBuilder]::new() try { $cacheMetrics = @( @{ Name = 'iis_cache_output_entries'; Counter = '\Web Service Cache\Current Files Cached'; Help = 'Current output cache entries' }, @{ Name = 'iis_cache_output_hits'; Counter = '\Web Service Cache\File Cache Hits'; Help = 'Output cache hit count' }, @{ Name = 'iis_cache_output_misses'; Counter = '\Web Service Cache\File Cache Misses'; Help = 'Output cache miss count' }, @{ Name = 'iis_cache_uri_hits'; Counter = '\Web Service Cache\URI Cache Hits'; Help = 'URI cache hit count' }, @{ Name = 'iis_cache_uri_misses'; Counter = '\Web Service Cache\URI Cache Misses'; Help = 'URI cache miss count' }, @{ Name = 'iis_cache_kernel_hits'; Counter = '\Web Service Cache\Kernel: URI Cache Hits'; Help = 'Kernel cache hit count' } ) foreach ($metric in $cacheMetrics) { $val = Get-SafeCounter $metric.Counter [void]$sb.AppendLine("# HELP $($metric.Name) $($metric.Help)") [void]$sb.AppendLine("# TYPE $($metric.Name) gauge") [void]$sb.AppendLine("$($metric.Name) $val") [void]$sb.AppendLine('') } } catch { Write-Warning "Failed to collect cache metrics: $_" } $sb.ToString() } # ============================================================================ # FAILED REQUESTS # ============================================================================ function Get-FailedRequestMetrics { $sb = [System.Text.StringBuilder]::new() try { Import-Module WebAdministration -ErrorAction Stop $sites = Get-Website -ErrorAction Stop $oneHourAgo = (Get-Date).AddHours(-1) $total500 = 0 [void]$sb.AppendLine('# HELP iis_failed_requests_total Total failed requests per site (from event log)') [void]$sb.AppendLine('# TYPE iis_failed_requests_total gauge') foreach ($site in $sites) { $logDir = "$($site.logFile.directory)\W3SVC$($site.id)" -replace '%SystemDrive%', $env:SystemDrive $count500 = 0 if (Test-Path $logDir) { $logFile = Get-ChildItem $logDir -Filter '*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($logFile) { $reader = [System.IO.StreamReader]::new($logFile.FullName) try { while ($null -ne ($line = $reader.ReadLine())) { if ($line.StartsWith('#')) { continue } $fields = $line -split '\s+' if ($fields.Count -lt 4) { continue } try { $logDate = [datetime]::ParseExact("$($fields[0]) $($fields[1])", 'yyyy-MM-dd HH:mm:ss', $null) if ($logDate -lt $oneHourAgo) { continue } } catch { continue } $statusIdx = if ($fields.Count -ge 12) { 11 } elseif ($fields.Count -ge 10) { 9 } else { continue } $statusCode = 0 if ([int]::TryParse($fields[$statusIdx], [ref]$statusCode)) { if ($statusCode -ge 500) { $count500++ } } } } finally { $reader.Close() } } } $total500 += $count500 [void]$sb.AppendLine("iis_failed_requests_total{site=`"$($site.Name)`"} $count500") } [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_failed_requests_500_last_hour HTTP 500 errors in the last hour') [void]$sb.AppendLine('# TYPE iis_failed_requests_500_last_hour gauge') [void]$sb.AppendLine("iis_failed_requests_500_last_hour $total500") [void]$sb.AppendLine('') } catch { Write-Warning "Failed to collect failed request metrics: $_" } $sb.ToString() } # ============================================================================ # COLLECT ALL METRICS # ============================================================================ function Get-AllMetrics { $scriptStart = Get-Date $sb = [System.Text.StringBuilder]::new() # Exporter up [void]$sb.AppendLine('# HELP iis_up Exporter status (1=up, 0=down)') [void]$sb.AppendLine('# TYPE iis_up gauge') [void]$sb.AppendLine('iis_up 1') [void]$sb.AppendLine('') # Exporter info [void]$sb.AppendLine('# HELP iis_exporter_info Exporter version information') [void]$sb.AppendLine('# TYPE iis_exporter_info gauge') [void]$sb.AppendLine('iis_exporter_info{version="1.0"} 1') [void]$sb.AppendLine('') # Collect all sections [void]$sb.Append((Get-SiteStatusMetrics)) [void]$sb.Append((Get-AppPoolMetrics)) [void]$sb.Append((Get-RequestThroughputMetrics)) [void]$sb.Append((Get-HttpMethodMetrics)) [void]$sb.Append((Get-HttpStatusMetrics)) [void]$sb.Append((Get-SslMetrics)) [void]$sb.Append((Get-WorkerProcessMetrics)) [void]$sb.Append((Get-CacheMetrics)) [void]$sb.Append((Get-FailedRequestMetrics)) # Exporter runtime $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds $timestamp = Get-UnixTimestamp [void]$sb.AppendLine('# HELP iis_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE iis_exporter_duration_seconds gauge') [void]$sb.AppendLine("iis_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP iis_exporter_last_run_timestamp Unix timestamp of last successful run') [void]$sb.AppendLine('# TYPE iis_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("iis_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 IIS 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 = @" IIS Metrics Exporter v1.0

IIS Metrics 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 'iis_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 ".iis_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 } }