Уведомления о критичных событиях сервера через Webhook
Сервер упал в три часа ночи, а ты узнал об этом только утром от возмущённых игроков в чате — знакомая ситуация. Мониторинг аптайма скажет, что сервис недоступен, но не скажет, что именно случилось: OOM-killer, паника плагина или просто забитый диск. В этой статье — рабочая связка Discord/Slack Webhook с systemd-хуками и парой cron-скриптов, которая пришлёт тебе в личку не абстрактное «сервер лежит», а конкретику: какой процесс упал, с каким кодом выхода, и что с бэкапом за сегодня.
Содержание
Почему обычного аптайм-мониторинга мало
Внешний аптайм-чекер (UptimeRobot, Pingdom и подобные — про них отдельно есть статья про аптайм-мониторинг игрового сервера) стучится в порт снаружи и видит только «отвечает / не отвечает». Он не знает:
- что процесс упал с segfault, а не просто завис;
- что рестарт уже случился три раза за час — значит, дело не в разовом сбое;
- что nightly-бэкап не создался, потому что кончилось место на диске;
- что средняя нагрузка на CPU держится выше 90% последние 20 минут и тикрейт вот-вот просядет.
Разница практическая: внешний мониторинг говорит «плохо», а вебхук-алерт — «плохо именно так, вот команда чтобы посмотреть логи». Второе экономит время диагностики, особенно когда сервер администрируешь не ты один, а вебхук летит сразу в общий канал команды.
Ниже — три источника событий: systemd (краш и рестарт процесса), cron-скрипт нагрузки (CPU/RAM/диск) и cron-скрипт бэкапа (успех/провал). Все три пишут в один и тот же Webhook, просто с разным текстом и, для Discord, разным цветом embed'а.
Заводим Webhook в Discord
В Discord вебхук создаётся на уровне текстового канала, без прав бота и без OAuth-возни:
- В настройках канала (шестерёнка рядом с названием) открой Integrations → Webhooks → New Webhook.
- Задай имя (например,
server-alerts) и, по желанию, иконку. - Скопируй Webhook URL — он вида
https://discord.com/api/webhooks/123456789012345678/AbCdEf....
Храни этот URL как секрет — любой, у кого он есть, может слать сообщения в канал от имени вебхука. Не коммить его в публичный репозиторий конфигов.
Быстрый тест из консоли:
curl -H "Content-Type: application/json" \
-d '{"content": "Тестовое сообщение с сервера"}' \
https://discord.com/api/webhooks/123456789012345678/AbCdEf...
Если в канале появилось сообщение — вебхук живой. Для алертов удобнее использовать embeds вместо голого content — так сообщение получает цветную полосу и структуру полей:
curl -H "Content-Type: application/json" -d '{
"embeds": [{
"title": "Краш процесса: mc-survival",
"description": "Сервис завершился с кодом 137 (OOM)",
"color": 15158332,
"fields": [
{"name": "Хост", "value": "de-fra-3", "inline": true},
{"name": "Время", "value": "2026-08-31 03:12 UTC", "inline": true}
]
}]
}' https://discord.com/api/webhooks/123456789012345678/AbCdEf...
color задаётся десятичным числом (15158332 — красный, 3066993 — зелёный, 15105570 — жёлтый) — пригодится, чтобы визуально отличать краш от предупреждения о нагрузке.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЗаводим Webhook в Slack
В Slack с апреля 2023 классические Incoming Webhooks для новых интеграций закрыты — вебхук теперь выпускается через Slack-приложение:
- На api.slack.com/apps создай приложение From scratch, привяжи к своему workspace.
- В разделе Incoming Webhooks включи тумблер и нажми Add New Webhook to Workspace.
- Выбери канал — получишь URL вида
https://hooks.slack.com/services/T000/B000/XXXXXXXX.
Тест:
curl -X POST -H 'Content-Type: application/json' \
-d '{"text": "Тестовое сообщение с сервера"}' \
https://hooks.slack.com/services/T000/B000/XXXXXXXX
Для структурированных алертов Slack ждёт blocks, а не embeds:
curl -X POST -H 'Content-Type: application/json' -d '{
"text": "Краш процесса: mc-survival",
"blocks": [
{"type": "section", "text": {"type": "mrkdwn",
"text": "*Краш процесса:* mc-survival\n*Код выхода:* 137 (OOM)\n*Хост:* de-fra-3"}}
]
}' https://hooks.slack.com/services/T000/B000/XXXXXXXX
Формат payload у Discord и Slack разный, поэтому в общем скрипте ниже я делаю простую JSON-обёртку под оба варианта одной функцией с параметром платформы.
Универсальный скрипт notify.sh
Чтобы не дублировать curl-вызовы по трём разным местам (systemd hook, cron нагрузки, cron бэкапа), вынеси отправку в отдельный скрипт /opt/scripts/notify.sh:
#!/usr/bin/env bash
# /opt/scripts/notify.sh — универсальная отправка алерта в Discord и/или Slack
set -euo pipefail
DISCORD_WEBHOOK="${DISCORD_WEBHOOK:-}"
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"
LEVEL="${1:-info}" # info | warning | critical
TITLE="${2:-Уведомление}"
MESSAGE="${3:-}"
case "$LEVEL" in
critical) COLOR=15158332 ;; # красный
warning) COLOR=15105570 ;; # жёлтый
*) COLOR=3066993 ;; # зелёный
esac
HOSTNAME_LOCAL=$(hostname)
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
if [ -n "$DISCORD_WEBHOOK" ]; then
curl -sf -H "Content-Type: application/json" -d "$(cat <<EOF
{
"embeds": [{
"title": "${TITLE}",
"description": "${MESSAGE}",
"color": ${COLOR},
"fields": [
{"name": "Хост", "value": "${HOSTNAME_LOCAL}", "inline": true},
{"name": "Время", "value": "${TIMESTAMP}", "inline": true}
]
}]
}
EOF
)" "$DISCORD_WEBHOOK" > /dev/null || echo "notify.sh: Discord webhook failed"
fi
if [ -n "$SLACK_WEBHOOK" ]; then
curl -sf -X POST -H 'Content-Type: application/json' -d "$(cat <<EOF
{"text": "*${TITLE}*\n${MESSAGE}\nХост: ${HOSTNAME_LOCAL} | ${TIMESTAMP}"}
EOF
)" "$SLACK_WEBHOOK" > /dev/null || echo "notify.sh: Slack webhook failed"
fi
chmod +x /opt/scripts/notify.sh
URL вебхуков не хардкодь в скрипте — держи их в /etc/default/game-alerts (или в .env, если так удобнее твоей связке) и подключай через EnvironmentFile у systemd-юнита или source в cron-обёртке:
# /etc/default/game-alerts
DISCORD_WEBHOOK="https://discord.com/api/webhooks/123456789012345678/AbCdEf..."
SLACK_WEBHOOK=""
Права на файл лучше выставить chmod 600 — читать его должен только root или пользователь, от которого крутится сервер.
Хук на краш и рестарт через systemd
Если игровой сервер уже запущен как systemd-юнит (см. статью про systemd, screen и tmux для игровых процессов), алерт на краш добавляется без сторонних демонов — двумя директивами.
Основной юнит mc-survival.service:
[Unit]
Description=Minecraft Server (survival)
After=network.target
OnFailure=alert-on-failure@%n.service
[Service]
Type=simple
User=mcserver
WorkingDirectory=/srv/minecraft/survival
EnvironmentFile=/etc/default/game-alerts
ExecStart=/usr/bin/java -Xms4G -Xmx8G -jar server.jar nogui
Restart=on-failure
RestartSec=10
ExecStopPost=/opt/scripts/notify-stop.sh %n
[Install]
WantedBy=multi-user.target
OnFailure= срабатывает, когда юнит завершается с ошибкой (ненулевой код выхода, таймаут, краш) — но не при штатном systemctl stop. Он запускает шаблонный юнит-алерт:
# /etc/systemd/system/alert-on-failure@.service
[Unit]
Description=Отправка алерта о краше %i
[Service]
Type=oneshot
EnvironmentFile=/etc/default/game-alerts
ExecStart=/opt/scripts/notify.sh critical "Краш сервиса %i" "Юнит завершился аварийно, проверь journalctl -u %i -n 50"
А ExecStopPost= идёт отдельным скриптом /opt/scripts/notify-stop.sh, потому что нужно знать код выхода — systemd передаёт его через переменную $EXIT_STATUS только внутри ExecStopPost того же юнита, а не в шаблон OnFailure:
#!/usr/bin/env bash
# /opt/scripts/notify-stop.sh
UNIT="$1"
CODE="${EXIT_STATUS:-unknown}"
if [ "$CODE" != "0" ] && [ "$CODE" != "unknown" ]; then
source /etc/default/game-alerts
/opt/scripts/notify.sh warning "Остановка ${UNIT}" "Код выхода: ${CODE}"
fi
После правок конфигов не забудь:
sudo systemctl daemon-reload
sudo systemctl restart mc-survival.service
Проверить, что хук реально сработает, проще всего искусственным крашем: sudo kill -9 $(systemctl show -p MainPID --value mc-survival.service). Через пару секунд должно прилететь сообщение в канал — если нет, смотри journalctl -u alert-on-failure@mc-survival.service -n 30.
Cron: нагрузка и бэкапы
Часть событий systemd не видит в принципе — например, «сервер жив, но CPU 95% последние 10 минут» или «бэкап отработал, но с кодом ошибки». Для них — два коротких cron-скрипта.
Нагрузка (проверка раз в 5 минут, порог задан условно — подстрой под свой профиль нагрузки):
#!/usr/bin/env bash
# /opt/scripts/check-load.sh
source /etc/default/game-alerts
LOAD1=$(cut -d' ' -f1 /proc/loadavg)
CORES=$(nproc)
THRESHOLD=$(echo "$CORES * 0.9" | bc)
if (( $(echo "$LOAD1 > $THRESHOLD" | bc -l) )); then
MEM_FREE=$(free -h | awk '/^Mem:/{print $4}')
/opt/scripts/notify.sh warning "Высокая нагрузка" \
"Load average 1m: ${LOAD1} (ядер: ${CORES}), свободно RAM: ${MEM_FREE}"
fi
*/5 * * * * /opt/scripts/check-load.sh
Бэкап — оборачивай существующий скрипт бэкапа, проверяя код выхода. Если у тебя уже настроено расписание бэкапов по статье про автобэкапы игрового сервера, добавь проверку прямо в конец того же cron-джоба:
#!/usr/bin/env bash
# /opt/scripts/backup-with-alert.sh
source /etc/default/game-alerts
/opt/scripts/backup.sh > /var/log/backup.log 2>&1
STATUS=$?
if [ $STATUS -ne 0 ]; then
/opt/scripts/notify.sh critical "Бэкап провален" \
"backup.sh завершился с кодом ${STATUS}. Лог: /var/log/backup.log"
else
SIZE=$(du -sh /srv/backups/latest.tar.gz 2>/dev/null | cut -f1)
/opt/scripts/notify.sh info "Бэкап готов" "Размер архива: ${SIZE:-неизвестен}"
fi
Успешный бэкап тоже стоит слать, но не в критичный канал, а либо с уровнем info в отдельный тред, либо вообще раз в сутки одним сводным сообщением — иначе Discord-канал через месяц превратится в стену «бэкап ок», которую никто не читает. Если алертов много, лучше сразу заводить второй вебхук на отдельный канал #server-info для некритичных событий и оставить #server-alerts только под критику.
| Событие | Уровень | Куда шлём | Частота проверки |
|---|---|---|---|
| Краш процесса (OnFailure) | critical | основной канал | мгновенно |
| Ненулевой код выхода (ExecStopPost) | warning | основной канал | мгновенно |
| Load average > 90% ядер | warning | основной канал | раз в 5 мин |
| Бэкап провален | critical | основной канал | по расписанию бэкапа |
| Бэкап успешен | info | вспомогательный канал | по расписанию бэкапа |
Что делать с алертами дальше
Сам вебхук — это только доставка сигнала, не решение проблемы. Если управляешь несколькими серверами и хочешь обрабатывать алерты прямо из чата (например, рестартовать сервис командой в Discord), логичное продолжение — полноценный бот с RCON-интеграцией, об этом отдельно в статье про Discord-бота для управления игровым сервером.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Вебхук не приходит, curl возвращает 200 — в чём дело?
Проверь, что канал не архивный и вебхук не был удалён вручную через настройки интеграций — Discord и Slack молча принимают запрос (200/204), даже если получатель канала недоступен, если URL синтаксически валиден. Пересоздай вебхук и обнови .env.
Можно ли слать алерты в личные сообщения, а не в канал?
Discord Webhook пишет только в канал, к которому привязан. Для личных уведомлений нужен полноценный бот с правами на DM или бесплатный сервис-мост вроде IFTTT/Zapier поверх вебхука — это уже отдельная настройка, не входит в объём этой статьи.
Не будет ли спама, если процесс падает в цикле рестартов?
Да, будет — Restart=on-failure с коротким RestartSec может гонять OnFailure= каждые 10 секунд. Добавь в юнит StartLimitIntervalSec=300 и StartLimitBurst=3 — после трёх падений за 5 минут systemd перестанет перезапускать сервис и хук сработает один раз с финальным состоянием failed.
Нужно ли хранить оба вебхука — и Discord, и Slack?
Нет, скрипт notify.sh просто пропускает пустую переменную. Заполни только тот _WEBHOOK, который реально используешь — второй можно оставить пустым в .env.
Как проверить, что cron-скрипты вообще выполняются?
Добавь логирование в отдельный файл (>> /var/log/game-alerts-cron.log 2>&1 в конце cron-строки) и проверь grep CRON /var/log/syslog — если строка cron не появляется в логе вообще, дело в правах на скрипт (chmod +x) или в неверном пути в crontab.