Почему это важно именно сейчас

Atlassian закрыл новые регистрации в Opsgenie в первом квартале 2026 года и назначил 5 апреля 2027 года датой окончания поддержки. Функции дежурств переезжают в Jira Service Management, а для многих команд в России переезд туда закрыт. PagerDuty, incident.io и Rootly либо недоступны, либо заточены под Slack, которым у нас почти никто не пользуется.

Что остаётся? Обычно одно из трёх: бот Zabbix, который шлёт всё подряд в общий чат; Karma поверх Alertmanager, на которую кто-то должен смотреть; или договорённость «если ночью упало — звоните Васе». Все три варианта показывают алерт, но ни один не отвечает на главный вопрос: кто сейчас отвечает за инцидент и что будет, если он не проснулся.

Я сделал «Дежурку» — небольшой сервис, который встаёт поверх уже имеющегося мониторинга и закрывает именно эту дыру. В статье расскажу, как он устроен, какие решения пришлось принять и как его попробовать.

Что должна уметь система дежурств

Прежде чем писать код, я сформулировал требования, исходя из того, что у типичной команды уже стоит Prometheus с Alertmanager или Zabbix, а общаются все в Telegram.

  1. Не заменять мониторинг, а встать после него. Алерты уже есть, их не надо собирать заново — нужно решить, кого будить.

  2. Знать график. Ротация на неделю или на сутки, разные часовые пояса, подмены «Петя заболел, сегодня дежурит Маша» без правки конфигов.

  3. Эскалировать. Если дежурный не отреагировал за 5 минут — звать следующего, потом тимлида, и так по кругу.

  4. Фиксировать ответственного. Кнопка «Беру» в Telegram: нажал — эскалация остановилась, а все видят, кто разбирается.

  5. Не спамить. Один и тот же алерт, пришедший 20 раз, — это один инцидент, а не 20 сообщений.

  6. Ставиться в закрытый контур. Без публичного HTTPS, без внешних SaaS, данные остаются у компании.

  7. Конфиг как код. Люди, графики и политики — в YAML рядом с остальной инфраструктурой.

Как устроена «Дежурка»

Алерт проходит две развилки. Сначала проверяется, нет ли уже открытого инцидента с тем же ключом: если есть, людей не будят повторно. Затем после уведомления «Дежурка» ждёт «Беру». Нет нажатия за отведённые минуты — переходит к следующему шагу политики и снова смотрит, кто сейчас в смене.

Под капотом это Python 3.12, FastAPI и SQLAlchemy, хранилище — SQLite или Postgres. Весь сервис — около 2000 строк кода и 24 теста, ставится одним Docker-контейнером.

Подключение за 5 минут

У каждого сервиса в конфиге есть секретный integration_key. Он входит в URL вебхука, а последний сегмент URL говорит, в каком формате пришёл алерт.

Prometheus Alertmanager — достаточно добавить ресивер:

receivers:
  - name: dezhurka
    webhook_configs:
      - url: https://dezhurka.example.ru/api/v1/ingest/<integration_key>/alertmanager
        send_resolved: true

С send_resolved: true инцидент закрывается сам, когда алерт уходит. Grafana Alerting шлёт совместимый формат — для неё тот же URL с окончанием /grafana.

Zabbix — медиа-тип Webhook с готовым JS-скриптом из репозитория. Если в действии включить Recovery operations, восстановление тоже закроет инцидент: «Дежурка» склеивает проблему и восстановление по {EVENT.ID}.

Свои скрипты и cron — обычный JSON:

curl -X POST https://dezhurka.example.ru/api/v1/ingest/<integration_key>/generic \
  -H 'Content-Type: application/json' \
  -d '{"title": "Ночной бэкап не прошёл", "dedup_key": "backup-db1", "details": "exit code 2"}'

Это удобно для бэкапов, проверок сертификатов и всего, что не живёт в Prometheus.

Графики и эскалации

Всё статичное описано в одном config.yaml, его удобно держать в git и ревьюить как обычный код:

schedules:
  - id: sre-primary
    timezone: Europe/Moscow
    rotation:
      start: 2026-11-02T10:00:00   # первая смена — понедельник, 10:00
      shift_hours: 168             # неделя; 24 — посуточно, 12 — день/ночь
      users: [ivan, maria, oleg]

escalation_policies:
  - id: sre
    repeat: 2                      # если никто не взял — ещё два круга
    steps:
      - targets: [{schedule: sre-primary}]
        escalate_after_min: 5
      - targets: [{user: lead}]
        escalate_after_min: 10

Когда приходит новый алерт, «Дежурка» смотрит, кто сейчас в смене по графику sre-primary, и зовёт его в личку Telegram и на почту. Параллельно инцидент дублируется в общий чат команды. Если за 5 минут никто не нажал «Беру», зовут тимлида, через 10 минут цикл начинается заново. После двух кругов в хронологии появляется запись, что эскалация исчерпана, — это сигнал пересмотреть график.

Подмены живут отдельно, в базе: их добавляют в панели, не трогая конфиг. Подмена всегда сильнее ротации, а из двух пересекающихся побеждает последняя.

Секреты (токен бота, пароль панели, ключи интеграций) в конфиге не хранятся: они подставляются из переменных окружения через ${VAR}.

Инженерные решения, которые пришлось продумать

Дедупликация по ключу, а не по тексту. У Alertmanager у каждого алерта есть fingerprint — хэш меток. Пока инцидент с таким ключом открыт, повторные алерты только увеличивают счётчик. Для Zabbix ключом служит {EVENT.ID}: Zabbix передаёт один и тот же ID и в проблеме, и в восстановлении, поэтому пара склеивается без эвристик. Если ключа нет, берётся хэш меток.

Long polling вместо вебхука Telegram. Чтобы принимать нажатия кнопок через вебхук, серверу нужен публичный HTTPS-адрес. В закрытом контуре банка его нет и не будет. Поэтому бот сам опрашивает Telegram через getUpdates: наружу нужен только исходящий доступ к api.telegram.org.

Один процесс — осознанно. Цикл эскалаций, бот и HTTP API живут в одном процессе uvicorn, а все изменения состояния инцидентов идут через один asyncio.Lock. Для системы, через которую проходят десятки инцидентов в день, это проще и надёжнее распределённых блокировок: нельзя случайно разбудить человека дважды или потерять «Беру» в гонке. По умолчанию данные лежат в SQLite, для продакшна можно подключить Postgres.

Обновление уже отправленных сообщений. Каждое сообщение об инциденте сохраняется с chat_id и message_id. Когда кто-то нажимает «Беру», все копии — в личках и в общем чате — редактируются: статус меняется на «В работе: Иван», кнопка «Беру» исчезает. В чате не остаётся «висящих» алертов, по которым непонятно, взял ли их кто-то.

Кнопки нажимают только свои. Человек привязывает Telegram по личной ссылке вида t.me/<бот>?start=<код>, где код — HMAC от его id с секретом сервера. Таблица кодов не нужна, подделать код нельзя. Нажатия от непривязанных аккаунтов и команды /open и /oncall от посторонних бот отклоняет — даже если его добавили в чужой чат.

Защита панели. Панель закрыта Basic-авторизацией. Так как браузер сам подставляет Basic-заголовок в любые запросы, POST-формы дополнительно проверяют Origin, иначе любая чужая страница могла бы закрыть ваш инцидент от вашего имени.

Как попробовать

Исходники лежат на GitHub: github.com/wonbix1409/dezhurka. В репозитории есть установщик демо-стенда: на чистой Ubuntu он одной командой поднимает «Дежурку», Prometheus, Alertmanager, Grafana и, по желанию, Zabbix, уже связанные между собой.

sudo bash deploy/setup.sh            # 1 vCPU, 2 ГБ RAM
sudo bash deploy/setup.sh --zabbix   # 2 vCPU, 4 ГБ RAM

Скрипт сам сгенерирует пароли, спросит токен бота от @BotFather и в конце напечатает адреса. Через пару минут можно отправить тестовый алерт и нажать «Беру» в Telegram.

Код открыт для ознакомления, условия использования описаны в LICENSE репозитория.

Что дальше

  • Звонки и SMS через российских провайдеров — без этого ночное дежурство неполноценно.

  • Мессенджер MAX как второй канал.

  • Статус-страница для клиентов и отчёты по нагрузке дежурных.

  • Для корпоративной установки — SSO через LDAP/Keycloak, роли, аудит и Helm-чарт.

Если вы сейчас сидите на Opsgenie и думаете, куда переезжать до апреля, расскажите в комментариях, как у вас устроены дежурства и чего не хватает. Приоритеты дорожной карты я буду расставлять по вашим ответам.