Перейти к содержанию

Правила уведомлений

Какие события 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, по порядку:

  1. Форма — JSON Schema спецификации (копия $defs.notificationRuleSpec схемы пакетов); ошибка — код invalid_spec. При ошибках формы остальные шаги не выполняются.
  2. Типы событий — по снимку каталога событий ядра в сервисе: on.type существует (префикс совпадает хотя бы с одним типом), каждый тип close.on существует; иначе unknown_event_type.
  3. Пути условий и шаблонов: корень допустим, payload.<поле> есть в схеме каждого типа, на который правило срабатывает, event.<поле> — в конверте, task.<поле> — в проекции задачи, и корень task допустим, только если у события есть задача; иначе unknown_field.
  4. Условие — грамматика правил ядра (глубина ≤ 16, узлов ≤ 256, документ ≤ 16 КиБ); иначе invalid_condition.
  5. Согласованность — 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}}"}}}'
{"valid": true, "specHash": "9c1f…", "changed": true}

Токен — обмен PAT или client credentials в IAM с audience: notification-service и scope notifications:admin (см. Токены, audiences, scopes).

Применение пакетом

Правила — объекты пакета в папке notification-rules/. Установщик tools/cp_packages.py применяет их к сервису уведомлений, а не к ядру и последними — после всех видов ядра:

  1. до первой записи — :validate всех правил установки; отказ сервиса останавливает установку целиком;
  2. POST только тех правил, где :validate ответил changed: true; остальные — «без изменений»;
  3. 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

См. также