fix(ops): убрать Watchdog из человеческой ленты + эскалация по длительности #3589
6 changed files with 310 additions and 40 deletions
|
|
@ -91,12 +91,16 @@ jobs:
|
||||||
METRICS_TELEGRAM_INFRA_TOPIC_ID: ${{ secrets.METRICS_TELEGRAM_INFRA_TOPIC_ID }}
|
METRICS_TELEGRAM_INFRA_TOPIC_ID: ${{ secrets.METRICS_TELEGRAM_INFRA_TOPIC_ID }}
|
||||||
METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }}
|
METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }}
|
||||||
ALERT_ACK_GLITCHTIP_SECRET: ${{ secrets.ALERT_ACK_GLITCHTIP_SECRET }}
|
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). Пусто —
|
# #3471: секрет ретранслятора Telegram Bot API (tg-relay). Пусто —
|
||||||
# профиль relay не включаем (см. PROFILES ниже), а не падаем в
|
# профиль relay не включаем (см. PROFILES ниже), а не падаем в
|
||||||
# рестарт-луп: контейнер сам делает SystemExit на пустом секрете.
|
# рестарт-луп: контейнер сам делает SystemExit на пустом секрете.
|
||||||
TG_RELAY_SECRET: ${{ secrets.TG_RELAY_SECRET }}
|
TG_RELAY_SECRET: ${{ secrets.TG_RELAY_SECRET }}
|
||||||
with:
|
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 }}
|
host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }}
|
||||||
username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }}
|
username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }}
|
||||||
key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }}
|
key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }}
|
||||||
|
|
@ -193,6 +197,24 @@ jobs:
|
||||||
echo "Инфраструктура: тема ${INFRA_TOPIC_ID} по умолчанию (METRICS_TELEGRAM_INFRA_TOPIC_ID не задана)."
|
echo "Инфраструктура: тема ${INFRA_TOPIC_ID} по умолчанию (METRICS_TELEGRAM_INFRA_TOPIC_ID не задана)."
|
||||||
fi
|
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 на любой
|
# Резервный приёмник GlitchTip (#3471) отвечает 503 на любой
|
||||||
# запрос, пока секрет пуст: тихо принимать чужие алерты настежь
|
# запрос, пока секрет пуст: тихо принимать чужие алерты настежь
|
||||||
# хуже, чем не принимать вовсе. Молчаливого отказа тут быть не
|
# хуже, чем не принимать вовсе. Молчаливого отказа тут быть не
|
||||||
|
|
@ -221,7 +243,8 @@ jobs:
|
||||||
METRICS_TELEGRAM_CHAT_ID="$METRICS_TELEGRAM_CHAT_ID" \
|
METRICS_TELEGRAM_CHAT_ID="$METRICS_TELEGRAM_CHAT_ID" \
|
||||||
METRICS_TELEGRAM_INFRA_TOPIC_LINE="$METRICS_TELEGRAM_INFRA_TOPIC_LINE" \
|
METRICS_TELEGRAM_INFRA_TOPIC_LINE="$METRICS_TELEGRAM_INFRA_TOPIC_LINE" \
|
||||||
METRICS_TELEGRAM_ONCALL="${METRICS_TELEGRAM_ONCALL:-}" \
|
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.tmpl \
|
||||||
> ops/metrics/alertmanager/alertmanager.yml
|
> ops/metrics/alertmanager/alertmanager.yml
|
||||||
chmod 600 ops/metrics/alertmanager/alertmanager.yml
|
chmod 600 ops/metrics/alertmanager/alertmanager.yml
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@
|
||||||
переменной — и инфраструктурная тема «метрики» оставалась пустой, а весь трафик,
|
переменной — и инфраструктурная тема «метрики» оставалась пустой, а весь трафик,
|
||||||
и клиентский, и инфраструктурный, копился в теме «алерты». Владелец решил
|
и клиентский, и инфраструктурный, копился в теме «алерты». Владелец решил
|
||||||
развести: инфраструктура — в «метрики», клиентские инциденты — в «алерты».
|
развести: инфраструктура — в «метрики», клиентские инциденты — в «алерты».
|
||||||
Тесты ниже закрепляют именно это: `telegram`/`telegram-heartbeat` получают
|
Тест ниже закрепляет именно это для единственного оставшегося прямого
|
||||||
ИНФРАСТРУКТУРНУЮ тему, а не общую.
|
получателя — `telegram` (Watchdog с 17.09 больше не шлёт в Telegram вовсе,
|
||||||
|
у него теперь внешний webhook-приёмник `watchdog-ping`, см. шаблон).
|
||||||
|
|
||||||
Тесты рендерят шаблон обоими способами и разбирают результат как YAML —
|
Тесты рендерят шаблон обоими способами и разбирают результат как YAML —
|
||||||
проверяется фактический конфиг, а не наличие нужных слов в тексте.
|
проверяется фактический конфиг, а не наличие нужных слов в тексте.
|
||||||
|
|
@ -43,9 +44,56 @@ WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
|
||||||
|
|
||||||
INFRA_TOPIC_LINE = " message_thread_id: 245"
|
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). Тема
|
Строка темы в шаблоне ровно одна — инфраструктурная (#3163). Тема
|
||||||
клиентских инцидентов сюда не подставляется вовсе: маршрут
|
клиентских инцидентов сюда не подставляется вовсе: маршрут
|
||||||
|
|
@ -55,11 +103,18 @@ def _render(infra_topic_line: str) -> dict:
|
||||||
"""
|
"""
|
||||||
assert TMPL.is_file(), f"нет {TMPL} — шаблон переехал, гейт ослеп"
|
assert TMPL.is_file(), f"нет {TMPL} — шаблон переехал, гейт ослеп"
|
||||||
text = TMPL.read_text(encoding="utf-8")
|
text = TMPL.read_text(encoding="utf-8")
|
||||||
rendered = (
|
|
||||||
text.replace("${METRICS_TELEGRAM_BOT_TOKEN}", "123:ABC")
|
values = dict(_DUMMY_VALUES)
|
||||||
.replace("${METRICS_TELEGRAM_CHAT_ID}", "-100123")
|
values["METRICS_TELEGRAM_INFRA_TOPIC_LINE"] = infra_topic_line
|
||||||
.replace("${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, (
|
assert "${" not in rendered, (
|
||||||
"в отрендеренном конфиге остался литерал плейсхолдера — "
|
"в отрендеренном конфиге остался литерал плейсхолдера — "
|
||||||
"значит в шаблоне появилась подстановка, о которой тест не знает"
|
"значит в шаблоне появилась подстановка, о которой тест не знает"
|
||||||
|
|
@ -67,6 +122,37 @@ def _render(infra_topic_line: str) -> dict:
|
||||||
return yaml.safe_load(rendered)
|
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]:
|
def _telegram_configs(cfg: dict) -> list[dict]:
|
||||||
out = []
|
out = []
|
||||||
for r in cfg.get("receivers", []):
|
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:
|
def test_topic_lands_in_every_telegram_receiver() -> None:
|
||||||
"""Оба прямых получателя адресуют ИНФРАСТРУКТУРНУЮ тему, а не клиентскую (#3163).
|
"""Единственный прямой получатель адресует ИНФРАСТРУКТУРНУЮ тему, а не клиентскую (#3163).
|
||||||
|
|
||||||
Получателей два — `telegram` и `telegram-heartbeat`. До разделения тем оба
|
До разделения тем `telegram` и `telegram-heartbeat` брали топик из одной
|
||||||
брали топик из одной переменной с клиентскими инцидентами, и тема «метрики»
|
переменной с клиентскими инцидентами, и тема «метрики» (245) оставалась
|
||||||
(245) оставалась пустой. Если heartbeat уйдёт не в ту тему, «мониторинг жив»
|
пустой. С 17.09 (устранение шума Watchdog) прямой Telegram-получатель
|
||||||
будет капать мимо, и это заметят не сразу — сюда же попадёт и весь
|
остался один — `telegram`; Watchdog теперь пингует внешний
|
||||||
инфраструктурный шум.
|
deadman-приёмник вебхуком (`watchdog-ping`, без topic вовсе — не Telegram).
|
||||||
"""
|
"""
|
||||||
cfgs = _telegram_configs(_render(INFRA_TOPIC_LINE))
|
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:
|
for c in cfgs:
|
||||||
assert c.get("message_thread_id") == 245, f"инфраструктурный топик не проставлен: {c}"
|
assert c.get("message_thread_id") == 245, f"инфраструктурный топик не проставлен: {c}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ def test_klientskiy_marshrut_idyot_v_servis_knopki() -> None:
|
||||||
"""
|
"""
|
||||||
text = TEMPLATE.read_text(encoding="utf-8")
|
text = TEMPLATE.read_text(encoding="utf-8")
|
||||||
block = text[text.index("- name: telegram-clients") :]
|
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 "webhook_configs" in block, "клиентский приёмник не переключён на сервис"
|
||||||
assert "alert-ack:8080/alertmanager" in block, "вебхук указывает не на сервис кнопки"
|
assert "alert-ack:8080/alertmanager" in block, "вебхук указывает не на сервис кнопки"
|
||||||
# У прочих приёмников прямой путь сохранён.
|
# У прочих приёмников прямой путь сохранён.
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
# клиентские инциденты МЕРЫ) и тему «метрики» (245, инфраструктурный шум —
|
# клиентские инциденты МЕРЫ) и тему «метрики» (245, инфраструктурный шум —
|
||||||
# диск, память, просевший экспортер). Инфраструктурный шум и клиентский
|
# диск, память, просевший экспортер). Инфраструктурный шум и клиентский
|
||||||
# инцидент не равны по срочности, а смешанные в одной теме они обучают
|
# инцидент не равны по срочности, а смешанные в одной теме они обучают
|
||||||
# пролистывать обе. Поэтому `telegram` и `telegram-heartbeat` ниже адресуют
|
# пролистывать обе. Поэтому `telegram` ниже адресует ИНФРАСТРУКТУРНУЮ тему
|
||||||
# ИНФРАСТРУКТУРНУЮ тему (`METRICS_TELEGRAM_INFRA_TOPIC_LINE`). Получателя
|
# (`METRICS_TELEGRAM_INFRA_TOPIC_LINE`). Получателя `telegram-clients` в этом
|
||||||
# `telegram-clients` в этом списке нет: он не шлёт в Telegram напрямую, а
|
# списке нет: он не шлёт в Telegram напрямую, а вебхуком уходит в alert-ack, и
|
||||||
# вебхуком уходит в alert-ack, и тему адресует сам, своей переменной
|
# тему адресует сам, своей переменной METRICS_TELEGRAM_TOPIC_ID. `watchdog-ping`
|
||||||
# METRICS_TELEGRAM_TOPIC_ID.
|
# тоже не шлёт в Telegram вовсе — см. комментарий у его маршрута ниже.
|
||||||
|
|
||||||
global:
|
global:
|
||||||
resolve_timeout: 5m
|
resolve_timeout: 5m
|
||||||
|
|
@ -35,14 +35,35 @@ route:
|
||||||
repeat_interval: 6h
|
repeat_interval: 6h
|
||||||
|
|
||||||
routes:
|
routes:
|
||||||
# Watchdog не должен смешиваться с настоящими алертами и не должен молчать:
|
# Watchdog — «сторож сторожа», горит ВСЕГДА по построению (`vector(1)`,
|
||||||
# это «сторож сторожа», он горит всегда и подтверждает, что канал доставки жив.
|
# см. infra.yml). Раньше уходил в Telegram раз в 12ч — 14 сообщений в
|
||||||
- receiver: telegram-heartbeat
|
# неделю ни о чём, и именно они приучили пролистывать инфра-тему: 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:
|
matchers:
|
||||||
- alertname = "Watchdog"
|
- alertname = "Watchdog"
|
||||||
group_wait: 0s
|
group_wait: 0s
|
||||||
group_interval: 12h
|
group_interval: 5m
|
||||||
repeat_interval: 12h
|
repeat_interval: 5m
|
||||||
|
|
||||||
# Клиентский инцидент. host="apps" — это продуктовая машина: если на ней
|
# Клиентский инцидент. host="apps" — это продуктовая машина: если на ней
|
||||||
# критично, значит МЕРА и Site Finder недоступны людям, а не «где-то в
|
# критично, значит МЕРА и Site Finder недоступны людям, а не «где-то в
|
||||||
|
|
@ -63,6 +84,20 @@ route:
|
||||||
group_wait: 10s
|
group_wait: 10s
|
||||||
repeat_interval: 30m
|
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
|
- receiver: telegram
|
||||||
matchers:
|
matchers:
|
||||||
|
|
@ -117,14 +152,17 @@ ${METRICS_TELEGRAM_INFRA_TOPIC_LINE}
|
||||||
- url: "http://alert-ack:8080/alertmanager"
|
- url: "http://alert-ack:8080/alertmanager"
|
||||||
send_resolved: true
|
send_resolved: true
|
||||||
|
|
||||||
- name: telegram-heartbeat
|
# Внешний deadman-приёмник вместо Telegram — см. комментарий у маршрута
|
||||||
telegram_configs:
|
# Watchdog выше. Блок целиком (не значение) подставляется деплоем в
|
||||||
- bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}"
|
# METRICS_WATCHDOG_PING_BLOCK — тот же приём, что у
|
||||||
chat_id: ${METRICS_TELEGRAM_CHAT_ID}
|
# METRICS_TELEGRAM_INFRA_TOPIC_LINE, и по той же причине: envsubst не умеет
|
||||||
${METRICS_TELEGRAM_INFRA_TOPIC_LINE}
|
# условий. Секрет ещё не заведён на хосте — деплой в этом случае подставит
|
||||||
api_url: "https://api.telegram.org"
|
# ПУСТУЮ строку, и receiver останется без единого `*_configs`. Это валидный
|
||||||
parse_mode: HTML
|
# Alertmanager-конфиг: приёмник без конфигов просто молча отбрасывает
|
||||||
send_resolved: false
|
# уведомление, амtool его пропускает. Одинарная подстановка ЗНАЧЕНИЯ url
|
||||||
message: |
|
# (переменная-URL напрямую внутри готового ключа `url:`) сюда не годится:
|
||||||
⚪ <b>Мониторинг жив</b> — сторож отчитался, канал доставки работает.
|
# непустой ключ с пустым значением или литералом плейсхолдера амtool валит
|
||||||
Если это сообщение перестало приходить дважды подряд, замолчал сам мониторинг.
|
# целиком (`unsupported scheme ""`), а с этим — весь Alertmanager, не
|
||||||
|
# только Watchdog.
|
||||||
|
- name: watchdog-ping
|
||||||
|
${METRICS_WATCHDOG_PING_BLOCK}
|
||||||
|
|
|
||||||
|
|
@ -442,3 +442,44 @@ groups:
|
||||||
annotations:
|
annotations:
|
||||||
summary: "WAL пишется быстрее 100 МБ/час"
|
summary: "WAL пишется быстрее 100 МБ/час"
|
||||||
description: "{{ $labels.host }} / {{ $labels.db }}: {{ $value | humanize1024 }}B/с. Стоит сверить с реальной пользовательской нагрузкой — расхождение означает лишние записи."
|
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 часов — похоже, её не заметили или на неё забили."
|
||||||
|
|
|
||||||
|
|
@ -170,3 +170,85 @@ tests:
|
||||||
exp_annotations:
|
exp_annotations:
|
||||||
summary: "Бэкенд «Меры» не отвечает"
|
summary: "Бэкенд «Меры» не отвечает"
|
||||||
description: "Агент на Poincare 5 минут не может снять /metrics с tradein-backend (up=0), либо цель пропала из скрейпа. Проверь `docker ps` и /health изнутри сети. Лэндинг meraocenka.ru может открываться из кэша и при мёртвом бэкенде — это не признак жизни."
|
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 часов — похоже, её не заметили или на неё забили."
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue