All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Метрик в проекте не было ни одной: ни экспортеров, ни /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
164 lines
12 KiB
Markdown
164 lines
12 KiB
Markdown
# Наблюдаемость: метрики, логи, алерты
|
||
|
||
Задача #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 врут, врут и графики
|