Files
EvoBGP/scripts/collect-backend-baseline.ps1
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 17:23:59 +07:00

134 lines
6.7 KiB
PowerShell

# Read-only Docker snapshot. Never persist raw Compose config, inspect or stderr.
[CmdletBinding()]
param(
[string[]]$ComposeFile = @('deploy/compose/docker-compose.yaml'),
[string]$Profile = 'microvps-full',
[string]$ProjectName,
[string]$OutputPath,
[ValidateRange(1, 120)][int]$Samples = 1,
[ValidateRange(1, 60)][int]$IntervalSeconds = 15
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path $PSScriptRoot -Parent
if (-not $OutputPath) {
$OutputPath = Join-Path $repoRoot ('artifacts/backend-baseline/{0}.json' -f (Get-Date -Format 'yyyyMMdd-HHmmss-fff'))
}
$report = [ordered]@{
schema_version = 1
collected_at_utc = [DateTime]::UtcNow.ToString('o')
status = 'unavailable'
git_commit = $null
profile = $Profile
requested_samples = $Samples
interval_seconds = $IntervalSeconds
host = $null
configured_services = @()
containers = @()
samples = @()
issues = @()
limitations = @('Read-only point samples, not a workload benchmark or peak-memory measurement.',
'No raw environment, commands, labels, mounts, logs or Compose configuration is stored.',
'CPU/RAM/IO measurements require a reachable Docker daemon and running project containers.')
}
function Invoke-DockerJSON {
param([string[]]$DockerArgs, [switch]$JsonLines)
# Native errors may contain expanded secrets; expose only a fixed category.
$priorPreference = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$raw = & docker @DockerArgs 2>$null
$code = $LASTEXITCODE
} finally { $ErrorActionPreference = $priorPreference }
if ($code -ne 0) { throw 'docker-command-failed' }
try {
if ($JsonLines) {
foreach ($line in $raw) { if ("$line".Trim()) { $line | ConvertFrom-Json } }
return
}
return (($raw -join "`n") | ConvertFrom-Json)
}
catch { throw 'docker-json-invalid' }
}
try {
if (Get-Command git -ErrorAction SilentlyContinue) {
$revision = & git -C $repoRoot rev-parse HEAD 2>$null
if ($LASTEXITCODE -eq 0) { $report.git_commit = "$revision".Trim() }
}
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw 'docker-cli-unavailable' }
$composeArgs = @('compose')
foreach ($file in $ComposeFile) {
$absolute = if ([IO.Path]::IsPathRooted($file)) { $file } else { Join-Path $repoRoot $file }
if (-not (Test-Path -LiteralPath $absolute -PathType Leaf)) { throw 'compose-file-unavailable' }
$composeArgs += @('-f', $absolute)
}
if ($Profile) { $composeArgs += @('--profile', $Profile) }
if ($ProjectName) { $composeArgs += @('--project-name', $ProjectName) }
# Config is parsed only in memory. Project metadata/environment are not copied.
$config = Invoke-DockerJSON -DockerArgs ($composeArgs + @('config', '--format', 'json'))
foreach ($service in $config.services.PSObject.Properties) {
$value = $service.Value
$report.configured_services += [ordered]@{
service = $service.Name
image = $value.image
profiles = @($value.profiles)
memory_limit = $value.deploy.resources.limits.memory
cpu_limit = $value.deploy.resources.limits.cpus
mem_limit = $value.mem_limit
cpus = $value.cpus
}
}
$config = $null
$report.host = Invoke-DockerJSON -DockerArgs @('info', '--format', '{"server_version":{{json .ServerVersion}},"os_type":{{json .OSType}},"architecture":{{json .Architecture}},"cpus":{{json .NCPU}},"memory_bytes":{{json .MemTotal}},"cgroup_version":{{json .CgroupVersion}}}')
$priorPreference = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$containerIDs = @(& docker @composeArgs ps --all --quiet 2>$null)
$code = $LASTEXITCODE
} finally { $ErrorActionPreference = $priorPreference }
if ($code -ne 0) { throw 'compose-ps-failed' }
$containerIDs = @($containerIDs | Where-Object { $_ -match '^[a-f0-9]{12,64}$' })
if ($containerIDs.Count -eq 0) { $report.issues += 'no-project-containers' }
$runningIDs = @()
foreach ($id in $containerIDs) {
$container = Invoke-DockerJSON -DockerArgs @('inspect', '--format', '{"id":{{json .Id}},"image_id":{{json .Image}},"status":{{json .State.Status}},"running":{{json .State.Running}},"oom_killed":{{json .State.OOMKilled}},"restart_count":{{json .RestartCount}},"memory_limit_bytes":{{json .HostConfig.Memory}},"nano_cpus":{{json .HostConfig.NanoCpus}},"cpu_quota":{{json .HostConfig.CpuQuota}},"cpu_period":{{json .HostConfig.CpuPeriod}}}', $id)
$report.containers += $container
if ($container.running) { $runningIDs += $id }
}
$allSamplesComplete = $true
for ($i = 0; $i -lt $Samples -and $runningIDs.Count -gt 0; $i++) {
if ($i -gt 0) { Start-Sleep -Seconds $IntervalSeconds }
$stats = @(Invoke-DockerJSON -JsonLines -DockerArgs (@('stats', '--no-stream', '--format', '{"id":{{json .ID}},"cpu_percent":{{json .CPUPerc}},"memory_usage":{{json .MemUsage}},"memory_percent":{{json .MemPerc}},"network_io":{{json .NetIO}},"block_io":{{json .BlockIO}},"pids":{{json .PIDs}}}') + $runningIDs))
$missingIDs = @()
foreach ($expectedID in $runningIDs) {
$matches = @($stats | Where-Object {
$sampleID = [string]$_.id
$sampleID -match '^[a-f0-9]{12,64}$' -and $expectedID.StartsWith($sampleID)
})
if ($matches.Count -ne 1) { $missingIDs += $expectedID }
}
$complete = $missingIDs.Count -eq 0 -and $stats.Count -eq $runningIDs.Count
if (-not $complete) {
$allSamplesComplete = $false
$report.issues += 'incomplete-stats-sample'
}
$report.samples += [ordered]@{ observed_at_utc = [DateTime]::UtcNow.ToString('o'); complete = $complete; containers = $stats }
}
$report.status = if ($runningIDs.Count -gt 0 -and $allSamplesComplete) { 'collected' } else { 'partial' }
} catch {
# Do not expose arbitrary exception text, which may include native output.
$known = @('docker-cli-unavailable', 'compose-file-unavailable', 'docker-command-failed', 'docker-json-invalid', 'compose-ps-failed')
$reason = $_.Exception.Message
if ($reason -notin $known) { $reason = 'collection-failed' }
$report.issues += $reason
} finally {
$absoluteOutput = [IO.Path]::GetFullPath($OutputPath)
$parent = Split-Path $absoluteOutput -Parent
if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $absoluteOutput -Encoding UTF8
Write-Output ('Baseline status: {0}; report: {1}' -f $report.status, $absoluteOutput)
}
if ($report.status -ne 'collected') { exit 2 }