MAATRIX GAMES / Блог / Уведомления о критичных событиях сервера через Webhook

Уведомления о критичных событиях сервера через Webhook

MAATRIX GAMES

Сервер упал в три часа ночи, а ты узнал об этом только утром от возмущённых игроков в чате — знакомая ситуация. Мониторинг аптайма скажет, что сервис недоступен, но не скажет, что именно случилось: 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-возни:

  1. В настройках канала (шестерёнка рядом с названием) открой Integrations → Webhooks → New Webhook.
  2. Задай имя (например, server-alerts) и, по желанию, иконку.
  3. Скопируй 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-приложение:

  1. На api.slack.com/apps создай приложение From scratch, привяжи к своему workspace.
  2. В разделе Incoming Webhooks включи тумблер и нажми Add New Webhook to Workspace.
  3. Выбери канал — получишь 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.