Compare commits
No commits in common. "f0b03112fcac23b2ab6738961de614fd876047b6" and "063485e0e2fb5fa9acc808f24ff9f3732d5de886" have entirely different histories.
f0b03112fc
...
063485e0e2
3 changed files with 8 additions and 187 deletions
|
|
@ -88,9 +88,8 @@ jobs:
|
||||||
METRICS_TELEGRAM_BOT_TOKEN: ${{ secrets.METRICS_TELEGRAM_BOT_TOKEN }}
|
METRICS_TELEGRAM_BOT_TOKEN: ${{ secrets.METRICS_TELEGRAM_BOT_TOKEN }}
|
||||||
METRICS_TELEGRAM_CHAT_ID: ${{ secrets.METRICS_TELEGRAM_CHAT_ID }}
|
METRICS_TELEGRAM_CHAT_ID: ${{ secrets.METRICS_TELEGRAM_CHAT_ID }}
|
||||||
METRICS_TELEGRAM_TOPIC_ID: ${{ secrets.METRICS_TELEGRAM_TOPIC_ID }}
|
METRICS_TELEGRAM_TOPIC_ID: ${{ secrets.METRICS_TELEGRAM_TOPIC_ID }}
|
||||||
METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }}
|
|
||||||
with:
|
with:
|
||||||
envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID,METRICS_TELEGRAM_ONCALL
|
envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID
|
||||||
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 }}
|
||||||
|
|
@ -151,27 +150,15 @@ jobs:
|
||||||
echo "Алерты: топик не задан — уйдут в общую тему чата."
|
echo "Алерты: топик не задан — уйдут в общую тему чата."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${METRICS_TELEGRAM_ONCALL:-}" ]; then
|
|
||||||
echo "Клиентские инциденты: зовём ${METRICS_TELEGRAM_ONCALL} поимённо."
|
|
||||||
else
|
|
||||||
echo "::warning title=Дежурный не задан::METRICS_TELEGRAM_ONCALL пуст — при клиентском инциденте сообщение придёт без упоминания и потеряется в общем потоке (#3078)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# rm перед записью обязателен: после chown ниже файл принадлежит 65534
|
|
||||||
# с правами 600, и на СЛЕДУЮЩЕМ деплое перенаправление в него уже не
|
|
||||||
# запишет. Каталог принадлежит деплой-пользователю, поэтому пересоздать
|
|
||||||
# файл он может, а перезаписать — нет.
|
|
||||||
#
|
|
||||||
# NB: rm обязан стоять ДО префикса переменных ниже. В #3127 он встал
|
|
||||||
# МЕЖДУ строками продолжения команды — и весь вызов envsubst уехал в
|
|
||||||
# комментарий, то есть конфиг переставал рендериться вовсе.
|
|
||||||
rm -f ops/metrics/alertmanager/alertmanager.yml
|
|
||||||
|
|
||||||
METRICS_TELEGRAM_BOT_TOKEN="$METRICS_TELEGRAM_BOT_TOKEN" \
|
METRICS_TELEGRAM_BOT_TOKEN="$METRICS_TELEGRAM_BOT_TOKEN" \
|
||||||
METRICS_TELEGRAM_CHAT_ID="$METRICS_TELEGRAM_CHAT_ID" \
|
METRICS_TELEGRAM_CHAT_ID="$METRICS_TELEGRAM_CHAT_ID" \
|
||||||
METRICS_TELEGRAM_TOPIC_LINE="$METRICS_TELEGRAM_TOPIC_LINE" \
|
METRICS_TELEGRAM_TOPIC_LINE="$METRICS_TELEGRAM_TOPIC_LINE" \
|
||||||
METRICS_TELEGRAM_ONCALL="${METRICS_TELEGRAM_ONCALL:-}" \
|
# rm перед записью обязателен: после chown ниже файл принадлежит
|
||||||
envsubst '${METRICS_TELEGRAM_BOT_TOKEN} ${METRICS_TELEGRAM_CHAT_ID} ${METRICS_TELEGRAM_TOPIC_LINE} ${METRICS_TELEGRAM_ONCALL}' \
|
# 65534 с правами 600, и на СЛЕДУЮЩЕМ деплое `>` в него уже не
|
||||||
|
# запишет. Каталог принадлежит деплой-пользователю, поэтому
|
||||||
|
# пересоздать файл он может, а перезаписать — нет.
|
||||||
|
rm -f ops/metrics/alertmanager/alertmanager.yml
|
||||||
|
envsubst '${METRICS_TELEGRAM_BOT_TOKEN} ${METRICS_TELEGRAM_CHAT_ID} ${METRICS_TELEGRAM_TOPIC_LINE}' \
|
||||||
< 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
|
||||||
|
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
r"""Клиентский инцидент зовёт дежурного поимённо — и шелл в workflow не разваливается (#3078).
|
|
||||||
|
|
||||||
ДВА ИНВАРИАНТА, и второй появился из-за собственной ошибки.
|
|
||||||
|
|
||||||
1. Маршрут для клиентских инцидентов. 27.08 продукты лежали 10 часов, и в канале
|
|
||||||
«диск занят на 86 %» и «клиенты не могут открыть сайт» выглядели одинаково.
|
|
||||||
Отдельный приёмник `telegram-clients` с упоминанием дежурного и коротким
|
|
||||||
`repeat_interval` — это и есть разница между «шумит» и «зовёт».
|
|
||||||
|
|
||||||
2. Комментарий не должен стоять между строками продолжения команды. В #3127 блок
|
|
||||||
`rm -f` вместе с комментарием встал МЕЖДУ строками, каждая из которых
|
|
||||||
заканчивалась обратным слешем:
|
|
||||||
|
|
||||||
VAR1="..." \
|
|
||||||
VAR2="..." \
|
|
||||||
# комментарий <- строки склеиваются, дальше всё уходит в комментарий
|
|
||||||
rm -f ...
|
|
||||||
envsubst ... > конфиг <- НИКОГДА не выполнялся
|
|
||||||
|
|
||||||
Синтаксически это корректный шелл, поэтому `bash -n` такое не ловит, а глазами
|
|
||||||
в диффе не видно: строки выглядят как отдельные. Результат — конфиг Alertmanager
|
|
||||||
молча перестаёт рендериться, то есть алертинг исчезает целиком при зелёном
|
|
||||||
деплое. Проверка структурная, потому что никакой линтер этого за нас не сделает.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
||||||
WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
|
|
||||||
TEMPLATE = REPO_ROOT / "ops" / "metrics" / "alertmanager" / "alertmanager.yml.tmpl"
|
|
||||||
|
|
||||||
|
|
||||||
def _shell_blocks() -> list[str]:
|
|
||||||
"""Все shell-скрипты из workflow: и `run:`, и `script:` у ssh-action."""
|
|
||||||
doc = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
|
|
||||||
blocks: list[str] = []
|
|
||||||
for job in (doc.get("jobs") or {}).values():
|
|
||||||
for step in job.get("steps") or []:
|
|
||||||
if isinstance(step.get("run"), str):
|
|
||||||
blocks.append(step["run"])
|
|
||||||
with_ = step.get("with") or {}
|
|
||||||
if isinstance(with_.get("script"), str):
|
|
||||||
blocks.append(with_["script"])
|
|
||||||
assert blocks, "в workflow не нашлось ни одного shell-блока — тест ослеп"
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
|
|
||||||
def test_kommentariy_ne_stoit_vnutri_prodolzheniya_komandy() -> None:
|
|
||||||
"""После строки с продолжением не может идти комментарий.
|
|
||||||
|
|
||||||
Ровно эта ошибка увела вызов envsubst в комментарий и оставила Alertmanager
|
|
||||||
без конфига. Проверка применяется ко ВСЕМ shell-блокам workflow, а не только
|
|
||||||
к тому месту, где обожглись.
|
|
||||||
"""
|
|
||||||
for block in _shell_blocks():
|
|
||||||
lines = block.split("\n")
|
|
||||||
for i, line in enumerate(lines[:-1]):
|
|
||||||
if not line.rstrip().endswith("\\"):
|
|
||||||
continue
|
|
||||||
nxt = lines[i + 1].strip()
|
|
||||||
assert not nxt.startswith("#"), (
|
|
||||||
"комментарий внутри продолжения команды — всё, что ниже, "
|
|
||||||
f"уедет в комментарий:\n {line.strip()}\n {nxt}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_priyomnik_klientskih_incidentov_est() -> None:
|
|
||||||
"""В шаблоне есть отдельный приёмник и маршрут на него."""
|
|
||||||
text = TEMPLATE.read_text(encoding="utf-8")
|
|
||||||
assert "- name: telegram-clients" in text, "пропал приёмник клиентских инцидентов"
|
|
||||||
assert "receiver: telegram-clients" in text, "на приёмник никто не маршрутизирует"
|
|
||||||
|
|
||||||
|
|
||||||
def test_marshrut_klientov_ranshe_obschego_critical() -> None:
|
|
||||||
"""Клиентский маршрут должен стоять ВЫШЕ общего `severity=critical`.
|
|
||||||
|
|
||||||
Alertmanager берёт первый подошедший маршрут. Если общий окажется выше,
|
|
||||||
клиентские инциденты уйдут в него и упоминание не сработает — отказ тихий:
|
|
||||||
сообщения приходят, просто без тега.
|
|
||||||
"""
|
|
||||||
text = TEMPLATE.read_text(encoding="utf-8")
|
|
||||||
i_clients = text.index("receiver: telegram-clients")
|
|
||||||
i_generic = text.index("# Прочее критичное")
|
|
||||||
assert i_clients < i_generic, "общий critical перехватит клиентские инциденты раньше"
|
|
||||||
|
|
||||||
|
|
||||||
def test_upominanie_iz_peremennoy_a_ne_zashito() -> None:
|
|
||||||
"""Дежурный задаётся переменной: он меняется, а конфиг в git — нет."""
|
|
||||||
text = TEMPLATE.read_text(encoding="utf-8")
|
|
||||||
assert "${METRICS_TELEGRAM_ONCALL}" in text, "упоминание дежурного не параметризовано"
|
|
||||||
assert "@leks361" not in text, "конкретный аккаунт зашит в репозиторий"
|
|
||||||
|
|
||||||
|
|
||||||
def test_peremennaya_dezhurnogo_dohodit_do_shablona() -> None:
|
|
||||||
"""Переменная реально прокидывается и подставляется, а не объявлена вхолостую.
|
|
||||||
|
|
||||||
Три звена, и каждое рвётся молча: секрет → env шага → список envsubst. Без
|
|
||||||
последнего `${METRICS_TELEGRAM_ONCALL}` останется в конфиге буквально.
|
|
||||||
"""
|
|
||||||
wf = WORKFLOW.read_text(encoding="utf-8")
|
|
||||||
assert "METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }}" in wf
|
|
||||||
m = re.search(r"envs:\s*(\S+)", wf)
|
|
||||||
assert m and "METRICS_TELEGRAM_ONCALL" in m.group(1), "переменная не форвардится в ssh-шаг"
|
|
||||||
m2 = re.search(r"envsubst '([^']+)'", wf)
|
|
||||||
assert m2 and "${METRICS_TELEGRAM_ONCALL}" in m2.group(1), "переменной нет в списке envsubst"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("marker", ['severity = "critical"', 'host = "apps"'])
|
|
||||||
def test_klientskiy_marshrut_suzhen_po_oboim_priznakam(marker: str) -> None:
|
|
||||||
"""Маршрут ловит именно критичное НА ПРОДУКТОВОМ хосте.
|
|
||||||
|
|
||||||
Без `host = "apps"` дежурного звали бы на каждую инфраструктурную мелочь, и
|
|
||||||
тег быстро перестал бы что-либо значить.
|
|
||||||
"""
|
|
||||||
text = TEMPLATE.read_text(encoding="utf-8")
|
|
||||||
block = text[text.index("receiver: telegram-clients") : text.index("# Прочее критичное")]
|
|
||||||
assert marker in block, f"в клиентском маршруте нет условия {marker}"
|
|
||||||
|
|
@ -34,26 +34,7 @@ route:
|
||||||
group_interval: 12h
|
group_interval: 12h
|
||||||
repeat_interval: 12h
|
repeat_interval: 12h
|
||||||
|
|
||||||
# Клиентский инцидент. host="apps" — это продуктовая машина: если на ней
|
# Критичное — без задержки на группировку.
|
||||||
# критично, значит МЕРА и Site Finder недоступны людям, а не «где-то в
|
|
||||||
# инфраструктуре тесно». Такое зовём поимённо и напоминаем часто.
|
|
||||||
#
|
|
||||||
# Почему отдельный маршрут, а не общий critical: 27.08 продукты лежали
|
|
||||||
# 10 часов, и разницы между «диск на 86 %» и «клиенты не могут открыть
|
|
||||||
# сайт» в канале не было никакой. Разный текст и разная настойчивость —
|
|
||||||
# это и есть разница.
|
|
||||||
#
|
|
||||||
# repeat_interval 30m против 3h у прочего критичного: пока инцидент не
|
|
||||||
# погашен, напоминание должно быть неудобным. Заглушить его — осознанное
|
|
||||||
# действие через Alertmanager, и оно же служит отметкой «принято».
|
|
||||||
- receiver: telegram-clients
|
|
||||||
matchers:
|
|
||||||
- severity = "critical"
|
|
||||||
- host = "apps"
|
|
||||||
group_wait: 10s
|
|
||||||
repeat_interval: 30m
|
|
||||||
|
|
||||||
# Прочее критичное — инфраструктура, клиенты пока не затронуты.
|
|
||||||
- receiver: telegram
|
- receiver: telegram
|
||||||
matchers:
|
matchers:
|
||||||
- severity = "critical"
|
- severity = "critical"
|
||||||
|
|
@ -82,30 +63,6 @@ ${METRICS_TELEGRAM_TOPIC_LINE}
|
||||||
{{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}
|
{{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
# Клиентский инцидент: зовём дежурного поимённо.
|
|
||||||
#
|
|
||||||
# Упоминание берётся из ${METRICS_TELEGRAM_ONCALL} и НЕ зашито в репозиторий:
|
|
||||||
# дежурный меняется, а конфиг в git — нет. Пустая переменная = обычное
|
|
||||||
# сообщение без тега, то есть поведение не ломается, если её забыли задать.
|
|
||||||
- name: telegram-clients
|
|
||||||
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
|
|
||||||
message: |
|
|
||||||
{{ if eq .Status "firing" }}🚨 <b>КЛИЕНТЫ ЗАТРОНУТЫ</b>{{ else }}✅ <b>Восстановлено</b>{{ end }} · {{ .CommonLabels.alertname }}
|
|
||||||
{{ if eq .Status "firing" }}${METRICS_TELEGRAM_ONCALL} — нужна реакция.{{ end }}
|
|
||||||
{{ range .Alerts }}
|
|
||||||
{{ .Annotations.summary }}
|
|
||||||
{{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
{{ if eq .Status "firing" }}
|
|
||||||
Напоминание придёт снова через 30 минут, пока инцидент не погашен.
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
- name: telegram-heartbeat
|
- name: telegram-heartbeat
|
||||||
telegram_configs:
|
telegram_configs:
|
||||||
- bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}"
|
- bot_token: "${METRICS_TELEGRAM_BOT_TOKEN}"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue