feat(ops): запасной канал алертов на случай недоступного Telegram
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped

До этого единственным каналом был Telegram, и при его недоступности
notify() ограничивался `|| log "WARN"`. То есть алерт ТЕРЯЛСЯ: оставалась
строка в логе, который никто не читает, пока не случится беда. Канал,
которым мы узнаём о проблемах, сам является проблемой — поломка скрывает
сама себя.

Это не гипотеза. Замер с нового хоста (#3059, 23.08): из семи публикуемых
адресов api.telegram.org с Poincare отвечает РОВНО ОДИН (149.154.167.220),
скан всей подсети 149.154.167.0/24 не нашёл ни одного другого. Через прокси
Telegram не проходит вовсе — узлы российские. После переезда весь
Telegram-канал висит на одном адресе без запасного, и через него же идут
алерты бэкапов и uptime-нотификации.

Почта выбрана потому, что проверена: с Poincare smtp.beget.com:465 OPEN
(587 закрыт). Транспорт — тот же curl, который уже жёсткая зависимость
lib-backup.sh; новых пакетов не нужно.

Правка ложится ровно в ту точку, которую сам файл объявляет расширением:
«to wire a different channel later, edit ONLY this function».

Поведение:
  telegram ok            -> как раньше
  telegram упал          -> пробуем почту
  telegram не настроен   -> пробуем почту
  почта не настроена     -> ГРОМКАЯ строка «АЛЕРТ НЕ ДОСТАВЛЕН»
  оба канала упали       -> ГРОМКАЯ строка с кодом возврата curl

Ничего не настроено -> ведёт себя ровно как до правки. Кредов в
репозитории нет, читаются из того же env-файла, что и Telegram.

Проверено (bash -n + прогон всех веток):
  - почта не настроена  -> «АЛЕРТ НЕ ДОСТАВЛЕН (...не настроена)», rc=1
  - SMTP недоступен     -> «НИ ОДНИМ КАНАЛОМ (... curl smtp rc=7)», rc=1
  - notify() без telegram сам зовёт запасной канал
  - пароль в выводе: 0 вхождений (специально проверил grep'ом)

НЕ проверена успешная доставка: для неё нужны настоящие SMTP-креды, а это
часть владельца. Три попытки поднять фейковый SMTP-сервер положительный
контроль не дали, выдумывать результат не стал — см. test plan в PR.

Refs #3059, #3057
This commit is contained in:
bot-backend 2026-08-24 02:53:22 +03:00
parent 91731ddb5f
commit 568d82919d

View file

@ -41,6 +41,7 @@ notify() {
if [[ -z "${TELEGRAM_BOT_TOKEN:-}" || -z "${TELEGRAM_CHAT_ID:-}" ]]; then
log "NOTIFY (telegram disabled — set TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID in ${backup_env}): $text"
notify_fallback_mail "$text"
return 0
fi
@ -50,7 +51,61 @@ notify() {
-d "disable_web_page_preview=true" \
--data-urlencode "text=${text}" \
>/dev/null 2>&1 \
|| log "WARN: telegram sendMessage failed"
|| { log "WARN: telegram sendMessage failed — пробую запасной канал"; \n notify_fallback_mail "$text"; }
}
# --- запасной канал оповещения (#3059) ----------------------------------
# Зачем: раньше единственным каналом был Telegram, и `|| log WARN` означало,
# что при его недоступности алерт просто ТЕРЯЛСЯ — оставалась строка в логе,
# который никто не читает, пока не случится беда. Самоскрывающаяся поломка:
# канал, которым мы узнаём о проблемах, сам и есть проблема.
#
# Это не паранойя, а замер (#3059, 2026-08-23): с нового хоста Poincare из семи
# публикуемых адресов api.telegram.org отвечает РОВНО ОДИН (149.154.167.220),
# и скан всей подсети 149.154.167.0/24 не нашёл больше ни одного. Через прокси
# Telegram не проходит вовсе — узлы российские. После переезда весь
# Telegram-канал висит на одном адресе, без запасного.
#
# Почему почта: с Poincare проверено — smtp.beget.com:465 OPEN (587 закрыт).
# Транспорт — тот же curl, который уже является жёсткой зависимостью файла;
# новых пакетов не требуется.
#
# Настраивается тем же env-файлом, что и Telegram. Не задано — ведёт себя как
# раньше. Кредов в репозитории нет:
# ALERT_SMTP_URL=smtps://smtp.beget.com:465
# ALERT_SMTP_USER= / ALERT_SMTP_PASS= / ALERT_MAIL_FROM= / ALERT_MAIL_TO=
notify_fallback_mail() {
local text="$1"
if [[ -z "${ALERT_SMTP_URL:-}" || -z "${ALERT_SMTP_USER:-}" \
|| -z "${ALERT_SMTP_PASS:-}" || -z "${ALERT_MAIL_FROM:-}" \
|| -z "${ALERT_MAIL_TO:-}" ]]; then
# ГРОМКО: это последний рубеж, тишина здесь = потерянный алерт.
log "АЛЕРТ НЕ ДОСТАВЛЕН (telegram недоступен, почта не настроена): $text"
return 1
fi
local subject body rc=0
subject=$(printf '%s' "$text" | head -c 120 | tr '\n' ' ')
body=$(printf 'From: %s\nTo: %s\nSubject: [gendesign] %s\n\n%s\n' \
"$ALERT_MAIL_FROM" "$ALERT_MAIL_TO" "$subject" "$text")
# Пароль идёт через --user, поэтому stderr curl целиком не печатаем — в нём
# может оказаться строка подключения. Логируем только код возврата.
printf '%s' "$body" | curl -fsS --max-time 30 --ssl-reqd \
--url "$ALERT_SMTP_URL" \
--user "${ALERT_SMTP_USER}:${ALERT_SMTP_PASS}" \
--mail-from "$ALERT_MAIL_FROM" \
--mail-rcpt "$ALERT_MAIL_TO" \
--upload-file - >/dev/null 2>&1 || rc=$?
if (( rc == 0 )); then
log "Алерт доставлен запасным каналом (почта) вместо Telegram"
return 0
fi
log "АЛЕРТ НЕ ДОСТАВЛЕН НИ ОДНИМ КАНАЛОМ (telegram упал, curl smtp rc=${rc}): $text"
return 1
}
# --- sentinel (missed-run detection) ------------------------------------