Public/healthcheck/Get-ExchangeServerHealth.ps1

#Requires -Version 5.1
function Get-ExchangeServerHealth {
    <#
        .SYNOPSIS
            Checks Exchange Server health on Windows servers
 
        .DESCRIPTION
            Retrieves comprehensive Exchange Server health information including service status,
            mailbox database mount status, mail queue lengths, DAG database copy health,
            and certificate expiry. Returns a single typed object per server with an overall
            health assessment suitable for dashboards and alerting pipelines.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local machine.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional PSCredential for authenticating to remote computers.
            Not used for local queries.
 
        .PARAMETER QueueWarningThreshold
            Number of messages in a single queue that triggers a Degraded health status.
            Defaults to 100.
 
        .PARAMETER QueueCriticalThreshold
            Number of messages in a single queue that triggers a Critical health status.
            Defaults to 500.
 
        .PARAMETER CertificateWarningDays
            Number of days before certificate expiry that triggers a Degraded health status.
            Defaults to 30.
 
        .EXAMPLE
            Get-ExchangeServerHealth
 
            Checks Exchange Server health on the local machine using default thresholds.
 
        .EXAMPLE
            Get-ExchangeServerHealth -ComputerName 'EX01' -QueueWarningThreshold 50
 
            Checks Exchange Server health on a single remote server with a custom queue warning threshold.
 
        .EXAMPLE
            'EX01', 'EX02' | Get-ExchangeServerHealth -Credential (Get-Credential)
 
            Checks Exchange Server health on multiple remote servers via pipeline with alternate credentials.
 
        .OUTPUTS
            PSWinOps.ExchangeServerHealth
            Returns one object per server with Exchange service statuses, database counts,
            queue metrics, DAG copy health, certificate expiry counts, and overall health.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.1.0
            Last Modified: 2026-04-02
            Requires: PowerShell 5.1+ / Windows only
            Requires: Exchange Server 2016+ Management Tools
            Requires: Module ExchangeManagementShell
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/powershell/exchange/exchange-management-shell
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.ExchangeServerHealth')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 10000)]
        [int]$QueueWarningThreshold = 100,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 100000)]
        [int]$QueueCriticalThreshold = 500,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 365)]
        [int]$CertificateWarningDays = 30
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        $scriptBlock = {
            param($certWarnDays)

            $data = @{
                TransportStatus          = 'NotFound'
                InformationStoreStatus   = 'NotFound'
                ADTopologyStatus         = 'NotFound'
                ServiceHostStatus        = 'NotFound'
                SnapinAvailable          = $false
                TotalDatabases           = 0
                MountedDatabases         = 0
                DismountedDatabases      = 0
                TotalQueues              = 0
                TotalQueueMessages       = 0
                HighestQueueLength       = 0
                DAGCopiesTotal           = 0
                DAGCopiesHealthy         = 0
                DAGCopiesUnhealthy       = 0
                CertificatesTotal        = 0
                CertificatesExpiringSoon = 0
                CertificatesExpired      = 0
            }

            # Check Exchange services
            $exchangeServices = @{
                'MSExchangeTransport'   = 'TransportStatus'
                'MSExchangeIS'          = 'InformationStoreStatus'
                'MSExchangeADTopology'  = 'ADTopologyStatus'
                'MSExchangeServiceHost' = 'ServiceHostStatus'
            }
            foreach ($svcName in $exchangeServices.Keys) {
                $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
                if ($svc) {
                    $data[$exchangeServices[$svcName]] = $svc.Status.ToString()
                }
            }

            # Try loading Exchange Management Shell
            # Add-PSSnapin is Desktop-edition only; on PS 7 Core skip straight to Import-Module
            if ($PSVersionTable.PSEdition -ne 'Core') {
                try {
                    Add-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.SnapIn' -ErrorAction Stop
                    $data.SnapinAvailable = $true
                } catch {
                    Write-Verbose "[$($MyInvocation.MyCommand)] Snapin not available - falling back to Import-Module"
                }
            }

            if (-not $data.SnapinAvailable) {
                try {
                    Import-Module -Name 'ExchangeManagementShell' -ErrorAction Stop
                    $data.SnapinAvailable = $true
                } catch {
                    $data.SnapinAvailable = $false
                }
            }

            if ($data.SnapinAvailable) {
                # Mailbox databases
                $databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue
                if ($databases) {
                    $data.TotalDatabases = @($databases).Count
                    $data.MountedDatabases = @($databases | Where-Object { $_.Mounted -eq $true }).Count
                    $data.DismountedDatabases = $data.TotalDatabases - $data.MountedDatabases
                }

                # Mail queues
                $queues = Get-Queue -ErrorAction SilentlyContinue
                if ($queues) {
                    $data.TotalQueues = @($queues).Count
                    $data.TotalQueueMessages = ($queues | Measure-Object -Property MessageCount -Sum).Sum
                    $data.HighestQueueLength = ($queues | Measure-Object -Property MessageCount -Maximum).Maximum
                }

                # DAG database copy status
                $copies = Get-MailboxDatabaseCopyStatus -ErrorAction SilentlyContinue
                if ($copies) {
                    $data.DAGCopiesTotal = @($copies).Count
                    $healthyCopyStatuses = @('Mounted', 'Healthy')
                    $data.DAGCopiesHealthy = @($copies | Where-Object { $_.Status -in $healthyCopyStatuses }).Count
                    $data.DAGCopiesUnhealthy = $data.DAGCopiesTotal - $data.DAGCopiesHealthy
                }

                # Exchange certificates
                $certs = Get-ExchangeCertificate -ErrorAction SilentlyContinue
                if ($certs) {
                    $now = Get-Date
                    $data.CertificatesTotal = @($certs).Count
                    $data.CertificatesExpired = @($certs | Where-Object { $_.NotAfter -lt $now }).Count
                    $data.CertificatesExpiringSoon = @($certs | Where-Object {
                            $_.NotAfter -ge $now -and $_.NotAfter -lt $now.AddDays($certWarnDays)
                        }).Count
                }
            }

            $data
        }
    }

    process {
        foreach ($machine in $ComputerName) {
            $displayName = $machine.ToUpper()
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying '${machine}'"

            try {
                $result = Invoke-RemoteOrLocal -ComputerName $machine -ScriptBlock $scriptBlock `
                    -Credential $Credential `
                    -ArgumentList @($CertificateWarningDays)

                # Compute OverallHealth outside the scriptblock
                if (-not $result.SnapinAvailable) {
                    $healthStatus = [PSWinOpsHealthStatus]::RoleUnavailable
                } elseif (
                    $result.TransportStatus -ne 'Running' -or
                    $result.InformationStoreStatus -ne 'Running' -or
                    $result.DismountedDatabases -gt 0 -or
                    $result.HighestQueueLength -ge $QueueCriticalThreshold
                ) {
                    $healthStatus = [PSWinOpsHealthStatus]::Critical
                } elseif (
                    $result.HighestQueueLength -ge $QueueWarningThreshold -or
                    $result.DAGCopiesUnhealthy -gt 0 -or
                    $result.CertificatesExpiringSoon -gt 0 -or
                    $result.CertificatesExpired -gt 0 -or
                    $result.ADTopologyStatus -ne 'Running' -or
                    $result.ServiceHostStatus -ne 'Running'
                ) {
                    $healthStatus = [PSWinOpsHealthStatus]::Degraded
                } else {
                    $healthStatus = [PSWinOpsHealthStatus]::Healthy
                }

                [PSCustomObject]@{
                    PSTypeName               = 'PSWinOps.ExchangeServerHealth'
                    ComputerName             = $displayName
                    TransportStatus          = $result.TransportStatus
                    InformationStoreStatus   = $result.InformationStoreStatus
                    ADTopologyStatus         = $result.ADTopologyStatus
                    ServiceHostStatus        = $result.ServiceHostStatus
                    TotalDatabases           = [int]$result.TotalDatabases
                    MountedDatabases         = [int]$result.MountedDatabases
                    DismountedDatabases      = [int]$result.DismountedDatabases
                    TotalQueues              = [int]$result.TotalQueues
                    TotalQueueMessages       = [int]$result.TotalQueueMessages
                    HighestQueueLength       = [int]$result.HighestQueueLength
                    DAGCopiesTotal           = [int]$result.DAGCopiesTotal
                    DAGCopiesHealthy         = [int]$result.DAGCopiesHealthy
                    DAGCopiesUnhealthy       = [int]$result.DAGCopiesUnhealthy
                    CertificatesTotal        = [int]$result.CertificatesTotal
                    CertificatesExpiringSoon = [int]$result.CertificatesExpiringSoon
                    CertificatesExpired      = [int]$result.CertificatesExpired
                    OverallHealth            = $healthStatus
                    Timestamp                = Get-Date -Format 'o'
                }
            } catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_"
                continue
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}