a1a17e81a1
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.
334 lines
13 KiB
PowerShell
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()
|
|
}
|
|
}
|
|
}
|