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
74 lines
3.7 KiB
Cheetah
74 lines
3.7 KiB
Cheetah
# Шаблон конфигурации Alertmanager. Боевой файл собирается на хосте при деплое
|
||
# (`envsubst` в deploy-metrics.yml) и в репозиторий не попадает — токен бота и
|
||
# идентификатор чата живут в окружении хоста, а не в git.
|
||
#
|
||
# ПОЧЕМУ TELEGRAM НАПРЯМУЮ, А НЕ ЧЕРЕЗ БЭКЕНД МЕРЫ. У МЕРЫ уже есть рабочий путь
|
||
# доставки (вебхук GlitchTip → бот), и соблазн переиспользовать его велик. Но тогда
|
||
# сообщение о том, что лёг Poincare, шло бы через сервис НА Poincare. Алерт обязан
|
||
# уметь уйти без участия хоста, про который он написан.
|
||
#
|
||
# Telegram с Beget работает — проверено серией замеров 25.08: TLS-рукопожатие
|
||
# 18 из 20, getMe 4 из 4. Ломается только длинный long-poll (30 с), а отправка
|
||
# сообщения — короткий запрос.
|
||
|
||
global:
|
||
resolve_timeout: 5m
|
||
|
||
route:
|
||
receiver: telegram
|
||
# Группируем по алерту и хосту: пятнадцать контейнеров одного хоста, упавших
|
||
# разом, — это одно событие, а не пятнадцать сообщений.
|
||
group_by: ["alertname", "host"]
|
||
group_wait: 45s
|
||
group_interval: 5m
|
||
# Повтор раз в 6 часов. Чаще — приучает игнорировать, реже — можно проспать.
|
||
repeat_interval: 6h
|
||
|
||
routes:
|
||
# Watchdog не должен смешиваться с настоящими алертами и не должен молчать:
|
||
# это «сторож сторожа», он горит всегда и подтверждает, что канал доставки жив.
|
||
- receiver: telegram-heartbeat
|
||
matchers:
|
||
- alertname = "Watchdog"
|
||
group_wait: 0s
|
||
group_interval: 12h
|
||
repeat_interval: 12h
|
||
|
||
# Критичное — без задержки на группировку.
|
||
- receiver: telegram
|
||
matchers:
|
||
- severity = "critical"
|
||
group_wait: 10s
|
||
repeat_interval: 3h
|
||
|
||
inhibit_rules:
|
||
# Если хост целиком недоступен, не сыпать отдельно про каждый его сервис.
|
||
- source_matchers: [alertname = "HostAgentDown"]
|
||
target_matchers: [severity =~ "warning|critical"]
|
||
equal: ["host"]
|
||
|
||
receivers:
|
||
- name: telegram
|
||
telegram_configs:
|
||
- bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}"
|
||
chat_id: ${METRICS_TELEGRAM_CHAT_ID}
|
||
api_url: "https://api.telegram.org"
|
||
parse_mode: HTML
|
||
send_resolved: true
|
||
message: |
|
||
{{ if eq .Status "firing" }}🔴{{ else }}🟢{{ end }} <b>{{ .CommonLabels.alertname }}</b>{{ if .CommonLabels.host }} · {{ .CommonLabels.host }}{{ end }}
|
||
{{ range .Alerts }}
|
||
{{ .Annotations.summary }}
|
||
{{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}
|
||
{{ end }}
|
||
|
||
- name: telegram-heartbeat
|
||
telegram_configs:
|
||
- bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}"
|
||
chat_id: ${METRICS_TELEGRAM_CHAT_ID}
|
||
api_url: "https://api.telegram.org"
|
||
parse_mode: HTML
|
||
send_resolved: false
|
||
message: |
|
||
⚪ <b>Мониторинг жив</b> — сторож отчитался, канал доставки работает.
|
||
Если это сообщение перестало приходить дважды подряд, замолчал сам мониторинг.
|