Public/System/Get-JIMServiceHealth.ps1

# Copyright (c) Tetron Limited. All rights reserved.
# Licensed under the Tetron Commercial License. See LICENSE file in the project root.

function Get-JIMServiceHealth {
    <#
    .SYNOPSIS
        Reports whether JIM's background services (the Worker and the Scheduler) are alive and what each is doing.
 
    .DESCRIPTION
        Reads the heartbeat every JIM background service writes to the database every 5 seconds and returns one
        object per service: WorkerSync (the Worker's synchronisation loop, which runs Run Profiles and other
        queued work), WorkerDelivery (the Worker's password delivery loop) and Scheduler (which starts
        Schedules when they fall due). This is the same report the Service Health strip on the Operations page
        shows, so a script and an administrator always see the same verdict.
 
        Each service's Status is one of:
 
        - Healthy: heartbeating within the last 15 seconds. Nothing to do.
        - Degraded: alive, but something is not right. Condition says what: HeartbeatOverdue (more than 15 seconds
          since the last heartbeat, not yet long enough to presume the process gone; it may be paused under load or
          the database may be slow) or Stalled (its current work has not moved forward for more than 10 minutes;
          the process is up, the task it is running may be wedged; only judged for work that reports progress).
        - Unhealthy: presumed down. Condition is NoHeartbeat (none for 60 seconds for the Worker services, 120
          seconds for the Scheduler) or NeverStarted (it has never reported at all). Queued and scheduled work will
          not run until it is back.
 
        A Healthy service's Condition is Heartbeating.
 
        Reason puts the condition in plain words with the figures that matter. CurrentWork names what the service was doing when it last reported (for
        example "Full Import: Corporate Directory"), and Version is the JIM version that instance runs: compare it
        with the web tier's version from -Summary, because a mismatch means a partial upgrade.
 
        With -Summary, one object is returned instead: Overall (the worst status present), WebVersion, GeneratedAt
        and the per-service objects under Services. A monitoring script that alerts on anything other than Healthy
        needs to read nothing but Overall.
 
        Get-JIMHealth answers a different question: it probes the web tier only, without authentication, and says
        nothing about the Worker or the Scheduler. This cmdlet needs a Connect-JIM session with the Administrator
        role.
 
    .PARAMETER Summary
        Returns one object carrying Overall, WebVersion, GeneratedAt and Services, instead of one object per
        service.
 
    .OUTPUTS
        JIM.ServiceHealth
        One object per service with Service, Status, Condition, Reason, CurrentWork, CurrentWorkStartedAt, LastSeenAt,
        StartedAt, HostName, Version, InstanceId, LastProgressAt and Detail. Fields a never-seen service cannot
        supply are null.
 
        JIM.ServiceHealthSummary
        With -Summary: one object with Overall, WebVersion, GeneratedAt and Services (the JIM.ServiceHealth
        objects above).
 
    .EXAMPLE
        Get-JIMServiceHealth
 
        Lists the services with their status, condition and reason. The quickest way to find out whether the Worker is
        running and what it is doing.
 
    .EXAMPLE
        Get-JIMServiceHealth | Format-Table Service, Status, CurrentWork, LastSeenAt, Version
 
        The columns that matter during a change window: what each service is doing, when it last reported, and
        which version it is running.
 
    .EXAMPLE
        $health = Get-JIMServiceHealth -Summary
        if ($health.Overall -ne 'Healthy') {
            $health.Services | Where-Object Status -ne 'Healthy' | Format-List Service, Status, Condition, Reason
            exit 1
        }
 
        A monitoring check. Exits non-zero when any service is Degraded or Unhealthy, printing which and why, so a
        scheduler or pipeline can raise an alert on the exit code.
 
    .EXAMPLE
        Get-JIMServiceHealth | Where-Object Condition -eq 'Stalled' | Select-Object Service, CurrentWork, LastProgressAt
 
        Names the work that has stalled. The process is up; the task is what needs looking at, and the Operations
        page's Queue tab is where to cancel it if it is genuinely wedged.
 
    .EXAMPLE
        $health = Get-JIMServiceHealth -Summary
        $health.Services | Where-Object { $_.Version -and $_.Version -ne $health.WebVersion } |
            Select-Object Service, HostName, Version, @{ Name = 'WebVersion'; Expression = { $health.WebVersion } }
 
        Finds services running a different version from the web tier, which after an upgrade means one container
        did not restart.
 
    .LINK
        Get-JIMHealth
        Get-JIMVersion
    #>

    [CmdletBinding(DefaultParameterSetName = 'Services')]
    [OutputType('JIM.ServiceHealth', ParameterSetName = 'Services')]
    [OutputType('JIM.ServiceHealthSummary', ParameterSetName = 'Summary')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Summary')]
        [switch]$Summary
    )

    process {
        if (-not $script:JIMConnection) {
            Write-Error "You are not connected to JIM. Run Connect-JIM -Url <your JIM URL> to authenticate, then try again."
            return
        }

        Write-Verbose "Getting service health"
        $response = Invoke-JIMApi -Endpoint '/api/v1/system/health'
        if ($null -eq $response) {
            return
        }

        # Built explicitly rather than passed through, so the output shape is the documented one whatever the
        # wire's casing, and so the property order reads as an operator wants it: what is it, is it well, why,
        # what is it doing; the identifying detail after.
        $services = foreach ($service in @($response.services)) {
            $item = [PSCustomObject]@{
                Service              = $service.service
                Status               = $service.status
                Condition            = $service.condition
                Reason               = $service.reason
                CurrentWork          = $service.currentWork
                CurrentWorkStartedAt = $service.currentWorkStartedAt
                LastSeenAt           = $service.lastSeenAt
                StartedAt            = $service.startedAt
                HostName             = $service.hostName
                Version              = $service.version
                InstanceId           = $service.instanceId
                LastProgressAt       = $service.lastProgressAt
                Detail               = $service.detail
            }
            $item.PSObject.TypeNames.Insert(0, 'JIM.ServiceHealth')
            $item
        }

        if ($PSCmdlet.ParameterSetName -eq 'Summary') {
            $summaryObject = [PSCustomObject]@{
                Overall     = $response.overall
                WebVersion  = $response.webVersion
                GeneratedAt = $response.generatedAt
                Services    = @($services)
            }
            $summaryObject.PSObject.TypeNames.Insert(0, 'JIM.ServiceHealthSummary')
            $summaryObject
            return
        }

        $services
    }
}