Files
linux-scripts/exchange-metrics.ps1
T

1257 lines
67 KiB
PowerShell

# Exchange Metrics Collector - Outputs Prometheus-compatible metrics
# Requires Exchange Management Shell and appropriate permissions
$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))"