# Exchange Metrics Collector - Outputs Prometheus-compatible metrics # Requires Exchange Management Shell and appropriate permissions param( [switch]$InstallScheduledTask, [int]$TaskIntervalMinutes = 5 ) if ($InstallScheduledTask) { $taskName = "ExchangeMetricsExporter" $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue if (-not $existingTask) { $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`"" 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 Exchange 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" } } $StartTime = Get-Date $Hostname = $env:COMPUTERNAME # Try to load Exchange Management Shell if not already loaded if (-not (Get-Command Get-TransportServer -ErrorAction SilentlyContinue)) { try { Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn -ErrorAction SilentlyContinue } catch { # Try loading as module for newer Exchange versions try { $ExchangeInstallPath = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup -ErrorAction SilentlyContinue).MsiInstallPath if ($ExchangeInstallPath) { . "$ExchangeInstallPath\bin\RemoteExchange.ps1" -ErrorAction SilentlyContinue Connect-ExchangeServer -auto -ErrorAction SilentlyContinue } } catch {} } } # Helper function to safely get counter values function Get-SafeCounter { param([string]$CounterPath) try { $counter = Get-Counter -Counter $CounterPath -ErrorAction SilentlyContinue if ($counter -and $counter.CounterSamples) { return [math]::Max(0, [math]::Round($counter.CounterSamples[0].CookedValue, 2)) } } catch {} return 0 } # Helper function to safely count event log entries function Get-EventLogCount { param( [string]$LogName, [string]$Source, [int]$EventId, [int]$Hours = 24 ) try { $startDate = (Get-Date).AddHours(-$Hours) $filter = @{ LogName = $LogName StartTime = $startDate } if ($Source) { $filter['ProviderName'] = $Source } if ($EventId) { $filter['Id'] = $EventId } return (Get-WinEvent -FilterHashtable $filter -ErrorAction SilentlyContinue | Measure-Object).Count } catch { return 0 } } # ============================================================================ # TRANSPORT QUEUES # ============================================================================ Write-Output "# HELP exchange_queue_length Number of messages in each transport queue" Write-Output "# TYPE exchange_queue_length gauge" try { $queues = Get-Queue -ErrorAction SilentlyContinue if ($queues) { foreach ($queue in $queues) { $queueType = $queue.DeliveryType -replace '\s+', '_' $queueStatus = $queue.Status $nextHop = ($queue.NextHopDomain -replace '["\s]', '') -replace '\.', '_' Write-Output "exchange_queue_length{queue_type=`"$queueType`",status=`"$queueStatus`",next_hop=`"$nextHop`",hostname=`"$Hostname`"} $($queue.MessageCount)" } } } catch {} # Queue totals by status Write-Output "# HELP exchange_queue_total_messages Total messages in queues by status" Write-Output "# TYPE exchange_queue_total_messages gauge" try { $queueStats = Get-Queue -ErrorAction SilentlyContinue | Group-Object Status foreach ($stat in $queueStats) { $total = ($stat.Group | Measure-Object -Property MessageCount -Sum).Sum Write-Output "exchange_queue_total_messages{status=`"$($stat.Name)`",hostname=`"$Hostname`"} $total" } } catch {} # Submission queue (messages waiting to be categorized) Write-Output "# HELP exchange_submission_queue_length Messages in submission queue" Write-Output "# TYPE exchange_submission_queue_length gauge" $submissionQueue = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Submission Queue Length" Write-Output "exchange_submission_queue_length{hostname=`"$Hostname`"} $submissionQueue" # Poison queue (messages that failed repeatedly) Write-Output "# HELP exchange_poison_queue_length Messages in poison queue" Write-Output "# TYPE exchange_poison_queue_length gauge" $poisonQueue = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Poison Queue Length" Write-Output "exchange_poison_queue_length{hostname=`"$Hostname`"} $poisonQueue" # Unreachable queue Write-Output "# HELP exchange_unreachable_queue_length Messages in unreachable queue" Write-Output "# TYPE exchange_unreachable_queue_length gauge" $unreachableQueue = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Unreachable Queue Length" Write-Output "exchange_unreachable_queue_length{hostname=`"$Hostname`"} $unreachableQueue" # Active mailbox delivery queue Write-Output "# HELP exchange_active_mailbox_queue_length Active mailbox delivery queue" Write-Output "# TYPE exchange_active_mailbox_queue_length gauge" $activeMailbox = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Active Mailbox Delivery Queue Length" Write-Output "exchange_active_mailbox_queue_length{hostname=`"$Hostname`"} $activeMailbox" # Active non-SMTP delivery queue Write-Output "# HELP exchange_active_nonsmtp_queue_length Active non-SMTP delivery queue" Write-Output "# TYPE exchange_active_nonsmtp_queue_length gauge" $activeNonSmtp = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Active Non-Smtp Delivery Queue Length" Write-Output "exchange_active_nonsmtp_queue_length{hostname=`"$Hostname`"} $activeNonSmtp" # Retry mailbox delivery queue Write-Output "# HELP exchange_retry_mailbox_queue_length Retry mailbox delivery queue" Write-Output "# TYPE exchange_retry_mailbox_queue_length gauge" $retryMailbox = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Retry Mailbox Delivery Queue Length" Write-Output "exchange_retry_mailbox_queue_length{hostname=`"$Hostname`"} $retryMailbox" # ============================================================================ # MESSAGE THROUGHPUT # ============================================================================ Write-Output "# HELP exchange_messages_received_total Total messages received" Write-Output "# TYPE exchange_messages_received_total counter" $messagesReceived = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Messages Received Total" Write-Output "exchange_messages_received_total{hostname=`"$Hostname`"} $messagesReceived" Write-Output "# HELP exchange_messages_sent_total Total messages sent" Write-Output "# TYPE exchange_messages_sent_total counter" $messagesSent = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\Messages Sent Total" Write-Output "exchange_messages_sent_total{hostname=`"$Hostname`"} $messagesSent" Write-Output "# HELP exchange_messages_received_per_second Messages received per second" Write-Output "# TYPE exchange_messages_received_per_second gauge" $messagesReceivedSec = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Messages Received/sec" Write-Output "exchange_messages_received_per_second{hostname=`"$Hostname`"} $messagesReceivedSec" Write-Output "# HELP exchange_messages_sent_per_second Messages sent per second" Write-Output "# TYPE exchange_messages_sent_per_second gauge" $messagesSentSec = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\Messages Sent/sec" Write-Output "exchange_messages_sent_per_second{hostname=`"$Hostname`"} $messagesSentSec" # Bytes transferred Write-Output "# HELP exchange_bytes_received_total Total bytes received" Write-Output "# TYPE exchange_bytes_received_total counter" $bytesReceived = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Bytes Received Total" Write-Output "exchange_bytes_received_total{hostname=`"$Hostname`"} $bytesReceived" Write-Output "# HELP exchange_bytes_sent_total Total bytes sent" Write-Output "# TYPE exchange_bytes_sent_total counter" $bytesSent = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\Bytes Sent Total" Write-Output "exchange_bytes_sent_total{hostname=`"$Hostname`"} $bytesSent" # ============================================================================ # SMTP CONNECTIONS # ============================================================================ Write-Output "# HELP exchange_smtp_connections_current Current SMTP connections" Write-Output "# TYPE exchange_smtp_connections_current gauge" $smtpInbound = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Connections Current" $smtpOutbound = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\Connections Current" Write-Output "exchange_smtp_connections_current{direction=`"inbound`",hostname=`"$Hostname`"} $smtpInbound" Write-Output "exchange_smtp_connections_current{direction=`"outbound`",hostname=`"$Hostname`"} $smtpOutbound" Write-Output "# HELP exchange_smtp_connections_total Total SMTP connections" Write-Output "# TYPE exchange_smtp_connections_total counter" $smtpInboundTotal = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Connections Total" $smtpOutboundTotal = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\Connections Total" Write-Output "exchange_smtp_connections_total{direction=`"inbound`",hostname=`"$Hostname`"} $smtpInboundTotal" Write-Output "exchange_smtp_connections_total{direction=`"outbound`",hostname=`"$Hostname`"} $smtpOutboundTotal" # SMTP connection failures Write-Output "# HELP exchange_smtp_connection_failures_total SMTP connection failures" Write-Output "# TYPE exchange_smtp_connection_failures_total counter" $smtpSendFailures = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\Connection Failures" Write-Output "exchange_smtp_connection_failures_total{hostname=`"$Hostname`"} $smtpSendFailures" # ============================================================================ # MESSAGE TRACKING (from logs) # ============================================================================ Write-Output "# HELP exchange_tracked_messages_total Messages by event type (last 24h)" Write-Output "# TYPE exchange_tracked_messages_total counter" try { $trackingStart = (Get-Date).AddHours(-24) $eventTypes = @('RECEIVE', 'SEND', 'DELIVER', 'FAIL', 'DSN', 'DEFER', 'EXPAND', 'REDIRECT', 'RESOLVE', 'DROP', 'BADMAIL', 'POISONMESSAGE') foreach ($eventType in $eventTypes) { try { $count = (Get-MessageTrackingLog -Start $trackingStart -EventId $eventType -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_tracked_messages_total{event_type=`"$eventType`",hostname=`"$Hostname`"} $count" } catch { Write-Output "exchange_tracked_messages_total{event_type=`"$eventType`",hostname=`"$Hostname`"} 0" } } } catch {} # Message delivery latency from tracking logs Write-Output "# HELP exchange_message_latency_seconds Message delivery latency statistics" Write-Output "# TYPE exchange_message_latency_seconds gauge" try { $deliveries = Get-MessageTrackingLog -Start (Get-Date).AddHours(-1) -EventId DELIVER -ResultSize 1000 -ErrorAction SilentlyContinue if ($deliveries -and $deliveries.Count -gt 0) { $latencies = $deliveries | ForEach-Object { $received = Get-MessageTrackingLog -MessageId $_.MessageId -EventId RECEIVE -ResultSize 1 -ErrorAction SilentlyContinue if ($received) { ($_.Timestamp - $received.Timestamp).TotalSeconds } } | Where-Object { $_ -gt 0 } if ($latencies -and $latencies.Count -gt 0) { $avgLatency = ($latencies | Measure-Object -Average).Average $maxLatency = ($latencies | Measure-Object -Maximum).Maximum Write-Output "exchange_message_latency_seconds{stat=`"avg`",hostname=`"$Hostname`"} $([math]::Round($avgLatency, 2))" Write-Output "exchange_message_latency_seconds{stat=`"max`",hostname=`"$Hostname`"} $([math]::Round($maxLatency, 2))" } else { Write-Output "exchange_message_latency_seconds{stat=`"avg`",hostname=`"$Hostname`"} 0" Write-Output "exchange_message_latency_seconds{stat=`"max`",hostname=`"$Hostname`"} 0" } } } catch { Write-Output "exchange_message_latency_seconds{stat=`"avg`",hostname=`"$Hostname`"} 0" Write-Output "exchange_message_latency_seconds{stat=`"max`",hostname=`"$Hostname`"} 0" } # ============================================================================ # MAILBOX STATISTICS # ============================================================================ Write-Output "# HELP exchange_mailbox_count Total number of mailboxes" Write-Output "# TYPE exchange_mailbox_count gauge" try { $mailboxCount = (Get-Mailbox -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_mailbox_count{hostname=`"$Hostname`"} $mailboxCount" } catch { Write-Output "exchange_mailbox_count{hostname=`"$Hostname`"} 0" } # Mailbox database sizes and stats Write-Output "# HELP exchange_database_size_bytes Mailbox database size in bytes" Write-Output "# TYPE exchange_database_size_bytes gauge" Write-Output "# HELP exchange_database_available_space_bytes Available space in mailbox database" Write-Output "# TYPE exchange_database_available_space_bytes gauge" Write-Output "# HELP exchange_database_mailbox_count Mailboxes per database" Write-Output "# TYPE exchange_database_mailbox_count gauge" try { $databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue foreach ($db in $databases) { $dbName = $db.Name -replace '\s+', '_' $dbSize = if ($db.DatabaseSize) { $db.DatabaseSize.ToBytes() } else { 0 } $dbAvailable = if ($db.AvailableNewMailboxSpace) { $db.AvailableNewMailboxSpace.ToBytes() } else { 0 } Write-Output "exchange_database_size_bytes{database=`"$dbName`",hostname=`"$Hostname`"} $dbSize" Write-Output "exchange_database_available_space_bytes{database=`"$dbName`",hostname=`"$Hostname`"} $dbAvailable" $mbxInDb = (Get-Mailbox -Database $db.Name -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_database_mailbox_count{database=`"$dbName`",hostname=`"$Hostname`"} $mbxInDb" } } catch {} # Database mount status Write-Output "# HELP exchange_database_mounted Database mount status (1=mounted, 0=dismounted)" Write-Output "# TYPE exchange_database_mounted gauge" try { $databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue foreach ($db in $databases) { $dbName = $db.Name -replace '\s+', '_' $mounted = if ($db.Mounted) { 1 } else { 0 } Write-Output "exchange_database_mounted{database=`"$dbName`",hostname=`"$Hostname`"} $mounted" } } catch {} # ============================================================================ # CONTENT FILTERING / ANTI-SPAM # ============================================================================ Write-Output "# HELP exchange_antispam_messages_total Anti-spam filter results" Write-Output "# TYPE exchange_antispam_messages_total counter" # Content Filter Agent $cfBlocked = Get-SafeCounter "\MSExchange Content Filter Agent\Messages that Triggered the Content Filter" $cfQuarantined = Get-SafeCounter "\MSExchange Content Filter Agent\Messages Quarantined" $cfRejected = Get-SafeCounter "\MSExchange Content Filter Agent\Messages Rejected" $cfDeleted = Get-SafeCounter "\MSExchange Content Filter Agent\Messages Deleted" Write-Output "exchange_antispam_messages_total{filter=`"content`",action=`"triggered`",hostname=`"$Hostname`"} $cfBlocked" Write-Output "exchange_antispam_messages_total{filter=`"content`",action=`"quarantined`",hostname=`"$Hostname`"} $cfQuarantined" Write-Output "exchange_antispam_messages_total{filter=`"content`",action=`"rejected`",hostname=`"$Hostname`"} $cfRejected" Write-Output "exchange_antispam_messages_total{filter=`"content`",action=`"deleted`",hostname=`"$Hostname`"} $cfDeleted" # Sender Filter Agent $sfBlocked = Get-SafeCounter "\MSExchange Sender Filter Agent\Messages Evaluated by Sender Filter" $sfRejected = Get-SafeCounter "\MSExchange Sender Filter Agent\Messages Rejected by Sender Filter" Write-Output "exchange_antispam_messages_total{filter=`"sender`",action=`"evaluated`",hostname=`"$Hostname`"} $sfBlocked" Write-Output "exchange_antispam_messages_total{filter=`"sender`",action=`"rejected`",hostname=`"$Hostname`"} $sfRejected" # Sender ID Agent $sidPass = Get-SafeCounter "\MSExchange Sender Id Agent\Messages That Passed Sender ID Validation" $sidFail = Get-SafeCounter "\MSExchange Sender Id Agent\Messages That Failed Sender ID Validation" $sidNeutral = Get-SafeCounter "\MSExchange Sender Id Agent\Messages with Neutral Sender ID Validation Result" Write-Output "exchange_antispam_messages_total{filter=`"sender_id`",action=`"pass`",hostname=`"$Hostname`"} $sidPass" Write-Output "exchange_antispam_messages_total{filter=`"sender_id`",action=`"fail`",hostname=`"$Hostname`"} $sidFail" Write-Output "exchange_antispam_messages_total{filter=`"sender_id`",action=`"neutral`",hostname=`"$Hostname`"} $sidNeutral" # Recipient Filter Agent $rfBlocked = Get-SafeCounter "\MSExchange Recipient Filter Agent\Recipients Rejected by Recipient Validation" Write-Output "exchange_antispam_messages_total{filter=`"recipient`",action=`"rejected`",hostname=`"$Hostname`"} $rfBlocked" # Connection Filter Agent (IP Block List) $connBlocked = Get-SafeCounter "\MSExchange Connection Filtering Agent\Connections on IP Block List" $connAllowed = Get-SafeCounter "\MSExchange Connection Filtering Agent\Connections on IP Allow List" Write-Output "exchange_antispam_messages_total{filter=`"connection`",action=`"blocked`",hostname=`"$Hostname`"} $connBlocked" Write-Output "exchange_antispam_messages_total{filter=`"connection`",action=`"allowed`",hostname=`"$Hostname`"} $connAllowed" # ============================================================================ # SPAM CONFIDENCE LEVEL (SCL) DISTRIBUTION # ============================================================================ Write-Output "# HELP exchange_scl_distribution Messages by SCL rating" Write-Output "# TYPE exchange_scl_distribution counter" for ($scl = -1; $scl -le 9; $scl++) { $sclCount = Get-SafeCounter "\MSExchange Content Filter Agent\Messages with SCL Rating $scl" Write-Output "exchange_scl_distribution{scl=`"$scl`",hostname=`"$Hostname`"} $sclCount" } # ============================================================================ # AUTHENTICATION # ============================================================================ Write-Output "# HELP exchange_auth_total Authentication attempts" Write-Output "# TYPE exchange_auth_total counter" # SMTP AUTH $smtpAuthSuccess = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Auth Succeeded" $smtpAuthFail = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Auth Failed" Write-Output "exchange_auth_total{type=`"smtp`",result=`"success`",hostname=`"$Hostname`"} $smtpAuthSuccess" Write-Output "exchange_auth_total{type=`"smtp`",result=`"failed`",hostname=`"$Hostname`"} $smtpAuthFail" # OWA Authentication (from event logs) $owaAuthSuccess = Get-EventLogCount -LogName "MSExchange Management" -Source "MSExchange OWA" -Hours 24 Write-Output "exchange_auth_total{type=`"owa`",result=`"success`",hostname=`"$Hostname`"} $owaAuthSuccess" # ActiveSync Authentication $easAuthFail = Get-EventLogCount -LogName "Application" -Source "MSExchange ActiveSync" -EventId 1053 -Hours 24 Write-Output "exchange_auth_total{type=`"activesync`",result=`"failed`",hostname=`"$Hostname`"} $easAuthFail" # ============================================================================ # TLS/ENCRYPTION # ============================================================================ Write-Output "# HELP exchange_tls_connections_total TLS connection statistics" Write-Output "# TYPE exchange_tls_connections_total counter" $tlsInbound = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\TLS Sessions Started" $tlsOutbound = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\TLS Sessions Started" Write-Output "exchange_tls_connections_total{direction=`"inbound`",hostname=`"$Hostname`"} $tlsInbound" Write-Output "exchange_tls_connections_total{direction=`"outbound`",hostname=`"$Hostname`"} $tlsOutbound" Write-Output "# HELP exchange_tls_negotiation_failures TLS negotiation failures" Write-Output "# TYPE exchange_tls_negotiation_failures counter" $tlsInboundFail = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\TLS Negotiations Failed" $tlsOutboundFail = Get-SafeCounter "\MSExchangeTransport SmtpSend(_total)\TLS Negotiations Failed" Write-Output "exchange_tls_negotiation_failures{direction=`"inbound`",hostname=`"$Hostname`"} $tlsInboundFail" Write-Output "exchange_tls_negotiation_failures{direction=`"outbound`",hostname=`"$Hostname`"} $tlsOutboundFail" # ============================================================================ # CLIENT ACCESS (OWA, EWS, ActiveSync, Outlook Anywhere) # ============================================================================ Write-Output "# HELP exchange_owa_requests_total OWA requests" Write-Output "# TYPE exchange_owa_requests_total counter" $owaRequests = Get-SafeCounter "\MSExchange OWA\Requests/sec" $owaRequestsTotal = Get-SafeCounter "\MSExchange OWA\Total Requests" Write-Output "exchange_owa_requests_total{hostname=`"$Hostname`"} $owaRequestsTotal" Write-Output "# HELP exchange_owa_requests_per_second OWA requests per second" Write-Output "# TYPE exchange_owa_requests_per_second gauge" Write-Output "exchange_owa_requests_per_second{hostname=`"$Hostname`"} $owaRequests" Write-Output "# HELP exchange_owa_current_users Current OWA users" Write-Output "# TYPE exchange_owa_current_users gauge" $owaUsers = Get-SafeCounter "\MSExchange OWA\Current Unique Users" Write-Output "exchange_owa_current_users{hostname=`"$Hostname`"} $owaUsers" # EWS Write-Output "# HELP exchange_ews_requests_per_second EWS requests per second" Write-Output "# TYPE exchange_ews_requests_per_second gauge" $ewsRequests = Get-SafeCounter "\MSExchangeWS\Requests/sec" Write-Output "exchange_ews_requests_per_second{hostname=`"$Hostname`"} $ewsRequests" Write-Output "# HELP exchange_ews_current_connections Current EWS connections" Write-Output "# TYPE exchange_ews_current_connections gauge" $ewsConns = Get-SafeCounter "\MSExchangeWS\Current Connections" Write-Output "exchange_ews_current_connections{hostname=`"$Hostname`"} $ewsConns" # ActiveSync Write-Output "# HELP exchange_activesync_requests_per_second ActiveSync requests per second" Write-Output "# TYPE exchange_activesync_requests_per_second gauge" $easRequests = Get-SafeCounter "\MSExchange ActiveSync\Requests/sec" Write-Output "exchange_activesync_requests_per_second{hostname=`"$Hostname`"} $easRequests" Write-Output "# HELP exchange_activesync_current_requests Current ActiveSync requests" Write-Output "# TYPE exchange_activesync_current_requests gauge" $easCurrent = Get-SafeCounter "\MSExchange ActiveSync\Current Requests" Write-Output "exchange_activesync_current_requests{hostname=`"$Hostname`"} $easCurrent" Write-Output "# HELP exchange_activesync_sync_commands ActiveSync sync commands per second" Write-Output "# TYPE exchange_activesync_sync_commands gauge" $easSync = Get-SafeCounter "\MSExchange ActiveSync\Sync Commands/sec" Write-Output "exchange_activesync_sync_commands{hostname=`"$Hostname`"} $easSync" # Outlook Anywhere (RPC over HTTP) Write-Output "# HELP exchange_rpc_requests_per_second RPC requests per second" Write-Output "# TYPE exchange_rpc_requests_per_second gauge" $rpcRequests = Get-SafeCounter "\MSExchange RpcClientAccess\RPC Requests" Write-Output "exchange_rpc_requests_per_second{hostname=`"$Hostname`"} $rpcRequests" Write-Output "# HELP exchange_rpc_active_users Active RPC users" Write-Output "# TYPE exchange_rpc_active_users gauge" $rpcUsers = Get-SafeCounter "\MSExchange RpcClientAccess\Active User Count" Write-Output "exchange_rpc_active_users{hostname=`"$Hostname`"} $rpcUsers" Write-Output "# HELP exchange_rpc_connection_count RPC connection count" Write-Output "# TYPE exchange_rpc_connection_count gauge" $rpcConns = Get-SafeCounter "\MSExchange RpcClientAccess\Connection Count" Write-Output "exchange_rpc_connection_count{hostname=`"$Hostname`"} $rpcConns" # Outlook MAPI/HTTP Write-Output "# HELP exchange_mapi_requests_per_second MAPI over HTTP requests per second" Write-Output "# TYPE exchange_mapi_requests_per_second gauge" $mapiRequests = Get-SafeCounter "\MSExchange MapiHttp Emsmdb\Requests/sec" Write-Output "exchange_mapi_requests_per_second{hostname=`"$Hostname`"} $mapiRequests" Write-Output "# HELP exchange_mapi_current_connections Current MAPI connections" Write-Output "# TYPE exchange_mapi_current_connections gauge" $mapiConns = Get-SafeCounter "\MSExchange MapiHttp Emsmdb\Current Unique Users" Write-Output "exchange_mapi_current_connections{hostname=`"$Hostname`"} $mapiConns" # ============================================================================ # POP3/IMAP # ============================================================================ Write-Output "# HELP exchange_pop3_connections Current POP3 connections" Write-Output "# TYPE exchange_pop3_connections gauge" $pop3Conns = Get-SafeCounter "\MSExchangePop3\Connections Current" Write-Output "exchange_pop3_connections{hostname=`"$Hostname`"} $pop3Conns" Write-Output "# HELP exchange_pop3_connections_total Total POP3 connections" Write-Output "# TYPE exchange_pop3_connections_total counter" $pop3Total = Get-SafeCounter "\MSExchangePop3\Connections Total" Write-Output "exchange_pop3_connections_total{hostname=`"$Hostname`"} $pop3Total" Write-Output "# HELP exchange_imap4_connections Current IMAP4 connections" Write-Output "# TYPE exchange_imap4_connections gauge" $imap4Conns = Get-SafeCounter "\MSExchangeImap4\Connections Current" Write-Output "exchange_imap4_connections{hostname=`"$Hostname`"} $imap4Conns" Write-Output "# HELP exchange_imap4_connections_total Total IMAP4 connections" Write-Output "# TYPE exchange_imap4_connections_total counter" $imap4Total = Get-SafeCounter "\MSExchangeImap4\Connections Total" Write-Output "exchange_imap4_connections_total{hostname=`"$Hostname`"} $imap4Total" # ============================================================================ # STORE (Information Store) # ============================================================================ Write-Output "# HELP exchange_store_rpc_requests RPC requests to Information Store" Write-Output "# TYPE exchange_store_rpc_requests gauge" $storeRpc = Get-SafeCounter "\MSExchangeIS Store(_total)\RPC Requests" Write-Output "exchange_store_rpc_requests{hostname=`"$Hostname`"} $storeRpc" Write-Output "# HELP exchange_store_rpc_latency_avg Average RPC latency (ms)" Write-Output "# TYPE exchange_store_rpc_latency_avg gauge" $storeLatency = Get-SafeCounter "\MSExchangeIS Store(_total)\RPC Average Latency" Write-Output "exchange_store_rpc_latency_avg{hostname=`"$Hostname`"} $storeLatency" Write-Output "# HELP exchange_store_messages_queued Messages queued for submission" Write-Output "# TYPE exchange_store_messages_queued gauge" $storeQueued = Get-SafeCounter "\MSExchangeIS Store(_total)\Messages Queued For Submission" Write-Output "exchange_store_messages_queued{hostname=`"$Hostname`"} $storeQueued" # ============================================================================ # TRANSPORT DUMPSTER / SAFETY NET # ============================================================================ Write-Output "# HELP exchange_safetynet_messages Messages in Safety Net" Write-Output "# TYPE exchange_safetynet_messages gauge" $safetynetMsgs = Get-SafeCounter "\MSExchangeTransport Safety Net\Safety Net Total Messages" Write-Output "exchange_safetynet_messages{hostname=`"$Hostname`"} $safetynetMsgs" Write-Output "# HELP exchange_safetynet_size_bytes Safety Net size in bytes" Write-Output "# TYPE exchange_safetynet_size_bytes gauge" $safetynetSize = Get-SafeCounter "\MSExchangeTransport Safety Net\Safety Net Total Size" Write-Output "exchange_safetynet_size_bytes{hostname=`"$Hostname`"} $safetynetSize" # ============================================================================ # DSN (DELIVERY STATUS NOTIFICATIONS) # ============================================================================ Write-Output "# HELP exchange_dsn_total DSN messages generated" Write-Output "# TYPE exchange_dsn_total counter" $dsnGenerated = Get-SafeCounter "\MSExchangeTransport DSN(_total)\Delivery Status Notifications Generated" Write-Output "exchange_dsn_total{type=`"generated`",hostname=`"$Hostname`"} $dsnGenerated" $dsnFailure = Get-SafeCounter "\MSExchangeTransport DSN(_total)\Failure DSNs Total" $dsnDelay = Get-SafeCounter "\MSExchangeTransport DSN(_total)\Delay DSNs Total" $dsnRelayed = Get-SafeCounter "\MSExchangeTransport DSN(_total)\Relayed DSNs Total" $dsnExpanded = Get-SafeCounter "\MSExchangeTransport DSN(_total)\Expanded DSNs Total" Write-Output "exchange_dsn_total{type=`"failure`",hostname=`"$Hostname`"} $dsnFailure" Write-Output "exchange_dsn_total{type=`"delay`",hostname=`"$Hostname`"} $dsnDelay" Write-Output "exchange_dsn_total{type=`"relayed`",hostname=`"$Hostname`"} $dsnRelayed" Write-Output "exchange_dsn_total{type=`"expanded`",hostname=`"$Hostname`"} $dsnExpanded" # ============================================================================ # RESOURCE HEALTH # ============================================================================ Write-Output "# HELP exchange_resource_health Resource health status" Write-Output "# TYPE exchange_resource_health gauge" # Back Pressure indicators $backPressure = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Messages Submitted Recently" Write-Output "exchange_resource_health{resource=`"back_pressure_messages`",hostname=`"$Hostname`"} $backPressure" # Database log generation $dbLogGen = Get-SafeCounter "\MSExchange Database ==> Instances(_total)\Log Generation Checkpoint Depth" Write-Output "exchange_resource_health{resource=`"log_checkpoint_depth`",hostname=`"$Hostname`"} $dbLogGen" # ============================================================================ # SERVICE STATUS # ============================================================================ Write-Output "# HELP exchange_service_status Exchange service status (1=running, 0=stopped)" Write-Output "# TYPE exchange_service_status gauge" $services = @( 'MSExchangeTransport', 'MSExchangeIS', 'MSExchangeMailboxAssistants', 'MSExchangeDelivery', 'MSExchangeSubmission', 'MSExchangeFrontEndTransport', 'MSExchangeEdgeSync', 'MSExchangeServiceHost', 'MSExchangeRPC', 'MSExchangeMailboxReplication', 'MSExchangeADTopology', 'MSExchangeAntispamUpdate', 'MSExchangeThrottling', 'MSExchangeHM', 'MSExchangeDiagnostics', 'MSExchangePop3', 'MSExchangeImap4', 'MSExchangeUM', 'MSExchangeUMCR' ) foreach ($svc in $services) { $service = Get-Service -Name $svc -ErrorAction SilentlyContinue if ($service) { $status = if ($service.Status -eq 'Running') { 1 } else { 0 } Write-Output "exchange_service_status{service=`"$svc`",hostname=`"$Hostname`"} $status" } } # ============================================================================ # SERVER HEALTH (Exchange 2013+) # ============================================================================ Write-Output "# HELP exchange_server_health Server component health (1=healthy, 0=unhealthy)" Write-Output "# TYPE exchange_server_health gauge" try { $health = Get-ServerComponentState -Identity $Hostname -ErrorAction SilentlyContinue foreach ($component in $health) { $state = if ($component.State -eq 'Active') { 1 } else { 0 } $compName = $component.Component -replace '\s+', '_' Write-Output "exchange_server_health{component=`"$compName`",hostname=`"$Hostname`"} $state" } } catch {} # ============================================================================ # HEALTH MANAGER MONITORS # ============================================================================ Write-Output "# HELP exchange_health_monitor Health monitor status (1=healthy, 0=unhealthy)" Write-Output "# TYPE exchange_health_monitor gauge" try { $monitors = Get-ServerHealth -Identity $Hostname -ErrorAction SilentlyContinue | Where-Object { $_.AlertValue -ne "Healthy" } | Select-Object Name, AlertValue -Unique $unhealthyCount = ($monitors | Measure-Object).Count Write-Output "exchange_health_monitor{status=`"unhealthy_count`",hostname=`"$Hostname`"} $unhealthyCount" } catch { Write-Output "exchange_health_monitor{status=`"unhealthy_count`",hostname=`"$Hostname`"} 0" } # ============================================================================ # DAG (DATABASE AVAILABILITY GROUP) STATUS # ============================================================================ Write-Output "# HELP exchange_dag_copy_status DAG database copy status (1=mounted/healthy, 0=failed)" Write-Output "# TYPE exchange_dag_copy_status gauge" try { $dagStatus = Get-MailboxDatabaseCopyStatus -ErrorAction SilentlyContinue foreach ($copy in $dagStatus) { $dbName = $copy.DatabaseName -replace '\s+', '_' $status = switch ($copy.Status) { 'Mounted' { 1 } 'Healthy' { 1 } default { 0 } } Write-Output "exchange_dag_copy_status{database=`"$dbName`",server=`"$($copy.MailboxServer)`",status=`"$($copy.Status)`",hostname=`"$Hostname`"} $status" # Copy queue length if ($copy.CopyQueueLength -ne $null) { Write-Output "exchange_dag_copy_queue_length{database=`"$dbName`",server=`"$($copy.MailboxServer)`",hostname=`"$Hostname`"} $($copy.CopyQueueLength)" } # Replay queue length if ($copy.ReplayQueueLength -ne $null) { Write-Output "exchange_dag_replay_queue_length{database=`"$dbName`",server=`"$($copy.MailboxServer)`",hostname=`"$Hostname`"} $($copy.ReplayQueueLength)" } } } catch {} # ============================================================================ # CERTIFICATE EXPIRY # ============================================================================ Write-Output "# HELP exchange_cert_expiry_seconds Seconds until certificate expires" Write-Output "# TYPE exchange_cert_expiry_seconds gauge" try { $certs = Get-ExchangeCertificate -ErrorAction SilentlyContinue | Where-Object { $_.NotAfter -gt (Get-Date) } foreach ($cert in $certs) { $services = ($cert.Services -join '_') -replace ',', '_' $thumbprint = $cert.Thumbprint.Substring(0, 8) $expirySeconds = [math]::Round(($cert.NotAfter - (Get-Date)).TotalSeconds) Write-Output "exchange_cert_expiry_seconds{thumbprint=`"$thumbprint`",services=`"$services`",hostname=`"$Hostname`"} $expirySeconds" } } catch {} # ============================================================================ # TOP SENDERS/RECIPIENTS (Last 24 hours) # ============================================================================ Write-Output "# HELP exchange_top_senders_total Top senders by message count (24h)" Write-Output "# TYPE exchange_top_senders_total counter" try { $topSenders = Get-MessageTrackingLog -Start (Get-Date).AddHours(-24) -EventId RECEIVE -ResultSize 10000 -ErrorAction SilentlyContinue | Group-Object Sender | Sort-Object Count -Descending | Select-Object -First 20 foreach ($sender in $topSenders) { $senderAddr = $sender.Name -replace '"', '' Write-Output "exchange_top_senders_total{sender=`"$senderAddr`",hostname=`"$Hostname`"} $($sender.Count)" } } catch {} Write-Output "# HELP exchange_top_recipients_total Top recipients by message count (24h)" Write-Output "# TYPE exchange_top_recipients_total counter" try { $topRecipients = Get-MessageTrackingLog -Start (Get-Date).AddHours(-24) -EventId DELIVER -ResultSize 10000 -ErrorAction SilentlyContinue | ForEach-Object { $_.Recipients } | Group-Object | Sort-Object Count -Descending | Select-Object -First 20 foreach ($recipient in $topRecipients) { $recipientAddr = $recipient.Name -replace '"', '' Write-Output "exchange_top_recipients_total{recipient=`"$recipientAddr`",hostname=`"$Hostname`"} $($recipient.Count)" } } catch {} # ============================================================================ # MESSAGE SIZE STATISTICS # ============================================================================ Write-Output "# HELP exchange_message_size_bytes Message size statistics" Write-Output "# TYPE exchange_message_size_bytes gauge" try { $messages = Get-MessageTrackingLog -Start (Get-Date).AddHours(-1) -EventId RECEIVE -ResultSize 1000 -ErrorAction SilentlyContinue if ($messages -and $messages.Count -gt 0) { $sizes = $messages | ForEach-Object { $_.TotalBytes } | Where-Object { $_ -gt 0 } if ($sizes -and $sizes.Count -gt 0) { $avgSize = [math]::Round(($sizes | Measure-Object -Average).Average) $maxSize = ($sizes | Measure-Object -Maximum).Maximum $totalSize = ($sizes | Measure-Object -Sum).Sum Write-Output "exchange_message_size_bytes{stat=`"avg`",hostname=`"$Hostname`"} $avgSize" Write-Output "exchange_message_size_bytes{stat=`"max`",hostname=`"$Hostname`"} $maxSize" Write-Output "exchange_message_size_bytes{stat=`"total_1h`",hostname=`"$Hostname`"} $totalSize" } } } catch {} # ============================================================================ # EDGE TRANSPORT (if applicable) # ============================================================================ Write-Output "# HELP exchange_edge_sync_status EdgeSync status" Write-Output "# TYPE exchange_edge_sync_status gauge" try { $edgeSync = Test-EdgeSynchronization -ErrorAction SilentlyContinue if ($edgeSync) { $syncStatus = if ($edgeSync.SyncStatus -eq 'Normal') { 1 } else { 0 } Write-Output "exchange_edge_sync_status{hostname=`"$Hostname`"} $syncStatus" } } catch {} # ============================================================================ # TRANSPORT AGENTS # ============================================================================ Write-Output "# HELP exchange_transport_agent_enabled Transport agent status (1=enabled, 0=disabled)" Write-Output "# TYPE exchange_transport_agent_enabled gauge" try { $agents = Get-TransportAgent -ErrorAction SilentlyContinue foreach ($agent in $agents) { $agentName = $agent.Identity -replace '\s+', '_' -replace '[^\w_]', '' $enabled = if ($agent.Enabled) { 1 } else { 0 } Write-Output "exchange_transport_agent_enabled{agent=`"$agentName`",hostname=`"$Hostname`"} $enabled" } } catch {} # ============================================================================ # PUBLIC FOLDER STATISTICS # ============================================================================ Write-Output "# HELP exchange_public_folder_count Number of public folders" Write-Output "# TYPE exchange_public_folder_count gauge" try { $pfCount = (Get-PublicFolder -Recurse -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_public_folder_count{hostname=`"$Hostname`"} $pfCount" } catch { Write-Output "exchange_public_folder_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # OUTLOOK WEB APP ERRORS (from Event Log) # ============================================================================ Write-Output "# HELP exchange_owa_errors_total OWA errors from event log (24h)" Write-Output "# TYPE exchange_owa_errors_total counter" $owaErrors = Get-EventLogCount -LogName "Application" -Source "MSExchange OWA" -Hours 24 Write-Output "exchange_owa_errors_total{hostname=`"$Hostname`"} $owaErrors" # ============================================================================ # TRANSPORT ERRORS (from Event Log) # ============================================================================ Write-Output "# HELP exchange_transport_errors_total Transport errors from event log (24h)" Write-Output "# TYPE exchange_transport_errors_total counter" $transportErrors = Get-EventLogCount -LogName "Application" -Source "MSExchangeTransport" -Hours 24 Write-Output "exchange_transport_errors_total{hostname=`"$Hostname`"} $transportErrors" # ============================================================================ # INFORMATION STORE ERRORS # ============================================================================ Write-Output "# HELP exchange_store_errors_total Information Store errors from event log (24h)" Write-Output "# TYPE exchange_store_errors_total counter" $storeErrors = Get-EventLogCount -LogName "Application" -Source "MSExchangeIS" -Hours 24 Write-Output "exchange_store_errors_total{hostname=`"$Hostname`"} $storeErrors" # ============================================================================ # REPLICATION HEALTH # ============================================================================ Write-Output "# HELP exchange_replication_health Replication health check results" Write-Output "# TYPE exchange_replication_health gauge" try { $replHealth = Test-ReplicationHealth -ErrorAction SilentlyContinue foreach ($check in $replHealth) { $checkName = $check.Check -replace '\s+', '_' $result = if ($check.Result -eq 'Passed') { 1 } else { 0 } Write-Output "exchange_replication_health{check=`"$checkName`",hostname=`"$Hostname`"} $result" } } catch {} # ============================================================================ # MAIL FLOW TEST # ============================================================================ Write-Output "# HELP exchange_mailflow_test_latency_seconds Mail flow test latency" Write-Output "# TYPE exchange_mailflow_test_latency_seconds gauge" try { $mailflow = Test-Mailflow -ErrorAction SilentlyContinue if ($mailflow -and $mailflow.TestMailflowResult -eq 'Success') { $latency = $mailflow.MessageLatencyTime.TotalSeconds Write-Output "exchange_mailflow_test_latency_seconds{hostname=`"$Hostname`"} $([math]::Round($latency, 2))" Write-Output "exchange_mailflow_test_success{hostname=`"$Hostname`"} 1" } else { Write-Output "exchange_mailflow_test_success{hostname=`"$Hostname`"} 0" } } catch { Write-Output "exchange_mailflow_test_success{hostname=`"$Hostname`"} 0" } # ============================================================================ # DOMAIN STATISTICS (Send/Receive by Domain) # ============================================================================ Write-Output "# HELP exchange_domain_messages_total Messages by remote domain (24h)" Write-Output "# TYPE exchange_domain_messages_total counter" try { $domainStats = Get-MessageTrackingLog -Start (Get-Date).AddHours(-24) -EventId SEND -ResultSize 10000 -ErrorAction SilentlyContinue | ForEach-Object { $_.Recipients | ForEach-Object { if ($_ -match '@(.+)$') { $matches[1] } } } | Group-Object | Sort-Object Count -Descending | Select-Object -First 20 foreach ($domain in $domainStats) { if ($domain.Name) { Write-Output "exchange_domain_messages_total{domain=`"$($domain.Name)`",direction=`"outbound`",hostname=`"$Hostname`"} $($domain.Count)" } } } catch {} # ============================================================================ # RETENTION POLICY TAGS (Compliance) # ============================================================================ Write-Output "# HELP exchange_retention_policy_count Number of retention policies" Write-Output "# TYPE exchange_retention_policy_count gauge" try { $retPolicies = (Get-RetentionPolicy -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_retention_policy_count{hostname=`"$Hostname`"} $retPolicies" } catch { Write-Output "exchange_retention_policy_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # JOURNAL RULES # ============================================================================ Write-Output "# HELP exchange_journal_rule_count Number of journal rules" Write-Output "# TYPE exchange_journal_rule_count gauge" try { $journalRules = (Get-JournalRule -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_journal_rule_count{hostname=`"$Hostname`"} $journalRules" } catch { Write-Output "exchange_journal_rule_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # TRANSPORT RULES # ============================================================================ Write-Output "# HELP exchange_transport_rule_count Number of transport rules" Write-Output "# TYPE exchange_transport_rule_count gauge" try { $transportRules = (Get-TransportRule -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_transport_rule_count{hostname=`"$Hostname`"} $transportRules" } catch { Write-Output "exchange_transport_rule_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # ACCEPTED DOMAINS # ============================================================================ Write-Output "# HELP exchange_accepted_domain_count Number of accepted domains" Write-Output "# TYPE exchange_accepted_domain_count gauge" try { $acceptedDomains = (Get-AcceptedDomain -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_accepted_domain_count{hostname=`"$Hostname`"} $acceptedDomains" } catch { Write-Output "exchange_accepted_domain_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # SEND/RECEIVE CONNECTORS # ============================================================================ Write-Output "# HELP exchange_send_connector_enabled Send connector status" Write-Output "# TYPE exchange_send_connector_enabled gauge" try { $sendConnectors = Get-SendConnector -ErrorAction SilentlyContinue foreach ($conn in $sendConnectors) { $connName = $conn.Name -replace '\s+', '_' -replace '[^\w_]', '' $enabled = if ($conn.Enabled) { 1 } else { 0 } Write-Output "exchange_send_connector_enabled{connector=`"$connName`",hostname=`"$Hostname`"} $enabled" } } catch {} Write-Output "# HELP exchange_receive_connector_enabled Receive connector status" Write-Output "# TYPE exchange_receive_connector_enabled gauge" try { $receiveConnectors = Get-ReceiveConnector -ErrorAction SilentlyContinue foreach ($conn in $receiveConnectors) { $connName = $conn.Name -replace '\s+', '_' -replace '[^\w_]', '' $enabled = if ($conn.Enabled) { 1 } else { 0 } Write-Output "exchange_receive_connector_enabled{connector=`"$connName`",hostname=`"$Hostname`"} $enabled" } } catch {} # ============================================================================ # EXECUTION TIME # ============================================================================ # ============================================================================ # AUTODISCOVER # ============================================================================ Write-Output "# HELP exchange_autodiscover_requests_per_second Autodiscover requests per second" Write-Output "# TYPE exchange_autodiscover_requests_per_second gauge" $autodiscoverReq = Get-SafeCounter "\MSExchange Autodiscover\Requests/sec" Write-Output "exchange_autodiscover_requests_per_second{hostname=`"$Hostname`"} $autodiscoverReq" Write-Output "# HELP exchange_autodiscover_errors_total Autodiscover errors" Write-Output "# TYPE exchange_autodiscover_errors_total counter" $autodiscoverErrors = Get-SafeCounter "\MSExchange Autodiscover\Error Responses/sec" Write-Output "exchange_autodiscover_errors_total{hostname=`"$Hostname`"} $autodiscoverErrors" # ============================================================================ # ADDRESS BOOK SERVICE # ============================================================================ Write-Output "# HELP exchange_addressbook_requests_per_second Address Book requests per second" Write-Output "# TYPE exchange_addressbook_requests_per_second gauge" $abRequests = Get-SafeCounter "\MSExchange AddressBook(_total)\Referral RPC Requests/sec" Write-Output "exchange_addressbook_requests_per_second{hostname=`"$Hostname`"} $abRequests" Write-Output "# HELP exchange_addressbook_latency_avg Average Address Book latency (ms)" Write-Output "# TYPE exchange_addressbook_latency_avg gauge" $abLatency = Get-SafeCounter "\MSExchange AddressBook(_total)\RPC Averaged Latency" Write-Output "exchange_addressbook_latency_avg{hostname=`"$Hostname`"} $abLatency" # ============================================================================ # SEARCH / CONTENT INDEXING # ============================================================================ Write-Output "# HELP exchange_search_mailboxes_left Mailboxes left to crawl" Write-Output "# TYPE exchange_search_mailboxes_left gauge" $searchLeft = Get-SafeCounter "\MSExchange Search Indexes(_total)\Mailboxes Left to Crawl" Write-Output "exchange_search_mailboxes_left{hostname=`"$Hostname`"} $searchLeft" Write-Output "# HELP exchange_search_documents_indexed Documents indexed per second" Write-Output "# TYPE exchange_search_documents_indexed gauge" $searchDocs = Get-SafeCounter "\MSExchange Search Indexes(_total)\Average Document Indexing Time" Write-Output "exchange_search_documents_indexed{hostname=`"$Hostname`"} $searchDocs" Write-Output "# HELP exchange_search_index_status Search index catalog status" Write-Output "# TYPE exchange_search_index_status gauge" try { $catalogStatus = Get-MailboxDatabaseCopyStatus -ErrorAction SilentlyContinue | Select-Object DatabaseName, ContentIndexState foreach ($cat in $catalogStatus) { $dbName = $cat.DatabaseName -replace '\s+', '_' $healthy = if ($cat.ContentIndexState -eq 'Healthy') { 1 } else { 0 } Write-Output "exchange_search_index_status{database=`"$dbName`",state=`"$($cat.ContentIndexState)`",hostname=`"$Hostname`"} $healthy" } } catch {} # ============================================================================ # THROTTLING POLICY # ============================================================================ Write-Output "# HELP exchange_throttling_rejected_total Requests rejected by throttling" Write-Output "# TYPE exchange_throttling_rejected_total counter" $throttleRejected = Get-SafeCounter "\MSExchange Throttling\Requests Rejected due to Budget Usage" Write-Output "exchange_throttling_rejected_total{hostname=`"$Hostname`"} $throttleRejected" Write-Output "# HELP exchange_throttling_delayed_total Requests delayed by throttling" Write-Output "# TYPE exchange_throttling_delayed_total counter" $throttleDelayed = Get-SafeCounter "\MSExchange Throttling\Requests Submitted to Delayed Execution" Write-Output "exchange_throttling_delayed_total{hostname=`"$Hostname`"} $throttleDelayed" # ============================================================================ # MAILBOX ASSISTANTS # ============================================================================ Write-Output "# HELP exchange_mailbox_assistant_events Events processed by mailbox assistants" Write-Output "# TYPE exchange_mailbox_assistant_events gauge" $assistantEvents = Get-SafeCounter "\MSExchange Assistants - Per Assistant(_total)\Events Polled" Write-Output "exchange_mailbox_assistant_events{hostname=`"$Hostname`"} $assistantEvents" Write-Output "# HELP exchange_calendar_assistant_requests Calendar assistant requests" Write-Output "# TYPE exchange_calendar_assistant_requests gauge" $calendarAssist = Get-SafeCounter "\MSExchange Calendar Attendant\Requests" Write-Output "exchange_calendar_assistant_requests{hostname=`"$Hostname`"} $calendarAssist" # ============================================================================ # MAILBOX REPLICATION SERVICE (MRS) # ============================================================================ Write-Output "# HELP exchange_mrs_active_moves Active mailbox moves" Write-Output "# TYPE exchange_mrs_active_moves gauge" try { $activeMoves = (Get-MoveRequest -MoveStatus InProgress -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_mrs_active_moves{hostname=`"$Hostname`"} $activeMoves" } catch { Write-Output "exchange_mrs_active_moves{hostname=`"$Hostname`"} 0" } Write-Output "# HELP exchange_mrs_queued_moves Queued mailbox moves" Write-Output "# TYPE exchange_mrs_queued_moves gauge" try { $queuedMoves = (Get-MoveRequest -MoveStatus Queued -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_mrs_queued_moves{hostname=`"$Hostname`"} $queuedMoves" } catch { Write-Output "exchange_mrs_queued_moves{hostname=`"$Hostname`"} 0" } Write-Output "# HELP exchange_mrs_failed_moves Failed mailbox moves" Write-Output "# TYPE exchange_mrs_failed_moves gauge" try { $failedMoves = (Get-MoveRequest -MoveStatus Failed -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_mrs_failed_moves{hostname=`"$Hostname`"} $failedMoves" } catch { Write-Output "exchange_mrs_failed_moves{hostname=`"$Hostname`"} 0" } # ============================================================================ # TRANSPORT PIPELINE LATENCY # ============================================================================ Write-Output "# HELP exchange_transport_latency_seconds Transport component latency" Write-Output "# TYPE exchange_transport_latency_seconds gauge" $latencySmtpReceive = Get-SafeCounter "\MSExchangeTransport SmtpReceive(_total)\Average bytes/message" $latencyCategor = Get-SafeCounter "\MSExchangeTransport Resolver(_total)\Ambiguous Recipients Rate" $latencyDelivery = Get-SafeCounter "\MSExchangeTransport Delivery Failures\Average Delivery Attempts Per Message" Write-Output "exchange_transport_latency_seconds{component=`"smtp_receive`",hostname=`"$Hostname`"} $latencySmtpReceive" Write-Output "exchange_transport_latency_seconds{component=`"categorizer`",hostname=`"$Hostname`"} $latencyCategor" Write-Output "exchange_transport_latency_seconds{component=`"delivery`",hostname=`"$Hostname`"} $latencyDelivery" # ============================================================================ # PROCESS MEMORY/CPU # ============================================================================ Write-Output "# HELP exchange_process_memory_bytes Memory usage by Exchange processes" Write-Output "# TYPE exchange_process_memory_bytes gauge" Write-Output "# HELP exchange_process_cpu_percent CPU usage by Exchange processes" Write-Output "# TYPE exchange_process_cpu_percent gauge" $exchangeProcesses = @( 'EdgeTransport', 'Microsoft.Exchange.Store.Worker', 'MSExchangeTransport', 'MSExchangeHMWorker', 'MSExchangeMailboxAssistants', 'MSExchangeDelivery', 'MSExchangeSubmission', 'w3wp' # IIS worker processes ) foreach ($procName in $exchangeProcesses) { try { $procs = Get-Process -Name $procName -ErrorAction SilentlyContinue if ($procs) { $totalMem = ($procs | Measure-Object -Property WorkingSet64 -Sum).Sum $procNameClean = $procName -replace '\.', '_' Write-Output "exchange_process_memory_bytes{process=`"$procNameClean`",hostname=`"$Hostname`"} $totalMem" } } catch {} } # ============================================================================ # ARCHIVE MAILBOX STATISTICS # ============================================================================ Write-Output "# HELP exchange_archive_mailbox_count Number of archive mailboxes" Write-Output "# TYPE exchange_archive_mailbox_count gauge" try { $archiveCount = (Get-Mailbox -Archive -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_archive_mailbox_count{hostname=`"$Hostname`"} $archiveCount" } catch { Write-Output "exchange_archive_mailbox_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # LITIGATION HOLD # ============================================================================ Write-Output "# HELP exchange_litigation_hold_count Mailboxes on litigation hold" Write-Output "# TYPE exchange_litigation_hold_count gauge" try { $litigationCount = (Get-Mailbox -ResultSize Unlimited -ErrorAction SilentlyContinue | Where-Object { $_.LitigationHoldEnabled -eq $true } | Measure-Object).Count Write-Output "exchange_litigation_hold_count{hostname=`"$Hostname`"} $litigationCount" } catch { Write-Output "exchange_litigation_hold_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # SHARED MAILBOXES # ============================================================================ Write-Output "# HELP exchange_shared_mailbox_count Number of shared mailboxes" Write-Output "# TYPE exchange_shared_mailbox_count gauge" try { $sharedCount = (Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_shared_mailbox_count{hostname=`"$Hostname`"} $sharedCount" } catch { Write-Output "exchange_shared_mailbox_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # ROOM/RESOURCE MAILBOXES # ============================================================================ Write-Output "# HELP exchange_resource_mailbox_count Number of resource mailboxes" Write-Output "# TYPE exchange_resource_mailbox_count gauge" try { $roomCount = (Get-Mailbox -RecipientTypeDetails RoomMailbox -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count $equipmentCount = (Get-Mailbox -RecipientTypeDetails EquipmentMailbox -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_resource_mailbox_count{type=`"room`",hostname=`"$Hostname`"} $roomCount" Write-Output "exchange_resource_mailbox_count{type=`"equipment`",hostname=`"$Hostname`"} $equipmentCount" } catch {} # ============================================================================ # DISTRIBUTION GROUPS # ============================================================================ Write-Output "# HELP exchange_distribution_group_count Number of distribution groups" Write-Output "# TYPE exchange_distribution_group_count gauge" try { $dgCount = (Get-DistributionGroup -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_distribution_group_count{hostname=`"$Hostname`"} $dgCount" } catch { Write-Output "exchange_distribution_group_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # DYNAMIC DISTRIBUTION GROUPS # ============================================================================ Write-Output "# HELP exchange_dynamic_distribution_group_count Number of dynamic distribution groups" Write-Output "# TYPE exchange_dynamic_distribution_group_count gauge" try { $ddgCount = (Get-DynamicDistributionGroup -ResultSize Unlimited -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_dynamic_distribution_group_count{hostname=`"$Hostname`"} $ddgCount" } catch { Write-Output "exchange_dynamic_distribution_group_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # REMOTE DOMAINS # ============================================================================ Write-Output "# HELP exchange_remote_domain_count Number of remote domains configured" Write-Output "# TYPE exchange_remote_domain_count gauge" try { $remoteDomains = (Get-RemoteDomain -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_remote_domain_count{hostname=`"$Hostname`"} $remoteDomains" } catch { Write-Output "exchange_remote_domain_count{hostname=`"$Hostname`"} 0" } # ============================================================================ # UNIFIED MESSAGING (if enabled) # ============================================================================ Write-Output "# HELP exchange_um_calls_current Current UM calls" Write-Output "# TYPE exchange_um_calls_current gauge" $umCalls = Get-SafeCounter "\MSExchangeUMAvailability\Current Calls" Write-Output "exchange_um_calls_current{hostname=`"$Hostname`"} $umCalls" Write-Output "# HELP exchange_um_calls_total Total UM calls" Write-Output "# TYPE exchange_um_calls_total counter" $umCallsTotal = Get-SafeCounter "\MSExchangeUMAvailability\Total Calls" Write-Output "exchange_um_calls_total{hostname=`"$Hostname`"} $umCallsTotal" # ============================================================================ # SUBMISSION QUEUE STATISTICS (detailed) # ============================================================================ Write-Output "# HELP exchange_submission_queue_items_expired Items expired from submission queue" Write-Output "# TYPE exchange_submission_queue_items_expired counter" $subExpired = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Items Expired from Submission Queue" Write-Output "exchange_submission_queue_items_expired{hostname=`"$Hostname`"} $subExpired" Write-Output "# HELP exchange_submission_queue_items_submitted Items submitted to queue" Write-Output "# TYPE exchange_submission_queue_items_submitted counter" $subSubmitted = Get-SafeCounter "\MSExchangeTransport Queues(_total)\Items Submitted Total" Write-Output "exchange_submission_queue_items_submitted{hostname=`"$Hostname`"} $subSubmitted" # ============================================================================ # EXTERNAL RELAY STATISTICS # ============================================================================ Write-Output "# HELP exchange_external_relay_messages External relay message count" Write-Output "# TYPE exchange_external_relay_messages counter" try { $externalRelay = Get-MessageTrackingLog -Start (Get-Date).AddHours(-24) -EventId SEND -ResultSize 5000 -ErrorAction SilentlyContinue | Where-Object { $_.ConnectorId -notmatch 'Internal' } | Measure-Object Write-Output "exchange_external_relay_messages{hostname=`"$Hostname`"} $($externalRelay.Count)" } catch { Write-Output "exchange_external_relay_messages{hostname=`"$Hostname`"} 0" } # ============================================================================ # SPAM/MALWARE QUARANTINE # ============================================================================ Write-Output "# HELP exchange_quarantine_messages Messages in quarantine" Write-Output "# TYPE exchange_quarantine_messages gauge" try { $quarantineCount = (Get-QuarantineMessage -ErrorAction SilentlyContinue | Measure-Object).Count Write-Output "exchange_quarantine_messages{hostname=`"$Hostname`"} $quarantineCount" } catch { Write-Output "exchange_quarantine_messages{hostname=`"$Hostname`"} 0" } # ============================================================================ # HYBRID CONFIGURATION (for O365 hybrid) # ============================================================================ Write-Output "# HELP exchange_hybrid_configured Hybrid configuration status" Write-Output "# TYPE exchange_hybrid_configured gauge" try { $hybrid = Get-HybridConfiguration -ErrorAction SilentlyContinue $isHybrid = if ($hybrid) { 1 } else { 0 } Write-Output "exchange_hybrid_configured{hostname=`"$Hostname`"} $isHybrid" } catch { Write-Output "exchange_hybrid_configured{hostname=`"$Hostname`"} 0" } # ============================================================================ # OAUTH TOKENS (for modern auth) # ============================================================================ Write-Output "# HELP exchange_oauth_token_requests OAuth token requests" Write-Output "# TYPE exchange_oauth_token_requests counter" $oauthRequests = Get-SafeCounter "\MSExchange OAuth\Inbound: Token Requests" Write-Output "exchange_oauth_token_requests{hostname=`"$Hostname`"} $oauthRequests" # ============================================================================ # LAST FULL BACKUP # ============================================================================ Write-Output "# HELP exchange_database_last_backup_seconds Seconds since last full backup" Write-Output "# TYPE exchange_database_last_backup_seconds gauge" try { $databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue foreach ($db in $databases) { $dbName = $db.Name -replace '\s+', '_' if ($db.LastFullBackup) { $backupAge = [math]::Round(((Get-Date) - $db.LastFullBackup).TotalSeconds) Write-Output "exchange_database_last_backup_seconds{database=`"$dbName`",hostname=`"$Hostname`"} $backupAge" } else { Write-Output "exchange_database_last_backup_seconds{database=`"$dbName`",hostname=`"$Hostname`"} -1" } } } catch {} # ============================================================================ # LOG GENERATION RATE # ============================================================================ Write-Output "# HELP exchange_log_generation_rate Log generation checkpoint depth" Write-Output "# TYPE exchange_log_generation_rate gauge" try { $databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue foreach ($db in $databases) { $dbName = $db.Name -replace '\s+', '_' $counterPath = "\MSExchange Database ==> Instances($dbName)\Log Generation Checkpoint Depth" $logDepth = Get-SafeCounter $counterPath Write-Output "exchange_log_generation_rate{database=`"$dbName`",hostname=`"$Hostname`"} $logDepth" } } catch {} # ============================================================================ # AVERAGE MAILBOX SIZE # ============================================================================ Write-Output "# HELP exchange_average_mailbox_size_bytes Average mailbox size" Write-Output "# TYPE exchange_average_mailbox_size_bytes gauge" try { $stats = Get-MailboxStatistics -Server $Hostname -ErrorAction SilentlyContinue | Where-Object { $_.TotalItemSize -ne $null } if ($stats -and $stats.Count -gt 0) { $avgSize = ($stats | ForEach-Object { $_.TotalItemSize.Value.ToBytes() } | Measure-Object -Average).Average Write-Output "exchange_average_mailbox_size_bytes{hostname=`"$Hostname`"} $([math]::Round($avgSize))" } } catch { Write-Output "exchange_average_mailbox_size_bytes{hostname=`"$Hostname`"} 0" } # ============================================================================ # LARGEST MAILBOXES (top 10) # ============================================================================ Write-Output "# HELP exchange_largest_mailbox_bytes Largest mailboxes by size" Write-Output "# TYPE exchange_largest_mailbox_bytes gauge" try { $topMailboxes = Get-MailboxStatistics -Server $Hostname -ErrorAction SilentlyContinue | Where-Object { $_.TotalItemSize -ne $null } | Sort-Object TotalItemSize -Descending | Select-Object -First 10 foreach ($mbx in $topMailboxes) { $mbxName = $mbx.DisplayName -replace '["\s]', '_' -replace '[^\w_]', '' $size = $mbx.TotalItemSize.Value.ToBytes() Write-Output "exchange_largest_mailbox_bytes{mailbox=`"$mbxName`",hostname=`"$Hostname`"} $size" } } catch {} # ============================================================================ # EXECUTION TIME # ============================================================================ $EndTime = Get-Date $Duration = ($EndTime - $StartTime).TotalSeconds Write-Output "# HELP exchange_collector_duration_seconds Time taken to collect metrics" Write-Output "# TYPE exchange_collector_duration_seconds gauge" Write-Output "exchange_collector_duration_seconds{hostname=`"$Hostname`"} $([math]::Round($Duration, 2))" Write-Output "# HELP exchange_collector_last_run_timestamp Unix timestamp of last collection" Write-Output "# TYPE exchange_collector_last_run_timestamp gauge" Write-Output "exchange_collector_last_run_timestamp{hostname=`"$Hostname`"} $([math]::Round((Get-Date -UFormat %s), 0))"