- PVSM.RU - https://www.pvsm.ru -
Есть типичная боль: ты вроде всё сделал правильно — контейнеры поднялись, 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 прогон.
Три причины, которые встречаются чаще всего.
На Windows localhost [1] нередко резолвится в ::1 (IPv6). Если сервис слушает IPv4, часть запросов превращается в “Empty reply / Connection aborted”. В браузере это может выглядеть как “панель открылась, но вкладки не грузятся”.
Лечение простое: для диагностики и скриптов я использую http://127.0.0.1:8080 [2] и curl -4, чтобы исключить IPv6-сюрпризы.
Если открыть operator_console.html через простой python -m http.server, то HTML отдаётся, но запросы в /api/* летят в этот же origin и получают 404 — потому что там нет прокси на backend.
Снаружи это выглядит как “вкладки не обновляются / данные пустые / кнопки не работают”.
Лечение: нужен локальный reverse-proxy/edge, который:
отдаёт HTML,
проксирует /api/* и /health/* на backend,
и (опционально) имеет /local/health, чтобы быстро понять “жив ли edge”.
docker compose ps может быть зелёным, а внутри:
не применились миграции,
воркер не видит брокер,
роутер не зарегистрировал нужные эндпойнты,
упала одна из зависимостей.
Лечение: минимальный набор проверок, который подтверждает не “контейнер жив”, а “контур реально работает”.
Я разделил диагностику на два слоя.
Цель — поймать “глупости”, которые не имеют отношения к бизнес-логике:
docker compose config (валидация compose и env на fail-fast)
docker compose ps (быстрая картина контейнеров)
проверка занятых портов (когда внезапно “почему не стартует?”)
доступность API по IPv4 (127.0.0.1 + curl -4)
Цель — за 30–90 секунд ответить:
backend жив?
нужные роуты на месте?
UI-origin действительно может достучаться до /api/* через edge (если edge есть)?
Цель — сохранить сырьё, чтобы потом:
сравнить “до/после”,
прикрепить к багу,
восстановить контекст через неделю (и не вспоминать “а что тогда было?”).
Каждый прогон создаёт папку вида:
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-прокси.
Ниже — рабочий скрипт. Он:
создаёт evidence-папку,
прогоняет preflight (docker compose config, docker compose ps, порты),
проверяет API (/health/extended, /diag/routers),
делает мини-smoke ключевых эндпойнтов (чтобы отличать “200/401/403” от “404/500”),
собирает SUMMARY.md [3].
Скрипт специально “приземлённый”: без модулей, без внешних зависимостей.
Важно: использую
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 }
Перейти в папку, где лежит 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:* пропустятся.
Симптом: UI открывается, но вкладки не обновляются, запросы иногда “молчат”.
Когда вы используете curl.exe -4 и 127.0.0.1, вы резко уменьшаете количество “призрачных” фейлов. Это не лечит всё, но убирает целый класс странностей ещё до анализа логики.
Симптом: 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/*.
Симптом: /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 [3].
Через несколько прогонов вы заметите, что:
спорить с реальностью стало сложнее,
а чинить стало быстрее, потому что «место поломки» видно сразу.
Автор: FoxProFlow
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/powershell/444837
Ссылки в тексте:
[1] localhost: http://localhost
[2] http://127.0.0.1:8080: http://127.0.0.1:8080
[3] SUMMARY.md: http://SUMMARY.md
[4] Источник: https://habr.com/ru/articles/996850/?utm_source=habrahabr&utm_medium=rss&utm_campaign=996850
Нажмите здесь для печати.