Документация

Архитектура шаблонов уведомлений

Версия v2.3.1 · Обновлено

Архитектура шаблонов уведомлений#

Статус: предложено Связанная задача: #65 — поддержка настраиваемых шаблонов уведомлений

Назначение#

Добавить в IncidentRelay единую архитектуру формирования уведомлений, не привязывая шаблоны к конкретным реализациям отправщиков уведомлений.

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

Основные принципы#

  • Шаблоны уведомлений — отдельная доменная сущность, а не часть NotificationChannel.config.
  • Каналы выбирают шаблоны, но не владеют реализацией рендеринга.
  • Все внешние отправщики используют единый нормализованный контекст уведомления и общий рендерер.
  • Шаблоны настраивают только содержимое уведомления. Специфичные для провайдера элементы управления, callback-вызовы, метаданные сообщения, кнопки ACK/Resolve/Shelve и отслеживание доставки остаются ответственностью отправщика.
  • Язык шаблонов намеренно ограничен и детерминирован. Это не универсальное окружение Jinja.
  • Если пользовательский шаблон не выбран, по умолчанию сохраняется встроенное поведение уведомлений.
  • Архитектура должна позволять в будущем добавлять правила выбора шаблонов без переработки отправщиков.
  • Единая система шаблонов — единственный поддерживаемый механизм настройки. Устаревшие форматы шаблонов отдельных каналов не входят в целевую архитектуру.

Доменная модель#

NotificationTemplate#

Начальная модель:

NotificationTemplate
- id
- team_id
- name
- title_template
- body_template
- format
- enabled
- created_at
- updated_at

Рекомендуемые ограничения:

  • шаблоны принадлежат команде;
  • имена уникальны в пределах команды;
  • отключённые шаблоны нельзя назначать новым каналам;
  • для шаблонов используется физическое удаление;
  • удаление запрещено, пока на шаблон ссылается хотя бы один активный канал;
  • внешний ключ между каналом и шаблоном использует RESTRICT;
  • формат шаблона проверяется по известному набору значений.

Начальные форматы:

text
markdown
html

Шаблоны не привязаны к транспорту. Не следует создавать отдельные модели вроде SlackTemplate, TelegramTemplate или EmailTemplate.

Состояние enabled#

enabled — состояние конфигурации, а не механизм жизненного цикла или истории.

Рекомендуемое поведение:

enabled = true
→ шаблон можно выбирать для новых назначений каналам

enabled = false
→ шаблон сохраняется, но скрыт или недоступен для новых назначений

Если канал уже ссылается на шаблон, который позднее отключили, предпочтительное поведение такое:

существующее назначение каналу
→ продолжает использовать шаблон

Это исключает незаметное изменение текста уведомлений, когда администратор временно отключает шаблон.

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

Удаление#

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

Поведение при удалении:

На шаблон ссылается один или несколько активных каналов
→ DELETE отклоняется с 409 Conflict

На шаблон никто не ссылается
→ физическое удаление

Внешний ключ канала должен использовать RESTRICT.

Не используйте SET NULL: удаление шаблона не должно незаметно переключать существующие каналы на встроенный вариант по умолчанию.

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

NotificationChannel#

Канал получает необязательную ссылку на шаблон:

notification_template_id = nullable

Значение NULL означает, что IncidentRelay использует для этого канала встроенный рендеринг по умолчанию.

Один шаблон может переиспользоваться несколькими каналами.

Пример:

Команда
 ├── Шаблон A
 ├── Шаблон B
 └── Шаблон C

Канал 1 -> Шаблон A
Канал 2 -> Шаблон A
Канал 3 -> Шаблон C
Канал 4 -> встроенный шаблон

Поток рендеринга#

Конвейер уведомлений должен иметь следующую структуру:

Алерт / событие уведомления
        |
        v
build_notification_context()
        |
        v
resolve_notification_template()
        |
        v
render_notification()
        |
        v
RenderedNotification
        |
        v
Отправщик
        |
        +--> Slack
        +--> Telegram
        +--> Mattermost
        +--> Email
        +--> Discord
        +--> Teams
        +--> другие внешние транспорты

После появления общего рендерера реализации отправщиков не должны самостоятельно заново составлять основной текст алерта.

Контекст уведомления#

Шаблоны должны получать нормализованный структурированный контекст, а не большое плоское пространство имён.

Начальные пространства имён:

alert.*
event.*
team.*
service.*
assignee.*
labels.*
annotations.*

Примеры:

{{ alert.title }}
{{ alert.priority }}
{{ alert.severity }}
{{ alert.status }}
{{ alert.source }}
{{ alert.url }}

{{ event.type }}

{{ team.name }}
{{ team.slug }}

{{ service.name }}
{{ service.slug }}
{{ service.environment }}
{{ service.criticality }}
{{ service.tier }}

{{ assignee.username }}
{{ assignee.display_name }}

{{ labels.instance }}
{{ labels.alertname }}

{{ annotations.description }}
{{ annotations.runbook_url }}

Точный набор поддерживаемых полей должен быть определён централизованно и задокументирован.

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

Нельзя передавать в рендерер шаблонов необработанные объекты Python.

Синтаксис шаблонов#

Везде используется единый синтаксис:

{{ alert.title }}
{{ labels.instance }}

Первая реализация должна поддерживать только подстановку переменных.

Изначально не следует поддерживать:

  • произвольный код Python;
  • выполнение Jinja;
  • вызовы функций;
  • циклы;
  • условия;
  • выражения;
  • динамический обход атрибутов вне разрешённого контекста;
  • доступ к файловой системе или переменным окружения.

Неизвестные плейсхолдеры должны приводить к детерминированному результату проверки или рендеринга, а не к выполнению произвольных действий.

Проверять шаблон следует при сохранении, а не только во время доставки уведомления.

Единая система шаблонов#

Все настраиваемые уведомления должны использовать новую единую систему шаблонов.

Не следует сохранять отдельные синтаксисы или пути рендеринга для разных каналов.

В частности:

  • Email должен использовать ту же сущность NotificationTemplate;
  • Email должен использовать тот же синтаксис плейсхолдеров {{ ... }};
  • Slack, Telegram, Mattermost, Discord, Teams, Email и будущие отправщики должны потреблять одну абстракцию сформированного уведомления;
  • шаблоны отдельных отправщиков, хранящиеся непосредственно в конфигурации канала, не входят в целевой дизайн.

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

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

Долгосрочного fallback на старый формат шаблонов Email во время выполнения быть не должно.

Заголовок и тело#

Шаблоны должны содержать отдельные поля заголовка и тела:

title_template
body_template

Это позволит избежать переработки под конкретный транспорт в будущем.

Примеры использования:

  • Email: тема и тело письма;
  • браузерное или мобильное уведомление: заголовок и тело;
  • Slack/Teams: заголовок и основной текст;
  • Telegram/Mattermost: тело, при необходимости включающее заголовок.

Отправщик без отдельного понятия заголовка может объединить сформированные заголовок и тело.

Абстракция сформированного уведомления#

Общий рендерер должен возвращать транспортно-независимый объект, например:

RenderedNotification
- title
- body
- format

Затем отправщик адаптирует этот результат к API провайдера.

Специфичное для провайдера форматирование, структура payload, блоки кнопок, идентификаторы callback-вызовов и отслеживание сообщений не входят в NotificationTemplate.

Системные элементы управления#

Шаблоны настраивают содержимое, но не должны управлять рабочей логикой IncidentRelay.

За пределами пользовательских шаблонов остаются:

  • элементы подтверждения (Acknowledge);
  • элементы разрешения (Resolve);
  • элементы эскалации;
  • callback payload;
  • идентификаторы блоков и действий Slack;
  • callback-данные Telegram;
  • метаданные действий Mattermost;
  • идентификаторы сообщений;
  • метаданные доставки и обновления;
  • внутренние идентификаторы алертов, необходимые для действий провайдера.

Концептуально:

Сформированное пользовательское содержимое
        +
Системные элементы управления IncidentRelay
        |
        v
Payload провайдера

Так пользовательский шаблон не сможет случайно нарушить работу ACK/Resolve/Shelve или обновление сообщения.

Выбор шаблона#

Логика поиска шаблона должна находиться за выделенным резолвером:

resolve_notification_template(
    channel,
    event_type,
    ...
)

Начальный алгоритм выбора:

channel.notification_template_id
        |
        +-- задан --> выбранный NotificationTemplate
        |
        +-- null ---> встроенный шаблон по умолчанию

Код отправщика должен вызывать резолвер, а не обращаться непосредственно к channel.notification_template_id.

Уже назначенный каналу отключённый шаблон остаётся доступен через резолвер.

В будущем резолвер сможет поддерживать:

переопределение политики/события
        |
шаблон канала
        |
шаблон команды по умолчанию
        |
системный шаблон по умолчанию

без изменения реализаций отправщиков.

Типы событий#

В первой версии не нужны отдельные шаблоны для каждого типа события.

Тем не менее контекст уведомления должен предоставлять тип события, например:

triggered
acknowledged
resolved
escalated
reminder

Это сохраняет возможность назначать шаблоны для отдельных типов событий в будущем.

Выбор шаблона по типу события намеренно отложен.

URL-адреса#

Общий рендерер не должен искать URL в произвольном сформированном тексте и автоматически превращать их в ссылки.

Ссылки следует задавать в шаблоне согласно его формату.

Пример Markdown:

[Runbook]({{ annotations.runbook_url }})

Пример HTML:

<a href="{{ annotations.runbook_url }}">Runbook</a>

В текстовых шаблонах просто выводится значение URL.

Рендерер должен применять экранирование, соответствующее выбранному формату и уровню адаптации провайдера.

Совместимость форматов и транспортов#

Шаблоны описывают формат содержимого, а не транспорт.

Начальную совместимость можно представить в виде возможностей:

text      -> все транспорты с поддержкой текста
markdown  -> Slack, Telegram, Mattermost, Discord, Teams, где поддерживается
html      -> Email

Точная адаптация к провайдеру относится к реализации отправщика.

Канал должен отклонять шаблон в формате, который его отправщик не может безопасно обработать.

Предпросмотр#

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

Не следует создавать независимый рендерер на JavaScript.

Поток:

шаблон
   +
пример контекста уведомления
   |
   v
render_notification()
   |
   v
предпросмотр

Так поведение предпросмотра не будет расходиться с настоящими уведомлениями.

Для начального предпросмотра можно использовать стабильный демонстрационный контекст. В более поздней версии можно будет просматривать шаблон на реальном алерте.

Начальный интерфейс#

Полноценный визуальный конструктор шаблонов не требуется.

Начальный интерфейс управления шаблонами:

Notification Templates

Name
Enabled
Format
Title template
Body template

[Preview]
[Save]
[Delete]

Удаление должно быть недоступно или возвращать понятное сообщение о конфликте, если на шаблон ссылаются активные каналы.

Настройка канала:

Notification template
[Default                     v]

Отключённые шаблоны не должны отображаться среди вариантов для новых назначений.

Если каналу уже назначен отключённый шаблон, интерфейс по-прежнему должен явно показывать это назначение.

Интерфейс канала также может содержать быстрый переход к созданию шаблона, однако результатом всё равно будет полноценная сущность NotificationTemplate.

Объём первой реализации#

Первая реализация должна включать:

  • модель NotificationTemplate;
  • CRUD в пределах команды;
  • состояние enabled;
  • физическое удаление с защитой от активных ссылок;
  • внешний ключ RESTRICT от каналов к шаблонам;
  • необязательное назначение шаблона каналам уведомлений;
  • нормализованный контекст уведомления;
  • безопасную проверку и подстановку плейсхолдеров;
  • единый синтаксис {{ ... }};
  • форматы text, markdown и html;
  • рендеринг заголовка и тела;
  • общий резолвер шаблонов;
  • общую абстракцию сформированного уведомления;
  • fallback на встроенный вариант по умолчанию;
  • предпросмотр шаблона через серверный рендерер;
  • разовую миграцию или преобразование существующих данных шаблонов Email отдельных каналов, если это потребуется;
  • отсутствие fallback на устаревшие шаблоны Email во время выполнения;
  • адаптацию отправщиков без предоставления шаблонам доступа к системным элементам управления;
  • тесты безопасности рендерера, выбора шаблонов, включения и отключения, защиты от удаления, совместимости форматов и fallback по умолчанию;
  • документацию API/OpenAPI.

Явно отложено#

В первую реализацию не следует включать перечисленные ниже возможности, если только они не понадобятся для выполнения другого требования:

  • глобальную библиотеку шаблонов;
  • визуальный drag-and-drop-конструктор шаблонов;
  • наследование шаблонов;
  • версионирование и историю шаблонов;
  • условные выражения;
  • циклы;
  • функции;
  • произвольную поддержку Jinja;
  • переопределение шаблонов на уровне политик;
  • отдельный шаблон для каждого типа события уведомления;
  • значения по умолчанию на уровне системы или группы;
  • импорт и экспорт шаблонов;
  • автоматическое распознавание URL;
  • модели шаблонов для отдельных транспортов.

Эти возможности следует добавлять через существующие границы резолвера, контекста и рендеринга, не меняя API отправщиков.

Архитектурные инварианты#

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

  • Отправщики не владеют хранилищем шаблонов.
  • Шаблоны не владеют элементами управления, специфичными для провайдера.
  • Пользовательские шаблоны не могут выполнять произвольный код.
  • Рендеринг работает только с нормализованным контекстом из разрешённого списка.
  • Встроенные уведомления по умолчанию продолжают работать без настройки шаблонов.
  • Переиспользуемый шаблон можно назначить нескольким каналам.
  • Выбор шаблонов централизован.
  • Предпросмотр и настоящая доставка используют один рендерер.
  • Адаптация к конкретному провайдеру выполняется после общего рендеринга.
  • В будущем выбор шаблона на уровне политики, события, команды или системы можно добавить без переработки отправщиков.
  • Шаблон удаляется физически, а удаление блокируется при наличии активных ссылок.
  • Отключённые шаблоны остаются действительными для существующих назначений, но их нельзя назначать заново.
  • Все настраиваемые уведомления используют единую модель и синтаксис шаблонов.
  • При доставке во время выполнения отсутствует устаревший путь рендеринга шаблонов Email.

Будущая модель выбора#

Возможное дальнейшее развитие:

Событие уведомления
        |
        v
Резолвер шаблонов
        |
        +--> переопределение события/политики
        |
        +--> шаблон канала
        |
        +--> шаблон команды по умолчанию
        |
        +--> системный шаблон по умолчанию
        |
        v
Контекст уведомления
        |
        v
Рендерер
        |
        v
RenderedNotification
        |
        +--> адаптер Slack + элементы управления IncidentRelay
        +--> адаптер Telegram + элементы управления IncidentRelay
        +--> адаптер Mattermost + элементы управления IncidentRelay
        +--> адаптер Email
        +--> другие адаптеры

В первой реализации не нужно предоставлять все эти уровни выбора. Важно, чтобы её границы не препятствовали их появлению в дальнейшем.

Итоговое решение#

Настраиваемые уведомления IncidentRelay следует реализовать как переиспользуемую подсистему рендеринга, а не как отдельные строки шаблонов, независимо встроенные в каждого отправщика.

Отдельная сущность NotificationTemplate, нормализованный контекст из разрешённого списка, ограниченный рендерер плейсхолдеров, централизованный резолвер и транспортно-независимый результат обеспечивают нужный баланс.

Начальный жизненный цикл намеренно прост:

создать
обновить
включить / отключить
удалить, если не используется

Применяется физическое удаление с защитой RESTRICT. Мягкое удаление и восстановление не требуются.

Все каналы, включая Email, используют новую единую систему шаблонов и одинаковый синтаксис плейсхолдеров. Старые данные шаблонов только для Email следует преобразовать при обновлении, а не поддерживать как постоянный путь совместимости во время выполнения.