<# .SYNOPSIS SMART Drive Health Prometheus Metrics Exporter (Windows) .DESCRIPTION Prometheus exporter for SMART drive health metrics on Windows -- reads SMART attributes from SATA and NVMe drives using smartctl (smartmontools) and exports temperature, reallocated sectors, pending sectors, uncorrectable errors, power-on hours, wear leveling, NVMe health, and overall drive health status as Prometheus-compatible text format. Outputs identical smart_drive_* metrics as the Linux bash version so the same Grafana dashboard works for both platforms. .PARAMETER Mode Output mode: 'stdout' (default), 'textfile', or 'http' .PARAMETER Port HTTP port for http mode (default: 9198) .PARAMETER TextfileDir Directory for textfile collector output (default: C:\ProgramData\windows_exporter\textfile_inputs) .PARAMETER SmartctlPath Path to smartctl.exe (default: C:\Program Files\smartmontools\bin\smartctl.exe) .PARAMETER Devices Comma-separated device list, e.g. '/dev/sda,/dev/nvme0' or 'auto' (default: auto) .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 Prerequisites: - smartmontools for Windows (https://www.smartmontools.org/) - Administrator privileges for smartctl access - windows_exporter for textfile collector mode Metrics Exported: Core Status: - smart_drive_up - smart_drive_exporter_info{version} Drive Health: - smart_drive_health_ok{device,model,serial,type} - smart_drive_temperature_celsius{device,model,serial} - smart_drive_power_on_hours{device,model,serial} - smart_drive_power_cycle_count{device,model,serial} - smart_drive_capacity_bytes{device,model,serial} SATA Attributes: - smart_drive_reallocated_sectors{device,model,serial} - smart_drive_pending_sectors{device,model,serial} - smart_drive_uncorrectable_errors{device,model,serial} - smart_drive_spin_retry_count{device,model,serial} - smart_drive_command_timeout{device,model,serial} - smart_drive_start_stop_count{device,model,serial} - smart_drive_wear_leveling_count{device,model,serial} - smart_drive_interface_speed{device,model,serial,speed} NVMe Attributes: - smart_drive_percentage_used{device,model,serial} - smart_drive_available_spare{device,model,serial} - smart_drive_available_spare_threshold{device,model,serial} - smart_drive_media_errors{device,model,serial} - smart_drive_critical_warning{device,model,serial} Exporter: - smart_drive_exporter_duration_seconds - smart_drive_exporter_last_run_timestamp - smart_drive_devices_total #> param( [ValidateSet('stdout', 'textfile', 'http')] [string]$Mode = 'stdout', [int]$Port = 9198, [string]$TextfileDir = 'C:\ProgramData\windows_exporter\textfile_inputs', [string]$SmartctlPath = 'C:\Program Files\smartmontools\bin\smartctl.exe', [string]$Devices = 'auto', [switch]$InstallScheduledTask, [int]$TaskIntervalMinutes = 5 ) # ============================================================================ # SCHEDULED TASK INSTALLATION # ============================================================================ if ($InstallScheduledTask) { $taskName = "SmartDriveExporter" $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 SMART drive health 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 scheduled 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-CleanLabelValue { param([string]$Value, [int]$MaxLength = 120) if (-not $Value) { return '' } $Value = $Value -replace '\\', '\\\\' $Value = $Value -replace '"', '\"' $Value = $Value -replace "`n", '\n' if ($Value.Length -gt $MaxLength) { $Value = $Value.Substring(0, $MaxLength) } return $Value } # ============================================================================ # DRIVE DETECTION # ============================================================================ function Get-DriveList { if ($Devices -ne 'auto') { return $Devices -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } } $scanOutput = & $SmartctlPath --scan 2>$null if (-not $scanOutput) { return @() } $drives = @() foreach ($line in $scanOutput) { if ($line -match '^(\S+)') { $drives += $Matches[1] } } return $drives } # ============================================================================ # DRIVE DATA PARSING # ============================================================================ function Get-DriveData { param([string]$Device) $rawOutput = & $SmartctlPath -iHA $Device 2>$null if (-not $rawOutput) { return $null } $data = @{ Device = $Device Model = '' Serial = '' Type = 'unknown' Health = -1 CapacityBytes = 0 Temperature = -1 PowerOnHours = -1 PowerCycleCount = -1 ReallocatedSectors = -1 PendingSectors = -1 Uncorrectable = -1 SpinRetry = -1 CommandTimeout = -1 StartStop = -1 WearLeveling = -1 NvmePctUsed = -1 NvmeSpare = -1 NvmeSpareThresh = -1 NvmeMediaErrors = -1 NvmeCriticalWarn = -1 SataSpeed = '' } $inSmartAttrs = $false $inNvmeHealth = $false foreach ($line in $rawOutput) { # Drive info if ($line -match '^Device Model:\s+(.+)$') { $data.Model = $Matches[1].Trim() } if ($line -match '^Model Number:\s+(.+)$') { $data.Model = $Matches[1].Trim() $data.Type = 'nvme' } if ($line -match '^Serial Number:\s+(.+)$') { $data.Serial = $Matches[1].Trim() } if ($line -match '^User Capacity:\s+([\d,]+)\s+bytes') { $data.CapacityBytes = [long]($Matches[1] -replace ',', '') } if ($line -match '^Total NVM Capacity:\s+([\d,]+)\s+bytes' -or $line -match '^Namespace 1 Size/Capacity:\s+([\d,]+)\s+bytes') { $data.CapacityBytes = [long]($Matches[1] -replace ',', '') } if ($line -match '^SATA Version is:.*?([\d.]+\s+Gb/s)') { $data.SataSpeed = $Matches[1] } if ($line -match '^Rotation Rate:') { if ($line -match 'Solid State') { $data.Type = 'ssd' } elseif ($line -match '\d+\s+rpm') { $data.Type = 'hdd' } } # Health status if ($line -match 'SMART overall-health self-assessment test result:\s+(\S+)') { $data.Health = if ($Matches[1] -eq 'PASSED') { 1 } else { 0 } } if ($line -match 'SMART Health Status:\s+(\S+)') { $data.Health = if ($Matches[1] -eq 'OK') { 1 } else { 0 } } # SATA SMART attributes table if ($line -match '^ID#\s+ATTRIBUTE_NAME') { $inSmartAttrs = $true continue } if ($inSmartAttrs -and $line -match '^\s*$') { $inSmartAttrs = $false } if ($inSmartAttrs -and $line -match '^\s*(\d+)\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+)') { $attrId = [int]$Matches[1] $rawVal = $Matches[2] # Extract first number from raw value (handles "35 (Min/Max 22/42)") $numVal = 0 if ($rawVal -match '^(\d+)') { $numVal = [long]$Matches[1] } switch ($attrId) { 4 { $data.StartStop = $numVal } 5 { $data.ReallocatedSectors = $numVal } 9 { $data.PowerOnHours = $numVal } 10 { $data.SpinRetry = $numVal } 12 { $data.PowerCycleCount = $numVal } 177 { $data.WearLeveling = $numVal } 188 { $data.CommandTimeout = $numVal } 190 { if ($data.Temperature -lt 0) { $data.Temperature = $numVal } } 194 { $data.Temperature = $numVal } 197 { $data.PendingSectors = $numVal } 198 { $data.Uncorrectable = $numVal } 233 { if ($data.WearLeveling -lt 0) { $data.WearLeveling = $numVal } } } } # NVMe SMART/Health Information if ($line -match '^SMART/Health Information') { $inNvmeHealth = $true continue } if ($inNvmeHealth -and $line -match '^\s*$') { $inNvmeHealth = $false } if ($inNvmeHealth) { if ($line -match '^Temperature:\s+(\d+)') { $data.Temperature = [int]$Matches[1] } if ($line -match '^Percentage Used:\s+(\d+)%') { $data.NvmePctUsed = [int]$Matches[1] } if ($line -match '^Available Spare:\s+(\d+)%' -and $line -notmatch 'Threshold') { $data.NvmeSpare = [int]$Matches[1] } if ($line -match '^Available Spare Threshold:\s+(\d+)%') { $data.NvmeSpareThresh = [int]$Matches[1] } if ($line -match '^Power On Hours:\s+([\d,]+)') { $data.PowerOnHours = [long]($Matches[1] -replace ',', '') } if ($line -match '^Power Cycles:\s+([\d,]+)') { $data.PowerCycleCount = [long]($Matches[1] -replace ',', '') } if ($line -match '^Media and Data Integrity Errors:\s+([\d,]+)') { $data.NvmeMediaErrors = [long]($Matches[1] -replace ',', '') } if ($line -match '^Critical Warning:\s+(\S+)') { $val = $Matches[1] if ($val -match '^0x') { $data.NvmeCriticalWarn = [Convert]::ToInt32($val, 16) } else { $data.NvmeCriticalWarn = [int]$val } } } } # Fix type for NVMe if not already detected if ($data.Type -eq 'unknown' -and $data.NvmePctUsed -ge 0) { $data.Type = 'nvme' } if ($data.Type -eq 'unknown') { $data.Type = 'sata' } return $data } # ============================================================================ # METRICS GENERATION # ============================================================================ function Get-AllMetrics { $scriptStart = Get-Date $sb = [System.Text.StringBuilder]::new() # ======================================================================== # Check smartctl availability # ======================================================================== if (-not (Test-Path $SmartctlPath)) { [void]$sb.AppendLine('# HELP smart_drive_up Exporter status (1=up, 0=down)') [void]$sb.AppendLine('# TYPE smart_drive_up gauge') [void]$sb.AppendLine('smart_drive_up 0') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP smart_drive_exporter_info Exporter version information') [void]$sb.AppendLine('# TYPE smart_drive_exporter_info gauge') [void]$sb.AppendLine('smart_drive_exporter_info{version="1.0"} 1') [void]$sb.AppendLine('') return $sb.ToString() } # ======================================================================== # Exporter Status # ======================================================================== [void]$sb.AppendLine('# HELP smart_drive_up Exporter status (1=up, 0=down)') [void]$sb.AppendLine('# TYPE smart_drive_up gauge') [void]$sb.AppendLine('smart_drive_up 1') [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP smart_drive_exporter_info Exporter version information') [void]$sb.AppendLine('# TYPE smart_drive_exporter_info gauge') [void]$sb.AppendLine('smart_drive_exporter_info{version="1.0"} 1') [void]$sb.AppendLine('') # ======================================================================== # Drive Detection & Parsing # ======================================================================== $driveList = Get-DriveList if (-not $driveList -or $driveList.Count -eq 0) { [void]$sb.AppendLine('# HELP smart_drive_devices_total Total drives detected') [void]$sb.AppendLine('# TYPE smart_drive_devices_total gauge') [void]$sb.AppendLine('smart_drive_devices_total 0') [void]$sb.AppendLine('') } else { # Collect per-drive data $allDrives = @() foreach ($device in $driveList) { $driveData = Get-DriveData -Device $device if ($driveData) { $allDrives += $driveData } } [void]$sb.AppendLine('# HELP smart_drive_devices_total Total drives detected') [void]$sb.AppendLine('# TYPE smart_drive_devices_total gauge') [void]$sb.AppendLine("smart_drive_devices_total $($allDrives.Count)") [void]$sb.AppendLine('') # ================================================================ # Health Status # ================================================================ $healthLines = @() foreach ($d in $allDrives) { if ($d.Health -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $healthLines += "smart_drive_health_ok{device=`"$dev`",model=`"$model`",serial=`"$serial`",type=`"$($d.Type)`"} $($d.Health)" } } if ($healthLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_health_ok SMART health status (1=passed, 0=failed)') [void]$sb.AppendLine('# TYPE smart_drive_health_ok gauge') foreach ($line in $healthLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Temperature # ================================================================ $tempLines = @() foreach ($d in $allDrives) { if ($d.Temperature -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $tempLines += "smart_drive_temperature_celsius{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.Temperature)" } } if ($tempLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_temperature_celsius Current drive temperature in Celsius') [void]$sb.AppendLine('# TYPE smart_drive_temperature_celsius gauge') foreach ($line in $tempLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Power-On Hours # ================================================================ $pohLines = @() foreach ($d in $allDrives) { if ($d.PowerOnHours -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $pohLines += "smart_drive_power_on_hours{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.PowerOnHours)" } } if ($pohLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_power_on_hours Total power-on hours') [void]$sb.AppendLine('# TYPE smart_drive_power_on_hours gauge') foreach ($line in $pohLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Power Cycle Count # ================================================================ $pccLines = @() foreach ($d in $allDrives) { if ($d.PowerCycleCount -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $pccLines += "smart_drive_power_cycle_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.PowerCycleCount)" } } if ($pccLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_power_cycle_count Total power cycle count') [void]$sb.AppendLine('# TYPE smart_drive_power_cycle_count gauge') foreach ($line in $pccLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Capacity # ================================================================ $capLines = @() foreach ($d in $allDrives) { if ($d.CapacityBytes -gt 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $capLines += "smart_drive_capacity_bytes{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.CapacityBytes)" } } if ($capLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_capacity_bytes Drive capacity in bytes') [void]$sb.AppendLine('# TYPE smart_drive_capacity_bytes gauge') foreach ($line in $capLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Reallocated Sectors # ================================================================ $reallocLines = @() foreach ($d in $allDrives) { if ($d.ReallocatedSectors -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $reallocLines += "smart_drive_reallocated_sectors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.ReallocatedSectors)" } } if ($reallocLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_reallocated_sectors Reallocated sector count') [void]$sb.AppendLine('# TYPE smart_drive_reallocated_sectors gauge') foreach ($line in $reallocLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Pending Sectors # ================================================================ $pendingLines = @() foreach ($d in $allDrives) { if ($d.PendingSectors -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $pendingLines += "smart_drive_pending_sectors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.PendingSectors)" } } if ($pendingLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_pending_sectors Current pending sector count') [void]$sb.AppendLine('# TYPE smart_drive_pending_sectors gauge') foreach ($line in $pendingLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Uncorrectable Errors # ================================================================ $uncorrLines = @() foreach ($d in $allDrives) { if ($d.Uncorrectable -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $uncorrLines += "smart_drive_uncorrectable_errors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.Uncorrectable)" } } if ($uncorrLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_uncorrectable_errors Offline uncorrectable error count') [void]$sb.AppendLine('# TYPE smart_drive_uncorrectable_errors gauge') foreach ($line in $uncorrLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Spin Retry Count # ================================================================ $spinLines = @() foreach ($d in $allDrives) { if ($d.SpinRetry -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $spinLines += "smart_drive_spin_retry_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.SpinRetry)" } } if ($spinLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_spin_retry_count Spin retry count') [void]$sb.AppendLine('# TYPE smart_drive_spin_retry_count gauge') foreach ($line in $spinLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Command Timeout # ================================================================ $cmdtoLines = @() foreach ($d in $allDrives) { if ($d.CommandTimeout -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $cmdtoLines += "smart_drive_command_timeout{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.CommandTimeout)" } } if ($cmdtoLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_command_timeout Command timeout count') [void]$sb.AppendLine('# TYPE smart_drive_command_timeout gauge') foreach ($line in $cmdtoLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Start/Stop Count # ================================================================ $ssLines = @() foreach ($d in $allDrives) { if ($d.StartStop -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $ssLines += "smart_drive_start_stop_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.StartStop)" } } if ($ssLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_start_stop_count Start/stop count') [void]$sb.AppendLine('# TYPE smart_drive_start_stop_count gauge') foreach ($line in $ssLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Wear Leveling Count # ================================================================ $wearLines = @() foreach ($d in $allDrives) { if ($d.WearLeveling -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $wearLines += "smart_drive_wear_leveling_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.WearLeveling)" } } if ($wearLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_wear_leveling_count SSD wear leveling count') [void]$sb.AppendLine('# TYPE smart_drive_wear_leveling_count gauge') foreach ($line in $wearLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # NVMe Percentage Used # ================================================================ $nvmePctLines = @() foreach ($d in $allDrives) { if ($d.NvmePctUsed -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $nvmePctLines += "smart_drive_percentage_used{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmePctUsed)" } } if ($nvmePctLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_percentage_used NVMe percentage used estimate') [void]$sb.AppendLine('# TYPE smart_drive_percentage_used gauge') foreach ($line in $nvmePctLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # NVMe Available Spare # ================================================================ $nvmeSpareLines = @() foreach ($d in $allDrives) { if ($d.NvmeSpare -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $nvmeSpareLines += "smart_drive_available_spare{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeSpare)" } } if ($nvmeSpareLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_available_spare NVMe available spare percentage') [void]$sb.AppendLine('# TYPE smart_drive_available_spare gauge') foreach ($line in $nvmeSpareLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # NVMe Available Spare Threshold # ================================================================ $nvmeThreshLines = @() foreach ($d in $allDrives) { if ($d.NvmeSpareThresh -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $nvmeThreshLines += "smart_drive_available_spare_threshold{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeSpareThresh)" } } if ($nvmeThreshLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_available_spare_threshold NVMe available spare threshold percentage') [void]$sb.AppendLine('# TYPE smart_drive_available_spare_threshold gauge') foreach ($line in $nvmeThreshLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # NVMe Media Errors # ================================================================ $nvmeMediaLines = @() foreach ($d in $allDrives) { if ($d.NvmeMediaErrors -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $nvmeMediaLines += "smart_drive_media_errors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeMediaErrors)" } } if ($nvmeMediaLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_media_errors NVMe media and data integrity errors') [void]$sb.AppendLine('# TYPE smart_drive_media_errors gauge') foreach ($line in $nvmeMediaLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # NVMe Critical Warning # ================================================================ $nvmeCritLines = @() foreach ($d in $allDrives) { if ($d.NvmeCriticalWarn -ge 0) { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $nvmeCritLines += "smart_drive_critical_warning{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeCriticalWarn)" } } if ($nvmeCritLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_critical_warning NVMe critical warning bitmap') [void]$sb.AppendLine('# TYPE smart_drive_critical_warning gauge') foreach ($line in $nvmeCritLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } # ================================================================ # Interface Speed # ================================================================ $speedLines = @() foreach ($d in $allDrives) { if ($d.SataSpeed -ne '') { $dev = Get-CleanLabelValue $d.Device $model = Get-CleanLabelValue $d.Model $serial = Get-CleanLabelValue $d.Serial $speed = Get-CleanLabelValue $d.SataSpeed $speedLines += "smart_drive_interface_speed{device=`"$dev`",model=`"$model`",serial=`"$serial`",speed=`"$speed`"} 1" } } if ($speedLines.Count -gt 0) { [void]$sb.AppendLine('# HELP smart_drive_interface_speed SATA interface speed info metric') [void]$sb.AppendLine('# TYPE smart_drive_interface_speed gauge') foreach ($line in $speedLines) { [void]$sb.AppendLine($line) } [void]$sb.AppendLine('') } } # ======================================================================== # Exporter Runtime # ======================================================================== $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds $timestamp = Get-UnixTimestamp [void]$sb.AppendLine('# HELP smart_drive_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE smart_drive_exporter_duration_seconds gauge') [void]$sb.AppendLine("smart_drive_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP smart_drive_exporter_last_run_timestamp Unix timestamp of last successful run') [void]$sb.AppendLine('# TYPE smart_drive_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("smart_drive_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 SMART drive 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 = @" SMART Drive Exporter v1.0

SMART Drive Exporter v1.0 (Windows)

Metrics

Sections

"@ $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' { $OutputFile = Join-Path $TextfileDir 'smart_drive.prom' $outputDir = Split-Path $OutputFile -Parent if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null } $tempFile = Join-Path $outputDir ".smart_drive_metrics.$PID.tmp" try { $metrics = Get-AllMetrics $metrics | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline $lineCount = ($metrics -split "`n").Count if ($lineCount -lt 3) { 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 } }