# .SYNOPSIS Hyper-V Replica Prometheus Metrics Exporter .DESCRIPTION Prometheus exporter for Hyper-V Replica - replication health, state, last replication time, replication frequency, pending bytes, missed replications, VM replication count, and failover readiness. Exports metrics as Prometheus-compatible text format. .PARAMETER Mode Output mode: 'stdout' (default), 'textfile', or 'http' .PARAMETER Port HTTP port for http mode (default: 9517) .PARAMETER TextfileDir Directory for textfile collector output (default: C:\ProgramData\node_exporter) .PARAMETER InstallScheduledTask Switch to create a scheduled task for auto-start on system boot .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 Status: - hyperv_replica_up - hyperv_replica_exporter_info{version} Per-VM Replication: - hyperv_replica_vm_state{vm, state} - hyperv_replica_vm_health{vm, health} - hyperv_replica_vm_last_replication_timestamp{vm} - hyperv_replica_vm_frequency_seconds{vm} - hyperv_replica_vm_pending_bytes{vm} - hyperv_replica_vm_missed_count{vm} - hyperv_replica_vm_failover_ready{vm} - hyperv_replica_vm_role{vm, role} Totals: - hyperv_replica_total_vms - hyperv_replica_healthy_vms Exporter: - hyperv_replica_exporter_duration_seconds - hyperv_replica_exporter_last_run_timestamp #> param( [ValidateSet('stdout', 'textfile', 'http')] [string]$Mode = 'stdout', [int]$Port = 9517, [string]$TextfileDir = 'C:\ProgramData\node_exporter', [switch]$InstallScheduledTask, [int]$TaskIntervalMinutes = 2 ) # Create a scheduled task to run this script every $TaskIntervalMinutes minutes if ($InstallScheduledTask) { $taskName = "HyperVReplicaMetricsExporter" $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 Hyper-V Replica 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) } # ============================================================================ # HYPER-V REPLICA METRICS # ============================================================================ function Get-HyperVReplicaMetrics { $sb = [System.Text.StringBuilder]::new() try { $replicas = @(Get-VMReplication -ErrorAction Stop) } catch { $replicas = @() } $replicationStates = @('Replicating', 'Suspended', 'WaitingForInitialReplication', 'WaitingForStartResynchronize', 'Resynchronizing', 'ResynchronizeSuspended', 'WaitingForUpdateCompletion', 'WaitingForRepurposeCompletion', 'ReadyForInitialReplication', 'FailedOver', 'NotApplicable') $healthStates = @('Normal', 'Warning', 'Critical') # --- hyperv_replica_vm_state --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_state Replication state of VM (1=current state)') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_state gauge') try { foreach ($vm in $replicas) { $vmName = $vm.VMName $currentState = $vm.State.ToString() foreach ($state in $replicationStates) { $val = if ($state -eq $currentState) { 1 } else { 0 } [void]$sb.AppendLine("hyperv_replica_vm_state{vm=`"$vmName`",state=`"$state`"} $val") } } } catch { [void]$sb.AppendLine("hyperv_replica_vm_state 0") } [void]$sb.AppendLine('') # --- hyperv_replica_vm_health --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_health Replication health of VM (1=current health)') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_health gauge') try { foreach ($vm in $replicas) { $vmName = $vm.VMName $currentHealth = $vm.Health.ToString() foreach ($health in $healthStates) { $val = if ($health -eq $currentHealth) { 1 } else { 0 } [void]$sb.AppendLine("hyperv_replica_vm_health{vm=`"$vmName`",health=`"$health`"} $val") } } } catch { [void]$sb.AppendLine("hyperv_replica_vm_health 0") } [void]$sb.AppendLine('') # --- hyperv_replica_vm_last_replication_timestamp --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_last_replication_timestamp Unix timestamp of last successful replication') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_last_replication_timestamp gauge') try { foreach ($vm in $replicas) { $vmName = $vm.VMName $lastReplTime = $vm.LastReplicationTime if ($lastReplTime) { $epoch = [int][double]::Parse((Get-Date $lastReplTime -UFormat '%s')) [void]$sb.AppendLine("hyperv_replica_vm_last_replication_timestamp{vm=`"$vmName`"} $epoch") } else { [void]$sb.AppendLine("hyperv_replica_vm_last_replication_timestamp{vm=`"$vmName`"} 0") } } } catch { [void]$sb.AppendLine("hyperv_replica_vm_last_replication_timestamp 0") } [void]$sb.AppendLine('') # --- hyperv_replica_vm_frequency_seconds --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_frequency_seconds Replication frequency in seconds') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_frequency_seconds gauge') try { foreach ($vm in $replicas) { $vmName = $vm.VMName $freqSec = $vm.FrequencySec if (-not $freqSec) { $freqSec = 0 } [void]$sb.AppendLine("hyperv_replica_vm_frequency_seconds{vm=`"$vmName`"} $freqSec") } } catch { [void]$sb.AppendLine("hyperv_replica_vm_frequency_seconds 0") } [void]$sb.AppendLine('') # --- hyperv_replica_vm_pending_bytes --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_pending_bytes Pending replication data in bytes') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_pending_bytes gauge') try { foreach ($vm in $replicas) { $vmName = $vm.VMName $pending = $vm.PendingReplicationSize if (-not $pending) { $pending = 0 } [void]$sb.AppendLine("hyperv_replica_vm_pending_bytes{vm=`"$vmName`"} $pending") } } catch { [void]$sb.AppendLine("hyperv_replica_vm_pending_bytes 0") } [void]$sb.AppendLine('') # --- hyperv_replica_vm_missed_count --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_missed_count Number of missed replication cycles') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_missed_count gauge') try { foreach ($vm in $replicas) { $vmName = $vm.VMName $missed = $vm.MissedReplicationCount if (-not $missed) { $missed = 0 } [void]$sb.AppendLine("hyperv_replica_vm_missed_count{vm=`"$vmName`"} $missed") } } catch { [void]$sb.AppendLine("hyperv_replica_vm_missed_count 0") } [void]$sb.AppendLine('') # --- hyperv_replica_vm_failover_ready --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_failover_ready Test replica available for failover (1=yes, 0=no)') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_failover_ready gauge') try { foreach ($vm in $replicas) { $vmName = $vm.VMName $testReplicaAvailable = $vm.TestReplicaObject $val = if ($testReplicaAvailable) { 1 } else { 0 } [void]$sb.AppendLine("hyperv_replica_vm_failover_ready{vm=`"$vmName`"} $val") } } catch { [void]$sb.AppendLine("hyperv_replica_vm_failover_ready 0") } [void]$sb.AppendLine('') # --- hyperv_replica_vm_role --- [void]$sb.AppendLine('# HELP hyperv_replica_vm_role Replication role of VM (1=current role)') [void]$sb.AppendLine('# TYPE hyperv_replica_vm_role gauge') try { $roles = @('Primary', 'Replica') foreach ($vm in $replicas) { $vmName = $vm.VMName $currentRole = $vm.ReplicationMode.ToString() foreach ($role in $roles) { $val = if ($role -eq $currentRole) { 1 } else { 0 } [void]$sb.AppendLine("hyperv_replica_vm_role{vm=`"$vmName`",role=`"$role`"} $val") } } } catch { [void]$sb.AppendLine("hyperv_replica_vm_role 0") } [void]$sb.AppendLine('') # --- hyperv_replica_total_vms --- [void]$sb.AppendLine('# HELP hyperv_replica_total_vms Total VMs with replication configured') [void]$sb.AppendLine('# TYPE hyperv_replica_total_vms gauge') try { $totalVms = $replicas.Count [void]$sb.AppendLine("hyperv_replica_total_vms $totalVms") } catch { [void]$sb.AppendLine("hyperv_replica_total_vms 0") } [void]$sb.AppendLine('') # --- hyperv_replica_healthy_vms --- [void]$sb.AppendLine('# HELP hyperv_replica_healthy_vms VMs with Normal replication health') [void]$sb.AppendLine('# TYPE hyperv_replica_healthy_vms gauge') try { $healthyCount = ($replicas | Where-Object { $_.Health.ToString() -eq 'Normal' } | Measure-Object).Count [void]$sb.AppendLine("hyperv_replica_healthy_vms $healthyCount") } catch { [void]$sb.AppendLine("hyperv_replica_healthy_vms 0") } [void]$sb.AppendLine('') $sb.ToString() } # ============================================================================ # COLLECT ALL METRICS # ============================================================================ function Get-AllMetrics { $scriptStart = Get-Date $sb = [System.Text.StringBuilder]::new() # Exporter up [void]$sb.AppendLine('# HELP hyperv_replica_up Hyper-V Replica exporter status (1=up, 0=down)') [void]$sb.AppendLine('# TYPE hyperv_replica_up gauge') try { $vmms = Get-Service -Name vmms -ErrorAction Stop $upVal = if ($vmms.Status -eq 'Running') { 1 } else { 0 } [void]$sb.AppendLine("hyperv_replica_up $upVal") } catch { [void]$sb.AppendLine("hyperv_replica_up 0") } [void]$sb.AppendLine('') # Exporter info [void]$sb.AppendLine('# HELP hyperv_replica_exporter_info Exporter version information') [void]$sb.AppendLine('# TYPE hyperv_replica_exporter_info gauge') [void]$sb.AppendLine('hyperv_replica_exporter_info{version="1.0"} 1') [void]$sb.AppendLine('') # Collect Hyper-V Replica metrics [void]$sb.Append((Get-HyperVReplicaMetrics)) # Exporter runtime $scriptEnd = Get-Date $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds $timestamp = Get-UnixTimestamp [void]$sb.AppendLine('# HELP hyperv_replica_exporter_duration_seconds Time to generate all metrics') [void]$sb.AppendLine('# TYPE hyperv_replica_exporter_duration_seconds gauge') [void]$sb.AppendLine("hyperv_replica_exporter_duration_seconds $duration") [void]$sb.AppendLine('') [void]$sb.AppendLine('# HELP hyperv_replica_exporter_last_run_timestamp Unix timestamp of last successful run') [void]$sb.AppendLine('# TYPE hyperv_replica_exporter_last_run_timestamp gauge') [void]$sb.AppendLine("hyperv_replica_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 Hyper-V Replica 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 = @"