diff --git a/DEPLOY.md b/DEPLOY.md index cb0f64f..6f1bb70 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -15,9 +15,13 @@ cd DispelEvilBot ```bash cp env/test.env.example env/test.env -# заполнить: BOT_TOKEN, DB_PASSWORD, CHAT_ID (основной чат), ALERT_CHAT_ID (опц.), ADMIN_IDS (опц.) +# заполнить: BOT_TOKEN, DB_PASSWORD, CHAT_ID (основной чат), ALERT_CHAT_ID (опц.), +# ADMIN_IDS (опц.), CLICKHOUSE_PASSWORD (аналитика событий) ``` +Порт ClickHouse на хосте (`CLICKHOUSE_HTTP_PORT`) у контуров должен различаться: +`prod` — `8123`, `test` — `8124` (иначе контейнеры конфликтуют на одной VPS). + ## 3. Логин в registry (если пакет приватный) ```bash @@ -46,7 +50,62 @@ docker compose -p test --env-file env/test.env -f docker-compose.prod.yml logs - 1. `postgres` поднимается и проходит healthcheck; 2. `migrate` (**Flyway**) накатывает миграции из `sql/` — для пустой БД с нуля; -3. `bot` стартует (`Запуск DispelEvilBot v<версия> (APP_ENV=...)`). +3. `clickhouse` поднимается и применяет `clickhouse/init/001_events.sql` — + **только при первой инициализации** пустого data-каталога; +4. `bot` стартует (`Запуск DispelEvilBot v<версия> (APP_ENV=...)`), аналитика и рассылки — в нём же. + +Проверка контура (сервисы, миграции, Postgres, ClickHouse): + +```bash +make check E=test +``` + +## ClickHouse (аналитика событий) + +События всех действий бота пишутся в `analytics.events` (контракт — [`docs/events.md`](docs/events.md)). +ClickHouse наружу **не публикуется**: только loopback хоста (`127.0.0.1:${CLICKHOUSE_HTTP_PORT}`). + +```bash +make ch E=test # clickhouse-client контура +# пример: события за сегодня по типам +docker compose -p test --env-file env/test.env -f docker-compose.prod.yml exec -T clickhouse \ + clickhouse-client --user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" \ + --query "SELECT event_type, count() FROM analytics.events WHERE toDate(event_time) = today() GROUP BY event_type" +``` + +Если ClickHouse недоступен, бот продолжает работать: события уходят в stdout (WARNING), +полностью выключить можно флагом `CLICKHOUSE_ENABLED=false`. + +Изменения схемы: `clickhouse/init/*.sql` применяются лишь на пустом data-каталоге, +поэтому на существующем контуре DDL нужно применить вручную (`make ch E=...`). + +## Бэкап событий на домашний сервер + +Сырые события живут на VPS ограниченное время: раз в сутки вчерашняя партиция +выгружается в `Parquet.gz`, передаётся на домашний сервер, импортируется в тамошний +ClickHouse и сверяется по количеству строк — **только после сверки** партиции старше +`BACKUP_GRACE_DAYS` (по умолчанию 7) удаляются на VPS. На VPS остаются агрегаты +(`events_daily`, `money_daily`). + +Разовая настройка: + +```bash +cp env/backup.env.example env/backup.env # BACKUP_REMOTE, BACKUP_DIR, BACKUP_SSH_KEY, ... +# на домашнем сервере (в каталоге, куда кладутся архивы) разместить: +# scripts/clickhouse_import.sh +# clickhouse/init/001_events.sql -> как 001_events.sql рядом со скриптом +``` + +Проверочный прогон (без cron): `make backup E=test`. + +Cron на VPS (вчерашний день уже закрыт, время московское): + +```cron +0 4 * * * cd /opt/DispelEvilBot && ./scripts/clickhouse_backup.sh test >> /var/log/clickhouse-backup-test.log 2>&1 +``` + +Ошибка на любом шаге оставляет данные на VPS (партиции не удаляются) и пишет понятный лог. +Импорт дня идемпотентен: повторный прогон не дублирует строки. ## Обновление и откат @@ -67,7 +126,13 @@ docker compose -p <контур> --env-file env/<контур>.env -f docker-com ## Переменные окружения -См. `env/prod.env.example` / `env/test.env.example`. +См. `env/prod.env.example` / `env/test.env.example` (контуры) и `env/backup.env.example` (бэкап событий). + +## Дашборд (позже) + +Superset поднимается на домашнем сервере и читает **локальный** ClickHouse с полной +историей событий (сюда ежедневно приезжают архивы). Отдельный read-only пользователь +ClickHouse и сам дашборд — финальный этап, в этот контур не входят. ## Перенос данных на прод (позже, короткое окно простоя) diff --git a/README.md b/README.md index 3434a76..f6408c9 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,12 @@ Telegram-бот клана Dispel Evil (aiogram 3 + Postgres). Монорепо ## Структура ``` -common/ # общий пакет: config, logging, db, repositories, models, texts -bot/ # сервис бота: handlers, services, keyboards, notifications +common/ # общий пакет: config, logging, db, repositories, models, texts, analytics +bot/ # сервис бота: handlers, services, keyboards, middlewares, notifications sql/ # миграции Flyway (V__*.sql, R__*.sql) +clickhouse/ # init-скрипты ClickHouse (схема аналитики) +docs/ # документация (docs/events.md — события аналитики) +scripts/ # эксплуатационные скрипты (проверка контура, бэкап событий) images/ # статичные картинки (приветствие) deploy/ # материалы деплоя (Caddyfile, инструкции) tests/ # тесты @@ -21,6 +24,7 @@ docker-compose.yml / docker-compose.prod.yml - Python 3.12 - PostgreSQL 14+ +- ClickHouse 25.3 (аналитика событий; можно выключить — `CLICKHOUSE_ENABLED=false`) ## Настройка @@ -44,9 +48,25 @@ APP_ENV=prod python -m bot # prod Схема управляется миграциями **Flyway** в `sql/` (`V__*.sql` — versioned, `R__*.sql` — repeatable). При Docker-развёртывании накат выполняется автоматически сервисом `migrate` перед ботом. +## Аналитика (ClickHouse) + +Все значимые действия бота пишутся в ClickHouse в единую таблицу `analytics.events`. +Контракт событий — [`docs/events.md`](docs/events.md) (типы, payload, примеры запросов). + +- схема — `clickhouse/init/001_events.sql` (применяется при первой инициализации контейнера); +- клиент и батчинг — `common/analytics/` (запись неблокирующая, сбой ClickHouse не влияет на бота); +- сырые события живут на VPS 7 дней и ежедневно уезжают архивом на домашний сервер, + на VPS остаются агрегаты (`events_daily`, `money_daily`) — см. [DEPLOY.md](DEPLOY.md); +- выключается флагом `CLICKHOUSE_ENABLED=false`. + +## Уведомления в чат логов + +В `ALERT_CHAT_ID` (если задан) уходят: вход и выход участников, успешные очищения и +ежечасный пинг «бот живой». Отправка не критична: ошибки только пишутся в лог. + ## Docker -Локальный запуск (postgres + migrate + bot): +Локальный запуск (postgres + migrate + clickhouse + bot): ```bash cp .env.example .env # заполнить @@ -71,9 +91,20 @@ black --check . pytest ``` +## Бэкап событий + +Сырые события ClickHouse ежедневно выгружаются архивом на домашний сервер (по cron), +на VPS остаются только агрегаты и последние дни сырых данных: + +```bash +make backup E=test # ./scripts/clickhouse_backup.sh test +``` + +Настройка и расписание — см. [`DEPLOY.md`](DEPLOY.md). + ## Версионирование -- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.4`. +- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.5`. - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`). - Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3). - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. @@ -90,14 +121,14 @@ pytest - Переход на «ветки + PR» и защиту ветки — когда появится второй разработчик. ```bash -# обычная работа +# обычная работа: сообщение без префиксов, по существу изменения git add -A -git commit -m "feat: ..." +git commit -m "Краткое описание изменения" git push origin master # выпуск релиза # 1) поднять __version__ в common/version.py -git commit -am "chore: bump version to X.Y" +git commit -am "Версия X.Y" git tag -a vX.Y -m "Release vX.Y" git push origin master --tags ``` @@ -109,4 +140,6 @@ git push origin master --tags ## Переменные окружения -См. `.env.example`. +- общие — `.env.example` (включая `CLICKHOUSE_*` и параметры батчинга `ANALYTICS_*`); +- контуры — `env/prod.env.example`, `env/test.env.example`; +- бэкап событий — `env/backup.env.example`. diff --git a/common/version.py b/common/version.py index 7d94984..cfd401a 100644 --- a/common/version.py +++ b/common/version.py @@ -7,4 +7,4 @@ from __future__ import annotations -__version__ = "0.3.4" +__version__ = "0.3.5" diff --git a/docs/events.md b/docs/events.md index 7477a37..1423737 100644 --- a/docs/events.md +++ b/docs/events.md @@ -8,6 +8,48 @@ > Расхождение со списком ниже ловит `tests/test_analytics.py`. > Добавляешь событие — добавь строку в таблицу. +## Схема в ClickHouse + +DDL — `clickhouse/init/001_events.sql` (применяется при первой инициализации контейнера). + +- `analytics.events` — сырые события, `MergeTree`, `PARTITION BY toYYYYMMDD(event_time)`, + `ORDER BY (event_time, event_type, user_id)`. Партиционирование по дню выбрано специально: + сырые партиции ежедневно удаляются после бэкапа (дешёвый `DROP PARTITION`, без mutation). +- `analytics.events_daily` — агрегат (`SummingMergeTree`): `day`, `event_type`, `user_id`, + `chat_id`, `events`. Строится материализованным представлением `events_daily_mv`. +- `analytics.money_daily` — агрегат по деньгам: `day`, `event_type`, `user_id`, + `amount_sum`, `events` (из `payload.amount` событий `money_accrual`/`money_spend`). + +Агрегаты остаются на VPS навсегда, поэтому активность, топ команд и денежные потоки +доступны и после удаления сырых событий. Запросы к агрегатам суммируют `events`/`amount_sum` +(фон слияний `SummingMergeTree` не гарантирует полную свёртку в любой момент). + +## Хранение и бэкап + +| Что | Где | Сколько | +| --- | --- | --- | +| Сырые события | VPS, `analytics.events` | последние `BACKUP_GRACE_DAYS` дней (по умолчанию 7) | +| Агрегаты | VPS | постоянно | +| Полная история | домашний сервер, свой ClickHouse | архивы `events_YYYY-MM-DD.parquet.gz` (ежедневно) | + +Каждую ночь (по Москве) `scripts/clickhouse_backup.sh` выгружает вчерашнюю партицию, +передаёт её на домашний сервер, импортирует и **сверяет количество строк**; только после +успешной сверки на VPS удаляются партиции старше грейса. Ошибка бэкапа данные не удаляет. +Подробности — `DEPLOY.md`. + +## Уведомления в чат логов + +Помимо записи в ClickHouse часть событий дублируется сообщением в `ALERT_CHAT_ID` +(если он задан; `0` — выключено). Отправка не критична: ошибки только логируются. + +| Уведомление | Когда | Событие в ClickHouse | +| --- | --- | --- | +| Вход участника | кто-то вошёл в основной чат | `join` | +| Выход участника | кто-то вышел из основного чата | `leave` | +| Очищение | успешное `/очищение` (`+опыт`, `+монеты`) | `purification` | +| Пинг «бот живой» | раз в час | `bot_alive` | +| Попытка доступа извне | не участник чата стучится в бота | `access_denied` | + ## Поля события | Поле | Тип | Описание | diff --git a/tests/test_version.py b/tests/test_version.py index 617387e..8a5bc2c 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,4 +7,4 @@ from common import __version__ def test_version_is_set(): assert __version__ - assert __version__ == "0.3.4" + assert __version__ == "0.3.5"