Метрик в проекте не было ни одной: ни экспортеров, ни /metrics в бэкендах, единственный канал наблюдения — journald, единственный сигнал об аварии — исключение в GlitchTip. Из-за этого целый класс отказов невидим в принципе: задача рапортует done, строк ноль, исключения нет. Так протухли данные на семь месяцев (#2998), 34 дня был мёртв house_imv_backfill (#2698), 8 суток писал ноль newbuilding_enrich (#2767), 91 день копилось раздутие listings (#2992). Grafana не заменяет GlitchTip: ошибки остаются там. Grafana OSS не принимает Sentry DSN ни одним компонентом, а скрубберы в before_send — требование 152-ФЗ. Здесь появляется другой класс данных: ряды и алерты по трендам. Наблюдатель поставлен у ДРУГОГО провайдера, чем наблюдаемое: серверная сторона на Beget, рядом с GlitchTip. Если ляжет Poincare, мониторинг должен об этом сказать, а не лечь вместе с ним. Транспорт push, а не pull: агент на Poincare шлёт remote_write и логи исходящим HTTPS, поэтому там не открывается ни одного входящего порта сверх 22/80/443. При обрыве канала Alloy копит в WAL и досылает — pull-скрейп в той же ситуации терял бы точки именно в аварии, ради которой мониторинг и нужен. Два контура доступа с разными учётками. Пароль приёмника по построению лежит открытым на продуктовом хосте, значит его компрометация неизбежна вместе с хостом; будь это учётка витрины, утёк бы и доступ к дашбордам. GlitchTip читается прямым SQL, а не Sentry-плагином: у плагина на 6.1.6 stats_v2 отдаёт 500 (баг GlitchTip #381), Events/Discover — 404 (#416), а в grafana/sentry-datasource слово glitchtip не встречается ни разу. Схема сверена на живой базе: колонка времени называется timestamp, а не received, и отдельной таблицы IssueIndex не существует — агрегаты лежат на самой issue_events_issue. Алерты за профилем alerts: канал доставки — открытый вопрос #3078, и стек не должен на нём стоять. Деплой предупреждает, что уведомлять пока некому. Каждая настройка, способная отказать молча, закрыта явно: ретенция Prometheus задана и по времени и по размеру, retention_enabled у компактора Loki (без него retention_period не работает вовсе), путь к журналу и запуск Alloy от root (иначе агент читает ноль записей без ошибки), проверка Caddy до перезагрузки (на этом хосте тот же Caddy держит git, errors и obsidian). Refs #3078
12 KiB
Наблюдаемость: метрики, логи, алерты
Задача #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 |
разовая подготовка учёток |
Первый запуск
# 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
Пароль витрины смотреть на хосте, в переписку не копировать:
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,culpritissue_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 врут, врут и графики