Update the project analysis after observability

Risks 1-3 closed, observations 2 and 5 closed, new observations on module
growth, stale knowledge graph and Docker image clutter; next step is the
run on real data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5 committed 2026-09-21 11:12:36 +03:00
1 parent cb935fef0d
commit 8a549a1452
1 file changed
+54 -49
+54 -49
View File
@@ -1,89 +1,94 @@
# Анализ проекта: сделано и осталось (2026-09-21, сверка с графом) # Анализ проекта: сделано и осталось (2026-09-21, актуализация после наблюдаемости)
Состояние после восьми доработок: надёжность и безопасность, тесты в контейнере, форматы вывода, разделение процессов, управление ASN/FQDN через API, контейнер приложения, SQLite, `POST /collect`, `GET /addresses/diff`. Предыдущий анализ: `analysis-2026-09-20.md`. Планы и итоги лежат рядом в `docs/`. Состояние после 17 доработок (план и итоги каждой лежат в `docs/`): надёжность и безопасность, тесты в контейнере, форматы вывода, разделение процессов, управление ASN/FQDN через API, контейнер приложения, SQLite, `POST /collect`, `GET /addresses/diff`, репозиторий git, закрепление версий, резервные копии и автовосстановление, защита ввода и данных, защита от пустой выдачи, исправления ревью 5-10, защита от пустых копий, наблюдаемость. Предыдущая версия этого файла описывала состояние после восьми доработок. Отчёт ревью: `review-2026-09-21.md` (находки 1-11 закрыты).
Проект: 7 модулей Python (API, сборщик, демон, БД, форматы, хранилище, healthcheck), 1235 строк, 18 тестов (15 функций с параметризацией) в 5 файлах, 20 документов в `docs/`. Проект: 7 модулей Python, 1719 строк (`db.py` 544, `cidr_collector.py` 386, `api_server.py` 381, `collector_daemon.py` 191, `formatters.py` 115, `storage.py` 70, `healthcheck.py` 32), 30 тестов (27 функций, 5 файлов и `conftest.py`), 37 документов в `docs/` (17 планов, 17 итогов, 2 анализа, ревью), 9 коммитов, ветка `main` синхронизирована с `origin` (`artstore.rxmsk.ru`).
**Граф знаний** (`graphify-out/`, построен по коду и `docs/`): 315 узлов, 643 связи, 13 сообществ. Использован для сверки: узлы-«хабы», связи между модулями и документами, изолированные узлы. Все выводы из графа ниже проверены по исходникам.
## Что изменилось с предыдущей версии этого файла ## Что изменилось с предыдущей версии этого файла
- **Выполнено:** `GET /addresses/diff` (журнал изменений на триггерах SQLite, курсор, `410` за горизонтом, заголовок `X-Changes-Cursor`). Из «Не сделано» убран diff; уведомления остались. - **Закрыты риски:** 1 (нет git), 2 (версии не закреплены), 3 (пустой список после порчи базы).
- **Исправлено по сверке:** - **Закрыты наблюдения:** 2 (`/health` не видел сбоев источников) и 5 (сообщение «data saved» без изменений); наблюдения 10 и 11 (структура модулей) переосмыслены, наблюдение 3 ухудшилось, добавлено наблюдение 13.
- Безымянных образов Docker не 65, а 3 (всего образов 31). Наблюдение 3 снято. - **Из «Не сделано» выполнено:** автоматическая резервная копия, diff, повторные запросы к RIPE; `/metrics` исключён из плана решением пользователя.
- Предположение, что в демоне не ловится `sqlite3.Error`, не подтвердилось: `run_job` перехватывает любое исключение сбора и пишет его в `last_error`. Пробела нет. - **Новое:** восстановление из копии, состояние источников (`/sources`), фильтрация DNS-адресов, защита от пустой выдачи и пустых копий, схема базы версии 3, ревью по графу знаний (11 находок).
- Метрики: строк кода 1235 (было 1102), тестов 18 (было 16). - **Метрики:** строк кода 1719 (было 1235), тестов 30 (было 18), документов 37 (было 20).
- **Новое:** `graphify-out/` (1,2 МБ) не был исключён из `.gitignore` и `.dockerignore`, то есть попадал бы в образ. Исправлено, в README добавлен пункт о графе.
## Сделано ## Сделано
| Направление | Результат | | Направление | Результат |
|---|---| |---|---|
| Токен на изменяющие запросы | `X-API-Key` на `POST`/`DELETE`, без токена запись отключена (503). Чтение открыто намеренно. | | Токен на изменяющие запросы | `X-API-Key` на `POST`/`DELETE`, без токена запись отключена (503); сравнение по байтам, любой неверный ключ - 401. Чтение открыто намеренно. |
| TTL, атомарная запись, защита от порчи | TTL (`ttl_days`) на SQLite. Битые JSON и база уходят в `*.corrupt-*`, API отвечает 503. | | TTL, атомарная запись, защита от порчи | TTL (`ttl_days`), атомарная запись JSON и транзакции SQLite, битые файлы уходят в `*.corrupt-*`. |
| Форматы вывода, агрегация CIDR, `ip_version` | nftables, mikrotik, bird, frr. | | Форматы вывода, агрегация CIDR, `ip_version` | nftables, mikrotik, bird, frr. |
| Разделение сборщика и API | Отдельный демон, singleton, heartbeat, `/health` по `status.json`. | | Разделение сборщика и API | Отдельный демон, singleton, heartbeat, `/health` по `status.json`. |
| Управление ASN и FQDN через API | Добавление, удаление, `purge`, старение данных удалённых источников. | | Управление ASN и FQDN через API | Добавление, удаление, `purge`, старение данных удалённых источников. |
| Контейнер приложения | `Dockerfile` и `docker-compose.yml`: два сервиса, том, non-root, read-only, healthcheck. | | Контейнер приложения | `Dockerfile` (все модули корня, проверка импорта при сборке) и `docker-compose.yml`: два сервиса, том, non-root, read-only, healthcheck. |
| SQLite | Таблица `addresses`, WAL, автоматическая миграция из JSON (оригиналы сохраняются), транзакции, чтение без блокировок. | | SQLite | Таблица `addresses`, WAL, автоматическая миграция из JSON, транзакции; схема версии 3 (журнал изменений, состояние источников). |
| `POST /collect` | Токен, ответ 202, запрос демону файлом `collect_request.json` (опрос каждые 5 с), объединение запросов, пропуск наложения, `503` без живого демона. | | `POST /collect` | Токен, ответ 202, запрос демону файлом (опрос каждые 5 с), объединение запросов, `503` без живого демона. |
| `GET /addresses/diff` | Журнал `changes` (триггеры, схема версии 2), итоговый эффект вместо истории, срок хранения `changes_retention_days` (30 дней), курсор или время, `410` за горизонтом. Проверено тестами и на копии базы версии 1. | | `GET /addresses/diff?since=` | Журнал изменений на триггерах, курсор или время, `410` за горизонтом, `X-Changes-Cursor` в `/addresses`; строгий разбор `since`. |
| Граф знаний | `graphify-out/`: интерактивный граф, отчёт, JSON; исключён из git и образа. | | Репозиторий и версии | `git`, `origin`; `requirements` с точными версиями и `constraints.txt` (полный `pip freeze`). |
| Тесты | 18 проверок, запуск в контейнере. | | Резервные копии и восстановление | Задание `backup` (онлайн-копия, `quick_check`, ротация `backup_keep`); при порче базы восстановление из последней исправной копии; `last_restore` в `/health`. |
| Защита от пустой выдачи | Потеря базы без копий или восстановление из пустой копии при настроенных источниках: метка `db_recreated.json`, `503` на `/addresses` и `/addresses/diff` до нового сбора; пустая база не копируется. |
| Данные DNS и внешний источник | Фильтр глобальных адресов (`allow_non_global_ips`), повторы запросов к RIPEstat с `sourceapp` и потолком `Retry-After`. |
| Наблюдаемость | Состояние каждого источника (таблица `source_status`), блок `sources` и `degraded` в `/health`, `GET /sources`, итоговая строка запуска в логе. |
| Ревью и граф знаний | Отчёт ревью (11 находок, все закрыты), граф `graphify-out/` (вне git и образа). |
| Тесты | 30 проверок, запуск в контейнере, изоляция файлов состояния (`conftest.py`). |
## Не сделано ## Не сделано
| Направление | Статус | | Направление | Статус |
|---|---| |---|---|
| Автоматическая резервная копия базы | Есть только команда в README (`sqlite3 ".backup"`), заданий в демоне нет | | Запуск на реальных данных | Не начато (см. наблюдение 1); следующий шаг |
| Метрики `/metrics`, алерты | Не начато | | Уведомления (webhook, Telegram) | Не начато; основа (журнал, `/health`, `/sources`) есть |
| Уведомления о изменениях (webhook, Telegram) | Не начато; основа (журнал) есть | | Метрики Prometheus (`/metrics`) | Исключены из плана решением пользователя; при необходимости - отдельная доработка |
| Повторные попытки запросов к RIPE | Не начато | | Ограничение частоты `POST /collect` | Не начато |
| Архитектурный рефакторинг | Не начато: вынос путей в `settings.py`, разделение `api_server.py`, `cidr_collector.py` и `db.py` |
| Копии вне сервера | Не начато: копии лежат на том же томе, что и база (вынос описан в README) |
| TLS перед API | Не начато (решение пользователя: порт наружу без TLS) | | TLS перед API | Не начато (решение пользователя: порт наружу без TLS) |
## Остаточные риски ## Остаточные риски
| # | Риск | Статус | | # | Риск | Статус |
|---|---|---| |---|---|---|
| 1 | Нет репозитория git, истории изменений нет. | Не закрыт | | 1 | ~~Нет репозитория git.~~ | Закрыт |
| 2 | Версии в `requirements.txt` не закреплены, это влияет и на образ. | Не закрыт | | 2 | ~~Версии в `requirements.txt` не закреплены.~~ Базовый образ `python:3.11-slim` не закреплён по дайджесту (осознанно). | Закрыт |
| 3 | После порчи базы API отдаёт пустой список, пока сборщик не наполнит новую базу; автовосстановления из копии нет. Курсоры diff после пересоздания базы недействительны (`410`), клиент делает полную выгрузку. | Не закрыт | | 3 | ~~Пустой список после порчи базы.~~ Остаются не покрытые случаи: ручное удаление файла базы (порчи нет, пустая база создаётся без метки) и порча внутри файла (503 при чтении, восстановление вручную). | Закрыт, остаток описан |
| 4 | MikroTik и FRR не проверены на реальном ПО. | Не закрыт | | 4 | MikroTik и FRR не проверены на реальном ПО. | Не закрыт |
| 5 | Токен идёт по HTTP открытым текстом. | Осознанный выбор | | 5 | Токен идёт по HTTP открытым текстом. | Осознанный выбор |
| 6 | Один общий токен, без ротации и аудита. | Не закрыт | | 6 | Один общий токен, без ротации и аудита. | Не закрыт |
| 7 | Тестов минимум, что соответствует правилам проекта. | Осознанно | | 7 | Тестов минимум, что соответствует правилам проекта. | Осознанно |
| 8 | `POST /collect` без ограничения частоты: повторные запросы во время идущего сбора пропускаются, но защиты от нагрузки на RIPE нет. | Не закрыт | | 8 | `POST /collect` без ограничения частоты: повторные запросы во время сбора пропускаются, но защиты от нагрузки на RIPE нет (при этом запросы к RIPEstat теперь идут с повторами). | Не закрыт |
| 9 | Журнал diff проверен только на копиях и тестах: размер и нагрузка на реальных данных неизвестны. | Не закрыт (новое) | | 9 | Журнал diff и состояние источников проверены только на копиях и тестах: размер и нагрузка на реальных данных неизвестны. | Не закрыт |
| 10 | Копии базы лежат на том же томе, что и база: защита от порчи файла, но не от потери тома. | Не закрыт (описан в README) |
| 11 | Нет активных оповещений: о сбое источников можно узнать только опросом `/health` или из лога. | Не закрыт |
## Наблюдения ## Наблюдения
| # | Наблюдение | Состояние | | # | Наблюдение | Состояние |
|---|---|---| |---|---|---|
| 1 | **Изменения не применены к реальным данным.** В каталоге проекта нет `ripe.db`, `data.json` и `fqdn_data.json` остались в старом формате; всё проверено только на копиях. Первый запуск на боевых данных выполнит миграцию до схемы версии 2: `last_seen` старых записей станет равным времени миграции, журнал diff начнётся пустым (импорт в него не пишется), клиентам стартовать с `X-Changes-Cursor`. | Без изменений | | 1 | **Изменения не применены к реальным данным.** В каталоге проекта нет `ripe.db`; `data.json` (6 ASN) и `fqdn_data.json` (4 имени) остались в старом формате; всё проверено только на копиях. Первый запуск на боевых данных выполнит миграцию сразу до схемы версии 3: `last_seen` старых записей станет равным времени миграции, журнал diff и состояние источников начнутся пустыми (клиентам стартовать с `X-Changes-Cursor`), первые запуски сбора создадут состояние источников. | Без изменений |
| 2 | **`/health` не видит сбоев источников.** Недоступный RIPE или DNS пишется в лог, задание считается успешным (`last_error` отражает только исключение всего сбора). | Без изменений | | 2 | ~~`/health` не видит сбоев источников.~~ Сбои учитываются по каждому источнику, порог `source_failure_threshold` (по умолчанию 3): при суточном расписании FQDN это три дня до `degraded`. | Закрыто |
| 3 | ~~Мусор от сборок: 65 безымянных образов.~~ Сейчас 3 безымянных образа. | Снято | | 3 | **Безымянные образы Docker: 21 на хосте.** Часть - от пересборок тестового образа `ripe-tests` при разработке, на хосте есть и чужие (`docker image prune` затронет все). | Ухудшилось (было 3) |
| 4 | **Повреждённый `config.json`** переименовывается при чтении; первый `POST /asns` после этого создаст файл без остальных источников, расписания и `ttl_days`. | Без изменений | | 4 | **Повреждённый `config.json`** переименовывается при чтении; первый успешный `POST /asns` после этого создаст файл без остальных источников, расписания и `ttl_days`. | Без изменений |
| 5 | **Логи:** сообщение «data saved» пишется после каждой транзакции, даже без изменений (`cidr_collector.py:152,206`). | Без изменений | | 5 | ~~Сообщение «data saved» после каждой транзакции.~~ Вместо него итоговая строка запуска. | Закрыто |
| 6 | **Структура README:** разделы 8 (Docker) и 9 (SQLite) дописаны в конец, разделы 1-4 описывают ручную установку. Стоит перестроить: Docker в начало, ручная установка ниже. | Без изменений | | 6 | **Структура README:** разделы 8 (Docker) и 9 (SQLite) в конце, разделы 1-4 описывают ручную установку; за время доработок README вырос до 540 строк. Стоит перестроить: Docker и быстрый старт в начало, ручная установка ниже. | Без изменений |
| 7 | **`google.com`** в данных не входит в конфигурацию, его адрес удалится через 90 дней после миграции. | Без изменений | | 7 | **`google.com`** есть в `fqdn_data.json`, но не входит в конфигурацию: его адрес после миграции истечёт через 90 дней. | Без изменений |
| 8 | **Ручной сбор стартует не мгновенно**, а в пределах 5 секунд (интервал опроса демона). | Без изменений | | 8 | **Ручной сбор стартует не мгновенно**, а в пределах 5 секунд (интервал опроса демона). | Без изменений |
| 9 | **Логи APScheduler о плановых запусках скрыты** (чтобы опрос запросов каждые 5 с не засорял лог); остаются сообщения самого приложения. | Без изменений | | 9 | **Логи APScheduler о плановых запусках скрыты** (чтобы опрос запросов каждые 5 с не засорял лог); остаются сообщения приложения. | Без изменений |
| 10 | **Общая точка отказа хранилища (по графу).** Главные узлы: `load_json()` (19 связей), `session()` (17), `StorageError` (16). Это осознанная развязка: одно исключение скрывает JSON и SQLite от API, демона и CLI (проверено по исходникам: `api_server.py:94,192`, `collector_daemon.py:79,102,147`, `cidr_collector.py:265`). Но любое изменение `storage.py`/`db.py` затрагивает все три процесса, покрытие тестами здесь важнее всего. | Новое | | 10 | **Общая точка отказа хранилища (по графу):** главные узлы `session()` (22 связи), `get_addresses()` (15), `load_full_config()` (14), `_recover()` (14). Развязка через один тип `StorageError` сохраняется, но `db.py` (544 строки) стал самым крупным модулем и совмещает схему, восстановление, копии, журнал и состояние источников. | Переосмыслено |
| 11 | **`api_server.py` растёт (320 строк, связность сообщества 0,06 по графу):** схемы, проверки, все эндпоинты в одном модуле; после diff стал больше. Разделять пока не нужно, но при следующем эндпоинте стоит вынести схемы и разбор параметров. | Новое | | 11 | **Рост модулей и цикл зависимостей:** `db.py` берёт пути и настройки из `cidr_collector.py` отложенным импортом внутри функций; `api_server.py` (381 строка) и `cidr_collector.py` (386) совмещают по несколько ролей. Пока работает, но следующий эндпоинт или задание усугубит. | Ухудшилось |
| 12 | **Граф не заменяет проверку.** 19 изолированных узлов (например, описания эндпоинтов в README) не связаны с обработчиками в коде: это ограничение семантической выгрузки, а не обязательно пробел в документации. Расход токенов на построение в `cost.json` не записан (нули). Документы `analysis-*.md` сами входят в граф, поэтому он частично отражает выводы анализа, а не независимую оценку. | Новое | | 12 | **Граф знаний устарел и не заменяет проверку.** Последнее построение (527 узлов, 1029 связей) было до доработки «наблюдаемость» (код `db.py`, `cidr_collector.py`, `api_server.py`, тесты и документы не отражены); хук обновления после коммита не установлен. Изолированных узлов 36 (описания без связи с кодом), расход токенов на построение не учтён (нули). | Требует обновления |
| 13 | **Порог `degraded` для FQDN:** имя, разрешившееся только в неглобальные адреса (без `allow_non_global_ips`), через три запуска даёт `degraded` (`no_global_addresses`); источник, вернувший 0 адресов корректным ответом, `degraded` не вызывает, виден только в `/sources`. | Новое |
## Рекомендуемый порядок ## Рекомендуемый порядок
1. **Гигиена и резервные копии:** `git init` и первый коммит (после этого можно поставить хук графа), закрепление версий, ежедневное задание демона `db_backup` (`.backup` с хранением нескольких копий) и автовосстановление из последней копии при порче. 1. **Запуск на реальных данных (главный шаг).** Перед запуском: копия `config.json`, `data.json`, `fqdn_data.json` вне каталога проекта и вне тома (миграция переименует оригиналы в `*.migrated-*`, но копия на случай ошибки обязательна). Затем `docker compose up -d` с переносом файлов в том (порядок в README), проверка `/health`, `/sources`, `/addresses` и `/addresses/diff`, первый ручной сбор через `POST /collect`, оценка размера базы и журнала, выбор порога `source_failure_threshold`. Нужен отдельный план с откатом.
2. **Наблюдаемость:** учёт ошибок по каждому источнику (`last_success`, число ошибок) в `status.json` и `/health`, затем `/metrics` для Prometheus и повторные попытки запросов к RIPE. 2. **Обновить граф** (`/graphify . --update`, документы выгружать целиком) и при желании поставить хук после коммита.
3. **Развёртывание на реальных данных:** запуск через compose с миграцией текущих файлов (наблюдение 1), оценка размера журнала (риск 9). 3. **Уведомления** (webhook, Telegram) на основе журнала и состояния источников: закрывает риск 11.
4. **TLS-прокси** перед API, если появится внешний доступ. 4. **Архитектурный рефакторинг:** `settings.py` (пути и умолчания), разделение `db.py` и `api_server.py`; снимает наблюдения 10-11.
5. **Уведомления** (webhook, Telegram): демон после сбора считает diff по журналу и отправляет непустой результат. 5. **По мере необходимости:** ограничение частоты `POST /collect` и ротация токена (риски 6, 8), проверка MikroTik и FRR на реальном ПО (риск 4), вынос копий за пределы тома (риск 10), TLS-прокси при внешнем доступе (риск 5), перестройка README (наблюдение 6), `docker image prune` (наблюдение 3).
Обновлять граф после доработок: `/graphify . --update`.
## Связанные документы ## Связанные документы
- `plan-*.md` / `summary-*.md`: reliability-security, tests-in-container, output-formats, process-split, asn-fqdn-api, app-container, sqlite-storage, collect-endpoint, diff-endpoint. - `plan-*.md` / `summary-*.md` (по 17): reliability-security, tests-in-container, output-formats, process-split, asn-fqdn-api, app-container, sqlite-storage, collect-endpoint, diff-endpoint, git-init, pin-dependencies, db-backup, input-hardening, loss-guard, review-fixes-5-10, empty-backup-guard, observability.
- Предыдущий анализ: `analysis-2026-09-20.md`. - Ревью: `review-2026-09-21.md`; предыдущий анализ: `analysis-2026-09-20.md`.
- Граф: `graphify-out/GRAPH_REPORT.md`, `graphify-out/graph.html`. - Граф: `graphify-out/GRAPH_REPORT.md`, `graphify-out/graph.html` (локально, вне git).