From d5a270f3af1656ae5c710031fd1d54b1f823163d Mon Sep 17 00:00:00 2001 From: viktot14-ai Date: Fri, 25 Sep 2026 09:38:39 +0300 Subject: [PATCH] =?UTF-8?q?B22:=20=D0=BF=D1=80=D0=B8=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BD=D0=BE=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20vector.*=20+=20=D0=B0=D0=B4?= =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=B8=D1=81=D1=82=D1=80=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B2=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D1=85=20(=D0=BF=D0=BE=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D1=86=D1=83=20=D0=A0=D0=92=D0=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend/logging_setup.py: setup_logging() (stdout INFO + файл /tmp/vector/vector.log, ротация 5×2 МБ, VectorLOGLEVEL/VectorLOGFILE/VectorLOGDIR), get_logger('vector.*') - события: vector.analyze (матрасчёт с длительностью), vector.auth (login_ok/failed/ lockout), vector.audit (каждая запись), vector.contour (ok/failed/rejected), vector.operations (operation_complete), vector.geocode (успех/timeout/ошибка) - существующие логгеры переведены в namespace vector.* (geo, geo.osm, rules, water, admin) - README §13а: таблица наблюдаемых событий + правила (без ПДн/секретов) - SETUP_GUIDE: §15 RPO/RTO + backup≠restore, §16 grep по vector.*, §17 диагностика по цепочкам (БД, 500, fallback-зоны, вход), §17а чек-лист передачи проекта - DEPLOY.md: вместо устаревшего дубля — ссылка на актуальные доки + шпаргалка стенда - тесты: 239 passed --- DEPLOY.md | 128 ++++++----------------------- README.md | 40 ++++++++- SETUP_GUIDE.md | 147 +++++++++++++++++++++++++++++++++- backend/audit.py | 6 ++ backend/logging_setup.py | 75 +++++++++++++++++ backend/main.py | 6 ++ backend/routers/admin.py | 4 +- backend/routers/analyze.py | 14 ++++ backend/routers/auth.py | 12 +++ backend/routers/contour.py | 8 ++ backend/routers/geocode.py | 8 ++ backend/routers/operations.py | 6 ++ backend/routers/water.py | 2 +- services/geo_service.py | 2 +- services/osm_local.py | 2 +- services/rules_analysis.py | 2 +- 16 files changed, 345 insertions(+), 117 deletions(-) create mode 100644 backend/logging_setup.py diff --git a/DEPLOY.md b/DEPLOY.md index 7527564..9792c20 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,118 +1,36 @@ -# Инструкция по развертыванию SAR-MCHS на LXC 108 +# Развёртывание ВЕКТОРа -## Текущее состояние +**Актуальная документация:** -Проект развернут на LXC 108 (Lab) по адресу: **192.168.0.99** +- **[SETUP_GUIDE.md](SETUP_GUIDE.md)** — полное руководство: требования, Docker, .env, + миграции/сиды, OSM-геослой, пользователи, КОНТУР, HTTPS, обновление, бэкапы, + мониторинг, диагностика, чек-листы (развёртывание §18, передача проекта §17а). +- **[README.md](README.md)** — описание системы: архитектура, API, БД, RBAC, + аудит, прикладное логирование (§13а). -## Доступ к сервисам +## Текущее состояние (тестовый стенд) -- **Backend API**: http://192.168.0.99:8000 - - Swagger документация: http://192.168.0.99:8000/docs - - ReDoc: http://192.168.0.99:8000/redoc - -- **Frontend**: http://192.168.0.99:3000 +Проект развёрнут на LXC 108 (Lab): **192.168.0.99**, путь `/root/vector`, docker compose. -- **Adminer** (управление БД): http://192.168.0.99:8080 - - Система: PostgreSQL - - Сервер: postgres - - Пользователь: postgres - - Пароль: postgres - - База данных: vector_mchs +| Сервис | Адрес | +|--------|-------| +| Backend API + Swagger | http://192.168.0.99:8000 (/docs) | +| Frontend | http://192.168.0.99:3000 | +| Adminer | http://192.168.0.99:8080 (postgres:postgres, БД vector_mchs) | +| PostgreSQL | 192.168.0.99:5432 | -- **PostgreSQL**: 192.168.0.99:5432 +## Управление (шпаргалка) -## Управление проектом - -### Запуск проекта ```bash cd /root/vector -docker compose up -d -``` - -### Остановка проекта -```bash -cd /root/vector -docker compose down -``` - -### Перезапуск с пересборкой -```bash -cd /root/vector -docker compose down -docker compose up -d --build -``` - -### Просмотр логов -```bash -# Все сервисы -docker compose logs -f - -# Конкретный сервис +docker compose up -d # запуск (после правки .env — пересоздание, restart env не перечитывает!) +docker compose ps # статус docker compose logs -f backend -docker compose logs -f frontend -docker compose logs -f postgres +docker compose down # остановка +docker compose up -d --build # пересборка ``` -### Статус сервисов -```bash -docker compose ps -``` +Типовой цикл обновления: push в Gitea → `git pull` → `docker restart vector-backend vector-frontend` +(после правки миграций — `alembic upgrade head` в контейнере, после правки env — `up -d backend`). -## Настройка - -Переменные окружения находятся в файле `/root/vector/.env` - -Важные переменные: -- `JWT_SECRET` - секрет для JWT токенов -- `DATABASE_URL` - строка подключения к PostgreSQL -- `CORS_ORIGINS` - разрешенные origins для CORS - -Примечание: внешний ИИ (Claude/Anthropic) из проекта УБРАН — анализ -полностью локальный и детерминированный (services/rules_analysis.py). -API-ключи LLM не нужны. - -## Структура проекта - -``` -/root/vector/ -├── backend/ # FastAPI backend -│ ├── api/v1/ # API endpoints -│ ├── main.py # Точка входа -│ ├── requirements.txt -│ └── Dockerfile -├── frontend/ # React frontend -│ ├── src/ -│ ├── public/ -│ ├── package.json -│ └── Dockerfile -├── docker-compose.yml # Конфигурация Docker Compose -├── .env # Переменные окружения -└── README.md -``` - -## Подключение к LXC 108 - -Из Proxmox хоста: -```bash -pct exec 108 -- bash -``` - -Через SSH с вашей машины: -```bash -ssh root@192.168.0.65 "pct exec 108 -- bash" -``` - -## Следующие шаги - -1. Настроить базу данных (миграции Alembic) -2. Создать модели SQLAlchemy -3. Реализовать аутентификацию с JWT -4. Разработать React компоненты для frontend - -## Проверка работоспособности - -Все сервисы запущены и доступны: -- ✅ PostgreSQL 16 (порт 5432) -- ✅ Adminer (порт 8080) -- ✅ Backend API (порт 8000) -- ✅ Frontend (порт 3000) +Подключение к LXC: `ssh root@192.168.0.65 "pct exec 108 -- bash"` (pve-node1). \ No newline at end of file diff --git a/README.md b/README.md index 066968d..5ae00bf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ВЕКТОР рассчитывает приоритетные направления поиска пропавшего человека по матмодели: на основании данных о потерявшемся (возраст, диагнозы, физподготовка, транспорт), обстоятельствах потери (время, причина, направление), среде (рельеф, погода, сезон) и точке последней пропажи (ТНП) система строит концентрическую зону вероятного нахождения и подсвечивает приоритетные направления обследования с учётом рельефа из OSM-данных. -> **Версия документа:** соответствует состоянию репозитория на 9 сентября 2026 г. +> **Версия документа:** соответствует состоянию репозитория на 25 сентября 2026 г. > **Репозиторий:** [`Sadmin/vector`](http://192.168.0.106:3000/Sadmin/vector) > **Развёртывание:** см. [SETUP_GUIDE.md](SETUP_GUIDE.md) — пошаговый гайд по развёртыванию на серверах МЧС. @@ -351,6 +351,41 @@ SPA на react-scripts (CRA). Роутинг: --- +## 13а. Прикладное логирование (B22) + +Единая настройка — `backend/logging_setup.py`, инициализируется один раз при старте +(`backend.main::lifespan → setup_logging()`). Все модульные логгеры проекта живут +в пространстве имён `vector.*` и пишут в: + +- **stdout** контейнера — уровень INFO (меняется env `VectorLOGLEVEL=debug`); +- **файл** `/tmp/vector/vector.log` внутри контейнера — ротация 5×2 МБ (отключается `VectorLOGFILE=0`, каталог `VectorLOGDIR`). + +Формат строки: `время LEVEL vector.<модуль>: сообщение`. Пороги для файла — INFO и выше. + +### Наблюдаемые события + +| Логгер | Событие | Что означает | +|--------|---------|--------------| +| `vector.app` | `Vector API стартует` / `остановлен` | Жизненный цикл процесса | +| `vector.analyze` | `анализ case=… радиус=… срочность=… зон=… время=…мс` | Каждый матрасчёт с длительностью | +| `vector.auth` | `login_ok` / `login_failed` / `lockout` | Входы и блокировки (user, счётчик, IP) | +| `vector.audit` | `audit <тип> user=… obj=… ip=…` | Каждая запись в audit_events | +| `vector.contour` | `contour_send_ok` / `contour_send_failed` / `contour_rejected` | Отправка зон в КОНТУР | +| `vector.operations` | `operation_complete op=… исход=…` | Завершение поиска (найден живым/погибшим) | +| `vector.admin` | `admin closed case …` | Синхронизация закрытия кейса с операциями | +| `vector.geocode` | `geocode q=… результатов=…` / `geocode timeout` | Прокси Nominatim | +| `vector.geo` / `vector.geo.osm` | Overpass недоступен, OSM PostGIS недоступен | Состояние геослоя (fallback, breaker) | +| `vector.rules` | `Geo/scoring service error` | Отказ расчёта зон → fallback-зоны | +| `vector.water` | `geo layer cache write failed` | Ошибки кэша геослоя | + +### Правила + +- ПДн и секреты в логи не пишутся: никаких паролей, JWT, содержимого карточек (только id и агрегаты). +- Аудит-события дублируются в БД (`audit_events`) — лог это операционный след, БД — юридический. +- Уровень по умолчанию INFO; WARNING/ERROR — для неисправностей. Не логировать на INFO каждый HTTP-запрос (это делает uvicorn access-log). + +--- + ## 15. Что готово и что в планах ### Реализовано @@ -363,9 +398,10 @@ SPA на react-scripts (CRA). Роутинг: - Мультипоиск: операции, статусная машина, дашборд «Активные поиски» - Завершение поиска с внесением исхода, архив завершённых с «прогноз vs факт» - Аудит 90 дней с field-level деталями +- Прикладное логирование `vector.*`: события анализа, входов, КОНТУРа, аудита (README §13а) - Интеграция с КОНТУР: отправка зон (контракт B20) - Журнал аудита пользователей: входы, изменения, отправки -- 231 автотест (pytest), 5 skipped +- 239 автотестов (pytest), 5 skipped ### В планах diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md index abf918f..5439b46 100644 --- a/SETUP_GUIDE.md +++ b/SETUP_GUIDE.md @@ -2,7 +2,7 @@ **Пошаговое руководство для IT-специалистов МЧС Республики Беларусь по развёртыванию системы определения приоритетных направлений поиска (ВЕКТОР) на серверах организации.** -> **Версия документа:** 1.0 от 9 сентября 2026 г. +> **Версия документа:** 1.1 от 25 сентября 2026 г. > **Репозиторий:** [`Sadmin/vector`](http://192.168.0.106:3000/Sadmin/vector) --- @@ -23,9 +23,10 @@ 12. [Настройка интеграции с КОНТУР](#12-настройка-интеграции-с-контуром) 13. [Обратный прокси и HTTPS](#13-обратный-прокси-и-https) 14. [Обновление проекта](#14-обновление-проекта) -15. [Резервное копирование](#15-резервное-копирование) +15. [Резервное копирование (RPO/RTO)](#15-резервное-копирование) 16. [Мониторинг и логи](#16-мониторинг-и-логи) 17. [Диагностика проблем](#17-диагностика-проблем) +17а. [Чек-лист передачи проекта](#17а-чек-лист-передачи-проекта) 18. [Чек-лист развёртывания](#18-чек-лист-развёртывания) --- @@ -329,23 +330,73 @@ docker exec vector-postgres pg_dump -U postgres -d vector_mchs \ Рекомендация: ежедневный cron прикладного дампа (он небольшой) + еженедельный полный. Хранить копии вне сервера. +**Docker volume — не backup.** Если сервер умер вместе с диском, volume умер вместе с ним. Что подлежит резервному копированию: + +```text +БД (прикладные таблицы + OSM) ++ .env (JWT_SECRET, пароль БД, CONTOUR_TOKEN) ++ конфигурация compose +``` + +**Backup ≠ восстановление.** Копия проверяется периодическим restore на отдельном стенде: +файл читается → БД восстанавливается → backend подключается → расчёт выполняется. Непроверенный restore — это не стратегия, а файл с именем «backup». + +### RPO и RTO + +| Величина | Вопрос | Рекомендация для пилота | +|----------|--------|------------------------| +| **RPO** | Сколько данных допустимо потерять | Ежедневный дамп → RPO ≤ 24 ч. Потеря последних суток карточек; активная операция восстановляется из бумажного журнала смены | +| **RTO** | Сколько времени допустимо восстанавливать | Развернуть compose + restore дампа на новом LXC ≈ 30–60 мин (без импорта OSM — прикладной дамп + повторный импорт по необходимости; расчёт временно fallback'ится на Overpass) | + --- ## 16. Мониторинг и логи ```bash docker compose logs -f # все сервисы -docker compose logs -f backend # API + аудит-сообщения +docker compose logs -f backend # API + прикладные логи vector.* docker compose logs -f frontend # сборка CRA / ошибки компиляции docker stats # потребление контейнеров +tail -f /tmp/vector/vector.log # файл-лог внутри контейнера (ротация 5×2 МБ) ``` -Ключевые события в логах backend: `admin closed case ... операция(ий) завершены` (синхронизация закрытия), ошибки расчёта (полный traceback uvicorn). +Прикладные события (B22, `backend/logging_setup.py`) — префикс `vector.<модуль>`: + +```bash +# Все матрасчёты с длительностью +docker compose logs backend | grep 'vector.analyze' + +# Входы и блокировки +docker compose logs backend | grep -E 'vector\.auth' + +# Отправки в КОНТУР (ok/failed/rejected) +docker compose logs backend | grep 'vector.contour' + +# Аудит-записи (операционный след; юридический — в БД audit_events) +docker compose logs backend | grep 'vector.audit' +``` + +Ключевые события: `анализ case=… радиус=… время=…мс` (каждый расчёт), +`login_ok/login_failed/lockout` (безопасность), `contour_send_ok/failed` (связь с КОНТУРом), +`operation_complete op=… исход=…` (завершение поиска), `admin closed case …` (синхронизация закрытия), +`Overpass … недоступен` / `OSM PostGIS недоступен` (геослой ушёл в fallback). + +Уровень детализации: `VectorLOGLEVEL=debug` в `.env` + `docker compose up -d backend` (restart не перечитывает env). Файл-лог отключается `VectorLOGFILE=0`. --- ## 17. Диагностика проблем +### Быстрые проверки + +```bash +docker compose ps # 4 контейнера Up/healthy +curl -s http://localhost:8000/api/v1/health # {"status":"ok"} +df -h && free -h # диск/память +``` + +### Типовые симптомы + | Симптом | Причина | Решение | |---------|---------|---------| | Логин даёт 422 | JSON вместо form-urlencoded | Логин строго `application/x-www-form-urlencoded` | @@ -359,6 +410,94 @@ docker stats # потребление контейне | КОНТУР недоступен при отправке зон | Нет связи/токена | curl до CONTOUR_API_URL; проверить CONTOUR_TOKEN; после правки env — up -d | | Тайлы карты не грузятся | Нет интернета у браузера | Для полного offline — локальный tile-сервер (в планах) | +### Диагностика по цепочке (начинать с места проблемы, не с перезапусков) + +**Бэкенд не подключается к БД** — цепочка `backend → docker network → postgres:5432 → authentication → vector_mchs`: + +```bash +docker compose logs backend | grep -iE 'error|failed|refused' +docker exec vector-backend python -c "from services.osm_local import osm_available; print(osm_available())" +docker exec vector-postgres pg_isready -U postgres +``` + +Проверить: `DATABASE_URL` (хост `postgres`, пароль совпадает с POSTGRES_PASSWORD), общую compose-сеть, состояние БД. + +**500 Internal Server Error** — «500» это результат, а не диагноз. Сначала traceback: + +```bash +docker compose logs backend --tail 100 +``` + +Затем определить слой: FastAPI / SQLAlchemy / БД / данные / конфигурация. Прикладной контекст ищется по `vector.*` в логах. + +**Расчёт отдал fallback-зоны («Ближняя зона N / Средняя E»)** — геослой не ответил: + +```bash +docker compose logs backend | grep -E 'vector\.(rules|geo)' +``` + +`Geo/scoring service error` → смотреть причину выше по стеку (PostGIS? Overpass breaker?). При работе на локальном OSM — проверить таблицы: + +```bash +docker exec vector-postgres psql -U postgres -d vector_mchs -t -c \ + "SELECT count(*) FROM planet_osm_line;" +``` + +**Пользователь не входит** — цепочка `login → bcrypt → lockout → session → JWT`: + +```bash +docker compose logs backend | grep 'vector.auth' +docker exec vector-postgres psql -U postgres -d vector_mchs -c \ + "SELECT username, is_active, status, failed_login_count, locked_until FROM users WHERE username='<логин>';" +``` + +401 после смены JWT_SECRET — все сессии инвалидны, ожидаемо: перелогин. 403 при валидном входе — проверить роль/юнит пользователя (скоуп). + +--- + +## 17а. Чек-лист передачи проекта (по образцу РВС) + +### Код + +- [ ] код в Git (Gitea, `Sadmin/vector`), история коммитов осмысленная +- [ ] production-конфигурация без паролей в репо (`.env.example` вместо `.env`) +- [ ] README актуален (архитектура, API, БД, логирование §13а) +- [ ] SETUP_GUIDE актуален (развертывание, диагностика) +- [ ] тесты зелёные: `pytest` (239 passed) + +### База + +- [ ] известен production-сервер PostgreSQL и БД `vector_mchs` +- [ ] схема актуальна: `alembic current` = `009_e3_operations` +- [ ] понятны таблицы OSM (planet_osm_*) и способ их восстановления (дамп + b16-import.sh) +- [ ] сиды (users, units) воспроизводимы + +### Docker + +- [ ] понятны compose-файлы, volumes, сети +- [ ] понятно, что можно пересоздавать (backend/frontend/adminer), а что содержит данные (postgres volume) +- [ ] известен порядок обновления (§14) и отката + +### Security + +- [ ] `JWT_SECRET` не дефолтный; пароль БД сменён +- [ ] пароль admin сменён; `must_change_password` для созданных пользователей работает +- [ ] `.env` защищён (chmod 600, не в репо) +- [ ] внешних ИИ/API-ключей нет (политика закрытого контура, тест `TestNoLLM`) +- [ ] Adminer и порты БД не торчат наружу без необходимости + +### Логи и аудит + +- [ ] прикладные логи `vector.*` доступны (`docker compose logs backend`, файл в контейнере) +- [ ] аудит 90 дней читается (вкладка «Аудит», право `view_audit`) +- [ ] ретенция настроена в `security_settings` + +### Backup + +- [ ] cron прикладного дампа настроен, копии вне сервера +- [ ] restore проверен на стенде +- [ ] RPO/RTO осознаны и приняты (§15) + --- ## 18. Чек-лист развёртывания diff --git a/backend/audit.py b/backend/audit.py index 823cd96..40de21a 100644 --- a/backend/audit.py +++ b/backend/audit.py @@ -12,8 +12,11 @@ from typing import Optional from sqlalchemy import text from sqlalchemy.orm import Session +from backend.logging_setup import get_logger from backend.models import AuditChange, AuditEvent, SecuritySetting, User +log = get_logger('audit') + def _client_meta(request) -> tuple[str | None, str | None]: if request is None: @@ -54,6 +57,9 @@ def audit_log( new_value=None if new is None else str(new), )) db.commit() + log.info('audit %s user=%s obj=%s/%s ip=%s', event_type, + event.username or '—', object_type or '—', event.object_id or '—', + event.ip_address or '—') def audit_retention_days(db: Session) -> int: diff --git a/backend/logging_setup.py b/backend/logging_setup.py new file mode 100644 index 0000000..88889a4 --- /dev/null +++ b/backend/logging_setup.py @@ -0,0 +1,75 @@ +"""Единая настройка логирования ВЕКТОРа (B22). + +Все модульные логгеры проекта — `logging.getLogger('vector.<модуль>')` — +конфигурируются один раз при старте приложения (backend.main → setup_logging()). + +Формат: время, уровень, логгер, сообщение. Вывод — stdout (контейнер), +дополнительно файл /tmp/vector/vector.log внутри контейнера (ротация 5×2 МБ). + +Наблюдаемые события (ключевые слова для grep по логам): + vector.analyze — анализ: case_id, радиус, зоны, время расчёта + vector.auth — login_ok / login_failed / lockout / logout + vector.contour — отправка зон в КОНТУР (ok/failed) + vector.admin — синхронизация закрытия кейса + vector.audit — запись аудит-события (кто, что) + vector.geo — геослой: PostGIS-режим, fallback на Overpass, ошибки +""" +from __future__ import annotations + +import logging +import logging.handlers +import os +from pathlib import Path + +_LOG_FORMAT = '%(asctime)s %(levelname)s %(name)s: %(message)s' +_LOG_DATEFMT = '%Y-%m-%d %H:%M:%S' + +# Порог вывода в stdout: INFO на сервере, DEBUG при VectorLOGLEVEL=debug +_LEVEL = os.getenv('VectorLOGLEVEL', 'info').upper() + +# Файл-хэндлер: по умолчанию включён (внутри контейнера /tmp/vector). +_FILE_ENABLED = os.getenv('VectorLOGFILE', '1').strip().lower() not in {'0', 'false', 'no'} +_FILE_DIR = Path(os.getenv('VectorLOGDIR', '/tmp/vector')) + + +def setup_logging() -> None: + """Конфигурация корневого пакета 'vector'. Идемпотентна.""" + root = logging.getLogger('vector') + if getattr(root, '_vector_configured', False): + return + + root.setLevel(logging.DEBUG) + fmt = logging.Formatter(_LOG_FORMAT, datefmt=_LOG_DATEFMT) + + stream = logging.StreamHandler() + stream.setLevel(getattr(logging, _LEVEL if _LEVEL in {'DEBUG', 'INFO', 'WARNING', 'ERROR'} else 'INFO')) + stream.setFormatter(fmt) + root.addHandler(stream) + + if _FILE_ENABLED: + try: + _FILE_DIR.mkdir(parents=True, exist_ok=True) + file_handler = logging.handlers.RotatingFileHandler( + _FILE_DIR / 'vector.log', + maxBytes=2 * 1024 * 1024, + backupCount=5, + encoding='utf-8', + ) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(fmt) + root.addHandler(file_handler) + except OSError: + root.warning('Файл-лог недоступен (%s) — только stdout', _FILE_DIR) + + # uvicorn-логи ошибок приложения (traceback-и) идут тем же форматом + uvicorn_error = logging.getLogger('uvicorn.error') + uvicorn_error.handlers.clear() + uvicorn_error.propagate = True + + root._vector_configured = True # type: ignore[attr-defined] + + +def get_logger(name: str) -> logging.Logger: + """Модульный логгер: get_logger('analyze') → 'vector.analyze'.""" + short = name.removeprefix('vector.') + return logging.getLogger(f'vector.{short}') \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 1869190..5d249ef 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import os from contextlib import asynccontextmanager @@ -7,6 +8,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from backend.database import init_db +from backend.logging_setup import setup_logging from backend.routers.admin import router as admin_router from backend.routers.admin_users import router as admin_users_router from backend.routers.operations import router as operations_router @@ -34,8 +36,12 @@ if '*' in cors_origins: @asynccontextmanager async def lifespan(app: FastAPI): + setup_logging() + log = logging.getLogger('vector.app') + log.info('Vector API стартует: CORS=%s', ','.join(cors_origins)) init_db() yield + log.info('Vector API остановлен') app = FastAPI(title='Vector API', version='0.1.0', lifespan=lifespan) diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 558b6ed..ebee208 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -1,15 +1,15 @@ from __future__ import annotations from datetime import datetime, timezone -import logging from fastapi import APIRouter, Depends, HTTPException, Query from backend.database import db +from backend.logging_setup import get_logger from backend.routers.auth import require_roles from backend.schemas import CaseListResponse, CaseResponse, CaseUpdate, DashboardResponse -log = logging.getLogger('vector.admin') +log = get_logger('admin') router = APIRouter(prefix='/api/v1/admin', tags=['admin'], dependencies=[Depends(require_roles(['admin']))]) diff --git a/backend/routers/analyze.py b/backend/routers/analyze.py index ae966dd..6d1ed55 100644 --- a/backend/routers/analyze.py +++ b/backend/routers/analyze.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + from datetime import datetime, timezone from typing import Any from uuid import UUID @@ -8,6 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from backend.database import db +from backend.logging_setup import get_logger from backend.routers.auth import require_roles from services.rules_analysis import rules_analyze from services.search_engine import ( @@ -15,6 +18,8 @@ from services.search_engine import ( build_search_model, ) +log = get_logger('analyze') + router = APIRouter(prefix='/api/v1/analyze', tags=['analyze']) @@ -44,6 +49,8 @@ class AnalysisRequest(BaseModel): async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]: # B14: роутер — тонкая обёртка. Вся расчётная логика — в # services/search_engine.py::build_search_model (без DB/auth/HTTP). + import time as _time + _t0 = _time.monotonic() case_data: dict[str, Any] = { 'age': payload.age, 'gender': payload.gender, @@ -109,6 +116,13 @@ async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]: result['case_id'] = str(payload.case_id) if payload.case_id else None result['analyzed_at'] = datetime.now(timezone.utc).isoformat() + log.info( + 'анализ case=%s радиус=%.1fкм срочность=%s зон=%d время=%.0fмс', + payload.case_id or '—', result.get('search_radius_km') or 0, + result.get('urgency'), len(result.get('primary_zones') or []), + (_time.monotonic() - _t0) * 1000, + ) + if payload.case_id is not None: db.update_case(str(payload.case_id), analysis_log=result, status='analyzed') diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 21b75ef..b126b2f 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -15,8 +15,11 @@ from sqlalchemy import text from sqlalchemy.orm import Session from backend.database import get_db +from backend.logging_setup import get_logger from backend.models import AuthEvent, SecuritySetting, User, UserSession +log = get_logger('auth') + router = APIRouter(prefix='/api/v1/auth', tags=['auth']) security = HTTPBearer(auto_error=False) SECRET_KEY = os.getenv('JWT_SECRET', 'change-me-in-production') @@ -127,9 +130,15 @@ def _register_failed_login(db: Session, user: User, request: Request | None) -> user.failed_login_count = (user.failed_login_count or 0) + 1 if user.failed_login_count >= max_failed: user.locked_until = datetime.now(timezone.utc) + timedelta(minutes=lockout_min) + log.warning('lockout user=%s неудачных=%d блокировка=%dмин ip=%s', + user.username, user.failed_login_count, lockout_min, + request.client.host if request and request.client else '—') _log_auth(db, 'lockout', user.username, user.id, request, {'failed_count': user.failed_login_count}) else: + log.warning('login_failed user=%s неудачных=%d ip=%s', + user.username, user.failed_login_count, + request.client.host if request and request.client else '—') _log_auth(db, 'login_failed', user.username, user.id, request, {'failed_count': user.failed_login_count}) db.commit() @@ -371,6 +380,9 @@ def login( user.last_login = datetime.now(timezone.utc) _log_auth(db, 'login', user.username, user.id, request) db.commit() + log.info('login_ok user=%s ip=%s', + getattr(user, 'username', '—'), + request.client.host if request.client else '—') token = create_access_token( {'sub': user.username, 'role': user.role, 'sid': sid_secret}, diff --git a/backend/routers/contour.py b/backend/routers/contour.py index ccacabb..67aa3c6 100644 --- a/backend/routers/contour.py +++ b/backend/routers/contour.py @@ -21,9 +21,12 @@ from pydantic import BaseModel from backend.audit import audit_log, can_access_unit from backend.database import get_db +from backend.logging_setup import get_logger from backend.models import SearchOperation, User from backend.routers.auth import get_current_user, require_permission +log = get_logger('contour') + router = APIRouter(prefix='/api/v1/operations', tags=['operations-contour']) def _contour_url() -> str: @@ -147,11 +150,14 @@ def send_to_contour( headers={'Authorization': f'Bearer {_contour_token()}'}, timeout=30.0) except httpx.HTTPError as e: + log.warning('contour_send_failed op=%s url=%s error=%s', op.id, _contour_url(), e) audit_log(db, current_user, 'contour_send_failed', object_type='operation', object_id=str(op.id), request=request, details={'error': str(e)}) raise HTTPException(status_code=502, detail=f'КОНТУР недоступен: {e}') from e if resp.status_code >= 400: + log.warning('contour_rejected op=%s status=%d body=%s', + op.id, resp.status_code, resp.text[:200]) audit_log(db, current_user, 'contour_send_failed', object_type='operation', object_id=str(op.id), request=request, details={'status': resp.status_code, 'body': resp.text[:500]}) @@ -159,6 +165,8 @@ def send_to_contour( detail=f'КОНТУР отклонил импорт: {resp.status_code} {resp.text[:300]}') result = resp.json() + log.info('contour_send_ok op=%s зон=%d contour_op=%s', + op.id, len(payload['zones']), op.contour_operation_id) audit_log(db, current_user, 'contour_send_ok', object_type='operation', object_id=str(op.id), request=request, details={'zones': result.get('zones_total'), 'status': result.get('status')}) diff --git a/backend/routers/geocode.py b/backend/routers/geocode.py index 56e0fe5..a00f5f8 100644 --- a/backend/routers/geocode.py +++ b/backend/routers/geocode.py @@ -11,15 +11,19 @@ Nominatim Usage Policy: User-Agent обязателен, ≤1 rps — прокс - Пункт ПЕРВЫМ словом. """ import os +import logging from typing import List, Optional import httpx from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel +from backend.logging_setup import get_logger from backend.routers.auth import require_roles from fastapi import Depends +log = get_logger('geocode') + router = APIRouter(prefix='/api/v1/geocode', tags=['geocode']) NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search' @@ -59,10 +63,14 @@ async def geocode( resp.raise_for_status() data = resp.json() except httpx.TimeoutException: + log.warning('geocode timeout q=%s', q.strip()[:100]) raise HTTPException(status_code=504, detail='Сервис геокодирования не ответил') except Exception as e: + log.warning('geocode error q=%s: %s', q.strip()[:100], e) raise HTTPException(status_code=502, detail=f'Ошибка геокодирования: {e}') + log.info('geocode q=%s результатов=%d', q.strip()[:100], len(data)) + return {'results': [ GeocodeResult( lat=float(item['lat']), diff --git a/backend/routers/operations.py b/backend/routers/operations.py index 4808639..52dc2f2 100644 --- a/backend/routers/operations.py +++ b/backend/routers/operations.py @@ -13,10 +13,13 @@ from pydantic import BaseModel from sqlalchemy.orm import Session from backend.audit import audit_log, visible_unit_ids +from backend.logging_setup import get_logger from backend.database import get_db from backend.models import Case, SearchOperation, User from backend.routers.auth import get_current_user, require_permission +oplog = get_logger('operations') + router = APIRouter(prefix='/api/v1/operations', tags=['operations']) ALLOWED_TRANSITIONS = { @@ -203,6 +206,9 @@ def complete_operation( db.commit() + oplog.info('operation_complete op=%s case=%s исход=%s юзер=%s', + op.id, case.id, 'найден живым' if payload.found_alive else 'найден погибшим', + getattr(current_user, 'username', '—')) audit_log(db, current_user, 'operation_complete', object_type='search_operation', object_id=str(op.id), details={'case_id': str(case.id), 'found_alive': payload.found_alive}) diff --git a/backend/routers/water.py b/backend/routers/water.py index a357e92..5093352 100644 --- a/backend/routers/water.py +++ b/backend/routers/water.py @@ -29,7 +29,7 @@ from backend.database import db from backend.routers.auth import require_roles from services.geo_service import _overpass_circuit_open, query_overpass -logger = logging.getLogger(__name__) +logger = logging.getLogger('vector.water') router = APIRouter(prefix='/api/v1/water', tags=['water'], dependencies=[Depends(require_roles(['operator', 'field', 'admin']))]) diff --git a/services/geo_service.py b/services/geo_service.py index 816678c..5eb5847 100644 --- a/services/geo_service.py +++ b/services/geo_service.py @@ -42,7 +42,7 @@ OVERPASS_URLS = [ ] OVERPASS_URL = OVERPASS_URLS[0] # обратная совместимость для импортов -logger = logging.getLogger(__name__) +logger = logging.getLogger('vector.geo') # --- Overpass circuit breaker --------------------------------------------- # build_search_zones issues ~128 Overpass calls per analysis. When the host has diff --git a/services/osm_local.py b/services/osm_local.py index ffa703a..a454282 100644 --- a/services/osm_local.py +++ b/services/osm_local.py @@ -16,7 +16,7 @@ from typing import Any, Optional from sqlalchemy import create_engine, text from sqlalchemy.engine import Engine -logger = logging.getLogger(__name__) +logger = logging.getLogger('vector.geo.osm') _PG_URL = os.getenv('DATABASE_URL', 'postgresql://postgres:postgres@postgres:5432/vector_mchs') if _PG_URL.startswith('postgresql+asyncpg://'): diff --git a/services/rules_analysis.py b/services/rules_analysis.py index ebb695d..03e8a39 100644 --- a/services/rules_analysis.py +++ b/services/rules_analysis.py @@ -15,7 +15,7 @@ from pydantic import BaseModel, Field from services.geo_service import build_search_zones from services.scoring_service import WeightedScorer -logger = logging.getLogger(__name__) +logger = logging.getLogger('vector.rules') class PrimaryZone(BaseModel):