From fa3b24fb1f562dd4445f19c173b49c01dcfc56be Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 7 Sep 2026 18:49:53 +0700 Subject: [PATCH] feat: add safe diagnostics export --- README.md | 2 +- apps/api/app/main.py | 22 ++++++++++++++++++++-- apps/api/tests/test_api.py | 8 +++++++- deploy/README.md | 6 ++++++ docs/ROADMAP.md | 2 +- 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cd582f6..192d9a6 100644 --- a/README.md +++ b/README.md @@ -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 и 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 и ошибок парсеров. Остановка: diff --git a/apps/api/app/main.py b/apps/api/app/main.py index 59cd7fb..04511e3 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -258,8 +258,26 @@ def _admin(authorization: Annotated[str | None, Header()] = None) -> str: @app.get("/api/v1/admin/diagnostics") -def admin_diagnostics(_: Annotated[str, Depends(_admin)]) -> dict[str, str]: - return {"version": settings.app_version, "revision": settings.app_revision, "environment": settings.deployment_environment} +def admin_diagnostics(db: Db, _: Annotated[str, Depends(_admin)]) -> JSONResponse: + 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]) diff --git a/apps/api/tests/test_api.py b/apps/api/tests/test_api.py index 18d0a16..45785f0 100644 --- a/apps/api/tests/test_api.py +++ b/apps/api/tests/test_api.py @@ -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 response = client.get("/api/v1/admin/diagnostics", headers={"Authorization": "Bearer change-me-in-production"}) 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: diff --git a/deploy/README.md b/deploy/README.md index 46f5eae..f4d8dd8 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -42,6 +42,12 @@ curl -fsS https://rf4spotter.ru/health 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 и получения сертификатов выполните внешний этап той же проверки: ```bash diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 55e0d2a..c143bd2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -143,7 +143,7 @@ - [x] Настроить долгий immutable cache для хешированных assets и разумный cache для изображений/favicon (7 сентября 2026). - [x] Добавить серверное «Показать ещё» для публичной ленты полевых сигналов с сохранением фильтров и пределом 48 записей (7 сентября 2026). - [x] Объединять одинаковые полевые сигналы в сюжеты без потери уникальных ссылок provenance и показывать число совпадений (7 сентября 2026). -- [ ] Добавить безопасный административный экспорт диагностики без персональных данных. +- [x] Добавить защищённый JSON-экспорт диагностики со сборкой и агрегированными счётчиками без персональных данных, URL и ошибок источников (7 сентября 2026). - [x] Контролировать рост PostgreSQL и MinIO host-monitor'ом с настраиваемыми порогами и runbook реакции (7 сентября 2026). - [ ] Добавить фоновую проверку битых исходных ссылок с соблюдением лимитов источников. - [x] Показывать версию и commit SHA в readiness и защищённой административной диагностике (7 сентября 2026).