Крутится там, где есть постоянный адрес (JumpHost, облако), ловит хук и пишет в Telegram или ntfy. Токен бота лежит у неё одной; всем остальным довольно уметь POST.
curl -X POST http://jumphost:8790/notify \
-H "X-SS-Token: $SS_NOTIFY_TOKEN" \
--data-urlencode "level=done" \
--data-urlencode "title=серия stage-final" \
--data-urlencode "text=готово за 4 ч 12 мин"Серия экспериментов идёт ~4 часа, свип веса — под сутки, и всё это время за
стендом никто не сидит. Наблюдаемость в
sensitivityscore-hpc-bench
была целиком pull: вотчдог честно писал в лог, что серия зависла, что
port-forward к Redis умер (а значит placement_regret с этого момента — NaN
до конца прогона) и что прогон кончился, — но лог лежит на хосте, а
статус-страница видна только из домашней сети. Узнавали постфактум: 20.07 хост
ушёл в сон на 6.4 часа, и метрика решения пропала у 120 строк из 180 — молча.
Почему служба, а не утилита на каждой машине:
- секрет в одном месте. Токен бота не расползается по ноутбуку, JumpHost'у и прод-узлу; отозвать или сменить его — одна правка в одном файле;
- вызывающему нужен только
curl. Ни docker, ни python, ни установки — вrun-series.shуведомление это буквально один вызов curl; - есть куда направить хуки, которые сейчас не идут никуда: в первую
очередь Alertmanager с правилами
SSLLCAxisSaturatedиSSAxisDegenerate— они долетают до Grafana и на этом останавливаются.
Служба НИЧЕГО не знает про эксперименты: что считать бедой, решает отправитель. Здесь только доставка.
make config # config.env из образца
# вписать TELEGRAM_BOT_TOKEN (@BotFather) и SS_NOTIFY_TOKEN (openssl rand -hex 24)
make up # сборка + запуск + ожидание готовности
make chat-id # написать боту любое сообщение, потом эта команда покажет chat_id
make check # живая проверка: служба + канал (отправит одно сообщение)На выделенном хосте — ещё и автозапуск:
make unit # systemd-юнит: служба переживает перезагрузкуЮнит не дублирует restart: unless-stopped из compose, а закрывает случай, до
которого та политика не достаёт: после docker compose down возвращать нечего,
и служба не поднимется уже никогда, причём молча. А отсутствие уведомлений
выглядит ровно как «всё хорошо» — потому эту дыру и затыкаем отдельно.
| путь | что делает |
|---|---|
POST /notify |
уведомление: level, title, text, необязательные file_name + file_b64 |
POST /alert |
вебхук Alertmanager: сам разбирает firing/resolved, метки и аннотации |
GET /healthz |
живость; ничего не отправляет и токена не требует |
GET / |
что это за служба и какой у неё канал |
/notify принимает и форму, и JSON. Форма — ради shell: --data-urlencode
экранирует хвост лога сам, а собирать JSON в bash значит городить escaping для
кавычек и переносов и однажды на нём ошибиться.
Уровни: info, warn, error, done — значок в сообщении и приоритет у
ntfy.
Вложение — base64 в поле file_b64 (клиент ss-notify -f делает это сам):
curl -X POST http://jumphost:8790/notify -H "X-SS-Token: $tok" \
--data-urlencode "title=отчёт готов" \
--data-urlencode "file_name=summary.md" \
--data-urlencode "file_b64=$(base64 < summary.md | tr -d '\n')"receivers:
- name: ss-notifier
webhook_configs:
- url: http://192.168.1.72:8790/alert
http_config:
authorization: { type: Bearer, credentials: <SS_NOTIFY_TOKEN> }Снятый алерт приходит как done, сработавший — как error; в тексте имя
правила, severity, узел и summary.
Служба держит токен бота и пишет от вашего имени, поэтому без
SS_NOTIFY_TOKEN она не стартует. Открыть приём без проверки можно, но
только явно — SS_NOTIFY_ALLOW_ANON=1, и это осмысленно лишь внутри
доверенной сети.
Токен приёма обязан быть ASCII: он едет HTTP-заголовком X-SS-Token, а
значения заголовков латиницей и ограничены. Кириллический токен служба
отвергает при старте — иначе отказ вылезал бы у клиента на первом уведомлении,
и не про токен, а про latin-1.
Контейнер работает не от root и с read-only файловой системой: служба ничего не пишет на диск, и это зафиксировано, а не задекларировано.
ss-notify — тонкий клиент: то же самое, что curl выше, но с ключами, сухим
прогоном и --check.
make install # -> ~/.local/bin/ss-notify
export SS_NOTIFY_URL=http://jumphost:8790 SS_NOTIFY_TOKEN=…
ss-notify -l done -t "серия stage-final" "готово"
ss-notify -l error -t "серия зависла" -f harness/stage-final.log "лог не растёт"Без SS_NOTIFY_URL тот же скрипт шлёт напрямую в Telegram/ntfy по своему
~/.config/ss-notify/config.env — режим для ноутбука, где службы нет. Вызовы и
коды возврата в обоих режимах одинаковы.
Коды возврата: 0 — отправлено (либо выключено, либо сухой прогон), 1 — не
удалось, 2 — ошибка вызова или конфигурации. Вызывающему, для которого
уведомление второстепенно, полагается дописывать || true.
Сообщения и вложения уезжают на серверы Telegram. Для прогонов это хвост лога
и summary.md отчёта — неопубликованные числа. Если это неприемлемо, в
config.env:
SS_NOTIFY_BACKEND=ntfy
NTFY_URL=https://ntfy.example.org # свой инстанс
NTFY_TOPIC=<длинная случайная строка> # тема — это и есть весь доступОтправители об этом не знают и не меняются: канал выбирает служба.
make test # служба (python, без сети) + клиент (bash)
make check # живая проверка канала
make lint # shellcheck + компиляция pythonТесты службы поднимают свой приёмник вместо api.telegram.org — поэтому
адрес API и сделан настройкой (TELEGRAM_API), а не константой: иначе
проверить можно было бы только «функция не упала», а не «ушло именно то, что
надо». Проверяется в том числе то, на чём легко ошибиться: ok:false при
HTTP 200 (обычный ответ Bot API на протухший токен) обязан давать 502, а не
рапорт об успехе; текст с кавычками и переносами обязан доезжать без порчи;
/healthz не должен ничего отправлять.