Перейти к содержимому

Heartbeat-уведомления (dead-man-switch)

Heartbeat — пассивный мониторинг по принципу «не позвонил — значит, что-то случилось». Ваш сервис/cron/скрипт регулярно дёргает короткий HTTP-эндпоинт Notifly, а если очередной ping не пришёл за заданное время — вы получаете push-уведомление с настраиваемым текстом.

Это особенно удобно для:

  • регулярных cron-задач (бэкапы, импорты, генераторы отчётов);
  • демонов, которые «должны быть всегда включены»;
  • IoT-устройств, которые «звонят домой»;
  • batch-пайплайнов, где важно не «успех», а «успех вовремя».
ваш cron ──ping──▶ POST /heartbeat/ping/<pingToken> (раз в N сек)
└─▶ Notifly обновляет last_ping и сдвигает next_check_at
каждую минуту: timer-trigger → notifly-heartbeat-checker
└─▶ SELECT WHERE next_check_at <= now AND status IN (ok, pending)
для каждого: создать сообщение + push в WS
  • Хранение — таблица heartbeats в YDB Serverless. Один индекс по next_check_at — checker сканирует только просроченные записи (дёшево).
  • Проверка — отдельная Cloud Function notifly-heartbeat, запускается таймер-триггером Yandex Cloud (cron * * * * ? * — раз в минуту).
  • Уведомление — обычное сообщение Notifly, отправляется через выбранный при создании heartbeat-а канал и доходит до всех ваших клиентов (web, Android, desktop), как любая другая push-нотификация. Если у канала настроена доставка, алерт эскалируется по её методам.
  1. Откройте в app.notifly.ru нужный канал → вкладка Heartbeats → «Создать heartbeat». Тот же диалог открывает мастер создания монитора (Мониторинг → Мониторы, «Что вы хотите проверять?») — карточка «Моя джоба сама шлёт пинг (dead-man switch)».

  2. Заполните поля:

    • Название — для отображения, например «Cron бэкапа базы».
    • Канал — через какой канал слать алёрт. Доступны свои каналы и расшаренные с правом отправки.
    • Интервал — ожидаемый период между ping-ами: в форме выбирается из списка (1 мин, 5 мин, 10 мин, 30 мин, 1 ч, 1 д). Произвольное значение от 30 секунд до 30 суток можно задать через REST API.
    • Допуск (сек) — сколько ещё ждать после интервала (защита от джиттера), по умолчанию 60.
    • Важность — «Критичный», «Обычный» или «Информационный», см. Важность и сводный статус канала.
    • Уведомление о падении — заголовок и текст (Markdown) push-а, когда ping пропустит дедлайн. Если оставить пустыми, подставится текст по умолчанию.
    • Уведомление о восстановлении — заголовок и текст, которые приходят, когда после алёрта снова пришёл ping (по умолчанию «Сервис вернулся в нормальное состояние.»). Время простоя добавляется автоматически.

    Поля приоритета в форме нет — у создаваемого в админке heartbeat-а приоритет alert по умолчанию равен 5. Задать другой приоритет (alertPriority, 0–10) можно через REST API или MCP.

  3. Скопируйте ping URL из колонки «URL пинга» таблицы heartbeat-ов (кнопка «Копировать URL»). У каждой строки есть вкладки «История» (график пингов и пропусков) и «Примеры кода» (готовые сниппеты для curl, cron и языков, в том числе ping с причиной).

Окно терминала
curl -X POST "$NOTIFLY_URL/heartbeat" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <api-token>" \
-d '{
"appid": 12345,
"name": "Cron бэкапа базы",
"intervalSec": 3600,
"graceSec": 300,
"tier": "critical",
"alertTitle": "Бэкап не запустился",
"alertMessage": "За последний час cron бэкапа не пришёл — проверьте сервер!",
"alertPriority": 9,
"recoveryTitle": "Бэкап восстановлен",
"recoveryMessage": "Бэкап снова работает."
}'
ПолеОбязательноеОграничения и умолчания
appidдасвой канал или расшаренный с правом отправки; менять нельзя
nameда1–200 символов
intervalSecда30 – 2 592 000 (30 суток)
graceSec—0 – 604 800 (7 суток), по умолчанию 0
alertMessageда1–2000 символов
alertTitle—заголовок alert-а
alertPriority—0–10; не передан — 0
recoveryTitle—заголовок recovery
recoveryMessage—текст recovery; без него test-recovery отвечает 400
tier—critical, normal или info; не передан — normal

В ответе будет объект heartbeat-а со статусом pending и pingToken (начинается с H...); итоговый URL — https://<домен>/heartbeat/ping/<pingToken>.

PUT /heartbeat/:id принимает тот же набор полей и заменяет настройки целиком: не переданные alertPriority, tier, graceSec сбросятся к умолчаниям, поэтому присылайте полный объект. Канал при обновлении не меняется: appid в теле обязателен, но игнорируется.

Если у вас настроен MCP-сервер Notifly (см. MCP), просто попросите ассистента:

Создай heartbeat для канала «бэкапы», который ждёт ping раз в час с допуском 5 минут, и шлёт «Бэкап не запустился» приоритета 9.

MCP-инструменты: list_heartbeats, create_heartbeat, update_heartbeat, pause_heartbeat, resume_heartbeat, delete_heartbeat, ping_heartbeat. Если приоритет не указан, create_heartbeat ставит 8. Важность (tier) MCP-инструменты не задают — её меняют в админке или через REST API.

Самый минимальный ping — обычный curl без авторизации:

Окно терминала
curl -fsS "$NOTIFLY_URL/heartbeat/ping/<pingToken>" -o /dev/null

pingToken — это и есть аутентификация. Никаких других заголовков не нужно. Поддерживается и GET, и POST — чтобы можно было дёргать прямо из cron-job или мониторингов, которые не умеют POST.

ОтветКогда
204ping принят (для heartbeat-а на паузе — принят и проигнорирован)
401токен не похож на ping-токен (не начинается с H)
404неизвестный токен
429исчерпана дневная квота событий — ping не засчитан

Каждый ping — одно событие дневной квоты тарифа, см. Квоты и тарифы.

POST может нести JSON {"fail_reason": "..."} (до 500 символов). Если этот ping выводит heartbeat из alerting, причина прикладывается к recovery-уведомлению (extras.fail_reason) — рядом с длительностью простоя (extras.downtime_sec, считается от последнего живого ping-а):

Окно терминала
curl -fsS -X POST "$NOTIFLY_URL/heartbeat/ping/H..." \
-H "Content-Type: application/json" \
-d '{"fail_reason": "DB connection timeout after 30s"}'
*/15 * * * * /usr/local/bin/backup.sh && curl -fsS "$NOTIFLY_URL/heartbeat/ping/H..." -o /dev/null

Ping вызывается только при успехе скрипта (благодаря &&). Если бэкап упал — ping не пройдёт, и через intervalSec+graceSec прилетит alert.

В service-юните регулярного таймера:

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/bin/curl -fsS https://api.notifly.ru/heartbeat/ping/H... -o /dev/null

ExecStartPost запускается только если основной ExecStart завершился успешно.

import requests, subprocess
subprocess.check_call(["/usr/local/bin/backup.sh"])
requests.post(f"{NOTIFLY_URL}/heartbeat/ping/{TOKEN}", timeout=10)
import {execSync} from 'child_process';
execSync('/usr/local/bin/backup.sh');
await fetch(`${NOTIFLY_URL}/heartbeat/ping/${TOKEN}`, {method: 'POST'});
СостояниеЧто значит
pendingСоздан (или снят с паузы), ping ещё не приходил
okПоследний ping в пределах интервала
alertingДедлайн пропущен, alert уже отправлен
pausedПроверка отключена (через UI или POST /heartbeat/:id/pause)

Дедлайн — intervalSec + graceSec от последнего ping-а (для нового heartbeat-а — от момента создания). После отправки алёрта heartbeat не «спамит»: пока он в alerting, повторных алёртов нет, сколько бы ping ни отсутствовал. Первый ping после alerting вернёт heartbeat в ok и отправит recovery-уведомление (с тем же приоритетом, что и alert). Переход pending → ok recovery не шлёт.

У каждого heartbeat-а есть важность (tier), от которой зависит сводный статус канала:

tierВ админкеСбой такого heartbeat-а даёт каналу
critical«Критичный»alerting («Сбой»)
normal«Обычный»alerting («Сбой»)
info«Информационный»только degraded («Деградация»)

Сводный статус считается с приоритетом alerting > degraded > pending > ok; канал без heartbeat-ов — none. Heartbeat-ы на паузе на статус не влияют. Важность меняет только цвет канала, а не доставку: приоритет алёрта всегда равен alertPriority.

В админке статус виден цветной точкой у канала в списке, а вкладка Heartbeats на странице канала показывает сводку по каналу и всем его вложенным каналам. Критичные и информационные heartbeat-ы помечены чипом.

Тот же статус отдаёт GET /application/:id/status (с ?recursive=1 — по всему поддереву канала):

{
"overall": "degraded",
"total": 4,
"alerting": 0,
"alertingCritical": 0,
"alertingInfo": 1,
"pending": 0,
"lastStatusAt": "2026-06-23T09:59:12Z",
"heartbeats": [
{
"id": 17,
"appid": 12345,
"name": "Отчёт в Slack",
"tier": "info",
"status": "alerting",
"lastPing": "2026-06-23T07:00:03Z",
"owner": "ops@example.com"
}
]
}

alerting считает только critical + normal, alertingCritical — их подмножество тира critical, alertingInfo — сбои info (в alerting не входят). Элементы heartbeats — полные объекты heartbeat-ов (как в GET /heartbeat); в примере показана часть полей.

В сводке расшаренного канала видны heartbeat-ы всех его участников. Чужие помечены readOnly: true, у них заполнен автор (owner) и пустой pingToken: менять, ставить на паузу и пинговать heartbeat может только его автор.

Метод и путьАвторизацияНазначение
GET /heartbeatAPI-токенсписок своих heartbeat-ов
POST /heartbeatAPI-токен (write)создание
PUT /heartbeat/:idAPI-токен (write)обновление (полная замена полей)
DELETE /heartbeat/:idAPI-токен (write)удаление
POST /heartbeat/:id/pauseAPI-токен (write)приостановить проверки
POST /heartbeat/:id/resumeAPI-токен (write)возобновить проверки (статус pending)
POST /heartbeat/:id/test-alertAPI-токен (write)отправить тестовый alert немедленно (статус не меняется)
POST /heartbeat/:id/test-recoveryAPI-токен (write)отправить тестовый recovery немедленно (статус не меняется)
GET /application/:id/statusAPI-токенсводный статус heartbeat-ов канала (?recursive=1)
GET/POST /heartbeat/ping/:tokenпубличныйping (token = аутентификация)

Заголовок — X-Notifly-Key: <api-token>, см. API-токены. Тестовые уведомления приходят с префиксом [ТЕСТ] в заголовке.

Архитектурно проверка «у каких heartbeat-ов next_check_at ≤ сейчас» — это индексный point-query, а не сканирование. В YDB Serverless у нас INDEX heartbeats_next_check_idx и одна короткая транзакция на минуту (несколько Request Units). В S3 пришлось бы делать LIST (1 запрос) + GET (по запросу на каждый объект), причём оплата у Object Storage идёт за каждый запрос, и таких запросов было бы много даже без алёртов. Heartbeat-ов в проекте быстро становится десятки и сотни — поэтому YDB заметно дешевле и быстрее.