Webhook-роутер
Webhook-роутер — это единая публичная точка приёма вебхуков. Любой
внешний сервис (GitHub, GitLab, Grafana, Stripe, ваш CI и т. д.)
шлёт POST с JSON-телом на один и тот же URL вида /router/R<token>/<путь>, а Notifly
сам разбирает payload и раскладывает его по каналам — по правилам, которые
вы настраиваете в админке.
Это замена старого «webhook»-механизма: вместо отдельной ссылки на каждый интегрируемый сервис у вас есть один роутер на аккаунт (singleton), а маршрутизацию задают правила. Один входящий запрос может совпасть сразу с несколькими правилами и улететь в несколько каналов.
Удобно для:
- разведения алертов Grafana по командам (
/grafana→ разные каналы поcommonLabels.severity); - маршрутизации событий GitHub (push в один канал, issues в другой);
- приёма JSON-вебхуков CRM, платёжек и CI без написания бэкенда;
- единой «воронки» вебхуков, которую потом можно пересортировать правилами, не меняя URL у отправителей.
Как это работает
Заголовок раздела «Как это работает»- У каждого аккаунта есть один роутер с токеном вида
R…(префиксR). Публичный URL —https://api.notifly.ru/router/R<token>/<путь>. - Внешний сервис шлёт
POST(илиGET) с JSON-телом на этот URL. - Роутер прогоняет запрос через все включённые правила. Каждое правило
проверяет две вещи:
- путь URL (
*pathпосле токена) — совпадает ли с паттерном правила; - фильтр — проходит ли JSON-payload (и заголовки) по условиям.
- путь URL (
- Для каждого совпавшего правила рендерятся шаблоны
title/message(Gotext/template) и в канал правила создаётся сообщение. - Запрос целиком сохраняется в историю событий — с заголовками и полным телом, чтобы можно было отладить правила или сгенерировать новое правило из примера.
Публичный эндпоинт
Заголовок раздела «Публичный эндпоинт»POST|GET https://api.notifly.ru/router/R<token>/<любой/путь>Content-Type: application/json- Поддерживаются методы
POSTиGET. - Тело должно быть JSON-объектом (form-urlencoded и прочие форматы не
разбираются). Пустое тело — например, у
GET— считается пустым объектом{}. - Лимит тела — 64 КиБ. Запрос крупнее не обрабатывается: ответ
{"accepted": true, "matched": 0}, событие в историю не попадает. - Если тело — не валидный JSON, событие сохраняется со статусом
error(invalid JSON payload), правила не оцениваются, ответ —{"accepted": true, "matched": 0, "error": "invalid JSON"}. - Авторизация не нужна — секретом служит сам токен
R…в URL. - Ответ —
200с телом вида{"accepted": true, "matched": 2}, гдеmatched— число сработавших правил. Если роутер выключен —{"accepted": false, "error": "router disabled"}. Неизвестный токен —404, слишком частые запросы —429.
Каждый входящий запрос — это одно событие дневной
квоты (пересылка в несколько каналов отдельно не
тарифицируется). При исчерпании квоты роутер вернёт {"accepted": false, "error": "daily event quota exceeded"} и пришлёт вам уведомление.
Пример: пингуем роутер вручную.
curl -X POST "https://api.notifly.ru/router/RAbcDef123/ci" \ -H "Content-Type: application/json" \ -d '{"status":"failed","message":"build #42 failed"}'# {"accepted":true,"matched":1}Правила маршрутизации
Заголовок раздела «Правила маршрутизации»Правило (WebhookRouterRule) состоит из:
| Поле | JSON-ключ | Смысл |
|---|---|---|
| Канал | appId | в какой канал слать — обязательно; только ваш собственный канал (не расшаренный вам) |
| Имя | name | человекочитаемое название правила |
| Путь | path | паттерн пути URL (см. ниже); пусто или / — любой путь |
| Фильтр | filter | условия над payload/заголовками (см. ниже) |
| Шаблон заголовка | titleTemplate | Go-шаблон для title сообщения |
| Шаблон текста | messageTemplate | Go-шаблон для message сообщения |
| Markdown | markdown | флаг extras.markdown у сообщения (админка всегда ставит true) |
| Приоритет | priority | приоритет создаваемого сообщения (в API по умолчанию 0, в админке — 5) |
| Включено | enabled | участвует ли правило в маршрутизации (по умолчанию true) |
| Позиция | position | порядок в списке правил |
Все включённые правила проверяются независимо — совпавших может быть несколько, и тогда сообщение уйдёт в каждый из их каналов.
Совпадение по пути
Заголовок раздела «Совпадение по пути»Паттерн path использует Gin-синтаксис сегментов:
/литерал— точное совпадение сегмента (/github,/grafana/prod);/:param— один любой сегмент, его значение доступно в шаблоне как{{.Path.Params.param}};/*wildcard— «хвост» пути (только в конце), доступен как{{.Path.Params.wildcard}}.
Правило без пути (пусто или /) совпадение по пути не проверяет вовсе —
оно работает на любом входящем пути, остаётся только фильтр. Лишние
сегменты в запросе не совпадают с литеральным паттерном.
Примеры (отправитель шлёт на /router/R<token>/<путь>):
path правила | Совпадает с путём запроса |
|---|---|
/github | /github |
/grafana/prod | /grafana/prod |
/teams/:team | /teams/backend (тогда {{.Path.Params.team}} = backend) |
/hooks/*rest | /hooks/ci/build/42 (тогда {{.Path.Params.rest}} = ci/build/42) |
/ или пусто | срабатывает на любой запрос (только по фильтру) |
Фильтр: условия и группы
Заголовок раздела «Фильтр: условия и группы»filter — это набор групп условий. Между группами действует логическое
И: запрос проходит фильтр, только если прошли все группы. Пустой фильтр
(null или без групп) совпадает всегда.
Группа (RouterConditionGroup):
{ "type": "and", "target": "payload", "conditions": [{"path": "alert.severity", "op": "eq", "value": "critical"}]}target— откуда брать данные:payload(тело JSON, по умолчанию) илиheaders(HTTP-заголовки запроса).type— логика внутри группы:
type | Срабатывает, когда… |
|---|---|
and | совпали все условия группы |
any | совпало хотя бы одно условие |
and_not | ни одно условие не совпало (NOT any) |
any_not | хотя бы одно условие НЕ совпало (NOT all) |
Условие (RouterCondition) — это path, op и value:
path— dotted-path до поля. Поддерживаются вложенность и индексы массивов:alert.severity,items.0.name.op— оператор:
op | Значение |
|---|---|
eq | поле существует и его строковое значение равно value |
neq | поле отсутствует или его значение не равно value |
contains | значение содержит value (регистронезависимо) |
exists | поле присутствует (value можно не указывать) |
regex | значение поля матчится регулярным выражением value |
gt | числовое значение поля больше value |
lt | числовое значение поля меньше value |
Сравнение eq/neq/contains/regex идёт по строковому представлению
значения (число 5 → "5", true → "true"). Для gt/lt значение поля
приводится к числу.
Для заголовков target: "headers" тот же синтаксис, но path — имя
заголовка в каноническом виде, как его хранит сервер: первая буква и буквы
после дефиса — заглавные, остальные — строчные. GitHub шлёт X-GitHub-Event,
а в условии нужно писать X-Github-Event; Content-Type, User-Agent —
как обычно. Точные имена видно в истории событий.
Шаблоны title и message
Заголовок раздела «Шаблоны title и message»titleTemplate и messageTemplate — это строки на Go text/template.
Если шаблон пуст или статичен (без {{), он используется как есть. Если
после рендера заголовок или текст пусты, подставляется «умный» fallback
из самого payload (берётся первое осмысленное поле вроде title/message/
status, иначе сводка верхнеуровневых полей или путь запроса).
Доступный контекст:
| Выражение | Что даёт |
|---|---|
{{.Payload.status}} | поле верхнего уровня из тела JSON |
{{.Payload.alert.severity}} | вложенное поле |
{{index .Headers "X-Github-Event"}} | значение HTTP-заголовка (имя — в каноническом виде) |
{{.Path.Full}} | полный путь запроса (после токена) |
{{.Path.Params.team}} | значение :param/*wildcard из пути |
{{.ReceivedAt}} | время приёма запроса |
Доступные функции:
| Функция | Пример |
|---|---|
upper | {{upper .Payload.level}} |
lower | {{lower .Payload.level}} |
default | {{default "unknown" .Payload.status}} |
json | {{json .Payload}} — значение как JSON-строка |
get | {{get "alert.text" .Payload}} — доступ по dotted-path |
Если шаблон не разобрался или упал при выполнении — например, upper или
default получили отсутствующее или нестроковое поле, — в сообщение
подставляется сам текст шаблона как есть. Поэтому upper, lower и default
лучше применять к результату get: {{default "—" (get "status" .Payload)}}.
Пример шаблонов:
titleTemplate: Grafana: {{upper .Payload.alert.severity}}messageTemplate: {{.Payload.message}} (статус: {{default "—" (get "status" .Payload)}})Текст сообщений роутера всегда помечается как Markdown
(client::display). В extras сообщения кладутся исходные
payload, path и headers запроса, отправитель — имя роутера. На такие
сообщения действуют эскалация, тихие
часы и окна обслуживания
канала — как на любые другие.
В диалоге правила в админке кнопка «Из шаблона…» предзаполняет форму готовыми рецептами: «GitHub Actions: упавший workflow», «Алерт Grafana» (только firing, title/message из payload) и «Оставить только title/message».
История событий
Заголовок раздела «История событий»Каждый входящий запрос сохраняется как событие (WebhookRouterEvent) с
полным контекстом: method, path, remoteIp, все headers, сырое тело
payloadRaw, флаг payloadTruncated, список сработавших правил
matchedRuleIds, созданных сообщений createdMessageIds и статус:
matched— совпало хотя бы одно правило;unmatched— ни одно правило не сработало;error— тело не распарсилось как JSON.
Историю можно фильтровать по статусу (status), подстроке (q — ищет по
телу запроса, без учёта регистра, внутри выбранной страницы), диапазону
времени (from/to в RFC3339) и листать курсором (cursor — id
последнего полученного события, limit — по умолчанию 50, максимум 100).
События хранятся 30 дней.
Подбор правила из события (AI)
Заголовок раздела «Подбор правила из события (AI)»Самый быстрый способ написать правило — взять реальный пример из истории.
Эндпоинт POST /webhook-router/rule/from-event/:eid по сохранённому
событию возвращает черновик правила:
{ "path": "/grafana", "suggested": [ { "type": "and", "target": "payload", "conditions": [{"path": "alert.severity", "op": "eq", "value": "critical"}] } ], "titleTemplate": "Alert: {{.Payload.status}}", "messageTemplate": "", "paths": ["status", "alert", "alert.severity", "message"]}- Без тела (или с пустым
prompt) работает эвристика без обращения к ИИ: берёт «дискриминирующие» поля (event/type/action/status/severity…) и полезные заголовки (X-…-Event), плюс отдаётpaths— все dotted-path-и из payload, удобные для ручной сборки условий. - С телом
{"prompt": "слать только критичные алерты в #ops"}подключается ИИ-ассистент: он понимает текстовое пожелание и собирает условия и шаблоны под него (списывается один запрос из дневной квоты ИИ-помощниковaiRequestsPerDay, см. квоты; при исчерпании —429). Если ИИ не настроен или вернул ошибку — автоматически откатывается к эвристике.
Результат — это черновик: проверьте и при необходимости поправьте его
перед сохранением через POST /webhook-router/rule.
Примеры маршрутизации
Заголовок раздела «Примеры маршрутизации»Grafana → канал «Критичные» по severity
Заголовок раздела «Grafana → канал «Критичные» по severity»Webhook contact point Grafana шлёт status, title, message и общие
метки группы в commonLabels. Правило: path = /grafana, фильтр — одна группа
and/payload с условиями status eq firing и commonLabels.severity eq critical, шаблоны {{get "title" .Payload}} и {{get "message" .Payload}},
канал — «Критичные».
curl -X POST "https://api.notifly.ru/router/R<token>/grafana" \ -H "Content-Type: application/json" \ -d '{"status":"firing","title":"[FIRING:1] HighMemory","commonLabels":{"severity":"critical"},"message":"OOM on db-1"}'GitHub → разные каналы по типу события
Заголовок раздела «GitHub → разные каналы по типу события»GitHub кладёт тип события в заголовок X-GitHub-Event (в условии и шаблоне —
X-Github-Event). В настройках вебхука GitHub выберите Content type
application/json. Заводим два правила с одним путём /github, но разными
фильтрами на заголовок:
- Правило «Push»: группа
and/headers, условиеX-Github-Event eq push, канал «Деплои». - Правило «Issues»: группа
and/headers, условиеX-Github-Event eq issues, канал «Задачи».
titleTemplate: GitHub {{index .Headers "X-Github-Event"}}messageTemplate: {{get "repository.full_name" .Payload}} {{get "action" .Payload}}REST API
Заголовок раздела «REST API»Управление роутером и правилами — по API-токену
(M…): для чтения достаточно токена только на чтение, для изменения роутера и
правил нужен токен с правом записи. Все эндпоинты ниже — относительно базового
URL (https://api.notifly.ru), заголовок авторизации — X-Notifly-Key. Токены
канала (в том числе расшаренного) сюда не пускаются — 403.
Публичный приём вебхуков (/router/...) авторизации не требует — секрет
в самом токене.
| Метод | Путь | Назначение |
|---|---|---|
GET | /webhook-router | получить роутер (создаётся при первом обращении), вместе с правилами |
PUT | /webhook-router | обновить роутер (name, enabled) |
POST | /webhook-router/rotate-token | сгенерировать новый токен R… (старый URL перестанет работать) |
GET | /webhook-router/rule | список правил |
POST | /webhook-router/rule | создать правило |
PUT | /webhook-router/rule/:rid | обновить правило |
DELETE | /webhook-router/rule/:rid | удалить правило |
GET | /webhook-router/event | история событий (q, status, from, to, cursor, limit) |
GET | /webhook-router/event/:eid | одно событие целиком (заголовки + тело) |
DELETE | /webhook-router/event/:eid | удалить событие из истории |
POST | /webhook-router/rule/from-event/:eid | сгенерировать черновик правила из события (эвристика или ИИ по prompt) |
GET | /webhook-router/message | сообщения, созданные роутером (q — фильтр по тексту) |
DELETE | /webhook-router/message/:mid | удалить созданное роутером сообщение |
POST|GET | /router/:token/*path | публичный приём вебхука (без авторизации) |
Пример: создать правило через API.
curl -X POST "$NOTIFLY_URL/webhook-router/rule" \ -H "X-Notifly-Key: <api-token>" \ -H "Content-Type: application/json" \ -d '{ "appId": 12, "name": "Grafana critical", "path": "/grafana", "filter": {"groups": [ {"type": "and", "target": "payload", "conditions": [ {"path": "status", "op": "eq", "value": "firing"}, {"path": "commonLabels.severity", "op": "eq", "value": "critical"} ]} ]}, "titleTemplate": "{{get \"title\" .Payload}}", "messageTemplate": "{{get \"message\" .Payload}}", "markdown": true, "priority": 8 }'См. также: push-сообщения, каналы и токены, email-инбокс (приём писем по тому же принципу), Alertmanager, Grafana и Zabbix, квоты и тарифы.