Files
linux-scripts/iis-metrics-exporter.ps1
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

863 lines
34 KiB
PowerShell

<#
.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 = @"
<!DOCTYPE html>
<html>
<head><title>IIS Metrics Exporter v1.0</title></head>
<body>
<h1>IIS Metrics Exporter v1.0</h1>
<p><a href="/metrics">Metrics</a></p>
<h2>Sections</h2>
<ul>
<li>Site status and bindings</li>
<li>Application pool state and worker process count</li>
<li>Request throughput (connections, requests/sec, bytes)</li>
<li>HTTP method counts (GET, POST, PUT, DELETE)</li>
<li>HTTP status codes (2xx, 3xx, 4xx, 5xx from log analysis)</li>
<li>SSL/TLS connections and handshake failures</li>
<li>Worker process CPU, memory, active requests, uptime</li>
<li>Cache hit/miss ratios (output, URI, kernel)</li>
<li>Failed requests (500 errors from log analysis)</li>
</ul>
</body>
</html>
"@
$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
}
}