feat(observability): алерты адресуются в топик форумной группы + проверка конфига до подъёма (#3078) #3111
3 changed files with 145 additions and 2 deletions
|
|
@ -120,13 +120,40 @@ jobs:
|
|||
if [ -n "${METRICS_TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${METRICS_TELEGRAM_CHAT_ID:-}" ]; then
|
||||
PROFILES="alerts"
|
||||
mkdir -p ops/metrics/alertmanager
|
||||
|
||||
# Топик форумной группы (#3078). Необязателен: без него алерты
|
||||
# уходят в общую тему. Подставляем ЦЕЛОЙ СТРОКОЙ, а не значением,
|
||||
# потому что envsubst не умеет условий — при пустом
|
||||
# METRICS_TELEGRAM_TOPIC_ID в конфиг попал бы `message_thread_id:`
|
||||
# без значения, и Alertmanager не стартовал бы вовсе.
|
||||
if [ -n "${METRICS_TELEGRAM_TOPIC_ID:-}" ]; then
|
||||
METRICS_TELEGRAM_TOPIC_LINE=" message_thread_id: ${METRICS_TELEGRAM_TOPIC_ID}"
|
||||
echo "Алерты: адресуются в топик ${METRICS_TELEGRAM_TOPIC_ID}."
|
||||
else
|
||||
METRICS_TELEGRAM_TOPIC_LINE=""
|
||||
echo "Алерты: топик не задан — уйдут в общую тему чата."
|
||||
fi
|
||||
|
||||
METRICS_TELEGRAM_BOT_TOKEN="$METRICS_TELEGRAM_BOT_TOKEN" \
|
||||
METRICS_TELEGRAM_CHAT_ID="$METRICS_TELEGRAM_CHAT_ID" \
|
||||
envsubst '${METRICS_TELEGRAM_BOT_TOKEN} ${METRICS_TELEGRAM_CHAT_ID}' \
|
||||
METRICS_TELEGRAM_TOPIC_LINE="$METRICS_TELEGRAM_TOPIC_LINE" \
|
||||
envsubst '${METRICS_TELEGRAM_BOT_TOKEN} ${METRICS_TELEGRAM_CHAT_ID} ${METRICS_TELEGRAM_TOPIC_LINE}' \
|
||||
< ops/metrics/alertmanager/alertmanager.yml.tmpl \
|
||||
> ops/metrics/alertmanager/alertmanager.yml
|
||||
chmod 600 ops/metrics/alertmanager/alertmanager.yml
|
||||
echo "Алерты: канал задан, Alertmanager поднимается."
|
||||
|
||||
# Проверяем ДО подъёма, как и Caddyfile ниже. Битый конфиг
|
||||
# Alertmanager не «деградирует» — контейнер не стартует вовсе, и
|
||||
# алертинг молча исчезает целиком. amtool берём из того же образа,
|
||||
# что и сам Alertmanager, иначе проверяли бы не ту версию схемы.
|
||||
if ! docker run --rm \
|
||||
-v "$PWD/ops/metrics/alertmanager/alertmanager.yml:/tmp/am.yml:ro" \
|
||||
--entrypoint amtool "$(grep -oE 'prom/alertmanager:[^ ]+' docker-compose.metrics.yml | head -1)" \
|
||||
check-config /tmp/am.yml; then
|
||||
echo "ОШИБКА: конфиг Alertmanager не проходит проверку — стек не поднимаем."
|
||||
exit 1
|
||||
fi
|
||||
echo "Алерты: канал задан, конфиг проверен, Alertmanager поднимается."
|
||||
else
|
||||
echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078."
|
||||
fi
|
||||
|
|
|
|||
114
backend/tests/ops/test_3078_alert_topic.py
Normal file
114
backend/tests/ops/test_3078_alert_topic.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"""Алерты можно адресовать в топик форумной группы (#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 не стартовал бы вовсе. А это не деградация, а полное исчезновение
|
||||
алертинга: контейнер просто не поднимется. Поэтому деплой формирует либо всю
|
||||
строку с отступом, либо пустую.
|
||||
|
||||
Тесты рендерят шаблон обоими способами и разбирают результат как 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"
|
||||
|
||||
TOPIC_LINE = " message_thread_id: 42"
|
||||
|
||||
|
||||
def _render(topic_line: str) -> dict:
|
||||
"""Повторяет подстановку деплоя и разбирает результат как YAML."""
|
||||
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_TOPIC_LINE}", topic_line)
|
||||
)
|
||||
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:
|
||||
"""Топик проставляется во ВСЕХ получателях, а не только в основном.
|
||||
|
||||
Получателей два — `telegram` и `telegram-heartbeat`. Если heartbeat уйдёт
|
||||
в общую тему, «мониторинг жив» будет капать мимо, и это заметят не сразу.
|
||||
"""
|
||||
cfgs = _telegram_configs(_render(TOPIC_LINE))
|
||||
assert len(cfgs) >= 2, f"ожидалось минимум два получателя telegram, найдено {len(cfgs)}"
|
||||
for c in cfgs:
|
||||
assert c.get("message_thread_id") == 42, f"топик не проставлен: {c}"
|
||||
|
||||
|
||||
def test_without_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_TOPIC_LINE}`, на котором Alertmanager не стартует.
|
||||
"""
|
||||
assert WORKFLOW.is_file(), f"нет {WORKFLOW} — воркфлоу переехал, гейт ослеп"
|
||||
text = WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
assert "METRICS_TELEGRAM_TOPIC_ID" in text, "деплой не читает переменную топика"
|
||||
assert re.search(
|
||||
r"METRICS_TELEGRAM_TOPIC_LINE=\"\s+message_thread_id: \$\{METRICS_TELEGRAM_TOPIC_ID\}\"",
|
||||
text,
|
||||
), "строка топика собирается не целиком — при пустом значении конфиг сломается"
|
||||
|
||||
envsubst = re.search(r"envsubst '([^']+)'", text)
|
||||
assert envsubst, "не нашёл вызов envsubst"
|
||||
assert "${METRICS_TELEGRAM_TOPIC_LINE}" in envsubst.group(1), (
|
||||
"переменная топика не объявлена в envsubst — в конфиг попадёт литерал плейсхолдера"
|
||||
)
|
||||
|
||||
|
||||
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 перед подъёмом стека"
|
||||
)
|
||||
|
|
@ -52,6 +52,7 @@ receivers:
|
|||
telegram_configs:
|
||||
- bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}"
|
||||
chat_id: ${METRICS_TELEGRAM_CHAT_ID}
|
||||
${METRICS_TELEGRAM_TOPIC_LINE}
|
||||
api_url: "https://api.telegram.org"
|
||||
parse_mode: HTML
|
||||
send_resolved: true
|
||||
|
|
@ -66,6 +67,7 @@ receivers:
|
|||
telegram_configs:
|
||||
- bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}"
|
||||
chat_id: ${METRICS_TELEGRAM_CHAT_ID}
|
||||
${METRICS_TELEGRAM_TOPIC_LINE}
|
||||
api_url: "https://api.telegram.org"
|
||||
parse_mode: HTML
|
||||
send_resolved: false
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue