B22: прикладное логирование vector.* + администрирование в доках (по образцу РВС)
- 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
This commit is contained in:
@@ -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
|
Проект развёрнут на LXC 108 (Lab): **192.168.0.99**, путь `/root/vector`, docker compose.
|
||||||
- Swagger документация: http://192.168.0.99:8000/docs
|
|
||||||
- ReDoc: http://192.168.0.99:8000/redoc
|
|
||||||
|
|
||||||
- **Frontend**: http://192.168.0.99:3000
|
|
||||||
|
|
||||||
- **Adminer** (управление БД): http://192.168.0.99:8080
|
| Сервис | Адрес |
|
||||||
- Система: PostgreSQL
|
|--------|-------|
|
||||||
- Сервер: postgres
|
| Backend API + Swagger | http://192.168.0.99:8000 (/docs) |
|
||||||
- Пользователь: postgres
|
| Frontend | http://192.168.0.99:3000 |
|
||||||
- Пароль: postgres
|
| Adminer | http://192.168.0.99:8080 (postgres:postgres, БД vector_mchs) |
|
||||||
- База данных: vector_mchs
|
| PostgreSQL | 192.168.0.99:5432 |
|
||||||
|
|
||||||
- **PostgreSQL**: 192.168.0.99:5432
|
## Управление (шпаргалка)
|
||||||
|
|
||||||
## Управление проектом
|
|
||||||
|
|
||||||
### Запуск проекта
|
|
||||||
```bash
|
```bash
|
||||||
cd /root/vector
|
cd /root/vector
|
||||||
docker compose up -d
|
docker compose up -d # запуск (после правки .env — пересоздание, restart env не перечитывает!)
|
||||||
```
|
docker compose ps # статус
|
||||||
|
|
||||||
### Остановка проекта
|
|
||||||
```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 logs -f backend
|
docker compose logs -f backend
|
||||||
docker compose logs -f frontend
|
docker compose down # остановка
|
||||||
docker compose logs -f postgres
|
docker compose up -d --build # пересборка
|
||||||
```
|
```
|
||||||
|
|
||||||
### Статус сервисов
|
Типовой цикл обновления: push в Gitea → `git pull` → `docker restart vector-backend vector-frontend`
|
||||||
```bash
|
(после правки миграций — `alembic upgrade head` в контейнере, после правки env — `up -d backend`).
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
## Настройка
|
Подключение к LXC: `ssh root@192.168.0.65 "pct exec 108 -- bash"` (pve-node1).
|
||||||
|
|
||||||
Переменные окружения находятся в файле `/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)
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
ВЕКТОР рассчитывает приоритетные направления поиска пропавшего человека по матмодели: на основании данных о потерявшемся (возраст, диагнозы, физподготовка, транспорт), обстоятельствах потери (время, причина, направление), среде (рельеф, погода, сезон) и точке последней пропажи (ТНП) система строит концентрическую зону вероятного нахождения и подсвечивает приоритетные направления обследования с учётом рельефа из OSM-данных.
|
ВЕКТОР рассчитывает приоритетные направления поиска пропавшего человека по матмодели: на основании данных о потерявшемся (возраст, диагнозы, физподготовка, транспорт), обстоятельствах потери (время, причина, направление), среде (рельеф, погода, сезон) и точке последней пропажи (ТНП) система строит концентрическую зону вероятного нахождения и подсвечивает приоритетные направления обследования с учётом рельефа из OSM-данных.
|
||||||
|
|
||||||
> **Версия документа:** соответствует состоянию репозитория на 9 сентября 2026 г.
|
> **Версия документа:** соответствует состоянию репозитория на 25 сентября 2026 г.
|
||||||
> **Репозиторий:** [`Sadmin/vector`](http://192.168.0.106:3000/Sadmin/vector)
|
> **Репозиторий:** [`Sadmin/vector`](http://192.168.0.106:3000/Sadmin/vector)
|
||||||
> **Развёртывание:** см. [SETUP_GUIDE.md](SETUP_GUIDE.md) — пошаговый гайд по развёртыванию на серверах МЧС.
|
> **Развёртывание:** см. [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. Что готово и что в планах
|
## 15. Что готово и что в планах
|
||||||
|
|
||||||
### Реализовано
|
### Реализовано
|
||||||
@@ -363,9 +398,10 @@ SPA на react-scripts (CRA). Роутинг:
|
|||||||
- Мультипоиск: операции, статусная машина, дашборд «Активные поиски»
|
- Мультипоиск: операции, статусная машина, дашборд «Активные поиски»
|
||||||
- Завершение поиска с внесением исхода, архив завершённых с «прогноз vs факт»
|
- Завершение поиска с внесением исхода, архив завершённых с «прогноз vs факт»
|
||||||
- Аудит 90 дней с field-level деталями
|
- Аудит 90 дней с field-level деталями
|
||||||
|
- Прикладное логирование `vector.*`: события анализа, входов, КОНТУРа, аудита (README §13а)
|
||||||
- Интеграция с КОНТУР: отправка зон (контракт B20)
|
- Интеграция с КОНТУР: отправка зон (контракт B20)
|
||||||
- Журнал аудита пользователей: входы, изменения, отправки
|
- Журнал аудита пользователей: входы, изменения, отправки
|
||||||
- 231 автотест (pytest), 5 skipped
|
- 239 автотестов (pytest), 5 skipped
|
||||||
|
|
||||||
### В планах
|
### В планах
|
||||||
|
|
||||||
|
|||||||
+143
-4
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Пошаговое руководство для IT-специалистов МЧС Республики Беларусь по развёртыванию системы определения приоритетных направлений поиска (ВЕКТОР) на серверах организации.**
|
**Пошаговое руководство для IT-специалистов МЧС Республики Беларусь по развёртыванию системы определения приоритетных направлений поиска (ВЕКТОР) на серверах организации.**
|
||||||
|
|
||||||
> **Версия документа:** 1.0 от 9 сентября 2026 г.
|
> **Версия документа:** 1.1 от 25 сентября 2026 г.
|
||||||
> **Репозиторий:** [`Sadmin/vector`](http://192.168.0.106:3000/Sadmin/vector)
|
> **Репозиторий:** [`Sadmin/vector`](http://192.168.0.106:3000/Sadmin/vector)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -23,9 +23,10 @@
|
|||||||
12. [Настройка интеграции с КОНТУР](#12-настройка-интеграции-с-контуром)
|
12. [Настройка интеграции с КОНТУР](#12-настройка-интеграции-с-контуром)
|
||||||
13. [Обратный прокси и HTTPS](#13-обратный-прокси-и-https)
|
13. [Обратный прокси и HTTPS](#13-обратный-прокси-и-https)
|
||||||
14. [Обновление проекта](#14-обновление-проекта)
|
14. [Обновление проекта](#14-обновление-проекта)
|
||||||
15. [Резервное копирование](#15-резервное-копирование)
|
15. [Резервное копирование (RPO/RTO)](#15-резервное-копирование)
|
||||||
16. [Мониторинг и логи](#16-мониторинг-и-логи)
|
16. [Мониторинг и логи](#16-мониторинг-и-логи)
|
||||||
17. [Диагностика проблем](#17-диагностика-проблем)
|
17. [Диагностика проблем](#17-диагностика-проблем)
|
||||||
|
17а. [Чек-лист передачи проекта](#17а-чек-лист-передачи-проекта)
|
||||||
18. [Чек-лист развёртывания](#18-чек-лист-развёртывания)
|
18. [Чек-лист развёртывания](#18-чек-лист-развёртывания)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -329,23 +330,73 @@ docker exec vector-postgres pg_dump -U postgres -d vector_mchs \
|
|||||||
|
|
||||||
Рекомендация: ежедневный cron прикладного дампа (он небольшой) + еженедельный полный. Хранить копии вне сервера.
|
Рекомендация: ежедневный 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. Мониторинг и логи
|
## 16. Мониторинг и логи
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f # все сервисы
|
docker compose logs -f # все сервисы
|
||||||
docker compose logs -f backend # API + аудит-сообщения
|
docker compose logs -f backend # API + прикладные логи vector.*
|
||||||
docker compose logs -f frontend # сборка CRA / ошибки компиляции
|
docker compose logs -f frontend # сборка CRA / ошибки компиляции
|
||||||
docker stats # потребление контейнеров
|
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. Диагностика проблем
|
## 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` |
|
| Логин даёт 422 | JSON вместо form-urlencoded | Логин строго `application/x-www-form-urlencoded` |
|
||||||
@@ -359,6 +410,94 @@ docker stats # потребление контейне
|
|||||||
| КОНТУР недоступен при отправке зон | Нет связи/токена | curl до CONTOUR_API_URL; проверить CONTOUR_TOKEN; после правки env — up -d |
|
| КОНТУР недоступен при отправке зон | Нет связи/токена | curl до CONTOUR_API_URL; проверить CONTOUR_TOKEN; после правки env — up -d |
|
||||||
| Тайлы карты не грузятся | Нет интернета у браузера | Для полного offline — локальный tile-сервер (в планах) |
|
| Тайлы карты не грузятся | Нет интернета у браузера | Для полного 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. Чек-лист развёртывания
|
## 18. Чек-лист развёртывания
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ from typing import Optional
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from backend.logging_setup import get_logger
|
||||||
from backend.models import AuditChange, AuditEvent, SecuritySetting, User
|
from backend.models import AuditChange, AuditEvent, SecuritySetting, User
|
||||||
|
|
||||||
|
log = get_logger('audit')
|
||||||
|
|
||||||
|
|
||||||
def _client_meta(request) -> tuple[str | None, str | None]:
|
def _client_meta(request) -> tuple[str | None, str | None]:
|
||||||
if request is None:
|
if request is None:
|
||||||
@@ -54,6 +57,9 @@ def audit_log(
|
|||||||
new_value=None if new is None else str(new),
|
new_value=None if new is None else str(new),
|
||||||
))
|
))
|
||||||
db.commit()
|
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:
|
def audit_retention_days(db: Session) -> int:
|
||||||
|
|||||||
@@ -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}')
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from backend.database import init_db
|
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 import router as admin_router
|
||||||
from backend.routers.admin_users import router as admin_users_router
|
from backend.routers.admin_users import router as admin_users_router
|
||||||
from backend.routers.operations import router as operations_router
|
from backend.routers.operations import router as operations_router
|
||||||
@@ -34,8 +36,12 @@ if '*' in cors_origins:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
setup_logging()
|
||||||
|
log = logging.getLogger('vector.app')
|
||||||
|
log.info('Vector API стартует: CORS=%s', ','.join(cors_origins))
|
||||||
init_db()
|
init_db()
|
||||||
yield
|
yield
|
||||||
|
log.info('Vector API остановлен')
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title='Vector API', version='0.1.0', lifespan=lifespan)
|
app = FastAPI(title='Vector API', version='0.1.0', lifespan=lifespan)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
from backend.database import db
|
from backend.database import db
|
||||||
|
from backend.logging_setup import get_logger
|
||||||
from backend.routers.auth import require_roles
|
from backend.routers.auth import require_roles
|
||||||
from backend.schemas import CaseListResponse, CaseResponse, CaseUpdate, DashboardResponse
|
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']))])
|
router = APIRouter(prefix='/api/v1/admin', tags=['admin'], dependencies=[Depends(require_roles(['admin']))])
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@@ -8,6 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from backend.database import db
|
from backend.database import db
|
||||||
|
from backend.logging_setup import get_logger
|
||||||
from backend.routers.auth import require_roles
|
from backend.routers.auth import require_roles
|
||||||
from services.rules_analysis import rules_analyze
|
from services.rules_analysis import rules_analyze
|
||||||
from services.search_engine import (
|
from services.search_engine import (
|
||||||
@@ -15,6 +18,8 @@ from services.search_engine import (
|
|||||||
build_search_model,
|
build_search_model,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger('analyze')
|
||||||
|
|
||||||
router = APIRouter(prefix='/api/v1/analyze', tags=['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]:
|
async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]:
|
||||||
# B14: роутер — тонкая обёртка. Вся расчётная логика — в
|
# B14: роутер — тонкая обёртка. Вся расчётная логика — в
|
||||||
# services/search_engine.py::build_search_model (без DB/auth/HTTP).
|
# services/search_engine.py::build_search_model (без DB/auth/HTTP).
|
||||||
|
import time as _time
|
||||||
|
_t0 = _time.monotonic()
|
||||||
case_data: dict[str, Any] = {
|
case_data: dict[str, Any] = {
|
||||||
'age': payload.age,
|
'age': payload.age,
|
||||||
'gender': payload.gender,
|
'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['case_id'] = str(payload.case_id) if payload.case_id else None
|
||||||
result['analyzed_at'] = datetime.now(timezone.utc).isoformat()
|
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:
|
if payload.case_id is not None:
|
||||||
db.update_case(str(payload.case_id), analysis_log=result, status='analyzed')
|
db.update_case(str(payload.case_id), analysis_log=result, status='analyzed')
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ from sqlalchemy import text
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
|
from backend.logging_setup import get_logger
|
||||||
from backend.models import AuthEvent, SecuritySetting, User, UserSession
|
from backend.models import AuthEvent, SecuritySetting, User, UserSession
|
||||||
|
|
||||||
|
log = get_logger('auth')
|
||||||
|
|
||||||
router = APIRouter(prefix='/api/v1/auth', tags=['auth'])
|
router = APIRouter(prefix='/api/v1/auth', tags=['auth'])
|
||||||
security = HTTPBearer(auto_error=False)
|
security = HTTPBearer(auto_error=False)
|
||||||
SECRET_KEY = os.getenv('JWT_SECRET', 'change-me-in-production')
|
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
|
user.failed_login_count = (user.failed_login_count or 0) + 1
|
||||||
if user.failed_login_count >= max_failed:
|
if user.failed_login_count >= max_failed:
|
||||||
user.locked_until = datetime.now(timezone.utc) + timedelta(minutes=lockout_min)
|
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,
|
_log_auth(db, 'lockout', user.username, user.id, request,
|
||||||
{'failed_count': user.failed_login_count})
|
{'failed_count': user.failed_login_count})
|
||||||
else:
|
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,
|
_log_auth(db, 'login_failed', user.username, user.id, request,
|
||||||
{'failed_count': user.failed_login_count})
|
{'failed_count': user.failed_login_count})
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -371,6 +380,9 @@ def login(
|
|||||||
user.last_login = datetime.now(timezone.utc)
|
user.last_login = datetime.now(timezone.utc)
|
||||||
_log_auth(db, 'login', user.username, user.id, request)
|
_log_auth(db, 'login', user.username, user.id, request)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
log.info('login_ok user=%s ip=%s',
|
||||||
|
getattr(user, 'username', '—'),
|
||||||
|
request.client.host if request.client else '—')
|
||||||
|
|
||||||
token = create_access_token(
|
token = create_access_token(
|
||||||
{'sub': user.username, 'role': user.role, 'sid': sid_secret},
|
{'sub': user.username, 'role': user.role, 'sid': sid_secret},
|
||||||
|
|||||||
@@ -21,9 +21,12 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from backend.audit import audit_log, can_access_unit
|
from backend.audit import audit_log, can_access_unit
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
|
from backend.logging_setup import get_logger
|
||||||
from backend.models import SearchOperation, User
|
from backend.models import SearchOperation, User
|
||||||
from backend.routers.auth import get_current_user, require_permission
|
from backend.routers.auth import get_current_user, require_permission
|
||||||
|
|
||||||
|
log = get_logger('contour')
|
||||||
|
|
||||||
router = APIRouter(prefix='/api/v1/operations', tags=['operations-contour'])
|
router = APIRouter(prefix='/api/v1/operations', tags=['operations-contour'])
|
||||||
|
|
||||||
def _contour_url() -> str:
|
def _contour_url() -> str:
|
||||||
@@ -147,11 +150,14 @@ def send_to_contour(
|
|||||||
headers={'Authorization': f'Bearer {_contour_token()}'},
|
headers={'Authorization': f'Bearer {_contour_token()}'},
|
||||||
timeout=30.0)
|
timeout=30.0)
|
||||||
except httpx.HTTPError as e:
|
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',
|
audit_log(db, current_user, 'contour_send_failed', object_type='operation',
|
||||||
object_id=str(op.id), request=request, details={'error': str(e)})
|
object_id=str(op.id), request=request, details={'error': str(e)})
|
||||||
raise HTTPException(status_code=502, detail=f'КОНТУР недоступен: {e}') from e
|
raise HTTPException(status_code=502, detail=f'КОНТУР недоступен: {e}') from e
|
||||||
|
|
||||||
if resp.status_code >= 400:
|
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',
|
audit_log(db, current_user, 'contour_send_failed', object_type='operation',
|
||||||
object_id=str(op.id), request=request,
|
object_id=str(op.id), request=request,
|
||||||
details={'status': resp.status_code, 'body': resp.text[:500]})
|
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]}')
|
detail=f'КОНТУР отклонил импорт: {resp.status_code} {resp.text[:300]}')
|
||||||
|
|
||||||
result = resp.json()
|
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',
|
audit_log(db, current_user, 'contour_send_ok', object_type='operation',
|
||||||
object_id=str(op.id), request=request,
|
object_id=str(op.id), request=request,
|
||||||
details={'zones': result.get('zones_total'), 'status': result.get('status')})
|
details={'zones': result.get('zones_total'), 'status': result.get('status')})
|
||||||
|
|||||||
@@ -11,15 +11,19 @@ Nominatim Usage Policy: User-Agent обязателен, ≤1 rps — прокс
|
|||||||
- Пункт ПЕРВЫМ словом.
|
- Пункт ПЕРВЫМ словом.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from backend.logging_setup import get_logger
|
||||||
from backend.routers.auth import require_roles
|
from backend.routers.auth import require_roles
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
|
log = get_logger('geocode')
|
||||||
|
|
||||||
router = APIRouter(prefix='/api/v1/geocode', tags=['geocode'])
|
router = APIRouter(prefix='/api/v1/geocode', tags=['geocode'])
|
||||||
|
|
||||||
NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
|
NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
|
||||||
@@ -59,10 +63,14 @@ async def geocode(
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
|
log.warning('geocode timeout q=%s', q.strip()[:100])
|
||||||
raise HTTPException(status_code=504, detail='Сервис геокодирования не ответил')
|
raise HTTPException(status_code=504, detail='Сервис геокодирования не ответил')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
log.warning('geocode error q=%s: %s', q.strip()[:100], e)
|
||||||
raise HTTPException(status_code=502, detail=f'Ошибка геокодирования: {e}')
|
raise HTTPException(status_code=502, detail=f'Ошибка геокодирования: {e}')
|
||||||
|
|
||||||
|
log.info('geocode q=%s результатов=%d', q.strip()[:100], len(data))
|
||||||
|
|
||||||
return {'results': [
|
return {'results': [
|
||||||
GeocodeResult(
|
GeocodeResult(
|
||||||
lat=float(item['lat']),
|
lat=float(item['lat']),
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ from pydantic import BaseModel
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from backend.audit import audit_log, visible_unit_ids
|
from backend.audit import audit_log, visible_unit_ids
|
||||||
|
from backend.logging_setup import get_logger
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
from backend.models import Case, SearchOperation, User
|
from backend.models import Case, SearchOperation, User
|
||||||
from backend.routers.auth import get_current_user, require_permission
|
from backend.routers.auth import get_current_user, require_permission
|
||||||
|
|
||||||
|
oplog = get_logger('operations')
|
||||||
|
|
||||||
router = APIRouter(prefix='/api/v1/operations', tags=['operations'])
|
router = APIRouter(prefix='/api/v1/operations', tags=['operations'])
|
||||||
|
|
||||||
ALLOWED_TRANSITIONS = {
|
ALLOWED_TRANSITIONS = {
|
||||||
@@ -203,6 +206,9 @@ def complete_operation(
|
|||||||
|
|
||||||
db.commit()
|
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',
|
audit_log(db, current_user, 'operation_complete',
|
||||||
object_type='search_operation', object_id=str(op.id),
|
object_type='search_operation', object_id=str(op.id),
|
||||||
details={'case_id': str(case.id), 'found_alive': payload.found_alive})
|
details={'case_id': str(case.id), 'found_alive': payload.found_alive})
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from backend.database import db
|
|||||||
from backend.routers.auth import require_roles
|
from backend.routers.auth import require_roles
|
||||||
from services.geo_service import _overpass_circuit_open, query_overpass
|
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'],
|
router = APIRouter(prefix='/api/v1/water', tags=['water'],
|
||||||
dependencies=[Depends(require_roles(['operator', 'field', 'admin']))])
|
dependencies=[Depends(require_roles(['operator', 'field', 'admin']))])
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ OVERPASS_URLS = [
|
|||||||
]
|
]
|
||||||
OVERPASS_URL = OVERPASS_URLS[0] # обратная совместимость для импортов
|
OVERPASS_URL = OVERPASS_URLS[0] # обратная совместимость для импортов
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('vector.geo')
|
||||||
|
|
||||||
# --- Overpass circuit breaker ---------------------------------------------
|
# --- Overpass circuit breaker ---------------------------------------------
|
||||||
# build_search_zones issues ~128 Overpass calls per analysis. When the host has
|
# build_search_zones issues ~128 Overpass calls per analysis. When the host has
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from typing import Any, Optional
|
|||||||
from sqlalchemy import create_engine, text
|
from sqlalchemy import create_engine, text
|
||||||
from sqlalchemy.engine import Engine
|
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')
|
_PG_URL = os.getenv('DATABASE_URL', 'postgresql://postgres:postgres@postgres:5432/vector_mchs')
|
||||||
if _PG_URL.startswith('postgresql+asyncpg://'):
|
if _PG_URL.startswith('postgresql+asyncpg://'):
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from pydantic import BaseModel, Field
|
|||||||
from services.geo_service import build_search_zones
|
from services.geo_service import build_search_zones
|
||||||
from services.scoring_service import WeightedScorer
|
from services.scoring_service import WeightedScorer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('vector.rules')
|
||||||
|
|
||||||
|
|
||||||
class PrimaryZone(BaseModel):
|
class PrimaryZone(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user