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.
This commit is contained in:
@@ -0,0 +1,862 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user