Public/Get-AzureFlightRecorder.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Get-AzureFlightRecorder { <# .SYNOPSIS Retrieves flight recorder dumps from the Azure-hosted Taxonomy Editor. .DESCRIPTION Reads flight recorder dumps from the GitHub data repo (works even when the app is not running). Uses 'gh api' for authentication. Use -TriggerDump to create a fresh dump on the running server (requires session cookie for Azure Easy Auth). .PARAMETER List List available dump files without downloading. .PARAMETER Last Download the N most recent dump files. Default: 1. .PARAMETER TriggerDump Trigger a fresh server-side flight recorder dump, then download it. Requires the app to be running and a valid session cookie. .PARAMETER Filename Download a specific dump file by name. .PARAMETER OutputPath Directory to save downloaded files. Default: current directory. .PARAMETER Open Open downloaded dumps in the flight recorder viewer after saving. .PARAMETER DataRepo GitHub data repo in owner/repo format. .PARAMETER SessionCookie AppServiceAuthSession cookie value for -TriggerDump. Falls back to $env:TAXONOMY_SESSION_COOKIE. .PARAMETER BaseUrl Base URL of the Taxonomy Editor instance (for -TriggerDump). .EXAMPLE Get-AzureFlightRecorder -List .EXAMPLE Get-AzureFlightRecorder -Last 3 .EXAMPLE Get-AzureFlightRecorder -TriggerDump .EXAMPLE Get-AzureFlightRecorder -Filename 'server-flight-recorder-2026-06-12T10-30-00.000Z.jsonl' #> [CmdletBinding(DefaultParameterSetName = 'List')] param( [Parameter(ParameterSetName = 'List')] [switch]$List, [Parameter(ParameterSetName = 'Download')] [int]$Last = 1, [Parameter(ParameterSetName = 'Trigger')] [switch]$TriggerDump, [Parameter(ParameterSetName = 'ByName', Mandatory)] [string]$Filename, [Parameter()] [string]$OutputPath = '.', [Parameter()] [switch]$Open, [Parameter()] [string]$DataRepo = 'jpsnover/ai-triad-data', [Parameter()] [string]$SessionCookie, [Parameter()] [string]$BaseUrl = 'https://taxonomy-editor.yellowbush-aeda037d.eastus.azurecontainerapps.io' ) $BaseUrl = $BaseUrl.TrimEnd('/') # ── Verify gh CLI is available ── function Assert-GhCli { if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { throw (New-ActionableError ` -Goal 'Read flight recorder dumps from GitHub' ` -Problem 'The GitHub CLI (gh) is not installed or not in PATH' ` -Location 'Get-AzureFlightRecorder' ` -NextSteps @( 'Install: winget install GitHub.cli', 'Then authenticate: gh auth login' )) } } # ── List files from GitHub ── function Get-GitHubDumpList { Assert-GhCli Write-Host "Fetching flight recorder dumps from $DataRepo ..." -ForegroundColor Cyan $raw = gh api "repos/$DataRepo/contents/flight-recorder" 2>&1 if ($LASTEXITCODE -ne 0) { if ("$raw" -match '404|Not Found') { Write-Warning "No flight-recorder directory in $DataRepo yet." Write-Warning 'Dumps appear after the server persists them (requires latest container image).' return @() } throw (New-ActionableError ` -Goal 'List flight recorder dumps from GitHub' ` -Problem "gh api failed: $raw" ` -Location 'Get-AzureFlightRecorder:Get-GitHubDumpList' ` -NextSteps @( 'Check gh auth status: gh auth status', "Verify repo access: gh api repos/$DataRepo" )) } $items = $raw | ConvertFrom-Json @($items) | Where-Object { $_.name -match '^(server-)?flight-recorder-.+\.jsonl$' } | Sort-Object { $_.name } -Descending } # ── Download a file from GitHub ── function Save-GitHubDumpFile { param([string]$Name, [int]$Size = 0) Assert-GhCli $outDir = Resolve-Path $OutputPath -ErrorAction SilentlyContinue if (-not $outDir) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null $outDir = Resolve-Path $OutputPath } $outFile = Join-Path $outDir $Name Write-Host " Downloading $Name ..." -ForegroundColor Cyan gh api "repos/$DataRepo/contents/flight-recorder/$Name" -H 'Accept: application/vnd.github.raw' > $outFile 2>&1 if ($LASTEXITCODE -ne 0) { $errContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue Remove-Item $outFile -ErrorAction SilentlyContinue throw (New-ActionableError ` -Goal "Download flight recorder dump $Name" ` -Problem "gh api download failed: $errContent" ` -Location 'Get-AzureFlightRecorder:Save-GitHubDumpFile' ` -NextSteps @('Check gh auth status', 'Verify the file exists in the repo')) } $fileInfo = Get-Item $outFile $fileInfo | Add-Member -NotePropertyName Source -NotePropertyValue 'GitHub' -Force # Parse header for summary $lines = Get-Content $outFile -TotalCount 5 $events = 0; $trigger = $null; $version = $null; $debateId = $null foreach ($line in $lines) { try { $obj = $line | ConvertFrom-Json if ($obj._type -eq 'header') { $events = $obj.ring_buffer_events_retained $version = $obj.app_version $debateId = $obj.active_debate_id } if ($obj._type -eq 'trigger') { $trigger = $obj.trigger_type } } catch { } } $fileInfo | Add-Member -NotePropertyName Events -NotePropertyValue $events -Force $fileInfo | Add-Member -NotePropertyName Trigger -NotePropertyValue $trigger -Force $fileInfo | Add-Member -NotePropertyName AppVersion -NotePropertyValue $version -Force $fileInfo | Add-Member -NotePropertyName DebateId -NotePropertyValue $debateId -Force Write-Host " Saved: $outFile ($([math]::Round($fileInfo.Length / 1024, 1)) KB, $events events)" -ForegroundColor Green return $fileInfo } # ── REST API helper (for TriggerDump only) ── function Invoke-FRApi { param([string]$Method, [string]$Endpoint, [switch]$Raw) $cookie = if ($SessionCookie) { $SessionCookie } else { $env:TAXONOMY_SESSION_COOKIE } if (-not $cookie) { throw (New-ActionableError ` -Goal 'Authenticate with Azure Taxonomy Editor' ` -Problem 'No session cookie provided. -TriggerDump requires Azure Easy Auth.' ` -Location 'Get-AzureFlightRecorder' ` -NextSteps @( "Log in at $BaseUrl in your browser", 'Open DevTools (F12) > Application > Cookies', "Copy the 'AppServiceAuthSession' cookie value", '$env:TAXONOMY_SESSION_COOKIE = ''<paste cookie value here>''' )) } $url = "$BaseUrl$Endpoint" try { $headers = @{ Cookie = "AppServiceAuthSession=$cookie" } $params = @{ Uri = $url Method = $Method Headers = $headers UseBasicParsing = $true TimeoutSec = 30 } if ($Method -eq 'POST') { $params.ContentType = 'application/json' $params.Body = '{}' } $response = Invoke-WebRequest @params if ($Raw) { return $response } $ct = $response.Headers['Content-Type'] if ($ct -and $ct -notmatch 'application/json') { if ($response.Content -match '<title>Sign In') { throw (New-ActionableError ` -Goal 'Authenticate with Azure Taxonomy Editor' ` -Problem 'Session cookie is expired or invalid' ` -Location 'Get-AzureFlightRecorder:Invoke-FRApi' ` -NextSteps @( "Log in at $BaseUrl in your browser", "Copy a fresh 'AppServiceAuthSession' cookie", '$env:TAXONOMY_SESSION_COOKIE = ''<paste new value>''' )) } throw "Unexpected response Content-Type: $ct" } return $response.Content | ConvertFrom-Json } catch [Microsoft.PowerShell.Commands.HttpResponseException] { $status = [int]$_.Exception.Response.StatusCode throw (New-ActionableError ` -Goal "Call flight recorder API" ` -Problem "HTTP $status from $Method $url" ` -Location 'Get-AzureFlightRecorder:Invoke-FRApi' ` -NextSteps @( "Verify the app is running: open $BaseUrl in a browser", 'If 401/403: refresh session cookie', 'If 404: deploy the latest container image' )) } } # ── TriggerDump (REST API — requires running app) ── if ($PSCmdlet.ParameterSetName -eq 'Trigger' -or $TriggerDump) { Write-Host 'Triggering server-side flight recorder dump...' -ForegroundColor Cyan $result = Invoke-FRApi -Method POST -Endpoint '/api/flight-recorder/server-dump' $fname = if ($result.PSObject.Properties['filename']) { $result.filename } else { $null } if (-not $fname) { throw (New-ActionableError ` -Goal 'Trigger server flight recorder dump' ` -Problem 'Server returned OK but no filename in response' ` -Location 'Get-AzureFlightRecorder' ` -NextSteps @( "Check server logs: az containerapp logs show -n taxonomy-editor -g rg-aitriad --tail 50", 'The server recorder may be uninitialized' )) } Write-Host " Server dump created: $fname" -ForegroundColor Green Write-Host ' Waiting for GitHub persistence ...' -ForegroundColor DarkGray Start-Sleep -Seconds 5 $saved = Save-GitHubDumpFile -Name $fname if ($Open) { $saved | Show-FlightRecorder } return $saved } # ── GitHub-based operations (work when app is down) ── $ghFiles = Get-GitHubDumpList if (-not $ghFiles -or @($ghFiles).Count -eq 0) { Write-Warning 'No flight recorder dumps found.' Write-Warning 'Trigger a dump with: Get-AzureFlightRecorder -TriggerDump' return } # ── ByName ── if ($PSCmdlet.ParameterSetName -eq 'ByName') { $match = @($ghFiles) | Where-Object { $_.name -eq $Filename } if (-not $match) { Write-Warning "File '$Filename' not found. Available dumps:" @($ghFiles) | ForEach-Object { Write-Warning " $($_.name)" } return } $saved = Save-GitHubDumpFile -Name $Filename -Size $match.size if ($Open) { $saved | Show-FlightRecorder } return $saved } # ── List ── if ($PSCmdlet.ParameterSetName -eq 'List') { Write-Host " $(@($ghFiles).Count) dump file(s) available:" -ForegroundColor Green @($ghFiles) | ForEach-Object { [PSCustomObject]@{ Name = $_.name Size = "$([math]::Round($_.size / 1024, 1)) KB" Type = if ($_.name -match '^server-') { 'server' } else { 'client' } } } | Format-Table -AutoSize return } # ── Download (most recent N) ── $toDownload = @($ghFiles) | Select-Object -First $Last Write-Host " Downloading $(@($toDownload).Count) of $(@($ghFiles).Count) dump(s)..." -ForegroundColor Cyan $downloaded = @() foreach ($f in $toDownload) { $downloaded += Save-GitHubDumpFile -Name $f.name -Size $f.size } if ($Open -and $downloaded.Count -gt 0) { $downloaded | ForEach-Object { $_ | Show-FlightRecorder } } return $downloaded } |