Public/Get-ContainerAppDiagnostics.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Get-ContainerAppDiagnostics {
    <#
    .SYNOPSIS
        Gathers revision state and logs for a failed Azure Container App revision.
    .DESCRIPTION
        Combines three az calls into one diagnostic snapshot (t/1500 Phase 3):
          - az containerapp revision show → RevisionState (RunningState, HealthState, etc.)
          - az containerapp logs show --type console → last -TailLines lines of app output
          - az containerapp logs show --type system → last 50 lines of platform events
 
        Console log availability is not instantaneous after deploy. The cmdlet polls
        up to 30 seconds (6 attempts × 5-second sleep) before declaring logs
        unavailable. System logs use a fixed tail of 50 lines (they are noisier).
 
        Per-log-fetch failures are non-fatal — a warning is emitted and the field
        returns an empty array. Only a failure in az containerapp revision show
        (revision doesn't exist / az not logged in) is fatal.
    .PARAMETER RevisionName
        The full revision name to inspect (e.g. 'taxonomy-editor--deploy-abc1234').
    .PARAMETER AppName
        Container App name. Default: 'taxonomy-editor'.
    .PARAMETER ResourceGroup
        Azure resource group. Default: 'ai-triad'.
    .PARAMETER TailLines
        Number of console log lines to return. Default: 100.
    .EXAMPLE
        Get-ContainerAppDiagnostics -RevisionName 'taxonomy-editor--deploy-abc1234'
    .EXAMPLE
        Get-ContainerAppDiagnostics -RevisionName 'taxonomy-editor--deploy-abc1234' -TailLines 200
    .LINK
        Get-ContainerAppRevision
    .LINK
        New-ContainerAppRevision
    .LINK
        Set-ContainerAppTraffic
    .LINK
        Disable-ContainerAppRevision
    .LINK
        Show-AITriadHelp
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RevisionName,

        [Parameter()]
        [string]$AppName = 'taxonomy-editor',

        [Parameter()]
        [string]$ResourceGroup = 'ai-triad',

        [Parameter()]
        [ValidateRange(1, 1000)]
        [int]$TailLines = 100
    )

    Set-StrictMode -Version Latest

    $AzCmd = Get-Command az -ErrorAction SilentlyContinue
    if (-not $AzCmd) {
        throw (New-ActionableError `
            -Goal "Get diagnostics for revision '$RevisionName'" `
            -Problem 'Azure CLI (az) not found on PATH' `
            -Location 'Get-ContainerAppDiagnostics' `
            -NextSteps @('Install Azure CLI: https://aka.ms/installazurecli',
                         'Ensure az is on your PATH'))
    }

    # --- Revision state (fatal if this fails) ---
    $RevJson = Invoke-Az @(
        'containerapp', 'revision', 'show',
        '--name', $AppName,
        '--resource-group', $ResourceGroup,
        '--revision', $RevisionName,
        '--output', 'json'
    ) -CallerName 'Get-ContainerAppDiagnostics'

    $RevObj = $RevJson | ConvertFrom-Json
    $RunningState  = ''
    $HealthState   = ''
    $Replicas      = 0
    $StatusMessage = ''

    if ($RevObj.PSObject.Properties['properties']) {
        $Props = $RevObj.properties
        if ($Props.PSObject.Properties['runningState'])  { $RunningState  = [string]$Props.runningState  }
        if ($Props.PSObject.Properties['healthState'])   { $HealthState   = [string]$Props.healthState   }
        if ($Props.PSObject.Properties['replicas'])      { $Replicas      = [int]$Props.replicas         }
        if ($Props.PSObject.Properties['statusMessage']) { $StatusMessage = [string]$Props.statusMessage }
    }

    $RevisionState = [PSCustomObject]@{
        RunningState  = $RunningState
        HealthState   = $HealthState
        Replicas      = $Replicas
        StatusMessage = $StatusMessage
    }

    # --- Console logs with poll loop (non-fatal if unavailable) ---
    $ConsoleLogs   = @()
    $LogsAvailable = $false
    $MaxPolls      = 6
    $PollSleepSec  = 5

    for ($i = 1; $i -le $MaxPolls; $i++) {
        try {
            $RawConsole = Invoke-Az @(
                'containerapp', 'logs', 'show',
                '--name', $AppName,
                '--resource-group', $ResourceGroup,
                '--revision', $RevisionName,
                '--type', 'console',
                '--tail', [string]$TailLines,
                '--output', 'table'
            ) -CallerName 'Get-ContainerAppDiagnostics'

            if (-not [string]::IsNullOrWhiteSpace($RawConsole)) {
                $ConsoleLogs   = @($RawConsole -split "`n" | Where-Object { $_ -ne '' })
                $LogsAvailable = $true
                break
            }
        }
        catch {
            Write-Warning "Get-ContainerAppDiagnostics: console log fetch attempt $i failed (non-fatal): $_"
            break
        }

        if ($i -lt $MaxPolls) {
            Write-Verbose "Get-ContainerAppDiagnostics: console logs not yet available, waiting ${PollSleepSec}s (attempt $i of $MaxPolls)..."
            Start-Sleep -Seconds $PollSleepSec
        }
    }

    if (-not $LogsAvailable) {
        Write-Verbose "Get-ContainerAppDiagnostics: console logs unavailable after $($MaxPolls * $PollSleepSec)s polling — returning empty array."
    }

    # --- System logs (non-fatal; fixed 50-line tail — system logs are noisier) ---
    $SystemLogs = @()
    try {
        $RawSystem = Invoke-Az @(
            'containerapp', 'logs', 'show',
            '--name', $AppName,
            '--resource-group', $ResourceGroup,
            '--type', 'system',
            '--tail', '50',
            '--output', 'table'
        ) -CallerName 'Get-ContainerAppDiagnostics'

        if (-not [string]::IsNullOrWhiteSpace($RawSystem)) {
            $SystemLogs = @($RawSystem -split "`n" | Where-Object { $_ -ne '' })
        }
    }
    catch {
        Write-Warning "Get-ContainerAppDiagnostics: system log fetch failed (non-fatal): $_"
    }

    [PSCustomObject]@{
        RevisionName   = $RevisionName
        RevisionState  = $RevisionState
        ConsoleLogs    = $ConsoleLogs
        SystemLogs     = $SystemLogs
        LogsAvailable  = $LogsAvailable
    }
}