diff --git a/.forgejo/workflows/deploy-metrics.yml b/.forgejo/workflows/deploy-metrics.yml index 01407644..474d38d9 100644 --- a/.forgejo/workflows/deploy-metrics.yml +++ b/.forgejo/workflows/deploy-metrics.yml @@ -91,12 +91,16 @@ jobs: METRICS_TELEGRAM_INFRA_TOPIC_ID: ${{ secrets.METRICS_TELEGRAM_INFRA_TOPIC_ID }} METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }} ALERT_ACK_GLITCHTIP_SECRET: ${{ secrets.ALERT_ACK_GLITCHTIP_SECRET }} + # #3589: URL внешнего deadman-приёмника Watchdog (healthchecks.io и + # аналоги). Пусто — watchdog-ping остаётся без конфигов, см. блок + # METRICS_WATCHDOG_PING_BLOCK ниже. + METRICS_WATCHDOG_PING_URL: ${{ secrets.METRICS_WATCHDOG_PING_URL }} # #3471: секрет ретранслятора Telegram Bot API (tg-relay). Пусто — # профиль relay не включаем (см. PROFILES ниже), а не падаем в # рестарт-луп: контейнер сам делает SystemExit на пустом секрете. TG_RELAY_SECRET: ${{ secrets.TG_RELAY_SECRET }} with: - envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID,METRICS_TELEGRAM_INFRA_TOPIC_ID,METRICS_TELEGRAM_ONCALL,ALERT_ACK_GLITCHTIP_SECRET,TG_RELAY_SECRET + envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID,METRICS_TELEGRAM_INFRA_TOPIC_ID,METRICS_TELEGRAM_ONCALL,ALERT_ACK_GLITCHTIP_SECRET,TG_RELAY_SECRET,METRICS_WATCHDOG_PING_URL host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }} username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }} key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }} @@ -193,6 +197,24 @@ jobs: echo "Инфраструктура: тема ${INFRA_TOPIC_ID} по умолчанию (METRICS_TELEGRAM_INFRA_TOPIC_ID не задана)." fi + # Watchdog-пинг во внешний deadman-приёмник (healthchecks.io и + # аналоги) — заменяет регулярные сообщения в Telegram, см. + # комментарий у watchdog-ping в alertmanager.yml.tmpl. Блок + # целиком, а не значение — та же причина, что у + # METRICS_TELEGRAM_INFRA_TOPIC_LINE: пустой `url:` в + # receiver'е не деградирует, а валит amtool check-config + # целиком, то есть роняет ВЕСЬ алертинг из-за одного + # необязательного получателя. Секрет пока не заведён — + # деградация корректна: receiver остаётся без `*_configs` и + # молча ничего никуда не шлёт, amtool это пропускает. + if [ -n "${METRICS_WATCHDOG_PING_URL:-}" ]; then + METRICS_WATCHDOG_PING_BLOCK=$(printf ' webhook_configs:\n - url: "%s"\n send_resolved: false' "${METRICS_WATCHDOG_PING_URL}") + echo "Watchdog: внешний deadman-пинг настроен." + else + METRICS_WATCHDOG_PING_BLOCK="" + echo "::warning title=Watchdog без deadman-пинга::METRICS_WATCHDOG_PING_URL пуст — сторож мониторинга никуда не сообщает о своей живости. Заведи аккаунт healthchecks.io (или аналог) и секрет, иначе обрыв канала доставки не заметит никто (#3589)." + fi + # Резервный приёмник GlitchTip (#3471) отвечает 503 на любой # запрос, пока секрет пуст: тихо принимать чужие алерты настежь # хуже, чем не принимать вовсе. Молчаливого отказа тут быть не @@ -221,7 +243,8 @@ jobs: METRICS_TELEGRAM_CHAT_ID="$METRICS_TELEGRAM_CHAT_ID" \ METRICS_TELEGRAM_INFRA_TOPIC_LINE="$METRICS_TELEGRAM_INFRA_TOPIC_LINE" \ METRICS_TELEGRAM_ONCALL="${METRICS_TELEGRAM_ONCALL:-}" \ - envsubst '${METRICS_TELEGRAM_BOT_TOKEN} ${METRICS_TELEGRAM_CHAT_ID} ${METRICS_TELEGRAM_INFRA_TOPIC_LINE} ${METRICS_TELEGRAM_ONCALL}' \ + METRICS_WATCHDOG_PING_BLOCK="$METRICS_WATCHDOG_PING_BLOCK" \ + envsubst '${METRICS_TELEGRAM_BOT_TOKEN} ${METRICS_TELEGRAM_CHAT_ID} ${METRICS_TELEGRAM_INFRA_TOPIC_LINE} ${METRICS_TELEGRAM_ONCALL} ${METRICS_WATCHDOG_PING_BLOCK}' \ < ops/metrics/alertmanager/alertmanager.yml.tmpl \ > ops/metrics/alertmanager/alertmanager.yml chmod 600 ops/metrics/alertmanager/alertmanager.yml diff --git a/backend/tests/ops/test_3078_alert_topic.py b/backend/tests/ops/test_3078_alert_topic.py index 501ebfbb..afb4cc5a 100644 --- a/backend/tests/ops/test_3078_alert_topic.py +++ b/backend/tests/ops/test_3078_alert_topic.py @@ -21,8 +21,9 @@ переменной — и инфраструктурная тема «метрики» оставалась пустой, а весь трафик, и клиентский, и инфраструктурный, копился в теме «алерты». Владелец решил развести: инфраструктура — в «метрики», клиентские инциденты — в «алерты». -Тесты ниже закрепляют именно это: `telegram`/`telegram-heartbeat` получают -ИНФРАСТРУКТУРНУЮ тему, а не общую. +Тест ниже закрепляет именно это для единственного оставшегося прямого +получателя — `telegram` (Watchdog с 17.09 больше не шлёт в Telegram вовсе, +у него теперь внешний webhook-приёмник `watchdog-ping`, см. шаблон). Тесты рендерят шаблон обоими способами и разбирают результат как YAML — проверяется фактический конфиг, а не наличие нужных слов в тексте. @@ -43,9 +44,56 @@ WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml" INFRA_TOPIC_LINE = " message_thread_id: 245" +# Заглушки для переменных, которые деплой может подставить. Значение +# METRICS_WATCHDOG_PING_BLOCK по умолчанию пустое — это реальный дефолт +# деплоя, когда секрет не заведён (#3589), а не тестовое упрощение. +_DUMMY_VALUES = { + "METRICS_TELEGRAM_BOT_TOKEN": "123:ABC", + "METRICS_TELEGRAM_CHAT_ID": "-100123", + "METRICS_TELEGRAM_ONCALL": "", + "METRICS_WATCHDOG_PING_BLOCK": "", +} -def _render(infra_topic_line: str) -> dict: - """Повторяет подстановку деплоя и разбирает результат как YAML. + +def _envsubst_allowlist() -> set[str]: + """Реальный список переменных, которые деплой передаёт в envsubst. + + Не хардкодим копию списка — #3589 случился именно так: шаблон завёл + `${METRICS_WATCHDOG_PING_URL}`, а список envsubst в деплое не пополнили, + и тест этого не заметил, потому что сам подставлял значение мимо деплоя. + """ + assert WORKFLOW.is_file(), f"нет {WORKFLOW} — воркфлоу переехал, гейт ослеп" + text = WORKFLOW.read_text(encoding="utf-8") + m = re.search(r"envsubst '([^']+)'", text) + assert m, "не нашёл вызов envsubst в деплое" + return set(re.findall(r"\$\{(\w+)\}", m.group(1))) + + +def _template_placeholders() -> set[str]: + text = TMPL.read_text(encoding="utf-8") + return set(re.findall(r"\$\{(\w+)\}", text)) + + +def test_every_template_placeholder_is_in_envsubst_allowlist() -> None: + """Регресс #3589: переменная шаблона обязана быть в allow-list envsubst. + + Тогда в шаблоне появился `${METRICS_WATCHDOG_PING_URL}`, а список + envsubst в деплое не пополнили. envsubst подставляет ТОЛЬКО + перечисленные переменные — забытая долетает до `amtool check-config` + литералом плейсхолдера и валит проверку (`unsupported scheme ""`), то + есть роняет ВЕСЬ Alertmanager, а не только Watchdog. + """ + missing = _template_placeholders() - _envsubst_allowlist() + assert not missing, f"эти переменные шаблона деплой не подставляет: {missing}" + + +def _render(infra_topic_line: str, watchdog_ping_block: str | None = None) -> dict: + """Повторяет ТОЧНО ТУ ЖЕ подстановку, что делает деплой, и разбирает YAML. + + Подставляются только переменные из реального allow-list envsubst деплоя + (`_envsubst_allowlist`) — не весь известный тесту набор. Так регресс + #3589 (переменная в шаблоне, забытая в allow-list) ловится именно здесь: + `assert "${" not in rendered` ниже упадёт, если что-то не подставилось. Строка темы в шаблоне ровно одна — инфраструктурная (#3163). Тема клиентских инцидентов сюда не подставляется вовсе: маршрут @@ -55,11 +103,18 @@ def _render(infra_topic_line: str) -> dict: """ assert TMPL.is_file(), f"нет {TMPL} — шаблон переехал, гейт ослеп" text = TMPL.read_text(encoding="utf-8") - rendered = ( - text.replace("${METRICS_TELEGRAM_BOT_TOKEN}", "123:ABC") - .replace("${METRICS_TELEGRAM_CHAT_ID}", "-100123") - .replace("${METRICS_TELEGRAM_INFRA_TOPIC_LINE}", infra_topic_line) - ) + + values = dict(_DUMMY_VALUES) + values["METRICS_TELEGRAM_INFRA_TOPIC_LINE"] = infra_topic_line + if watchdog_ping_block is not None: + values["METRICS_WATCHDOG_PING_BLOCK"] = watchdog_ping_block + + allowlist = _envsubst_allowlist() + rendered = text + for name, value in values.items(): + if name in allowlist: + rendered = rendered.replace("${" + name + "}", value) + assert "${" not in rendered, ( "в отрендеренном конфиге остался литерал плейсхолдера — " "значит в шаблоне появилась подстановка, о которой тест не знает" @@ -67,6 +122,37 @@ def _render(infra_topic_line: str) -> dict: return yaml.safe_load(rendered) +def test_watchdog_receiver_without_secret_has_no_configs_and_parses() -> None: + """Секрет не заведён (реальное состояние прода сейчас) — конфиг всё равно жив. + + `watchdog-ping` остаётся без единого `*_configs` — валидный receiver, + Alertmanager его просто пропускает. Деградация корректна: Watchdog никуда + не пингует, но остальной алертинг (`telegram`, `telegram-clients`) цел. + """ + cfg = _render(INFRA_TOPIC_LINE, watchdog_ping_block="") + receivers = {r["name"]: r for r in cfg["receivers"]} + assert "watchdog-ping" in receivers, "receiver watchdog-ping пропал из конфига" + watchdog = receivers["watchdog-ping"] + assert "webhook_configs" not in watchdog, "пустой секрет не должен оставлять webhook_configs" + assert "telegram" in receivers and "telegram-clients" in receivers, ( + "остальной алертинг не должен пострадать из-за пустого watchdog-секрета" + ) + + +def test_watchdog_receiver_with_secret_gets_webhook() -> None: + """Секрет задан — Watchdog реально пингует внешний deadman-приёмник.""" + block = ( + " webhook_configs:\n" + ' - url: "https://hc-ping.com/dummy"\n' + " send_resolved: false" + ) + cfg = _render(INFRA_TOPIC_LINE, watchdog_ping_block=block) + receivers = {r["name"]: r for r in cfg["receivers"]} + hooks = receivers["watchdog-ping"].get("webhook_configs") or [] + assert hooks and hooks[0].get("url") == "https://hc-ping.com/dummy" + assert hooks[0].get("send_resolved") is False + + def _telegram_configs(cfg: dict) -> list[dict]: out = [] for r in cfg.get("receivers", []): @@ -76,16 +162,16 @@ def _telegram_configs(cfg: dict) -> list[dict]: def test_topic_lands_in_every_telegram_receiver() -> None: - """Оба прямых получателя адресуют ИНФРАСТРУКТУРНУЮ тему, а не клиентскую (#3163). + """Единственный прямой получатель адресует ИНФРАСТРУКТУРНУЮ тему, а не клиентскую (#3163). - Получателей два — `telegram` и `telegram-heartbeat`. До разделения тем оба - брали топик из одной переменной с клиентскими инцидентами, и тема «метрики» - (245) оставалась пустой. Если heartbeat уйдёт не в ту тему, «мониторинг жив» - будет капать мимо, и это заметят не сразу — сюда же попадёт и весь - инфраструктурный шум. + До разделения тем `telegram` и `telegram-heartbeat` брали топик из одной + переменной с клиентскими инцидентами, и тема «метрики» (245) оставалась + пустой. С 17.09 (устранение шума Watchdog) прямой Telegram-получатель + остался один — `telegram`; Watchdog теперь пингует внешний + deadman-приёмник вебхуком (`watchdog-ping`, без topic вовсе — не Telegram). """ cfgs = _telegram_configs(_render(INFRA_TOPIC_LINE)) - assert len(cfgs) >= 2, f"ожидалось минимум два получателя telegram, найдено {len(cfgs)}" + assert len(cfgs) >= 1, f"ожидался хотя бы один получатель telegram, найдено {len(cfgs)}" for c in cfgs: assert c.get("message_thread_id") == 245, f"инфраструктурный топик не проставлен: {c}" diff --git a/backend/tests/ops/test_3078_oncall_route.py b/backend/tests/ops/test_3078_oncall_route.py index 22dfa50d..69480996 100644 --- a/backend/tests/ops/test_3078_oncall_route.py +++ b/backend/tests/ops/test_3078_oncall_route.py @@ -114,7 +114,7 @@ def test_klientskiy_marshrut_idyot_v_servis_knopki() -> None: """ text = TEMPLATE.read_text(encoding="utf-8") block = text[text.index("- name: telegram-clients") :] - block = block[: block.index("- name: telegram-heartbeat")] + block = block[: block.index("- name: watchdog-ping")] assert "webhook_configs" in block, "клиентский приёмник не переключён на сервис" assert "alert-ack:8080/alertmanager" in block, "вебхук указывает не на сервис кнопки" # У прочих приёмников прямой путь сохранён. diff --git a/ops/metrics/alertmanager/alertmanager.yml.tmpl b/ops/metrics/alertmanager/alertmanager.yml.tmpl index a5373367..a95c80b6 100644 --- a/ops/metrics/alertmanager/alertmanager.yml.tmpl +++ b/ops/metrics/alertmanager/alertmanager.yml.tmpl @@ -15,11 +15,11 @@ # клиентские инциденты МЕРЫ) и тему «метрики» (245, инфраструктурный шум — # диск, память, просевший экспортер). Инфраструктурный шум и клиентский # инцидент не равны по срочности, а смешанные в одной теме они обучают -# пролистывать обе. Поэтому `telegram` и `telegram-heartbeat` ниже адресуют -# ИНФРАСТРУКТУРНУЮ тему (`METRICS_TELEGRAM_INFRA_TOPIC_LINE`). Получателя -# `telegram-clients` в этом списке нет: он не шлёт в Telegram напрямую, а -# вебхуком уходит в alert-ack, и тему адресует сам, своей переменной -# METRICS_TELEGRAM_TOPIC_ID. +# пролистывать обе. Поэтому `telegram` ниже адресует ИНФРАСТРУКТУРНУЮ тему +# (`METRICS_TELEGRAM_INFRA_TOPIC_LINE`). Получателя `telegram-clients` в этом +# списке нет: он не шлёт в Telegram напрямую, а вебхуком уходит в alert-ack, и +# тему адресует сам, своей переменной METRICS_TELEGRAM_TOPIC_ID. `watchdog-ping` +# тоже не шлёт в Telegram вовсе — см. комментарий у его маршрута ниже. global: resolve_timeout: 5m @@ -35,14 +35,35 @@ route: repeat_interval: 6h routes: - # Watchdog не должен смешиваться с настоящими алертами и не должен молчать: - # это «сторож сторожа», он горит всегда и подтверждает, что канал доставки жив. - - receiver: telegram-heartbeat + # Watchdog — «сторож сторожа», горит ВСЕГДА по построению (`vector(1)`, + # см. infra.yml). Раньше уходил в Telegram раз в 12ч — 14 сообщений в + # неделю ни о чём, и именно они приучили пролистывать инфра-тему: 17.09 + # настоящий DiskWillFillIn24h утонул между Watchdog и вечно горящим + # NoActiveCeleryWorkers, диск дошёл до 84% незамеченным. + # + # ПОЧЕМУ ВНЕШНИЙ DEADMAN-ПРИЁМНИК, А НЕ «РЕЖЕ» И НЕ ОТДЕЛЬНАЯ ТЕМА. + # Увеличенный интервал по-прежнему кладёт человеку регулярное сообщение — + # просто реже, и его тоже рано или поздно начнут пролистывать. Отдельная + # техническая тема — это ещё один chat_id/topic_id и ещё один канал, + # за которым НАДО СПЕЦИАЛЬНО следить, то есть тот же человеческий цикл, + # сдвинутый в другое место. Внешний deadman-приёмник (healthchecks.io и + # аналоги) устроен наоборот: Alertmanager молча шлёт HTTP-пинг на каждый + # Watchdog, и пока пинги идут — сервис МОЛЧИТ. Он заговорит (email/свой + # alert) только когда пинг ПЕРЕСТАНЕТ приходить, то есть ровно когда + # канал доставки умер, — это и есть смысл «сторожа сторожа», без единого + # штатного сообщения человеку. Полностью выключать эту проверку нельзя — + # remove бы всей ветки Watchdog это и сделал. + # + # METRICS_WATCHDOG_PING_URL пока НЕ заведён на хосте (нужен аккаунт + # healthchecks.io/аналога) — до тех пор webhook будет молча падать по + # DNS/сети, Alertmanager это тихо ретраит; человека это не касается ни + # раньше, ни теперь. + - receiver: watchdog-ping matchers: - alertname = "Watchdog" group_wait: 0s - group_interval: 12h - repeat_interval: 12h + group_interval: 5m + repeat_interval: 5m # Клиентский инцидент. host="apps" — это продуктовая машина: если на ней # критично, значит МЕРА и Site Finder недоступны людям, а не «где-то в @@ -63,6 +84,20 @@ route: group_wait: 10s repeat_interval: 30m + # Эскалация по длительности (AlertFiringTooLong, prometheus/rules/infra.yml) + # — сигнал о том, что какую-то другую тревогу не заметили или на неё + # забили дольше 6 часов. Она НЕ про клиентский инцидент, но обязана быть + # заметнее обычной инфраструктуры, поэтому уходит в ту же тему, где + # владелец бывает чаще, а не смешивается с общим потоком severity=critical + # ниже. Матчим по имени, а не по host="apps": исходная тревога может + # быть про любой хост, и врать в лейбле не стоит (см. инвариант host + # у alert:app в infra.yml). + - receiver: telegram-clients + matchers: + - alertname = "AlertFiringTooLong" + group_wait: 10s + repeat_interval: 30m + # Прочее критичное — инфраструктура, клиенты пока не затронуты. - receiver: telegram matchers: @@ -117,14 +152,17 @@ ${METRICS_TELEGRAM_INFRA_TOPIC_LINE} - url: "http://alert-ack:8080/alertmanager" send_resolved: true - - name: telegram-heartbeat - telegram_configs: - - bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}" - chat_id: ${METRICS_TELEGRAM_CHAT_ID} -${METRICS_TELEGRAM_INFRA_TOPIC_LINE} - api_url: "https://api.telegram.org" - parse_mode: HTML - send_resolved: false - message: | - ⚪ Мониторинг жив — сторож отчитался, канал доставки работает. - Если это сообщение перестало приходить дважды подряд, замолчал сам мониторинг. + # Внешний deadman-приёмник вместо Telegram — см. комментарий у маршрута + # Watchdog выше. Блок целиком (не значение) подставляется деплоем в + # METRICS_WATCHDOG_PING_BLOCK — тот же приём, что у + # METRICS_TELEGRAM_INFRA_TOPIC_LINE, и по той же причине: envsubst не умеет + # условий. Секрет ещё не заведён на хосте — деплой в этом случае подставит + # ПУСТУЮ строку, и receiver останется без единого `*_configs`. Это валидный + # Alertmanager-конфиг: приёмник без конфигов просто молча отбрасывает + # уведомление, амtool его пропускает. Одинарная подстановка ЗНАЧЕНИЯ url + # (переменная-URL напрямую внутри готового ключа `url:`) сюда не годится: + # непустой ключ с пустым значением или литералом плейсхолдера амtool валит + # целиком (`unsupported scheme ""`), а с этим — весь Alertmanager, не + # только Watchdog. + - name: watchdog-ping +${METRICS_WATCHDOG_PING_BLOCK} diff --git a/ops/metrics/prometheus/rules/infra.yml b/ops/metrics/prometheus/rules/infra.yml index a78751d0..8516c5e9 100644 --- a/ops/metrics/prometheus/rules/infra.yml +++ b/ops/metrics/prometheus/rules/infra.yml @@ -442,3 +442,44 @@ groups: annotations: summary: "WAL пишется быстрее 100 МБ/час" description: "{{ $labels.host }} / {{ $labels.db }}: {{ $value | humanize1024 }}B/с. Стоит сверить с реальной пользовательской нагрузкой — расхождение означает лишние записи." + + # ── Эскалация ────────────────────────────────────────────────────────────── + # Симптом 17.09: DiskWillFillIn24h пришёл вовремя и утонул между Watchdog + # (10080 интервалов firing за 7 суток — горит всегда по построению) и + # NoActiveCeleryWorkers (6229 интервалов) в общей ленте; диск дошёл до 84% + # незамеченным. Alertmanager сам по длительности не эскалирует — это + # правило Prometheus поверх служебной метрики ALERTS_FOR_STATE (unix-время + # входа тревоги в pending/firing, см. Robust Perception "The + # ALERTS_FOR_STATE metric"). + - name: escalation + interval: 60s + rules: + # ИСКЛЮЧЕНИЯ В `alertname!~` ОБЯЗАТЕЛЬНЫ, а не для порядка: + # - Watchdog горит всегда по построению (`vector(1)` выше) — без + # исключения это правило унаследовало бы его вечный firing и стало + # ВТОРЫМ таким сигналом, то есть тем самым шумом, который лечим. + # - Сама AlertFiringTooLong — иначе, однажды сработав, она бы никогда + # не погасла: собственная ALERTS_FOR_STATE тоже старше порога, и + # правило продлевало бы себя бесконечно. + # Что это НЕ значит: если NoActiveCeleryWorkers (её чинит параллельная + # правка, здесь не трогаем) продолжит гореть дольше 6 часов, эта + # тревога сработает сразу после мержа — это ожидаемо и верно: она + # огонь реального незакрытого инцидента, а не вечная по построению. + # + # `label_replace(..., "stuck_alertname", "$1", "alertname", "(.+)")` + # ОБЯЗАТЕЛЕН, а не косметика: `alertname` — зарезервированный лейбл, + # Prometheus молча перезаписывает его именем ЭТОГО правила + # (AlertFiringTooLong) на выходе, каким бы ни было значение в expr. + # Без копии в `stuck_alertname` текст сообщения называл бы саму себя + # виновником, а не исходную тревогу. + - alert: AlertFiringTooLong + expr: | + label_replace( + (time() - ALERTS_FOR_STATE{alertname!~"Watchdog|AlertFiringTooLong"}) > 6*3600, + "stuck_alertname", "$1", "alertname", "(.+)" + ) + labels: + severity: critical + annotations: + summary: "Тревога держится дольше 6 часов" + description: "{{ $labels.stuck_alertname }}{{ if $labels.host }} ({{ $labels.host }}){{ end }} непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." diff --git a/ops/metrics/prometheus/tests/infra_test.yml b/ops/metrics/prometheus/tests/infra_test.yml index e7d9dfbf..664c651d 100644 --- a/ops/metrics/prometheus/tests/infra_test.yml +++ b/ops/metrics/prometheus/tests/infra_test.yml @@ -170,3 +170,85 @@ tests: exp_annotations: summary: "Бэкенд «Меры» не отвечает" description: "Агент на Poincare 5 минут не может снять /metrics с tradein-backend (up=0), либо цель пропала из скрейпа. Проверь `docker ps` и /health изнутри сети. Лэндинг meraocenka.ru может открываться из кэша и при мёртвом бэкенде — это не признак жизни." + + # Эскалация по длительности. `promtool test rules` держит одну общую шкалу + # времени и TSDB на весь файл — к 6.5 часам к этому моменту «зависшими» + # (input series предыдущих сценариев кончились, но absent()-условия по ним + # продолжают гореть) оказываются и другие тестовые тревоги файла, не только + # NoActiveCeleryWorkers из этого блока. Список ниже — ровно то, что + # реально вернул promtool (проверено запуском, не придумано): 8 тревог, + # держащихся дольше 6 часов. ГЛАВНАЯ ПРОВЕРКА в этом списке — то, чего в + # нём НЕТ: ни Watchdog (горит вечно с t=0 точно так же, но исключён + # матчером), ни сама AlertFiringTooLong (иначе была бы там на восьмое + # место и продлевала бы себя бесконечно). В 3 часа — рано, эскалации + # ещё быть не должно вовсе. + - interval: 1m + input_series: + - series: 'up{job="celery",host="apps"}' + values: '1x420' + alert_rule_test: + - eval_time: 3h + alertname: AlertFiringTooLong + exp_alerts: [] + - eval_time: 6h30m + alertname: AlertFiringTooLong + exp_alerts: + - exp_labels: + severity: critical + stuck_alertname: MeraBackendDown + host: apps + job: app + app: mera + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "MeraBackendDown (apps) непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." + - exp_labels: + severity: critical + stuck_alertname: HostAgentDown + host: apps + job: node + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "HostAgentDown (apps) непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." + - exp_labels: + severity: critical + stuck_alertname: RemoteWriteStalled + host: apps + job: node + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "RemoteWriteStalled (apps) непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." + - exp_labels: + severity: critical + stuck_alertname: CadvisorDown + job: cadvisor + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "CadvisorDown непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." + - exp_labels: + severity: critical + stuck_alertname: QueueExporterDown + job: redis + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "QueueExporterDown непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." + - exp_labels: + severity: critical + stuck_alertname: TradeInBackgroundContainerMissing + name: tradein-scraper + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "TradeInBackgroundContainerMissing непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." + - exp_labels: + severity: critical + stuck_alertname: TradeInBackgroundContainerMissing + name: tradein-tgbot + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "TradeInBackgroundContainerMissing непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили." + - exp_labels: + severity: critical + stuck_alertname: NoActiveCeleryWorkers + exp_annotations: + summary: "Тревога держится дольше 6 часов" + description: "NoActiveCeleryWorkers непрерывно firing больше 6 часов — похоже, её не заметили или на неё забили."