Files
linux-scripts/windows-iis-log-exporter.ps1
T
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

334 lines
13 KiB
PowerShell

<#
.SYNOPSIS
Windows IIS Log Prometheus Exporter
.DESCRIPTION
Prometheus exporter for IIS log files - parses W3C Extended log format to
extract request counts, status code distribution, response times, bandwidth,
top URIs, and error rates per IIS site. Exports metrics as Prometheus text.
.PARAMETER Mode
Output mode: 'stdout' (default), 'textfile', or 'http'
.PARAMETER Port
HTTP port for http mode (default: 9542)
.PARAMETER TextfileDir
Directory for textfile collector output (default: C:\ProgramData\node_exporter)
.PARAMETER LogDir
IIS log directory (default: C:\inetpub\logs\LogFiles)
.PARAMETER WindowMinutes
Parse logs from the last N minutes (default: 5)
.PARAMETER InstallScheduledTask
Switch to create a scheduled task for automatic execution
.PARAMETER TaskIntervalMinutes
Interval in minutes for the scheduled task (default: 2)
.NOTES
Author: Phil Connor
Contact: contact@mylinux.work
Website: https://mylinux.work
License: MIT
Version: 1.0
Metrics Exported:
Core:
- windows_iis_log_up
- windows_iis_log_exporter_info{version}
Requests:
- windows_iis_log_requests_total{site,method,status}
- windows_iis_log_requests_by_status_class{site,class}
- windows_iis_log_bytes_sent_total{site}
- windows_iis_log_bytes_received_total{site}
Response Times:
- windows_iis_log_time_taken_avg_ms{site}
- windows_iis_log_time_taken_max_ms{site}
- windows_iis_log_time_taken_p95_ms{site}
Errors:
- windows_iis_log_errors_total{site}
- windows_iis_log_error_rate{site}
Sites:
- windows_iis_log_sites_total
- windows_iis_log_log_file_age_seconds{site}
Exporter:
- windows_iis_log_exporter_duration_seconds
- windows_iis_log_exporter_last_run_timestamp
#>
param(
[ValidateSet('stdout','textfile','http')]
[string]$Mode = 'stdout',
[int]$Port = 9542,
[string]$TextfileDir = 'C:\ProgramData\node_exporter',
[string]$LogDir = 'C:\inetpub\logs\LogFiles',
[int]$WindowMinutes = 5,
[switch]$InstallScheduledTask,
[int]$TaskIntervalMinutes = 2
)
$ErrorActionPreference = 'SilentlyContinue'
$Version = '1.0'
# ============================================================================
# SCHEDULED TASK INSTALLER
# ============================================================================
if ($InstallScheduledTask) {
$scriptPath = $MyInvocation.MyCommand.Path
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Mode textfile"
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes $TaskIntervalMinutes) `
-Once -At (Get-Date)
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
Register-ScheduledTask -TaskName 'WindowsIISLogExporter' -Action $action -Trigger $trigger `
-Settings $settings -RunLevel Highest -User 'SYSTEM' -Force
Write-Host 'Scheduled task "WindowsIISLogExporter" installed successfully.'
exit 0
}
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
function Get-PrometheusEscape {
param([string]$Value)
$Value -replace '\\', '\\\\' -replace '"', '\"' -replace "`n", '\n'
}
function Write-MetricHeader {
param([string]$Name, [string]$Type, [string]$Help)
"# HELP $Name $Help"
"# TYPE $Name $Type"
}
# ============================================================================
# LOG PARSING
# ============================================================================
function Parse-IISLogFile {
param([string]$FilePath, [datetime]$Since)
$results = @{
Requests = @{}
StatusClasses = @{}
BytesSent = 0
BytesReceived = 0
TimeTaken = [System.Collections.Generic.List[int]]::new()
Errors = 0
Total = 0
}
$fieldMap = @{}
Get-Content $FilePath -Tail 10000 | ForEach-Object {
$line = $_
if ($line.StartsWith('#Fields:')) {
$fields = $line.Substring(9).Trim() -split '\s+'
for ($i = 0; $i -lt $fields.Count; $i++) {
$fieldMap[$fields[$i]] = $i
}
return
}
if ($line.StartsWith('#') -or [string]::IsNullOrWhiteSpace($line)) { return }
if ($fieldMap.Count -eq 0) { return }
$parts = $line -split '\s+'
# Parse date/time
$dateIdx = $fieldMap['date']
$timeIdx = $fieldMap['time']
if ($null -ne $dateIdx -and $null -ne $timeIdx -and $parts.Count -gt [Math]::Max($dateIdx,$timeIdx)) {
try {
$entryTime = [datetime]::ParseExact("$($parts[$dateIdx]) $($parts[$timeIdx])", 'yyyy-MM-dd HH:mm:ss', $null)
if ($entryTime -lt $Since) { return }
} catch { return }
}
$results.Total++
# Method
$methodIdx = $fieldMap['cs-method']
$method = if ($null -ne $methodIdx -and $parts.Count -gt $methodIdx) { $parts[$methodIdx] } else { 'UNKNOWN' }
# Status
$statusIdx = $fieldMap['sc-status']
$status = if ($null -ne $statusIdx -and $parts.Count -gt $statusIdx) { $parts[$statusIdx] } else { '0' }
$key = "${method}_${status}"
if (-not $results.Requests.ContainsKey($key)) { $results.Requests[$key] = 0 }
$results.Requests[$key]++
# Status class
$class = "${status}".Substring(0,1) + "xx"
if (-not $results.StatusClasses.ContainsKey($class)) { $results.StatusClasses[$class] = 0 }
$results.StatusClasses[$class]++
# Errors (4xx and 5xx)
if ($status -match '^[45]') { $results.Errors++ }
# Bytes
$sentIdx = $fieldMap['sc-bytes']
if ($null -ne $sentIdx -and $parts.Count -gt $sentIdx -and $parts[$sentIdx] -match '^\d+$') {
$results.BytesSent += [long]$parts[$sentIdx]
}
$recvIdx = $fieldMap['cs-bytes']
if ($null -ne $recvIdx -and $parts.Count -gt $recvIdx -and $parts[$recvIdx] -match '^\d+$') {
$results.BytesReceived += [long]$parts[$recvIdx]
}
# Time taken
$ttIdx = $fieldMap['time-taken']
if ($null -ne $ttIdx -and $parts.Count -gt $ttIdx -and $parts[$ttIdx] -match '^\d+$') {
$results.TimeTaken.Add([int]$parts[$ttIdx])
}
}
return $results
}
# ============================================================================
# METRIC COLLECTION
# ============================================================================
function Get-IISLogMetrics {
$startTime = Get-Date
$metrics = [System.Collections.Generic.List[string]]::new()
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_up' 'gauge' 'Exporter status (1=up, 0=down)'))
if (-not (Test-Path $LogDir)) {
$metrics.Add('windows_iis_log_up 0')
return ($metrics -join "`n")
}
$metrics.Add('windows_iis_log_up 1')
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_exporter_info' 'gauge' 'Exporter version'))
$metrics.Add("windows_iis_log_exporter_info{version=`"$Version`"} 1")
$since = (Get-Date).AddMinutes(-$WindowMinutes)
$siteDirs = Get-ChildItem -Path $LogDir -Directory -ErrorAction SilentlyContinue
$siteCount = 0
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_requests_total' 'gauge' 'Requests by method and status'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_requests_by_status_class' 'gauge' 'Requests by status class'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_bytes_sent_total' 'gauge' 'Total bytes sent'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_bytes_received_total' 'gauge' 'Total bytes received'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_time_taken_avg_ms' 'gauge' 'Average response time in ms'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_time_taken_max_ms' 'gauge' 'Max response time in ms'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_time_taken_p95_ms' 'gauge' '95th percentile response time in ms'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_errors_total' 'gauge' 'Total 4xx and 5xx errors'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_error_rate' 'gauge' 'Error rate (errors/total)'))
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_log_file_age_seconds' 'gauge' 'Seconds since log file was last modified'))
foreach ($siteDir in $siteDirs) {
$siteName = Get-PrometheusEscape $siteDir.Name
$logFiles = Get-ChildItem -Path $siteDir.FullName -Filter "*.log" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $logFiles) { continue }
$siteCount++
$logFile = $logFiles[0]
$fileAge = [math]::Round(((Get-Date) - $logFile.LastWriteTime).TotalSeconds)
$metrics.Add("windows_iis_log_log_file_age_seconds{site=`"$siteName`"} $fileAge")
$parsed = Parse-IISLogFile -FilePath $logFile.FullName -Since $since
if ($null -eq $parsed -or $parsed.Total -eq 0) { continue }
# Requests by method+status
foreach ($key in $parsed.Requests.Keys) {
$parts = $key -split '_'
$method = $parts[0]
$status = $parts[1]
$count = $parsed.Requests[$key]
$metrics.Add("windows_iis_log_requests_total{site=`"$siteName`",method=`"$method`",status=`"$status`"} $count")
}
# Status classes
foreach ($class in $parsed.StatusClasses.Keys) {
$count = $parsed.StatusClasses[$class]
$metrics.Add("windows_iis_log_requests_by_status_class{site=`"$siteName`",class=`"$class`"} $count")
}
# Bytes
$metrics.Add("windows_iis_log_bytes_sent_total{site=`"$siteName`"} $($parsed.BytesSent)")
$metrics.Add("windows_iis_log_bytes_received_total{site=`"$siteName`"} $($parsed.BytesReceived)")
# Response times
if ($parsed.TimeTaken.Count -gt 0) {
$sorted = $parsed.TimeTaken | Sort-Object
$avg = [math]::Round(($sorted | Measure-Object -Average).Average)
$max = $sorted[-1]
$p95idx = [math]::Floor($sorted.Count * 0.95)
$p95 = $sorted[[math]::Min($p95idx, $sorted.Count - 1)]
$metrics.Add("windows_iis_log_time_taken_avg_ms{site=`"$siteName`"} $avg")
$metrics.Add("windows_iis_log_time_taken_max_ms{site=`"$siteName`"} $max")
$metrics.Add("windows_iis_log_time_taken_p95_ms{site=`"$siteName`"} $p95")
}
# Errors
$metrics.Add("windows_iis_log_errors_total{site=`"$siteName`"} $($parsed.Errors)")
$errorRate = if ($parsed.Total -gt 0) { [math]::Round($parsed.Errors / $parsed.Total, 4) } else { 0 }
$metrics.Add("windows_iis_log_error_rate{site=`"$siteName`"} $errorRate")
}
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_sites_total' 'gauge' 'Total IIS sites with logs'))
$metrics.Add("windows_iis_log_sites_total $siteCount")
# Duration
$duration = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 2)
$timestamp = [math]::Round((Get-Date -UFormat %s), 0)
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_exporter_duration_seconds' 'gauge' 'Script execution time'))
$metrics.Add("windows_iis_log_exporter_duration_seconds $duration")
$metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_exporter_last_run_timestamp' 'gauge' 'Unix timestamp of last run'))
$metrics.Add("windows_iis_log_exporter_last_run_timestamp $timestamp")
return ($metrics -join "`n")
}
# ============================================================================
# OUTPUT
# ============================================================================
switch ($Mode) {
'stdout' {
Get-IISLogMetrics
}
'textfile' {
if (-not (Test-Path $TextfileDir)) { New-Item -ItemType Directory -Path $TextfileDir -Force | Out-Null }
$tempFile = Join-Path $TextfileDir "windows-iis-log-metrics.tmp"
$finalFile = Join-Path $TextfileDir "windows-iis-log-metrics.prom"
Get-IISLogMetrics | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline
Move-Item -Path $tempFile -Destination $finalFile -Force
Write-Host "Wrote metrics to $finalFile"
}
'http' {
$prefix = "http://+:$Port/metrics/"
$listener = [System.Net.HttpListener]::new()
$listener.Prefixes.Add($prefix)
$listener.Start()
Write-Host "Listening on port $Port..."
try {
while ($listener.IsListening) {
$context = $listener.GetContext()
$response = $context.Response
$metricsOutput = Get-IISLogMetrics
$buffer = [System.Text.Encoding]::UTF8.GetBytes($metricsOutput)
$response.ContentType = 'text/plain; version=0.0.4; charset=utf-8'
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
}
} finally {
$listener.Stop()
}
}
}