Public/Get-GkServiceHealth.ps1

function Get-GkServiceHealth {
    <#
    .SYNOPSIS
        Report the current health of each Microsoft 365 service, with active incidents.

    .DESCRIPTION
        Reads GET /admin/serviceAnnouncement/healthOverviews for the per-service status, and with
        -IncludeIssue also pulls the open issues behind any service that is not healthy. This is the
        "is it us or is it Microsoft" check — worth running before you start debugging a tenant.

        Requires the ServiceHealth.Read.All scope, which is the only permission Graph accepts for
        this API.

    .PARAMETER Service
        Only return services whose name contains this text (case-insensitive), e.g. 'Exchange'.

    .PARAMETER UnhealthyOnly
        Only return services that are not reporting serviceOperational.

    .PARAMETER IncludeIssue
        Add the open issues for each returned service. Costs one extra Graph call.

    .PARAMETER AsReport
        Add a ReportGeneratedUtc column.

    .EXAMPLE
        Get-GkServiceHealth -UnhealthyOnly

        Only the services currently degraded or in incident.

    .EXAMPLE
        Get-GkServiceHealth -UnhealthyOnly -IncludeIssue

        Degraded services with the incidents behind them.

    .EXAMPLE
        Get-GkServiceHealth -AsReport | Export-Csv .\service-health.csv -NoTypeInformation

    .OUTPUTS
        PSGraphKit.ServiceHealth
    #>

    [CmdletBinding()]
    [OutputType('PSGraphKit.ServiceHealth')]
    param(
        [string] $Service,

        [switch] $UnhealthyOnly,

        [switch] $IncludeIssue,

        [switch] $AsReport
    )

    begin {
        Test-GkConnection -FunctionName 'Get-GkServiceHealth' -Caller $PSCmdlet | Out-Null
        $now = [datetime]::UtcNow
    }

    process {
        $overviews = Invoke-GkGraphRequest -Uri '/admin/serviceAnnouncement/healthOverviews' -CallerFunction 'Get-GkServiceHealth'

        # Pull issues once and group them, rather than per service.
        $issuesByService = @{}
        if ($IncludeIssue) {
            try {
                $issues = Invoke-GkGraphRequest -Uri '/admin/serviceAnnouncement/issues' -CallerFunction 'Get-GkServiceHealth'
                foreach ($i in $issues) {
                    if ([string](Get-GkDictValue $i 'isResolved') -eq 'True') { continue }
                    $svc = [string](Get-GkDictValue $i 'service')
                    if (-not $issuesByService.ContainsKey($svc)) { $issuesByService[$svc] = @() }
                    $issuesByService[$svc] += "$([string](Get-GkDictValue $i 'id')): $([string](Get-GkDictValue $i 'title'))"
                }
            }
            catch {
                Write-Warning "Could not read service issues; reporting status only. $($_.Exception.Message)"
            }
        }

        foreach ($o in $overviews) {
            $name = [string](Get-GkDictValue $o 'service')
            $status = [string](Get-GkDictValue $o 'status')

            if ($Service -and $name -notlike "*$Service*") { continue }
            if ($UnhealthyOnly -and $status -eq 'serviceOperational') { continue }

            $obj = [ordered]@{
                PSTypeName = 'PSGraphKit.ServiceHealth'
                Service    = $name
                Status     = $status
                IsHealthy  = ($status -eq 'serviceOperational')
                Id         = [string](Get-GkDictValue $o 'id')
            }
            if ($IncludeIssue) {
                # A missing key yields $null, and @($null) is a one-element array — check the key.
                $open = @()
                if ($issuesByService.ContainsKey($name)) { $open = @($issuesByService[$name]) }
                $obj['OpenIssueCount'] = $open.Count
                if ($AsReport) { $obj['OpenIssues'] = ($open -join '; ') }
                else { $obj['OpenIssues'] = [string[]]$open }
            }
            if ($AsReport) { $obj['ReportGeneratedUtc'] = $now }
            [pscustomobject]$obj
        }
    }
}