From b63f0db51fbe8eeff6a1a45d3615ab324aed7d11 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 1 Oct 2026 17:06:01 +0100 Subject: [PATCH] fix: render health monitoring service details --- .../Invoke-DailySupportCheck.ps1 | 53 ++++++++++- .../Tests/Invoke-DailySupportCheck.Tests.ps1 | 95 +++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 b/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 index 45b88e8..41f444c 100644 --- a/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 +++ b/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 @@ -393,7 +393,22 @@ function Test-HealthMonitoring { $provider = $Context.HealthMonitoringProvider } - $services = @(& $provider $uri $timeoutSeconds) + $services = @((& $provider $uri $timeoutSeconds)) + $serviceIdValue = if ($services.Count -eq 1 -and $services[0].PSObject.Properties['ServiceId']) { $services[0].ServiceId } + $isServiceCollection = $serviceIdValue -is [System.Collections.IEnumerable] -and $serviceIdValue -isnot [string] + if ($services.Count -eq 1 -and $isServiceCollection) { + $wrappedServices = $services[0] + $serviceCount = @($serviceIdValue).Count + $services = for ($index = 0; $index -lt $serviceCount; $index++) { + $service = [ordered]@{} + foreach ($property in $wrappedServices.PSObject.Properties) { + $value = $property.Value + $isCollection = $value -is [System.Collections.IEnumerable] -and $value -isnot [string] + $service[$property.Name] = if ($isCollection -and @($value).Count -eq $serviceCount) { @($value)[$index] } else { $value } + } + [pscustomobject] $service + } + } if ($services.Count -eq 0) { return Get-CheckResult -Name 'HealthMonitoring' -Status Warning -Summary 'HealthMonitoring returned no monitored services.' -Details @() } @@ -403,6 +418,13 @@ function Test-HealthMonitoring { if ([string]::IsNullOrWhiteSpace($status)) { $status = 'Unknown' } + $status = switch ($status) { + '0' { 'Unknown' } + '1' { 'Healthy' } + '2' { 'Degraded' } + '3' { 'Unhealthy' } + default { $status } + } $status = (Get-Culture).TextInfo.ToTitleCase($status.ToLowerInvariant()) [pscustomobject]@{ ServiceId = [string] $service.ServiceId @@ -980,17 +1002,44 @@ function Write-SupportReports { [void] $subscriptionDetails.AppendLine('') } + $healthMonitoringDetails = [System.Text.StringBuilder]::new() + $healthMonitoringCheck = @($Report.Checks | Where-Object Name -eq 'HealthMonitoring') | Select-Object -First 1 + $healthMonitoringItems = if ($null -ne $healthMonitoringCheck) { + @($healthMonitoringCheck.Details | Where-Object { $_.PSObject.Properties['ServiceId'] }) + } + $healthMonitoringItems = @($healthMonitoringItems) + if ($healthMonitoringItems.Count -gt 0) { + [void] $healthMonitoringDetails.AppendLine('

HealthMonitoring details

') + [void] $healthMonitoringDetails.AppendLine('
') + foreach ($service in $healthMonitoringItems) { + $detailStatus = ConvertTo-HtmlEncodedText $service.Status + $detailStatusClass = [System.Net.WebUtility]::HtmlEncode("status-$([string] $service.Status)") + $detailStatusIcon = switch ([string] $service.Status) { + 'Passed' { '🟢' } + 'Healthy' { '🟢' } + 'Warning' { '🟠' } + 'Degraded' { '🟠' } + 'Failed' { '🔴' } + 'Unhealthy' { '🔴' } + default { '⚪' } + } + [void] $healthMonitoringDetails.AppendLine("") + } + [void] $healthMonitoringDetails.AppendLine('
Service IDNameStatusLast observedLast error
$(ConvertTo-HtmlEncodedText $service.ServiceId)$(ConvertTo-HtmlEncodedText $service.Name)$detailStatusIcon $detailStatus$(ConvertTo-HtmlEncodedText $service.LastObservedAtUtc)$(ConvertTo-HtmlEncodedText $service.LastError)
') + } + $overallStatus = [System.Net.WebUtility]::HtmlEncode([string] $Report.OverallStatus) $html = @" Daily Support Check - +

Daily Support Check

Overall status: $overallStatus

Started: $([System.Net.WebUtility]::HtmlEncode([string] $Report.StartedAt))
Completed: $([System.Net.WebUtility]::HtmlEncode([string] $Report.CompletedAt))

$rows
CheckStatusSummary
$subscriptionDetails +$healthMonitoringDetails "@ $html | Set-Content -LiteralPath $htmlPath -Encoding UTF8 diff --git a/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 b/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 index c0445ec..42c67ce 100644 --- a/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 +++ b/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 @@ -163,6 +163,42 @@ Describe 'Daily support check' { $html | Should -Match 'Main & Ordered' } + It 'renders health-monitoring service details beneath the check summary in the HTML report' { + $outputPath = Join-Path $testOutputRoot 'health-monitoring-details' + $details = @( + [pscustomobject]@{ + ServiceId = 'orders' + Name = 'Orders & Payments' + Status = 'Healthy' + LastObservedAtUtc = '2026-10-01T15:00:00Z' + LastError = $null + } + [pscustomobject]@{ + ServiceId = 'payments' + Name = 'Payments' + Status = 'Unhealthy' + LastObservedAtUtc = '2026-10-01T14:59:00Z' + LastError = 'Health endpoint unavailable' + } + ) + $check = Get-CheckResult -Name 'HealthMonitoring' -Status Failed -Summary '1 monitored service(s) are unhealthy or unknown.' -Details $details + $report = Get-SupportReport -StartedAt ([datetime]::UtcNow) -Results @($check) + + $paths = Write-SupportReports -Report $report -OutputPath $outputPath + $html = Get-Content -Raw -Path $paths.HtmlPath + + $html | Should -Match 'HealthMonitoring details' + $html | Should -Match 'Service IDNameStatus' + $html | Should -Match 'orders' + $html | Should -Match 'Orders & Payments' + $html | Should -Match 'Health endpoint unavailable' + $html | Should -Match 'status-Healthy' + $html | Should -Match 'status-Unhealthy' + $html | Should -Match '🔴 Unhealthy' + $html | Should -Match '\.status-Passed,.status-Healthy\{background:#e4f4e4\}' + $html | Should -Match '\.status-Failed,.status-Unhealthy\{background:#fde2e2\}' + } + It 'removes matching reports older than the configured retention period' { $outputPath = Join-Path $testOutputRoot 'retention' New-Item -ItemType Directory -Path $outputPath -Force | Out-Null @@ -456,6 +492,65 @@ Describe 'Daily support check' { $result.Summary | Should -Match '2 monitored service' } + It 'expands wrapped enumerable health-monitoring services returned by the API' { + $context = [pscustomobject]@{ + Configuration = [pscustomobject]@{ + HealthMonitoring = [pscustomobject]@{ + Enabled = $true + BaseUrl = 'http://health-monitoring' + ServicesPath = '/api/services' + TimeoutSeconds = 10 + } + } + HealthMonitoringProvider = { + [pscustomobject]@{ + ServiceId = [System.Collections.ArrayList]::new(@('orders', 'payments')) + Name = [System.Collections.ArrayList]::new(@('Orders', 'Payments')) + Status = [System.Collections.ArrayList]::new(@(1, 1)) + LastObservedAtUtc = $null + LastError = $null + } + } + } + + $result = Test-HealthMonitoring -Context $context + + $result.Status | Should -Be 'Passed' + $result.Details.Count | Should -Be 2 + $result.Details[0].ServiceId | Should -Be 'orders' + $result.Details[0].Name | Should -Be 'Orders' + $result.Details[0].Status | Should -Be 'Healthy' + $result.Details[1].ServiceId | Should -Be 'payments' + $result.Details[1].Name | Should -Be 'Payments' + $result.Details[1].Status | Should -Be 'Healthy' + } + + It 'unwraps an array returned as one health-monitoring provider result' { + $context = [pscustomobject]@{ + Configuration = [pscustomobject]@{ + HealthMonitoring = [pscustomobject]@{ + Enabled = $true + BaseUrl = 'http://health-monitoring' + ServicesPath = '/api/services' + TimeoutSeconds = 10 + } + } + HealthMonitoringProvider = { + $apiResponse = @( + [pscustomobject]@{ ServiceId = 'orders'; Name = 'Orders'; Status = 1 } + [pscustomobject]@{ ServiceId = 'payments'; Name = 'Payments'; Status = 1 } + ) + ,$apiResponse + } + } + + $result = Test-HealthMonitoring -Context $context + + $result.Status | Should -Be 'Passed' + $result.Details.Count | Should -Be 2 + $result.Details.ServiceId | Should -Be @('orders', 'payments') + } + It 'returns Warning for degraded services and Failed for unhealthy services' { $configuration = [pscustomobject]@{ HealthMonitoring = [pscustomobject]@{