feat: add safe diagnostics export

This commit is contained in:
ik
2026-09-07 18:49:53 +07:00
parent 6477681e41
commit fa3b24fb1f
5 changed files with 35 additions and 5 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ docker compose up --build
Контейнер API сам выполняет `alembic upgrade head`, затем идемпотентный seed. PostgreSQL хранит данные в именованном volume `postgres_data`, а MinIO — в `minio_data`. Compose ожидает readiness PostgreSQL и MinIO перед API, а API-контейнер проверяет `/ready`. Версия и commit SHA задаются через `APP_VERSION`/`APP_REVISION`; те же значения доступны администратору в `/api/v1/admin/diagnostics`. Официальный импорт по умолчанию необязателен; при включённом scheduler установите `OFFICIAL_IMPORT_REQUIRED=true`, тогда отсутствующий, неуспешный или просроченный запуск сделает readiness отрицательным. Контейнер API сам выполняет `alembic upgrade head`, затем идемпотентный seed. PostgreSQL хранит данные в именованном volume `postgres_data`, а MinIO — в `minio_data`. Compose ожидает readiness PostgreSQL и MinIO перед API, а API-контейнер проверяет `/ready`. Версия и commit SHA задаются через `APP_VERSION`/`APP_REVISION`; те же значения доступны администратору в `/api/v1/admin/diagnostics`. Официальный импорт по умолчанию необязателен; при включённом scheduler установите `OFFICIAL_IMPORT_REQUIRED=true`, тогда отсутствующий, неуспешный или просроченный запуск сделает readiness отрицательным.
API и scheduler пишут по одной JSON-записи на событие. HTTP-лог содержит только сгенерированный `request_id`, метод, путь без query string, статус и длительность; IP, заголовок авторизации и пользовательский payload не журналируются. `X-Request-ID` возвращается клиенту. Стандартный access-log Uvicorn отключён. Уровень управляется `LOG_LEVEL`. API и scheduler пишут по одной JSON-записи на событие. HTTP-лог содержит только сгенерированный `request_id`, метод, путь без query string, статус и длительность; IP, заголовок авторизации и пользовательский payload не журналируются. `X-Request-ID` возвращается клиенту. Стандартный access-log Uvicorn отключён. Уровень управляется `LOG_LEVEL`. Защищённый `/api/v1/admin/diagnostics` скачивает JSON только с идентификатором сборки и агрегированными счётчиками, без имён игроков, исходных URL, payload и ошибок парсеров.
Остановка: Остановка:
+20 -2
View File
@@ -258,8 +258,26 @@ def _admin(authorization: Annotated[str | None, Header()] = None) -> str:
@app.get("/api/v1/admin/diagnostics") @app.get("/api/v1/admin/diagnostics")
def admin_diagnostics(_: Annotated[str, Depends(_admin)]) -> dict[str, str]: def admin_diagnostics(db: Db, _: Annotated[str, Depends(_admin)]) -> JSONResponse:
return {"version": settings.app_version, "revision": settings.app_revision, "environment": settings.deployment_environment} report_counts = {status.value: count for status, count in db.execute(
select(CatchReport.moderation_status, func.count()).group_by(CatchReport.moderation_status)
)}
observation_counts = {status: count for status, count in db.execute(
select(ExternalObservation.status, func.count()).group_by(ExternalObservation.status)
)}
payload = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"build": {"version": settings.app_version, "revision": settings.app_revision, "environment": settings.deployment_environment},
"counts": {
"catch_reports": report_counts,
"external_observations": observation_counts,
"data_sources": db.scalar(select(func.count()).select_from(DataSource)) or 0,
"enabled_data_sources": db.scalar(select(func.count()).select_from(DataSource).where(DataSource.enabled.is_(True))) or 0,
"official_import_runs": db.scalar(select(func.count()).select_from(OfficialRecordImport)) or 0,
"community_import_runs": db.scalar(select(func.count()).select_from(CommunityImportRun)) or 0,
},
}
return JSONResponse(payload, headers={"Content-Disposition": "attachment; filename=rf4spotter-diagnostics.json"})
@app.get("/api/v1/imports", response_model=list[ImportRunOut]) @app.get("/api/v1/imports", response_model=list[ImportRunOut])
+7 -1
View File
@@ -89,7 +89,13 @@ def test_admin_diagnostics_exposes_build_identity_only_to_admin() -> None:
assert client.get("/api/v1/admin/diagnostics").status_code == 401 assert client.get("/api/v1/admin/diagnostics").status_code == 401
response = client.get("/api/v1/admin/diagnostics", headers={"Authorization": "Bearer change-me-in-production"}) response = client.get("/api/v1/admin/diagnostics", headers={"Authorization": "Bearer change-me-in-production"})
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"version": "0.1.0", "revision": "dev", "environment": "development"} payload = response.json()
assert payload["build"] == {"version": "0.1.0", "revision": "dev", "environment": "development"}
assert set(payload) == {"generated_at", "build", "counts"}
assert response.headers["Content-Disposition"] == "attachment; filename=rf4spotter-diagnostics.json"
serialized = response.text.lower()
for forbidden in ("player_name", "source_url", "error_summary", "raw_payload", "admin_token", "s3_"):
assert forbidden not in serialized
def test_spot_detail_and_catches() -> None: def test_spot_detail_and_catches() -> None:
+6
View File
@@ -42,6 +42,12 @@ curl -fsS https://rf4spotter.ru/health
curl -fsS https://rf4spotter.ru/ready curl -fsS https://rf4spotter.ru/ready
``` ```
Безопасный диагностический снимок для администратора:
```bash
curl -fsS -H "Authorization: Bearer $ADMIN_TOKEN" -o rf4spotter-diagnostics.json https://rf4spotter.ru/api/v1/admin/diagnostics
```
После обновления DNS и получения сертификатов выполните внешний этап той же проверки: После обновления DNS и получения сертификатов выполните внешний этап той же проверки:
```bash ```bash
+1 -1
View File
@@ -143,7 +143,7 @@
- [x] Настроить долгий immutable cache для хешированных assets и разумный cache для изображений/favicon (7 сентября 2026). - [x] Настроить долгий immutable cache для хешированных assets и разумный cache для изображений/favicon (7 сентября 2026).
- [x] Добавить серверное «Показать ещё» для публичной ленты полевых сигналов с сохранением фильтров и пределом 48 записей (7 сентября 2026). - [x] Добавить серверное «Показать ещё» для публичной ленты полевых сигналов с сохранением фильтров и пределом 48 записей (7 сентября 2026).
- [x] Объединять одинаковые полевые сигналы в сюжеты без потери уникальных ссылок provenance и показывать число совпадений (7 сентября 2026). - [x] Объединять одинаковые полевые сигналы в сюжеты без потери уникальных ссылок provenance и показывать число совпадений (7 сентября 2026).
- [ ] Добавить безопасный административный экспорт диагностики без персональных данных. - [x] Добавить защищённый JSON-экспорт диагностики со сборкой и агрегированными счётчиками без персональных данных, URL и ошибок источников (7 сентября 2026).
- [x] Контролировать рост PostgreSQL и MinIO host-monitor'ом с настраиваемыми порогами и runbook реакции (7 сентября 2026). - [x] Контролировать рост PostgreSQL и MinIO host-monitor'ом с настраиваемыми порогами и runbook реакции (7 сентября 2026).
- [ ] Добавить фоновую проверку битых исходных ссылок с соблюдением лимитов источников. - [ ] Добавить фоновую проверку битых исходных ссылок с соблюдением лимитов источников.
- [x] Показывать версию и commit SHA в readiness и защищённой административной диагностике (7 сентября 2026). - [x] Показывать версию и commit SHA в readiness и защищённой административной диагностике (7 сентября 2026).