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,749 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
MSSQL Prometheus Metrics Exporter
|
||||
.DESCRIPTION
|
||||
Prometheus exporter for Microsoft SQL Server. Queries DMVs (Dynamic Management
|
||||
Views) and system catalog views to collect instance health, database sizes,
|
||||
backup freshness, connection counts, wait statistics, buffer cache performance,
|
||||
SQL Agent job status, and TempDB usage. Outputs Prometheus-compatible text
|
||||
format for consumption by windows_exporter textfile collector.
|
||||
.PARAMETER Mode
|
||||
Output mode: 'stdout' (default), 'textfile', or 'http'
|
||||
.PARAMETER SqlInstance
|
||||
SQL Server instance name or hostname (default: localhost)
|
||||
.PARAMETER ConnectionString
|
||||
Full connection string (overrides SqlInstance)
|
||||
.PARAMETER TextfileDir
|
||||
Directory for textfile collector output
|
||||
.PARAMETER HttpPort
|
||||
HTTP port for http mode (default: 9399)
|
||||
.PARAMETER InstallScheduledTask
|
||||
Switch to create a scheduled task for periodic collection
|
||||
.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:
|
||||
Instance:
|
||||
- mssql_up
|
||||
- mssql_instance_uptime_seconds
|
||||
- mssql_version_info{version,edition}
|
||||
|
||||
Database Sizes:
|
||||
- mssql_database_size_bytes{database}
|
||||
- mssql_database_log_size_bytes{database}
|
||||
- mssql_database_log_usage_percent{database}
|
||||
|
||||
Backup Freshness:
|
||||
- mssql_backup_age_full_hours{database}
|
||||
- mssql_backup_age_diff_hours{database}
|
||||
|
||||
Connections:
|
||||
- mssql_connections_active{database}
|
||||
- mssql_sessions_total
|
||||
- mssql_sessions_blocked
|
||||
|
||||
Wait Statistics:
|
||||
- mssql_wait_time_seconds{wait_type}
|
||||
- mssql_waiting_tasks{wait_type}
|
||||
|
||||
Buffer Cache:
|
||||
- mssql_buffer_cache_hit_ratio
|
||||
- mssql_page_life_expectancy_seconds
|
||||
|
||||
SQL Agent Jobs:
|
||||
- mssql_agent_job_status{job}
|
||||
- mssql_agent_job_duration_seconds{job}
|
||||
|
||||
TempDB:
|
||||
- mssql_tempdb_size_bytes
|
||||
- mssql_tempdb_usage_bytes
|
||||
|
||||
Errors:
|
||||
- mssql_error_count
|
||||
|
||||
Exporter:
|
||||
- mssql_collector_duration_seconds
|
||||
#>
|
||||
|
||||
param(
|
||||
[ValidateSet('stdout', 'textfile', 'http')]
|
||||
[string]$Mode = 'stdout',
|
||||
|
||||
[string]$SqlInstance = 'localhost',
|
||||
|
||||
[string]$ConnectionString,
|
||||
|
||||
[string]$TextfileDir = 'C:\Program Files\windows_exporter\textfile_inputs',
|
||||
|
||||
[int]$HttpPort = 9399,
|
||||
|
||||
[switch]$InstallScheduledTask,
|
||||
|
||||
[int]$TaskIntervalMinutes = 5
|
||||
)
|
||||
|
||||
# Handle --textfile and --http as positional arguments
|
||||
if ($args -contains '--textfile') { $Mode = 'textfile' }
|
||||
if ($args -contains '--http') { $Mode = 'http' }
|
||||
|
||||
# ============================================================================
|
||||
# SCHEDULED TASK INSTALLATION
|
||||
# ============================================================================
|
||||
|
||||
if ($InstallScheduledTask) {
|
||||
$taskName = "MSSQLMetricsExporter"
|
||||
$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 MSSQL 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"
|
||||
}
|
||||
if ($Mode -eq 'stdout') { return }
|
||||
}
|
||||
|
||||
$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 Invoke-SqlQuery {
|
||||
param([string]$Query)
|
||||
try {
|
||||
if ($ConnectionString) {
|
||||
Invoke-Sqlcmd -ConnectionString $ConnectionString -Query $Query -ErrorAction Stop
|
||||
} else {
|
||||
Invoke-Sqlcmd -ServerInstance $SqlInstance -Query $Query -TrustServerCertificate -ErrorAction Stop
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "SQL query failed: $_"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Format-Label {
|
||||
param([string]$Value)
|
||||
$Value -replace '"', '' -replace '\\', '' -replace "`n", ' ' -replace "`r", ''
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# INSTANCE METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-InstanceMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
# Version info
|
||||
$version = Invoke-SqlQuery "SELECT SERVERPROPERTY('ProductVersion') AS Version, SERVERPROPERTY('Edition') AS Edition"
|
||||
if ($version) {
|
||||
$ver = Format-Label $version.Version
|
||||
$ed = Format-Label $version.Edition
|
||||
[void]$sb.AppendLine('# HELP mssql_version_info SQL Server version information (always 1)')
|
||||
[void]$sb.AppendLine('# TYPE mssql_version_info gauge')
|
||||
[void]$sb.AppendLine("mssql_version_info{version=`"$ver`",edition=`"$ed`"} 1")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
|
||||
# Uptime
|
||||
$uptime = Invoke-SqlQuery "SELECT DATEDIFF(SECOND, sqlserver_start_time, GETDATE()) AS uptime_seconds FROM sys.dm_os_sys_info"
|
||||
if ($uptime) {
|
||||
[void]$sb.AppendLine('# HELP mssql_instance_uptime_seconds Seconds since SQL Server instance started')
|
||||
[void]$sb.AppendLine('# TYPE mssql_instance_uptime_seconds gauge')
|
||||
[void]$sb.AppendLine("mssql_instance_uptime_seconds $($uptime.uptime_seconds)")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect instance metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# DATABASE SIZE METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-DatabaseSizeMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
$sizes = Invoke-SqlQuery "
|
||||
SELECT
|
||||
d.name AS database_name,
|
||||
SUM(CASE WHEN mf.type = 0 THEN mf.size END) * 8 * 1024 AS data_size_bytes,
|
||||
SUM(CASE WHEN mf.type = 1 THEN mf.size END) * 8 * 1024 AS log_size_bytes
|
||||
FROM sys.databases d
|
||||
JOIN sys.master_files mf ON d.database_id = mf.database_id
|
||||
WHERE d.state = 0
|
||||
GROUP BY d.name"
|
||||
|
||||
if ($sizes) {
|
||||
[void]$sb.AppendLine('# HELP mssql_database_size_bytes Data file size per database in bytes')
|
||||
[void]$sb.AppendLine('# TYPE mssql_database_size_bytes gauge')
|
||||
foreach ($row in $sizes) {
|
||||
$dbName = Format-Label $row.database_name
|
||||
$dataSize = if ($row.data_size_bytes) { $row.data_size_bytes } else { 0 }
|
||||
[void]$sb.AppendLine("mssql_database_size_bytes{database=`"$dbName`"} $dataSize")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
|
||||
[void]$sb.AppendLine('# HELP mssql_database_log_size_bytes Log file size per database in bytes')
|
||||
[void]$sb.AppendLine('# TYPE mssql_database_log_size_bytes gauge')
|
||||
foreach ($row in $sizes) {
|
||||
$dbName = Format-Label $row.database_name
|
||||
$logSize = if ($row.log_size_bytes) { $row.log_size_bytes } else { 0 }
|
||||
[void]$sb.AppendLine("mssql_database_log_size_bytes{database=`"$dbName`"} $logSize")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
|
||||
# Log usage percentage
|
||||
$logUsage = Invoke-SqlQuery "DBCC SQLPERF(LOGSPACE) WITH NO_INFOMSGS"
|
||||
if ($logUsage) {
|
||||
[void]$sb.AppendLine('# HELP mssql_database_log_usage_percent Log file usage percentage per database')
|
||||
[void]$sb.AppendLine('# TYPE mssql_database_log_usage_percent gauge')
|
||||
foreach ($row in $logUsage) {
|
||||
$dbName = Format-Label $row.'Database Name'
|
||||
$usage = Format-MetricValue $row.'Log Space Used (%)'
|
||||
[void]$sb.AppendLine("mssql_database_log_usage_percent{database=`"$dbName`"} $usage")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect database size metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# BACKUP FRESHNESS METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-BackupMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
# Full backup age
|
||||
$fullBackups = Invoke-SqlQuery "
|
||||
SELECT
|
||||
d.name AS database_name,
|
||||
CASE
|
||||
WHEN MAX(b.backup_finish_date) IS NULL THEN -1
|
||||
ELSE DATEDIFF(HOUR, MAX(b.backup_finish_date), GETDATE())
|
||||
END AS hours_since_backup
|
||||
FROM sys.databases d
|
||||
LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
|
||||
WHERE d.database_id > 4 AND d.state = 0
|
||||
GROUP BY d.name"
|
||||
|
||||
if ($fullBackups) {
|
||||
[void]$sb.AppendLine('# HELP mssql_backup_age_full_hours Hours since last full backup per database (-1 = no backup)')
|
||||
[void]$sb.AppendLine('# TYPE mssql_backup_age_full_hours gauge')
|
||||
foreach ($row in $fullBackups) {
|
||||
$dbName = Format-Label $row.database_name
|
||||
[void]$sb.AppendLine("mssql_backup_age_full_hours{database=`"$dbName`"} $($row.hours_since_backup)")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
|
||||
# Differential backup age
|
||||
$diffBackups = Invoke-SqlQuery "
|
||||
SELECT
|
||||
d.name AS database_name,
|
||||
CASE
|
||||
WHEN MAX(b.backup_finish_date) IS NULL THEN -1
|
||||
ELSE DATEDIFF(HOUR, MAX(b.backup_finish_date), GETDATE())
|
||||
END AS hours_since_backup
|
||||
FROM sys.databases d
|
||||
LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'I'
|
||||
WHERE d.database_id > 4 AND d.state = 0
|
||||
GROUP BY d.name"
|
||||
|
||||
if ($diffBackups) {
|
||||
[void]$sb.AppendLine('# HELP mssql_backup_age_diff_hours Hours since last differential backup per database (-1 = no backup)')
|
||||
[void]$sb.AppendLine('# TYPE mssql_backup_age_diff_hours gauge')
|
||||
foreach ($row in $diffBackups) {
|
||||
$dbName = Format-Label $row.database_name
|
||||
[void]$sb.AppendLine("mssql_backup_age_diff_hours{database=`"$dbName`"} $($row.hours_since_backup)")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect backup metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# CONNECTION METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-ConnectionMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
# Active connections per database
|
||||
$connections = Invoke-SqlQuery "
|
||||
SELECT DB_NAME(database_id) AS database_name, COUNT(*) AS active_connections
|
||||
FROM sys.dm_exec_sessions
|
||||
WHERE is_user_process = 1 AND database_id > 0
|
||||
GROUP BY database_id"
|
||||
|
||||
if ($connections) {
|
||||
[void]$sb.AppendLine('# HELP mssql_connections_active Active connections per database')
|
||||
[void]$sb.AppendLine('# TYPE mssql_connections_active gauge')
|
||||
foreach ($row in $connections) {
|
||||
$dbName = Format-Label $row.database_name
|
||||
[void]$sb.AppendLine("mssql_connections_active{database=`"$dbName`"} $($row.active_connections)")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
|
||||
# Total active sessions
|
||||
$totalSessions = Invoke-SqlQuery "SELECT COUNT(*) AS total FROM sys.dm_exec_sessions WHERE is_user_process = 1"
|
||||
if ($totalSessions) {
|
||||
[void]$sb.AppendLine('# HELP mssql_sessions_total Total active user sessions')
|
||||
[void]$sb.AppendLine('# TYPE mssql_sessions_total gauge')
|
||||
[void]$sb.AppendLine("mssql_sessions_total $($totalSessions.total)")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
|
||||
# Blocked sessions
|
||||
$blocked = Invoke-SqlQuery "SELECT COUNT(*) AS blocked FROM sys.dm_exec_requests WHERE blocking_session_id > 0"
|
||||
if ($blocked) {
|
||||
[void]$sb.AppendLine('# HELP mssql_sessions_blocked Currently blocked sessions')
|
||||
[void]$sb.AppendLine('# TYPE mssql_sessions_blocked gauge')
|
||||
[void]$sb.AppendLine("mssql_sessions_blocked $($blocked.blocked)")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect connection metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# WAIT STATISTICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-WaitMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
$waits = Invoke-SqlQuery "
|
||||
SELECT TOP 10
|
||||
wait_type,
|
||||
wait_time_ms / 1000.0 AS wait_time_seconds,
|
||||
waiting_tasks_count
|
||||
FROM sys.dm_os_wait_stats
|
||||
WHERE wait_type NOT IN (
|
||||
'SLEEP_TASK', 'BROKER_TO_FLUSH', 'SQLTRACE_BUFFER_FLUSH',
|
||||
'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT', 'LAZYWRITER_SLEEP',
|
||||
'CHECKPOINT_QUEUE', 'WAITFOR', 'XE_TIMER_EVENT',
|
||||
'BROKER_EVENTHANDLER', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
|
||||
'XE_DISPATCHER_WAIT', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
|
||||
'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'DIRTY_PAGE_POLL',
|
||||
'SP_SERVER_DIAGNOSTICS_SLEEP', 'BROKER_TASK_STOP',
|
||||
'HADR_LOGCAPTURE_WAIT', 'ONDEMAND_TASK_QUEUE',
|
||||
'DBMIRROR_EVENTS_QUEUE', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',
|
||||
'QDS_ASYNC_QUEUE', 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
|
||||
'DISPATCHER_QUEUE_SEMAPHORE', 'REQUEST_FOR_DEADLOCK_SEARCH',
|
||||
'HADR_TIMER_TASK', 'BROKER_RECEIVE_WAITFOR',
|
||||
'PREEMPTIVE_XE_GETTARGETSTATE', 'PREEMPTIVE_XE_SESSIONCOMMIT',
|
||||
'SLEEP_BPOOL_FLUSH', 'SLEEP_DBSTARTUP', 'SLEEP_DCOMSTARTUP',
|
||||
'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY', 'SLEEP_MASTERUPGRADED',
|
||||
'SLEEP_MSDBSTARTUP', 'SLEEP_SYSTEMTASK', 'SLEEP_TEMPDBSTARTUP',
|
||||
'SNI_HTTP_ACCEPT', 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG'
|
||||
)
|
||||
AND wait_time_ms > 0
|
||||
ORDER BY wait_time_ms DESC"
|
||||
|
||||
if ($waits) {
|
||||
[void]$sb.AppendLine('# HELP mssql_wait_time_seconds Cumulative wait time by wait type')
|
||||
[void]$sb.AppendLine('# TYPE mssql_wait_time_seconds gauge')
|
||||
foreach ($row in $waits) {
|
||||
$waitType = Format-Label $row.wait_type
|
||||
$waitTime = Format-MetricValue $row.wait_time_seconds
|
||||
[void]$sb.AppendLine("mssql_wait_time_seconds{wait_type=`"$waitType`"} $waitTime")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
|
||||
[void]$sb.AppendLine('# HELP mssql_waiting_tasks Waiting task count by wait type')
|
||||
[void]$sb.AppendLine('# TYPE mssql_waiting_tasks gauge')
|
||||
foreach ($row in $waits) {
|
||||
$waitType = Format-Label $row.wait_type
|
||||
[void]$sb.AppendLine("mssql_waiting_tasks{wait_type=`"$waitType`"} $($row.waiting_tasks_count)")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect wait metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# BUFFER CACHE METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-BufferCacheMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
# Buffer cache hit ratio
|
||||
$hitRatio = Invoke-SqlQuery "
|
||||
SELECT
|
||||
CAST(
|
||||
(SELECT cntr_value FROM sys.dm_os_performance_counters
|
||||
WHERE counter_name = 'Buffer cache hit ratio'
|
||||
AND object_name LIKE '%Buffer Manager%') * 100.0
|
||||
/
|
||||
NULLIF((SELECT cntr_value FROM sys.dm_os_performance_counters
|
||||
WHERE counter_name = 'Buffer cache hit ratio base'
|
||||
AND object_name LIKE '%Buffer Manager%'), 0)
|
||||
AS DECIMAL(5,2)) AS hit_ratio"
|
||||
|
||||
if ($hitRatio -and $hitRatio.hit_ratio) {
|
||||
[void]$sb.AppendLine('# HELP mssql_buffer_cache_hit_ratio Buffer cache hit ratio (0-100)')
|
||||
[void]$sb.AppendLine('# TYPE mssql_buffer_cache_hit_ratio gauge')
|
||||
[void]$sb.AppendLine("mssql_buffer_cache_hit_ratio $(Format-MetricValue $hitRatio.hit_ratio)")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
|
||||
# Page life expectancy
|
||||
$ple = Invoke-SqlQuery "
|
||||
SELECT cntr_value AS ple_seconds
|
||||
FROM sys.dm_os_performance_counters
|
||||
WHERE counter_name = 'Page life expectancy'
|
||||
AND object_name LIKE '%Buffer Manager%'"
|
||||
|
||||
if ($ple) {
|
||||
[void]$sb.AppendLine('# HELP mssql_page_life_expectancy_seconds Page life expectancy in seconds')
|
||||
[void]$sb.AppendLine('# TYPE mssql_page_life_expectancy_seconds gauge')
|
||||
[void]$sb.AppendLine("mssql_page_life_expectancy_seconds $($ple.ple_seconds)")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect buffer cache metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# SQL AGENT JOB METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-AgentJobMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
$jobs = Invoke-SqlQuery "
|
||||
SELECT
|
||||
j.name AS job_name,
|
||||
CASE h.run_status
|
||||
WHEN 1 THEN 1
|
||||
ELSE 0
|
||||
END AS last_run_succeeded,
|
||||
CASE
|
||||
WHEN h.run_duration IS NULL THEN 0
|
||||
ELSE (h.run_duration / 10000) * 3600
|
||||
+ ((h.run_duration / 100) % 100) * 60
|
||||
+ (h.run_duration % 100)
|
||||
END AS duration_seconds
|
||||
FROM msdb.dbo.sysjobs j
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 run_status, run_duration
|
||||
FROM msdb.dbo.sysjobhistory
|
||||
WHERE job_id = j.job_id AND step_id = 0
|
||||
ORDER BY run_date DESC, run_time DESC
|
||||
) h
|
||||
WHERE j.enabled = 1"
|
||||
|
||||
if ($jobs) {
|
||||
[void]$sb.AppendLine('# HELP mssql_agent_job_status Last run outcome per job (1=success, 0=fail)')
|
||||
[void]$sb.AppendLine('# TYPE mssql_agent_job_status gauge')
|
||||
foreach ($row in $jobs) {
|
||||
$jobName = Format-Label $row.job_name
|
||||
[void]$sb.AppendLine("mssql_agent_job_status{job=`"$jobName`"} $($row.last_run_succeeded)")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
|
||||
[void]$sb.AppendLine('# HELP mssql_agent_job_duration_seconds Last run duration per job in seconds')
|
||||
[void]$sb.AppendLine('# TYPE mssql_agent_job_duration_seconds gauge')
|
||||
foreach ($row in $jobs) {
|
||||
$jobName = Format-Label $row.job_name
|
||||
[void]$sb.AppendLine("mssql_agent_job_duration_seconds{job=`"$jobName`"} $($row.duration_seconds)")
|
||||
}
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect SQL Agent job metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# TEMPDB METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-TempDbMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
$tempdb = Invoke-SqlQuery "
|
||||
SELECT
|
||||
SUM(size) * 8 * 1024 AS total_size_bytes,
|
||||
SUM(FILEPROPERTY(name, 'SpaceUsed')) * 8 * 1024 AS used_bytes
|
||||
FROM tempdb.sys.database_files
|
||||
WHERE type = 0"
|
||||
|
||||
if ($tempdb) {
|
||||
[void]$sb.AppendLine('# HELP mssql_tempdb_size_bytes TempDB total data file size in bytes')
|
||||
[void]$sb.AppendLine('# TYPE mssql_tempdb_size_bytes gauge')
|
||||
$totalSize = if ($tempdb.total_size_bytes) { $tempdb.total_size_bytes } else { 0 }
|
||||
[void]$sb.AppendLine("mssql_tempdb_size_bytes $totalSize")
|
||||
[void]$sb.AppendLine('')
|
||||
|
||||
[void]$sb.AppendLine('# HELP mssql_tempdb_usage_bytes TempDB space used in bytes')
|
||||
[void]$sb.AppendLine('# TYPE mssql_tempdb_usage_bytes gauge')
|
||||
$usedBytes = if ($tempdb.used_bytes) { $tempdb.used_bytes } else { 0 }
|
||||
[void]$sb.AppendLine("mssql_tempdb_usage_bytes $usedBytes")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect TempDB metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# ERROR COUNT
|
||||
# ============================================================================
|
||||
|
||||
function Get-ErrorMetrics {
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
try {
|
||||
$errors = Invoke-SqlQuery "
|
||||
SELECT COUNT(*) AS error_count
|
||||
FROM sys.dm_exec_requests
|
||||
WHERE status = 'running' AND total_elapsed_time > 0
|
||||
AND sql_handle IS NOT NULL"
|
||||
|
||||
[void]$sb.AppendLine('# HELP mssql_error_count Active error count from current requests')
|
||||
[void]$sb.AppendLine('# TYPE mssql_error_count gauge')
|
||||
$count = if ($errors -and $errors.error_count) { $errors.error_count } else { 0 }
|
||||
[void]$sb.AppendLine("mssql_error_count $count")
|
||||
[void]$sb.AppendLine('')
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect error metrics: $_"
|
||||
}
|
||||
|
||||
$sb.ToString()
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# COLLECT ALL METRICS
|
||||
# ============================================================================
|
||||
|
||||
function Get-AllMetrics {
|
||||
$scriptStart = Get-Date
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
|
||||
# Test connectivity
|
||||
$testResult = Invoke-SqlQuery "SELECT 1 AS connected"
|
||||
$isUp = if ($testResult) { 1 } else { 0 }
|
||||
|
||||
[void]$sb.AppendLine('# HELP mssql_up SQL Server reachable (1=up, 0=down)')
|
||||
[void]$sb.AppendLine('# TYPE mssql_up gauge')
|
||||
[void]$sb.AppendLine("mssql_up $isUp")
|
||||
[void]$sb.AppendLine('')
|
||||
|
||||
if ($isUp -eq 1) {
|
||||
[void]$sb.Append((Get-InstanceMetrics))
|
||||
[void]$sb.Append((Get-DatabaseSizeMetrics))
|
||||
[void]$sb.Append((Get-BackupMetrics))
|
||||
[void]$sb.Append((Get-ConnectionMetrics))
|
||||
[void]$sb.Append((Get-WaitMetrics))
|
||||
[void]$sb.Append((Get-BufferCacheMetrics))
|
||||
[void]$sb.Append((Get-AgentJobMetrics))
|
||||
[void]$sb.Append((Get-TempDbMetrics))
|
||||
[void]$sb.Append((Get-ErrorMetrics))
|
||||
}
|
||||
|
||||
# Collector duration
|
||||
$scriptEnd = Get-Date
|
||||
$duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds
|
||||
|
||||
[void]$sb.AppendLine('# HELP mssql_collector_duration_seconds Time to generate all metrics')
|
||||
[void]$sb.AppendLine('# TYPE mssql_collector_duration_seconds gauge')
|
||||
[void]$sb.AppendLine("mssql_collector_duration_seconds $duration")
|
||||
[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 MSSQL 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>MSSQL Metrics Exporter v1.0</title></head>
|
||||
<body>
|
||||
<h1>MSSQL Metrics Exporter v1.0</h1>
|
||||
<p><a href="/metrics">Metrics</a></p>
|
||||
<h2>Sections</h2>
|
||||
<ul>
|
||||
<li>Instance health (version, uptime)</li>
|
||||
<li>Database sizes (data files, log files, log usage)</li>
|
||||
<li>Backup freshness (full, differential)</li>
|
||||
<li>Connections and sessions (active, blocked)</li>
|
||||
<li>Wait statistics (top 10 waits)</li>
|
||||
<li>Buffer cache (hit ratio, page life expectancy)</li>
|
||||
<li>SQL Agent jobs (status, duration)</li>
|
||||
<li>TempDB (size, usage)</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 $HttpPort
|
||||
}
|
||||
'textfile' {
|
||||
$outputFile = Join-Path $TextfileDir 'mssql_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 ".mssql_metrics.$PID.tmp"
|
||||
|
||||
try {
|
||||
$metrics = Get-AllMetrics
|
||||
$metrics | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline
|
||||
|
||||
$lineCount = ($metrics -split "`n").Count
|
||||
if ($lineCount -lt 5) {
|
||||
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