All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / changes (pull_request) Successful in 9s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m57s
CI / backend-tests (pull_request) Successful in 17m26s
Форумная группа имеет три темы, но тема «метрики» была пуста: оба прямых получателя Alertmanager — telegram и telegram-heartbeat — брали топик из той же переменной METRICS_TELEGRAM_TOPIC_ID, что и сервис alert-ack. Развести их было нечем, и heartbeat вместе со всем инфраструктурным шумом падал в ленту клиентских инцидентов. Смешанные в одной теме, инфраструктура и клиентский инцидент не равны по срочности и приучают пролистывать обе. Вводится METRICS_TELEGRAM_INFRA_TOPIC_ID для прямых получателей Alertmanager. alert-ack и вебхук GlitchTip остаются на прежней переменной, тема поддержки не тронута. Пока новая переменная не задана, берётся старая — до этого момента поведение ровно прежнее, а не сломанное. Клиентская METRICS_TELEGRAM_TOPIC_LINE убрана целиком: после переезда обоих получателей на инфраструктурную строку шаблон её не содержит, а деплой продолжал бы её собирать и объявлять в envsubst. Тест, закрепляющий сборку такой строки, зеленел бы вечно и мешал бы её убрать. Проверено рендером, а не чтением: при заданной теме telegram и telegram-heartbeat дают 245, telegram-clients уходит вебхуком без темы; при незаданной — поля message_thread_id нет вовсе (пустое значение уронило бы Alertmanager целиком). Логика отката прогнана во всех трёх состояниях переменных. backend/tests/ops — 86 passed. Closes #3163
162 lines
10 KiB
Python
162 lines
10 KiB
Python
"""Алерты можно адресовать в топик форумной группы (#3078).
|
||
|
||
Зачем. Бот, которым шлются тревоги, — тот же, что пересылает сообщения
|
||
поддержки, а его чат форумный. Без `message_thread_id` Alertmanager кладёт
|
||
тревоги в общую тему, вперемешку с клиентской перепиской.
|
||
|
||
Поле поддерживается: проверено `amtool check-config` на том же образе, что
|
||
поднимается в проде (`prom/alertmanager:v0.28.0`) — конфиг с
|
||
`message_thread_id: 42` принимается. Схема Alertmanager строгая и неизвестные
|
||
поля отвергает, так что успешная проверка означает именно поддержку поля.
|
||
|
||
Почему подставляется ЦЕЛАЯ СТРОКА, а не значение. `envsubst` не умеет условий.
|
||
Если бы в шаблоне стояло `message_thread_id: ${METRICS_TELEGRAM_TOPIC_ID}`, то
|
||
при незаданном топике в конфиг попало бы `message_thread_id:` без значения —
|
||
и Alertmanager не стартовал бы вовсе. А это не деградация, а полное исчезновение
|
||
алертинга: контейнер просто не поднимется. Поэтому деплой формирует либо всю
|
||
строку с отступом, либо пустую.
|
||
|
||
ПОЧЕМУ ДВЕ ТЕМЫ (#3163), А НЕ ОДНА. До этого тикета оба прямых получателя
|
||
(`telegram`, `telegram-heartbeat`) и клиентские инциденты брали топик из ОДНОЙ
|
||
переменной — и инфраструктурная тема «метрики» оставалась пустой, а весь трафик,
|
||
и клиентский, и инфраструктурный, копился в теме «алерты». Владелец решил
|
||
развести: инфраструктура — в «метрики», клиентские инциденты — в «алерты».
|
||
Тесты ниже закрепляют именно это: `telegram`/`telegram-heartbeat` получают
|
||
ИНФРАСТРУКТУРНУЮ тему, а не общую.
|
||
|
||
Тесты рендерят шаблон обоими способами и разбирают результат как YAML —
|
||
проверяется фактический конфиг, а не наличие нужных слов в тексте.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
yaml = pytest.importorskip("yaml", reason="PyYAML нужен для разбора конфига")
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||
TMPL = REPO_ROOT / "ops" / "metrics" / "alertmanager" / "alertmanager.yml.tmpl"
|
||
WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
|
||
|
||
INFRA_TOPIC_LINE = " message_thread_id: 245"
|
||
|
||
|
||
def _render(infra_topic_line: str) -> dict:
|
||
"""Повторяет подстановку деплоя и разбирает результат как YAML.
|
||
|
||
Строка темы в шаблоне ровно одна — инфраструктурная (#3163). Тема
|
||
клиентских инцидентов сюда не подставляется вовсе: маршрут
|
||
`telegram-clients` уходит вебхуком в alert-ack, и тему адресует уже он,
|
||
своей переменной окружения. Держать здесь второй параметр было бы враньём
|
||
— он ни на что не влиял бы, а тест выглядел бы строже, чем он есть.
|
||
"""
|
||
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)
|
||
)
|
||
assert "${" not in rendered, (
|
||
"в отрендеренном конфиге остался литерал плейсхолдера — "
|
||
"значит в шаблоне появилась подстановка, о которой тест не знает"
|
||
)
|
||
return yaml.safe_load(rendered)
|
||
|
||
|
||
def _telegram_configs(cfg: dict) -> list[dict]:
|
||
out = []
|
||
for r in cfg.get("receivers", []):
|
||
out.extend(r.get("telegram_configs", []) or [])
|
||
assert out, "в конфиге не нашлось ни одного telegram_configs"
|
||
return out
|
||
|
||
|
||
def test_topic_lands_in_every_telegram_receiver() -> None:
|
||
"""Оба прямых получателя адресуют ИНФРАСТРУКТУРНУЮ тему, а не клиентскую (#3163).
|
||
|
||
Получателей два — `telegram` и `telegram-heartbeat`. До разделения тем оба
|
||
брали топик из одной переменной с клиентскими инцидентами, и тема «метрики»
|
||
(245) оставалась пустой. Если heartbeat уйдёт не в ту тему, «мониторинг жив»
|
||
будет капать мимо, и это заметят не сразу — сюда же попадёт и весь
|
||
инфраструктурный шум.
|
||
"""
|
||
cfgs = _telegram_configs(_render(INFRA_TOPIC_LINE))
|
||
assert len(cfgs) >= 2, f"ожидалось минимум два получателя telegram, найдено {len(cfgs)}"
|
||
for c in cfgs:
|
||
assert c.get("message_thread_id") == 245, f"инфраструктурный топик не проставлен: {c}"
|
||
|
||
|
||
def test_without_infra_topic_field_is_absent_not_empty() -> None:
|
||
"""Без инфраструктурной темы поля нет вовсе — не пустое значение.
|
||
|
||
Ядро регресса: `message_thread_id:` без значения уронил бы Alertmanager,
|
||
то есть выключил бы алертинг целиком, а не «просто отправил бы в общую тему».
|
||
"""
|
||
cfgs = _telegram_configs(_render(""))
|
||
for c in cfgs:
|
||
assert "message_thread_id" not in c, f"поле осталось при незаданном топике: {c}"
|
||
assert c.get("chat_id") == -100123, "chat_id пострадал при пустой подстановке"
|
||
|
||
|
||
def test_deploy_computes_whole_line_and_passes_it_to_envsubst() -> None:
|
||
"""Деплой формирует строку темы целиком и объявляет её в envsubst.
|
||
|
||
`envsubst` подставляет ТОЛЬКО перечисленные ему переменные. Забыть добавить
|
||
новую в список — значит оставить в готовом конфиге литерал
|
||
`${METRICS_TELEGRAM_INFRA_TOPIC_LINE}`, на котором Alertmanager не стартует
|
||
вовсе (#3163) — та же ловушка, из-за которой изначально завели этот файл
|
||
для клиентской строки в #3078.
|
||
"""
|
||
assert WORKFLOW.is_file(), f"нет {WORKFLOW} — воркфлоу переехал, гейт ослеп"
|
||
text = WORKFLOW.read_text(encoding="utf-8")
|
||
|
||
assert "METRICS_TELEGRAM_TOPIC_ID" in text, (
|
||
"деплой не читает переменную клиентского топика — она нужна как откат"
|
||
)
|
||
assert "METRICS_TELEGRAM_INFRA_TOPIC_ID" in text, (
|
||
"деплой не читает переменную инфраструктурного топика"
|
||
)
|
||
assert re.search(
|
||
r"METRICS_TELEGRAM_INFRA_TOPIC_LINE=\"\s+message_thread_id: \$\{INFRA_TOPIC_ID\}\"",
|
||
text,
|
||
), "инфраструктурная строка собирается не целиком — при пустом значении конфиг сломается"
|
||
|
||
envsubst = re.search(r"envsubst '([^']+)'", text)
|
||
assert envsubst, "не нашёл вызов envsubst"
|
||
assert "${METRICS_TELEGRAM_INFRA_TOPIC_LINE}" in envsubst.group(1), (
|
||
"переменная инфраструктурного топика не объявлена в envsubst — "
|
||
"в конфиг попадёт литерал плейсхолдера"
|
||
)
|
||
|
||
|
||
def test_infra_topic_falls_back_to_client_topic_when_unset() -> None:
|
||
"""Без METRICS_TELEGRAM_INFRA_TOPIC_ID — откат на METRICS_TELEGRAM_TOPIC_ID (#3163).
|
||
|
||
Разделение тем не должно ломать деплой у тех, кто ещё не прописал новую
|
||
переменную на хосте: пока её нет, поведение обязано остаться ровно
|
||
прежним (топик из METRICS_TELEGRAM_TOPIC_ID), а не откатиться в общую тему
|
||
чата или сломать рендер конфига.
|
||
"""
|
||
text = WORKFLOW.read_text(encoding="utf-8")
|
||
assert re.search(
|
||
r'if \[ -n "\$\{METRICS_TELEGRAM_INFRA_TOPIC_ID:-\}" \]; then'
|
||
r'[\s\S]*?INFRA_TOPIC_ID="\$\{METRICS_TELEGRAM_INFRA_TOPIC_ID\}"'
|
||
r'[\s\S]*?else'
|
||
r'[\s\S]*?INFRA_TOPIC_ID="\$\{METRICS_TELEGRAM_TOPIC_ID:-\}"',
|
||
text,
|
||
), "нет отката на METRICS_TELEGRAM_TOPIC_ID при незаданной METRICS_TELEGRAM_INFRA_TOPIC_ID"
|
||
|
||
|
||
def test_config_is_validated_before_stack_comes_up() -> None:
|
||
"""Конфиг проверяется до подъёма — как Caddyfile.
|
||
|
||
Битый Alertmanager не деградирует, а не стартует: алертинг исчезает молча.
|
||
"""
|
||
text = WORKFLOW.read_text(encoding="utf-8")
|
||
assert "amtool" in text and "check-config" in text, (
|
||
"нет проверки конфига Alertmanager перед подъёмом стека"
|
||
)
|