# Наблюдаемость: метрики, логи, алерты Задача #3078. Стек живёт по адресу `https://metrics.gendsgn.ru/`. ## Зачем это заведено Метрик в проекте не было ни одной — ни Prometheus, ни экспортеров, ни `/metrics` в бэкендах. Единственным каналом наблюдения оставался journald, а единственным сигналом об аварии — исключение в GlitchTip. Из-за этого целый класс отказов был невидим в принципе: задача рапортует `done`, строк ноль, исключения нет, метрики нет. Так протухли данные на семь месяцев (#2846/#2998), так 34 дня был мёртв `house_imv_backfill` (#2698), так 8 суток писал ноль записей `newbuilding_enrich` (#2767), так 91 день копилось раздутие `listings` (#2992). Grafana **не заменяет** GlitchTip. Ошибки остаются там: Grafana OSS не принимает Sentry DSN ни одним компонентом, а 549 строк скрубберов в `before_send` — это требование 152-ФЗ, а не фича трекера. Здесь появляется другой класс данных — числовые ряды и алерты по трендам вместо алертов по событиям. ## Топология ``` Poincare (188.246.224.93) — продукт Beget (46.173.16.127) — инфраструктура ┌──────────────────────────────┐ ┌────────────────────────────────────┐ │ node-exporter │ │ Prometheus ← приёмник remote_write │ │ cAdvisor │ HTTPS │ Loki ← приёмник логов │ │ postgres-exporter × 2 │ ──────────► │ Grafana ← витрина │ │ Alloy ──── push ────────────┼─ 443 ─────► │ Alertmanager (за профилем alerts) │ └──────────────────────────────┘ │ node-exporter, cAdvisor, Alloy │ │ postgres-exporter (infra) │ └────────────────────────────────────┘ ``` **Наблюдатель стоит у другого провайдера, чем наблюдаемое.** Если ляжет Poincare, мониторинг обязан выжить и сказать об этом, а не лечь вместе с ним. **Транспорт — push.** Prometheus не ходит на Poincare: там агент сам шлёт исходящим HTTPS. Поэтому на продуктовом хосте не открыто ни одного входящего порта сверх 22/80/443. Побочный выигрыш: при обрыве канала Alloy копит в WAL и досылает, а pull-скрейп в той же ситуации просто потерял бы точки — то есть именно в аварии, ради которой мониторинг и заводится. ## Что где лежит | Файл | Назначение | |---|---| | `docker-compose.metrics.yml` | серверная сторона, только Beget | | `docker-compose.metrics-agent.yml` | агенты, оба хоста; профили `apps` / `infra` | | `ops/metrics/prometheus/` | конфиг и правила алертов | | `ops/metrics/loki/` | конфиг Loki | | `ops/metrics/alloy/alloy-infra.alloy` | агент на Beget — пишет напрямую по docker-сети | | `ops/metrics/alloy/alloy-apps.alloy` | агент на Poincare — пишет по HTTPS под basic_auth | | `ops/metrics/postgres/queries.yml` | дополнительные запросы экспортера БД | | `ops/metrics/grafana/` | источники данных и дашборды | | `caddy/sites/infra.caddy` | site-блок `metrics.gendsgn.ru` | | `scripts/setup-metrics-secrets.sh` | разовая подготовка учёток | ## Первый запуск ```bash # 1. На инфраструктурном хосте — один раз. Пароли не печатает, печатает хеши. ssh gendesign 'cd /opt/gendesign && bash scripts/setup-metrics-secrets.sh' # 2. Хеши из вывода вставить в caddy/metrics-ui.caddy.snippet и # caddy/metrics-ingest.caddy.snippet, закоммитить. # 3. Деплой. # Forgejo → Actions → Deploy Metrics → Run workflow ``` Пароль витрины смотреть на хосте, в переписку не копировать: ```bash ssh gendesign "grep '^METRICS_UI_PASSWORD=' /opt/gendesign/backend/.env.runtime" ``` ## Два контура доступа, две учётки `metrics.gendsgn.ru/ingest/*` — машинный, для агента. Пароль этого контура по построению лежит в открытом виде на продуктовом хосте: агенту нужно им авторизоваться. Значит, компрометация Poincare раскрывает его автоматически. Если бы это была учётка витрины, вместе с ней утёк бы доступ к дашбордам и к Prometheus, где видна вся инфраструктура. Разделение ограничивает ущерб записью. Всё остальное — витрина Grafana под отдельным паролем. Это **второй слой**: у Grafana остаётся собственный вход с ролями. Внешний basic_auth отсекает сканеры и любую будущую дыру в самой Grafana до того, как она станет достижимой снаружи. Если два запроса пароля подряд неудобны — убрать одну строку `import caddy/metrics-ui.caddy.snippet` из `infra.caddy`; вход Grafana останется. ## GlitchTip читается прямым SQL, а не плагином Плагин `grafana/sentry-datasource` GlitchTip официально документирует, но на нашей 6.1.6 половина путей нерабочая: `stats_v2` с фильтром по проекту отдаёт 500 (открытый баг GlitchTip #381 с 2025-01-10), Events/Discover — 404 (#416), Metrics/Spans/Tags вообще Sentry-only. В самом `grafana/sentry-datasource` слово «glitchtip» не встречается ни разу — апстрим связку не тестирует. Прямой SQL правок в GlitchTip не требует вовсе, нужен только read-only пользователь (`scripts/setup-metrics-grafana-role.sh`, прав на запись нет). Схема проверена на живой базе 26.08: - `issue_events_issue` — `count`, `first_seen`, `last_seen`, `status`, `level`, `project_id`, `title`, `culprit` - `issue_events_issueaggregate` — `(issue_id, organization_id, date, count)`, партиционирована по неделям с почасовыми под-партициями - `issue_events_issueevent` — колонка времени называется **`timestamp`** (плюс `created`); колонки `received` в этой версии нет Отдельной таблицы `IssueIndex` **не существует** — все агрегаты лежат на самой `issue_events_issue`. В более ранних описаниях этой задачи она упоминалась; это была ошибка, проверено глазами. **Граница:** доступ read-only. Тренды — в Grafana, а assign / resolve / ignore, стектрейсы и breadcrumbs — только в GlitchTip. Окна остаётся два: витрина и рабочее место. Это осознанно, а не недоделка. ## Алерты Пока выключены профилем. Канал доставки — открытый вопрос #3078: тот же чат, что у вебхука GlitchTip, или отдельный, и при каком пороге будить ночью. Стек метрик работает и без них, но **при срабатывании правила никто не будет уведомлён** — деплой пишет об этом предупреждением, чтобы это не стало сюрпризом. Включение: задать `METRICS_TELEGRAM_BOT_TOKEN` и `METRICS_TELEGRAM_CHAT_ID` в окружении инфраструктурного хоста и перезапустить деплой. Профиль `alerts` включится сам. Alertmanager шлёт в Telegram **напрямую с Beget**, а не через бэкенд МЕРЫ, хотя рабочий путь доставки там уже есть. Причина простая: сообщение о том, что лёг Poincare, не должно идти через сервис на Poincare. Отдельно живёт правило `Watchdog` — оно горит всегда и раз в 12 часов подтверждает, что цепочка правило → Alertmanager → Telegram → человек цела. Существует ради того, чтобы его отсутствие было заметно: молчащий канал — самый частый способ узнать об аварии последним. В проекте МЕРЫ наружу не ушло ни одного сообщения с 30 мая, и выяснилось это случайно (#2673). ## Известные грабли - **Alloy запущен от root.** Штатный пользователь `alloy` требует членства в группах `adm` и `systemd-journal`; внутри контейнера этих групп с нужными gid нет, и агент молча читает ноль записей журнала. Отказ выглядит как «логов просто нет». - **Путь к журналу задан явно** (`/var/log/journal`). Без него libsystemd применяет `SD_JOURNAL_LOCAL_ONLY`, и хостовый журнал из контейнера не виден — тоже без ошибки. - **`retention_period` у Loki не работает без `retention_enabled` у компактора.** Loki примет конфиг и будет копить вечно. - **Ретенция Prometheus задана и по времени, и по размеру.** Только по времени — значит, объём в байтах зависит от числа рядов, которое растёт само. - **Caddy проверяется до перезагрузки.** На Beget тот же Caddy обслуживает `git.`, `errors.` и `obsidian.`; ошибка в `infra.caddy` положила бы их все, включая Forgejo, из которого идёт деплой. ## Что ещё не сделано - `/metrics` в бэкендах (часть 3) — единственная часть, трогающая прод-код - экспортер поверх `scrape_runs`: success_ratio, свежесть приёмника, утилизация, счётчик `cancelled` (часть 4) - алерты и синтетический heartbeat (часть 5) - `pg_stat_statements` для кластера Птицы — требует рестарта прод-БД, отдельно - честные `started_at` / `finished_at` у прогонов (#2702) — предусловие для графиков пропускной способности: пока start/finish врут, врут и графики