Правила уведомлений¶
Какие события Control Plane становятся уведомлениями людям — кому, с каким
текстом, ссылками и кнопками решения — описывают правила уведомлений: вид
каталога NotificationRule. Их хранит, проверяет и исполняет
notification-service, а в git они живут в пакете, как типы задач и правила
вывода работы. Статья для авторов пакетов и администраторов установки.
Обоснование — TAI-ADR-0053 (п.4) и ADR-0005 сервиса уведомлений.
Зачем правила¶
Новый вид уведомления о событии ядра — правка пакета, а не выпуск сервиса. Каналы, настройки получателя, обязательные правила организации и группы уже данные сервиса (см. Уведомления); правило добавляет недостающее — что превращается в уведомление.
flowchart LR
P["Пакет<br/>notification-rules/*.yaml"] -->|cp_packages apply| NS["notification-service<br/>notification_rules"]
CP["Control Plane<br/>журнал событий"] -->|"фильтр = on.type ∪ close.on"| C["Потребитель событий"]
NS --> C
C -->|"правило: on.when → recipient → шаблон"| N["Уведомление"]
N --> W["web / email / telegram"]
C -->|"close.on"| X["Закрыть кнопки<br/>уведомления с тем же ключом"]
Без правил сервис событий не читает
Встроенной таблицы «событие → уведомление» в сервисе нет. Пока в tenant'е нет
ни одного включённого правила, потребитель событий не запускается. Прежнее
поведение — три правила пакета notify (ниже): применяйте пакет сразу после
установки сервиса.
Пример¶
# yaml-language-server: $schema=../../schema/v1/object.schema.json
apiVersion: taimen.ai/v1
kind: NotificationRule
key: approval-requested
spec:
description: >-
Назначенному решающему — запрос решения с кнопками; исход решения
закрывает кнопки.
"on": {type: approval.requested}
recipient: {kind: assigned}
notification:
type: approval.requested
title: "Нужно решение: {{task.publicId}} {{task.title}}"
body: |-
Работа: {{task.publicId}} {{task.title}}
Запрашивает: {{payload.requestedBy.displayName}}
Комментарий: {{payload.comment}}
links:
- {label: Открыть задачу, url: "${TASK_URL_BASE}/{{task.publicId}}"}
actions: [approvalDecide]
dedupKeyTemplate: "control-plane:approval:{{event.entityId}}"
close:
"on": [approval.approved, approval.rejected, approval.cancelled]
Ключ on — в кавычках
Загрузчики YAML 1.1 (в том числе тот, которым установщик читает пакеты) читают
голый on как true, и спецификация теряет обязательное поле. Пишите "on":.
Спецификация¶
| Поле | Обязательно | Смысл |
|---|---|---|
on.type |
да | Тип события каталога ядра (approval.requested) или префикс (approval.*) |
on.when |
нет | Условие грамматики правил ядра над корнями payload, event, task; по умолчанию true |
recipient |
да | Кому (см. Адресат) |
notification.type |
да | Тип уведомления (^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$): по нему работают настройки получателя и обязательные правила организации |
notification.title |
да | Шаблон заголовка, до 300 символов |
notification.body |
нет | Шаблон текста, до 4000 символов |
notification.links |
нет | До 5 ссылок {label, url}, оба — шаблоны |
notification.actions |
нет | [approvalDecide] — кнопки «Одобрить» / «Отклонить» решения |
dedupKeyTemplate |
нет | Шаблон ключа дедупликации; по умолчанию rule:<key>:event:{{event.id}} |
close.on |
нет | Типы событий, которые закрывают кнопки уведомления с тем же ключом |
close.outcome |
нет | Шаблон исхода вместо кнопок; по умолчанию — последний сегмент типа закрывающего события (approved, rejected, cancelled) |
status |
нет | enabled (по умолчанию) или disabled — правило хранится, но не исполняется |
description |
нет | Текст для людей, до 2000 символов |
Неизвестное поле на любом уровне — отказ проверки. Ключ правила —
^[a-z0-9][a-z0-9._-]*$, до 128 символов.
Адресат¶
recipient.kind |
Кто получает |
|---|---|
assigned |
Назначенный решающий: principal по пути ref (по умолчанию payload.assignedPrincipalId); путь пуст — держатели роли payload.requiredRoleId в workspace (workspace, по умолчанию payload.workspaceId, иначе event.workspaceId) |
role |
Держатели роли, id которой по пути ref, в workspace по пути workspace (по умолчанию event.workspaceId) |
taskOwner |
Владелец задачи события (task.ownerId) |
taskAssignee |
Исполнитель задачи события (task.assigneeId) |
principal |
Конкретный principal: ref — UUID. ${ПЕРЕМЕННАЯ} установки подставляет установщик пакетов; сервис хранит и принимает только UUID |
fallback — taskOwner, taskAssignee или none (по умолчанию): второй
адресат, если первый пуст. Адресата нет и после fallback — уведомление не
создаётся, в журнал сервиса пишутся ключ правила и id события. Роль без
держателей — запись в журнале доставки без адресатов. Principal или роль,
неизвестные ядру, — событие для этого правила пропускается.
Шаблоны и корни¶
Шаблон — строка с плейсхолдерами {{ путь }}, путь — корень(.сегмент)*.
Условий, циклов, вызовов и фильтров в шаблонах нет: разные тексты — разные
правила с разными on.when.
| Корень | Что это |
|---|---|
payload |
Тело события по схеме каталога событий ядра |
event |
Конверт события: id, type, entityType, entityId, workspaceId, actorId, occurredAt, … |
task |
Проекция задачи ядра (id, publicId, title, status, ownerId, assigneeId, typeKey, customFields, …): задача сущности события или payload.taskId. Читается, только если правило обращается к корню task |
Путь, который заканчивается displayName после идентификатора principal'а
({{payload.requestedBy.displayName}}, {{event.actorId.displayName}},
{{task.ownerId.displayName}}), подставляет имя principal'а из каталога ядра.
Правила подстановки:
- отсутствующее значение — пустая строка; скаляр — его текст; объект и список не подставляются;
- в
titleпереводы строк сворачиваются в пробел; пустой после подстановки заголовок заменяетсяnotification.type; - строка
body, в которой все плейсхолдеры дали пусто, опускается целиком — так необязательные строки («Комментарий: …») исчезают сами; - ссылка опускается, если хоть один плейсхолдер её
urlдал пусто или результат не абсолютныйhttp(s)URL. Базу адреса интерфейса пишите переменной установки (${TASK_URL_BASE}), её подставляет установщик пакетов; - ключ дедупликации, в котором плейсхолдер дал пусто, или длиннее 200 символов — правило для этого события пропускается с записью в журнал: ключ без части склеил бы уведомления разных событий;
- секретов шаблон не видит: корни — только поля события и проекции задачи.
approvalDecide строит действия approve и reject с data = {kind:
approval.decide, approvalId, decision}, где approvalId — event.entityId
события approval.*. Их исполняет канал с кнопками (см.
Telegram).
Исполнение¶
- Фильтр потребителя — объединение
on.typeиclose.onвключённых правил; префиксx.*передаётся ядру какx.. Набор правил изменился — потребитель перезапускается с тем же курсором на новом фильтре в течение одного цикла опроса (NS_EVENTS_POLL_SECONDS, по умолчанию 30 с); применение правила через API будит потребитель своего процесса сразу. - На событие — все подходящие правила по порядку ключей (
on.typeсовпадает,on.whenистинно); каждое даёт не больше одного уведомления. Ошибка одного правила пишется в журнал с ключом правила и не мешает остальным. Недоступность ядра — отказ обработки события целиком, потребитель повторит его. - Закрытие — событие из
close.on: сервис рендеритdedupKeyTemplateнад закрывающим событием и закрывает действия уведомления с этим ключом:actionsOutcome = {status, by, channel, at}. Побеждает первое закрытие. Ключ поэтому должен выводиться из того, что общее у открывающего и закрывающего событий (для решений —event.entityId, id approval). - Версия — событие обрабатывается версиями, действующими на момент обработки. Уже созданные уведомления не меняются ни при новой версии, ни при выводе правила из оборота.
Уведомление создаёт сам сервис своим service account'ом, как любое другое, — с каналами, настройками получателя и журналом доставки (см. Уведомления).
Версии¶
Правила хранятся в таблице notification_rules по tenant'у: ключ, версия (1, 2,
…), нормализованная спецификация, specHash (sha256 канонического JSON), состояние
active | superseded | retired, автор и время.
- Версии неизменяемы.
POSTспецификации с тем же хэшем, что у действующей версии, ничего не создаёт (200) — повторное применение пакета идемпотентно. Другой хэш — новая версияactive, прежняяsuperseded(201). :retireпереводит действующую версию вretired;POSTв выведенный ключ заводит следующую версию и снова делает её действующей.status: disabled— часть спецификации и хэша: версия действует, но не исполняется.
Проверка спецификации¶
Одна проверка для POST и :validate, по порядку:
- Форма — JSON Schema спецификации (копия
$defs.notificationRuleSpecсхемы пакетов); ошибка — кодinvalid_spec. При ошибках формы остальные шаги не выполняются. - Типы событий — по снимку каталога событий ядра в сервисе:
on.typeсуществует (префикс совпадает хотя бы с одним типом), каждый типclose.onсуществует; иначеunknown_event_type. - Пути условий и шаблонов: корень допустим,
payload.<поле>есть в схеме каждого типа, на который правило срабатывает,event.<поле>— в конверте,task.<поле>— в проекции задачи, и кореньtaskдопустим, только если у события есть задача; иначеunknown_field. - Условие — грамматика правил ядра (глубина ≤ 16, узлов ≤ 256, документ ≤ 16
КиБ); иначе
invalid_condition. - Согласованность —
approvalDecideтолько для событий сущностиapproval;principal—refзадан и это UUID;role—refзадан; иначеinvalid_rule.
Отказ — 422 invalid_notification_rule, в details.errors — все найденные
ошибки {path, code, message}, path — JSON Pointer в спецификации.
{
"error": {
"code": "invalid_notification_rule",
"message": "…",
"details": {"errors": [
{"path": "/on/type", "code": "unknown_event_type", "message": "…"}
]}
}
}
API сервиса¶
Все маршруты — https://platform.example.com/notify/api/v1/… за периметром (см.
Периметр и TLS), только scope
notifications:admin; отправители с notifications:send их не видят.
| Метод и путь | Что делает |
|---|---|
GET /notification-rules?key=&includeRetired=&limit=&cursor= |
Действующие версии правил tenant'а: {items: [{key, version, spec, specHash, state, createdBy, createdAt}], nextCursor}; по умолчанию 100, не больше 500 |
POST /notification-rules — {key, spec} |
Применить: 201 — новая версия, 200 — спецификация не изменилась; 422 invalid_notification_rule |
POST /notification-rules:validate — {key, spec} |
Та же проверка без записи: 200 {valid: true, specHash, changed} или 422 |
POST /notification-rules/{key}:retire |
Вывести из оборота: 200 (повтор — тот же ответ), 404 — ключа нет |
curl -sS -X POST https://platform.example.com/notify/api/v1/notification-rules:validate \
-H "Authorization: Bearer $NOTIFY_TOKEN" -H 'Content-Type: application/json' \
-d '{"key": "task-verified", "spec": {
"on": {"type": "task.verified"},
"recipient": {"kind": "taskOwner", "fallback": "taskAssignee"},
"notification": {"type": "task.verified",
"title": "Принято: {{task.publicId}} {{task.title}}"}}}'
Токен — обмен PAT или client credentials в IAM с audience:
notification-service и scope notifications:admin (см. Токены, audiences,
scopes).
Применение пакетом¶
Правила — объекты пакета в папке notification-rules/. Установщик
tools/cp_packages.py применяет их к сервису уведомлений, а не к ядру и
последними — после всех видов ядра:
- до первой записи —
:validateвсех правил установки; отказ сервиса останавливает установку целиком; POSTтолько тех правил, где:validateответилchanged: true; остальные — «без изменений»;retire.NotificationRuleфайла установки —:retire; отправленные уведомления остаются.
Адрес сервиса — переменная установки NOTIFICATION_SERVICE_URL (в .env или
окружении), например https://platform.example.com/notify. Токен — переменная
NOTIFY_TOKEN (access token audience notification-service, scope
notifications:admin) или обмен того же IAM credential, которым установщик ходит
в ядро, на этот audience (PAT должен допускать audience в потолке). Без адреса
сервиса apply пропускает правила уведомлений с предупреждением.
export CP_TOKEN=<access-token audience control-plane>
export NOTIFY_TOKEN=<access-token audience notification-service>
python3 tools/cp_packages.py apply --install deploy/<окружение>/packages.yaml \
--server https://platform.example.com
NotificationRule/approval-requested: v1 без изменений
NotificationRule/task-verified: опубликована v1 (нет в сервисе)
Выгрузка действующей версии в пакет (ядро не нужно):
python3 tools/cp_packages.py export --kind NotificationRule --key task-verified \
--package packages/<пакет>
Правила пакета notify¶
Пакет notify несёт три правила — поведение сервиса по умолчанию. Им нужна
переменная установки TASK_URL_BASE — база ссылки «Открыть задачу», к которой
приклеивается /<publicId>.
| Ключ | Событие и условие | Адресат | Что делает |
|---|---|---|---|
approval-requested |
approval.requested |
assigned |
Запрос решения с кнопками «Одобрить» / «Отклонить»; approval.approved, approval.rejected, approval.cancelled закрывают кнопки (ключ control-plane:approval:<approval-id>) |
verification-failed |
task.verification_failed, payload.blocked ≠ true |
taskOwner, иначе taskAssignee |
Проверка приёмки не пройдена, задача вернулась в работу |
verification-blocked |
task.verification_failed, payload.blocked = true |
taskOwner, иначе taskAssignee |
Проверка не пройдена несколько раз подряд, задача ждёт человека |
Два правила на task.verification_failed — потому что в шаблонах нет условий:
разные тексты задаются разными правилами с противоположными on.when. Ключи
дедупликации совпадают с прежними ключами сервиса, поэтому переход на правила не
дублирует уже созданные уведомления.
Своё уведомление добавляется новым файлом в своём пакете (например, «задача
принята» на task.verified владельцу) и apply — без выпуска сервиса.
Типичные проблемы¶
| Симптом | Причина | Что делать |
|---|---|---|
| Уведомлений о событиях нет совсем | В tenant'е нет включённых правил — потребитель не запущен | Применить пакет notify (NOTIFICATION_SERVICE_URL и токен заданы) |
apply пишет «NotificationRule не применены: не задан сервис уведомлений» |
Нет NOTIFICATION_SERVICE_URL или токена |
Задать переменную и NOTIFY_TOKEN (или PAT с audience notification-service) |
422 invalid_notification_rule, unknown_event_type |
Тип события не из каталога ядра, известного сервису | Проверить тип; новый тип события появляется у сервиса с его обновлением |
invalid_spec на /on |
on без кавычек превратился в true |
Писать "on": |
unknown_field на пути task.… |
У события нет задачи или поле не из проекции задачи | Убрать корень task или сменить событие |
| Ссылки «Открыть задачу» нет | TASK_URL_BASE пуст или результат не абсолютный URL |
Задать переменную установки и применить пакет |
| Кнопки не закрылись после решения | dedupKeyTemplate открывающего и закрывающего событий дают разные ключи |
Строить ключ из event.entityId |
403 на /notification-rules |
Токен без scope notifications:admin |
Выпустить токен с этим scope |
См. также¶
- Уведомления — каналы, адресаты, журнал доставки, потребитель событий.
- Telegram — решения кнопками.
- Пакеты каталога — вид
NotificationRule. - События — каталог событий ядра.
- Цели, приёмка и evidence —
task.verification_failed. - Approvals