Есть типичная боль: ты вроде всё сделал правильно — контейнеры поднялись, API отвечает, UI открывается… а потом оказывается, что “не работает”. Причём не “сломано в пепел”, а именно “почти”: где-то 404, где-то таймаут, где-то UI открывается, но вкладки пустые, где-то один запрос проходит, другой — молчит.
И самое неприятное: когда начинаешь чинить “по ощущениям”, можно потратить часы, а потом выяснить, что причина была не в коде, а в порте, origin, IPv6, миграциях или в том, что UI ходит не туда.
Я перестал спорить с реальностью и сделал себе простой подход evidence-first:
-
сначала фиксируем факты о состоянии системы (preflight),
-
затем делаем минимальный smoke (дышит/не дышит),
-
сохраняем артефакты (evidence),
-
и только потом лезем в код.
Ниже — как это устроено и как повторить у себя.
Что внутри (оглавление)
-
Контекст стенда и типовые “почти-ошибки”
-
Концепция: Preflight → Smoke → Evidence
-
Структура evidence-папки (что сохраняем и зачем)
-
PowerShell-скрипт (рабочий, без магии)
-
Как запускать (3 команды)
-
3 мини-кейса: как оно ловит проблемы
-
Что улучшить дальше
Контекст стенда
Стек максимально приземлённый:
-
FastAPI как backend
-
PostgreSQL
-
Redis
-
Celery worker + beat
-
Docker Compose
-
небольшой web-UI (статический HTML/JS), который ходит в
/api/*и/health/*
Хост — Windows, PowerShell 7.x, но подход одинаково применим и на Linux.
Ключевой нюанс: UI ожидает same-origin (то есть /api/* на том же origin, где открыт HTML). Если этот нюанс упустить — получаются “призраки”: UI есть, данных нет.
Внутри проекта FoxProFlow я упёрся в повторяющиеся “почти-ошибки” и вынес диагностику в отдельный smoke+evidence прогон.
Почему “вроде всё правильно” ломается на практике
Три причины, которые встречаются чаще всего.
1) localhost — это не всегда то, что вы думаете
На Windows localhost нередко резолвится в ::1 (IPv6). Если сервис слушает IPv4, часть запросов превращается в “Empty reply / Connection aborted”. В браузере это может выглядеть как “панель открылась, но вкладки не грузятся”.
Лечение простое: для диагностики и скриптов я использую http://127.0.0.1:8080 и curl -4, чтобы исключить IPv6-сюрпризы.
2) UI стучится в /api/*, а вы раздаёте HTML “как есть”
Если открыть operator_console.html через простой python -m http.server, то HTML отдаётся, но запросы в /api/* летят в этот же origin и получают 404 — потому что там нет прокси на backend.
Снаружи это выглядит как “вкладки не обновляются / данные пустые / кнопки не работают”.
Лечение: нужен локальный reverse-proxy/edge, который:
-
отдаёт HTML,
-
проксирует
/api/*и/health/*на backend, -
и (опционально) имеет
/local/health, чтобы быстро понять “жив ли edge”.
3) Compose поднялся, но “система” не поднялась
docker compose ps может быть зелёным, а внутри:
-
не применились миграции,
-
воркер не видит брокер,
-
роутер не зарегистрировал нужные эндпойнты,
-
упала одна из зависимостей.
Лечение: минимальный набор проверок, который подтверждает не “контейнер жив”, а “контур реально работает”.
Идея пайплайна: Preflight → Smoke → Evidence
Я разделил диагностику на два слоя.
Preflight (до любых правок)
Цель — поймать “глупости”, которые не имеют отношения к бизнес-логике:
-
docker compose config(валидация compose и env на fail-fast) -
docker compose ps(быстрая картина контейнеров) -
проверка занятых портов (когда внезапно “почему не стартует?”)
-
доступность API по IPv4 (
127.0.0.1+curl -4)
Smoke (самое важное “дышит?”)
Цель — за 30–90 секунд ответить:
-
backend жив?
-
нужные роуты на месте?
-
UI-origin действительно может достучаться до
/api/*через edge (если edge есть)?
Evidence (доказательства)
Цель — сохранить сырьё, чтобы потом:
-
сравнить “до/после”,
-
прикрепить к багу,
-
восстановить контекст через неделю (и не вспоминать “а что тогда было?”).
Как я сохраняю evidence
Каждый прогон создаёт папку вида:
evidence/
run_YYYYMMDD_HHMMSS/
meta.json
01_compose_config.txt
02_compose_ps.txt
03_ports.txt
10_health_extended.json
11_diag_routers.json
20_smoke_endpoints.txt
90_summary.md
Логика простая: не один гигантский лог, а набор маленьких файлов. Тогда глазами быстро видно, где именно сломалось: compose/env, порты, health, роуты или origin-прокси.
Реализация: один PowerShell-скрипт (preflight + smoke + evidence)
Ниже — рабочий скрипт. Он:
-
создаёт evidence-папку,
-
прогоняет preflight (
docker compose config,docker compose ps, порты), -
проверяет API (
/health/extended,/diag/routers), -
делает мини-smoke ключевых эндпойнтов (чтобы отличать “200/401/403” от “404/500”),
-
собирает
SUMMARY.md.
Скрипт специально “приземлённый”: без модулей, без внешних зависимостей.
Важно: использую
curl.exe -4и127.0.0.1, чтобы исключить IPv6-сюрпризы.
#requires -Version 7.0
[CmdletBinding(PositionalBinding=$false)]
param(
[string]$ApiBase = "http://127.0.0.1:8080",
[string]$EdgeBase = "",
[string]$ComposeDir = ".",
[string]$OutRoot = ".evidence",
[switch]$Open
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function _NowStamp { (Get-Date).ToString("yyyyMMdd_HHmmss") }
function _WriteUtf8NoBom([string]$Path, [string]$Text) {
$enc = [System.Text.UTF8Encoding]::new($false)
$dir = Split-Path -Parent $Path
if($dir -and -not (Test-Path -LiteralPath $dir)){
New-Item -ItemType Directory -Force -Path $dir | Out-Null
}
[System.IO.File]::WriteAllText($Path, $Text, $enc)
}
function _Run([string]$Label, [string]$File, [scriptblock]$Action) {
try {
$out = & $Action 2>&1 | Out-String
_WriteUtf8NoBom $File $out
return @{ ok=$true; label=$Label; file=$File; err=$null }
} catch {
$msg = $_ | Out-String
_WriteUtf8NoBom $File ($msg + "`n")
return @{ ok=$false; label=$Label; file=$File; err=$msg }
}
}
function _CurlText([string]$Url, [string]$File) {
_Run "curl $Url" $File { curl.exe -4 -sS $Url }
}
$ts = _NowStamp
$ev = Join-Path $OutRoot ("run_" + $ts)
New-Item -ItemType Directory -Force -Path $ev | Out-Null
$meta = @{
ts_local = (Get-Date).ToString("o")
api_base = $ApiBase
edge_base = $EdgeBase
compose_dir = (Resolve-Path -LiteralPath $ComposeDir).Path
host = $env:COMPUTERNAME
user = $env:USERNAME
pwsh = $PSVersionTable.PSVersion.ToString()
} | ConvertTo-Json -Depth 5
_WriteUtf8NoBom (Join-Path $ev "meta.json") $meta
# --- PRE-FLIGHT
$null = _Run "docker compose config" (Join-Path $ev "01_compose_config.txt") {
Push-Location $ComposeDir
try { docker compose config } finally { Pop-Location }
}
$null = _Run "docker compose ps" (Join-Path $ev "02_compose_ps.txt") {
Push-Location $ComposeDir
try { docker compose ps } finally { Pop-Location }
}
$null = _Run "ports" (Join-Path $ev "03_ports.txt") {
# полезно, когда внезапно занят порт
netstat -ano | Select-String -Pattern ":8080s|:8793s" -SimpleMatch
}
# --- SMOKE: health/routers
$r1 = _CurlText "$ApiBase/health/extended" (Join-Path $ev "10_health_extended.json")
$r2 = _CurlText "$ApiBase/diag/routers" (Join-Path $ev "11_diag_routers.json")
# --- SMOKE: ключевые эндпойнты (быстро проверить 200/401/403 вместо 404/500)
$smokeFile = Join-Path $ev "20_smoke_endpoints.txt"
$sb = New-Object System.Text.StringBuilder
function _Probe([string]$Name, [string]$Url) {
try {
$code = curl.exe -4 -sS -o NUL -w "%{http_code}" $Url
[void]$sb.AppendLine(("{0,-28} {1} {2}" -f $Name, $code, $Url))
} catch {
[void]$sb.AppendLine(("{0,-28} ERR {1}" -f $Name, $Url))
}
}
# API напрямую
_Probe "api:health" "$ApiBase/health"
_Probe "api:health_ext" "$ApiBase/health/extended"
_Probe "api:diag_routers" "$ApiBase/diag/routers"
# Edge (если есть): проверяем same-origin прокси /api/* и /health/*
if($EdgeBase -and $EdgeBase.Trim().Length -gt 0){
_Probe "edge:local_health" "$EdgeBase/local/health"
_Probe "edge:api_health" "$EdgeBase/api/health"
_Probe "edge:health_ext" "$EdgeBase/health/extended"
}
_WriteUtf8NoBom $smokeFile $sb.ToString()
# --- SUMMARY
$ok = $true
$fail = @()
foreach($r in @($r1,$r2)){
if(-not $r.ok){ $ok = $false; $fail += $r.label }
}
$summary = @()
$summary += "# Smoke summary ($ts)"
$summary += ""
$summary += "- API: $ApiBase"
$summary += "- EDGE: $EdgeBase"
$summary += "- Evidence: $((Resolve-Path $ev).Path)"
$summary += ""
$summary += "## Key files"
$summary += "- meta.json"
$summary += "- 01_compose_config.txt"
$summary += "- 02_compose_ps.txt"
$summary += "- 03_ports.txt"
$summary += "- 10_health_extended.json"
$summary += "- 11_diag_routers.json"
$summary += "- 20_smoke_endpoints.txt"
$summary += ""
if($ok){
$summary += "## Result"
$summary += "**PASS** (минимальный контур отвечает)"
} else {
$summary += "## Result"
$summary += "**FAIL**: " + ($fail -join ", ")
$summary += ""
$summary += "Сначала открываю evidence-файлы выше и смотрю, где именно рвётся."
}
_WriteUtf8NoBom (Join-Path $ev "90_summary.md") ($summary -join "`n")
if($Open){
Invoke-Item $ev
}
if($ok){ exit 0 } else { exit 1 }
Как запускать (3 команды)
-
Перейти в папку, где лежит
docker-compose.yml:
Set-Location "C:pathtoproject"
-
Прогон smoke + evidence:
.Invoke-EvidenceSmoke.ps1 `
-ApiBase "http://127.0.0.1:8080" `
-EdgeBase "http://127.0.0.1:8793" `
-ComposeDir "." `
-OutRoot ".evidence" `
-Open
-
Открыть итог:
notepad .evidencerun_*90_summary.md
Если edge не используете — просто запускайте без -EdgeBase (по умолчанию он пустой), тогда проверки edge:* пропустятся.
Что именно это ловит: 3 мини-кейса
Кейс A: localhost → ::1 и “пустые вкладки” в UI
Симптом: UI открывается, но вкладки не обновляются, запросы иногда “молчат”.
Когда вы используете curl.exe -4 и 127.0.0.1, вы резко уменьшаете количество “призрачных” фейлов. Это не лечит всё, но убирает целый класс странностей ещё до анализа логики.
Кейс B: UI раздаётся без прокси, /api/* даёт 404
Симптом: HTML грузится, но данные не приходят, кнопки “не работают”.
В 20_smoke_endpoints.txt это видно как контраст:
-
API напрямую отвечает (200/401/403 — зависит от авторизации),
-
а edge-эндпойнты возвращают 404.
Пример того, как это выглядит:
api:health 200 http://127.0.0.1:8080/health
edge:api_health 404 http://127.0.0.1:8793/api/health
То есть backend жив, но origin UI не проксирует /api/*.
Кейс C: Compose “зелёный”, но роутов нет
Симптом: /health отвечает, но нужные эндпойнты отсутствуют.
Тогда 11_diag_routers.json помогает понять, что проблема не “во фронте” и не “в тайминге”, а в регистрации роутов/конфигурации приложения.
Почему это быстрее, чем “сразу чинить”
Главная выгода — снятие неоднозначности. Когда “не работает”, люди часто прыгают в код, но причина может быть в:
-
сетевом origin,
-
портах,
-
env-переменных,
-
несовпадении health-контуров,
-
или в том, что вы тестируете “не тот” входной URL.
Evidence-first заставляет сначала ответить:
“что именно сломано: инфраструктура/маршрутизация/контракт/логика?”
И только потом править.
Что улучшить дальше (если делать “взрослее”)
-
Добавить проверку Celery (
inspect ping/registered) и сохранять ответы как отдельные файлы evidence. -
Добавить “контракт-проверки”: список ожидаемых роутов и падение, если роутера нет (fail fast).
-
Паковать evidence в zip, чтобы одним файлом передавать “как есть”.
В FoxProFlow это дальше превращается в “accept-run”: фикс → smoke → evidence → только потом merge.
Заключение
Если вы часто ловите «вроде всё правильно, но не работает», попробуйте начать не с нового кода, а с доказуемости: один smoke‑скрипт, одна папка evidence, один SUMMARY.md.
Через несколько прогонов вы заметите, что:
-
спорить с реальностью стало сложнее,
-
а чинить стало быстрее, потому что «место поломки» видно сразу.
Автор: FoxProFlow
