Когда система мониторинга отправляет алерт, он редко выглядит так, как хотелось бы дежурному инженеру.

Один источник пишет critical, другой — disaster, третий передаёт severity: 5. В одном payload сервис называется checkout, в другом — payments-api, а в третьем его приходится угадывать по namespace.

Если все эти события приходят через общий Alertmanager или webhook, одной статической настройки маршрута быстро становится недостаточно.

Обычно проблему решают одним из трёх способов:

  1. Усложняют правила на стороне каждой системы мониторинга.

  2. Создают много почти одинаковых endpoint’ов и маршрутов.

  3. Добавляют промежуточный сервис, который преобразует payload перед отправкой в on-call-систему.

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

В IncidentRelay 2.0 мы добавили Event Orchestration. В этой статье разберём одну из основных частей новой функции — Global Orchestration: зачем она нужна, где находится в потоке обработки, как устроены правила и почему мы не ограничились обычным rule engine с кнопкой «Включить».

IncidentRelay Global Orchestration explain trace
IncidentRelay Global Orchestration explain trace

Какую задачу решает Global Orchestration

Global Orchestration — это набор упорядоченных правил, принадлежащий группе IncidentRelay. Он обрабатывает нормализованное событие до того, как будет окончательно выбран сервис и запущен обычный жизненный цикл алерта.

Упрощённо поток выглядит так:

Payload интеграции
        ↓
Аутентификация и нормализация
        ↓
Global Orchestration группы
        ↓
Оркестрация выбранного сервиса, если он известен
        ↓
Обычный жизненный цикл IncidentRelay
        ↓
Группа алертов, дочерний алерт,
эскалация и уведомления
IncidentRelay Global Orchestration
IncidentRelay Global Orchestration

На глобальном уровне можно:

  • выбрать команду, маршрут и сервис;

  • нормализовать severity, title и другие поля события;

  • назначить приоритет и политики;

  • добавить или удалить метки;

  • настроить group_key и dedup_key;

  • подавить уведомления;

  • отложить создание алерта;

  • полностью отбросить событие;

  • поставить безопасный асинхронный webhook в очередь.

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

Иными словами, мы добавили не второй жизненный цикл алерта, а слой принятия решений перед ним.

Почему global и service — разные области

В IncidentRelay есть две области оркестрации:

Область

Когда запускать

Типичные задачи

global

Владелец события ещё неизвестен или правило относится к нескольким сервисам

Нормализация, выбор команды и сервиса, общие правила подавления

service

Сервис уже выбран

Политики конкретного сервиса, локальное обогащение, задержка известного сигнала, запуск диагностики

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

Хорошая граница ответственности выглядит так:

Global Orchestration:
    Кому принадлежит событие?
    В какой общий формат его привести?

Service Orchestration:
    Как именно этот сервис хочет его обработать?

Глобальное правило может выбрать сервис. После этого IncidentRelay запускает оркестрацию, прикреплённую к выбранному сервису.

Если service orchestration передаёт событие другому сервису, runtime защищает цепочку от циклов и чрезмерного количества переходов. Потому что бесконечная маршрутизация — это тоже вид мониторинга, только уже за состоянием CPU.

Так общие соглашения хранятся в одном месте, а знания о конкретном сервисе остаются рядом с самим сервисом.

Пример: один Alertmanager и несколько команд

Представим, что общий Alertmanager принимает алерты всей production-платформы.

В IncidentRelay приходит нормализованное событие:

{
  "source": "alertmanager",
  "title": "High error rate",
  "message": "Error rate is above 20%",
  "severity": "fatal",
  "status": "firing",
  "dedup_key": "checkout-db-2:high-error-rate",
  "labels": {
    "environment": "production",
    "application": "checkout",
    "component": "database",
    "instance": "checkout-db-2",
    "customer_impacting": true
  }
}

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

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

  1. Преобразовать fatal в каноническое значение critical.

  2. По application=checkout выбрать команду Payments.

  3. По component=database выбрать сервис Checkout Database.

  4. Для критического события с customer_impacting=true задать приоритет P1.

  5. Построить стабильные ключи группировки и дедупликации.

  6. Остановить дальнейшие глобальные правила маршрутизации.

  7. Передать результат в оркестрацию сервиса и обычный lifecycle.

В интерфейсе правило можно представить примерно так:

WHEN ALL
├── labels.environment equals production
├── labels.application equals checkout
├── labels.component equals database
└── labels.customer_impacting is_true

THEN
├── set_team       → Payments
├── set_service    → Checkout Database
├── set_severity   → critical
├── set_priority   → P1
├── set_label      → orchestrated=true
└── set_grouping
      group_key    → {{ labels.application }}:
                     {{ labels.environment }}
      dedup_key    → {{ labels.alertname }}:
                     {{ labels.instance }}

AFTER
└── stop

stop здесь прекращает обработку следующих соседних правил этой оркестрации.

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

Почему правила выполняются последовательно

Мы рассматривали вариант, при котором все правила получают один неизменяемый исходный контекст, а затем результаты как-то объединяются.

На бумаге это выглядит функционально и аккуратно. На практике быстро возникает вопрос: что делать, если два правила выбрали разные сервисы или установили разные значения priority?

В текущей модели правила выполняются сверху вниз, а действия внутри совпавшего правила — в указанном порядке. Последующие правила видят изменения предыдущих.

Например:

Правило 1:
    IF event.severity equals disaster
    THEN set_severity critical
    AFTER continue

Правило 2:
    IF event.severity equals critical
    AND labels.customer_impacting is_true
    THEN set_priority P1

Второе правило уже увидит critical.

Это позволяет строить определение как понятный конвейер:

Нормализовать значения разных интеграций
Добавить общие метки
Выбрать владельца
Выбрать priority и policies
Настроить grouping и deduplication
Принять решение suppress / pause / drop
Поставить автоматизацию в очередь

Минус у подхода тоже есть: порядок становится частью логики.

Поэтому в execution trace сохраняются совпавшие условия, выполненные действия и значения до и после каждого изменения.

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

В условиях используются dotted paths к разрешённым частям контекста:

event.severity
event.status
labels.environment
labels.application
raw.alerts.0.labels.namespace
variables.service
route.id
service.id
team.id
integration.source
result.disposition

Движок читает JSON-объекты и индексы массивов, но не выполняет методы или произвольные выражения.

Основные операторы:

equals              not_equals
contains            not_contains
starts_with         ends_with
regex               not_regex
in                  not_in
exists              not_exists
greater_than        less_than
greater_or_equal    less_or_equal
is_true             is_false

Для логики доступны вложенные группы:

  • ALL — AND;

  • ANY — OR;

  • NONE — NOT.

Пример:

ALL
├── labels.environment equals production
└── ANY
    ├── event.severity equals critical
    └── labels.priority equals p1

То есть:

production AND (critical OR p1)

Шаблоны тоже ограничены

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

{{ labels.service | upper }}:
{{ event.title | trim }}

Поддерживается небольшой набор детерминированных фильтров:

lower
upper
trim
default
replace
truncate

Например:

{{ labels.cluster | default("unknown") | trim | lower }}

Если необязательное поле может отсутствовать, это следует явно обработать через default.

В противном случае ошибка шаблона попадёт в трассировку и будет обработана согласно on_failure действия.

У каждого действия есть одна из трёх стратегий ошибки:

Значение

Поведение

continue

Записать ошибку и перейти к следующему действию

stop_rule

Остановить действия текущего правила

stop_orchestration

Остановить всю оркестрацию

Для необязательного обогащения подходит continue.

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

Валидация выбранных сущностей

Недостаточно просто получить из правила числовой service_id. Во время выполнения IncidentRelay проверяет согласованность выбранных объектов.

Например, будут отклонены случаи, когда:

  • маршрут принадлежит другой группе;

  • route source равен sentry, а источник события — alertmanager;

  • выбранный сервис принадлежит другой команде, чем маршрут;

  • escalation policy или notification policy принадлежит другой команде;

  • выбранный объект был отключён или удалён после публикации версии.

Поведение после такой ошибки зависит от compatibility mode.

Два переключателя режима — и это не одно и то же

У определения есть runtime mode и compatibility mode. Поначалу их легко перепутать.

Runtime mode

Он отвечает на вопрос: влияет ли опубликованная версия на реальные события?

Режим

Что происходит

disabled

Production-события не вычисляются; доступны редактирование, validation и simulation

shadow

Опубликованная версия вычисляется и записывается, но её решения не меняют production

active

Допустимые решения применяются к реальной обработке

Для shadow и active нужна опубликованная версия.

Compatibility mode

Он отвечает на другой вопрос: как оркестрация делит ответственность с существующим lifecycle?

Режим

Что происходит

legacy

Существующая логика остаётся главным источником решений

hybrid

Явные решения оркестрации сохраняются, а старый lifecycle заполняет оставшиеся значения

orchestration

Оркестрация становится главным источником маршрутизации; требуется валидный route

Например, в hybrid:

Оркестрация явно установила priority=P1
→ priority policy не заменяет это значение

Оркестрация не выбрала notification policy
→ обычный механизм выбора policy продолжает работать

Если комбинация выбранных сущностей оказалась невалидной, hybrid может отклонить кандидата и продолжить legacy-обработку.

В режиме orchestration аналогичная ошибка блокирует дальнейшую обработку вместо fallback.

Для первого production-внедрения мы рекомендуем hybrid.

Отдельный крайний случай: конфигурация active + legacy не применяет решения оркестрации к production. Слово active здесь относится к runtime mode, но compatibility mode всё ещё оставляет старый lifecycle главным.

Название немного обманчивое, зато конфигурация безопасная. Иногда программное обеспечение шутит само, даже если разработчики не просили.

Simulator: проверить не только счастливый payload

Simulator вычисляет текущий draft изолированно.

Он не:

  • создаёт алерты;

  • отправляет уведомления;

  • вызывает webhooks;

  • меняет режим выполнения;

  • публикует версию;

  • изменяет production-данные.

Можно передать уже нормализованное событие или сырой payload поддерживаемой интеграции.

Во втором случае payload проходит через тот же зарегистрированный normalizer, который используется при production ingestion.

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

  1. Точное совпадение.

  2. Почти совпадающее событие, которое не должно пройти.

  3. Другое окружение.

  4. Другой source.

  5. Отсутствие необязательной метки.

  6. Статус resolved.

  7. Повторное событие, которое должно получить тот же dedup_key.

Результат симуляции содержит:

  • исходное нормализованное событие;

  • совпавшие правила;

  • condition trace;

  • значения до и после каждого действия;

  • итоговый контекст;

  • выбранные сущности;

  • disposition;

  • разницу между draft и active version.

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

Зелёный HTTP 200 вполне способен сообщить, что вы очень успешно направили все production-алерты команде стажёров.

Grouping и deduplication — тоже часть маршрутизации

Правильный владелец не спасает ситуацию, если каждый повтор создаёт новый алерт.

Для группы однотипных событий можно задать:

group_key: {{ labels.alertname }}:
           {{ labels.environment }}

dedup_key: {{ labels.alertname }}:
           {{ labels.instance }}

window_seconds: 900

Так события с одним alert name в одном окружении попадут в общую группу, а каждый instance сохранит собственный дочерний алерт. Повторный сигнал обновит нужный child alert.

Ключи должны быть стабильными.

Временная метка в dedup_key формально допустима, но фактически означает «создавай новый алерт при каждом запросе». Очень удобно, если ваша цель — нагрузочно протестировать дежурного инженера.

suppress, pause и drop — три разных решения

Эти действия легко объединить в голове как «не уведомлять», но последствия отличаются принципиально.

Действие

Результат

suppress

Алерт создаётся и остаётся видимым, но уведомления и эскалация подавляются

pause

Событие хранится как pending и активируется после задержки, если раньше не пришёл resolve

drop

Алерт и группа вообще не создаются

suppress подходит для событий, которые нужно сохранить для поиска или корреляции.

pause полезен для кратковременных сбоев. Например, можно подождать пять минут и отменить создание алерта, если за это время пришёл совпадающий resolve.

drop следует использовать только для сигналов без операционной ценности — например, точно определённого тестового heartbeat.

Пустое условие совпадает со всеми событиями. Поэтому catch-all вместе с drop, suppress, pause или обязательной маршрутизацией требует особенно осторожной проверки.

Catch-all с drop — очень эффективное средство против alert fatigue, примерно как демонтаж пожарной сигнализации против писка разряженной батарейки.

Асинхронные webhooks вместо произвольного кода

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

HTTP-запрос не выполняется внутри ingestion request. IncidentRelay ставит выполнение в очередь, а scheduler доставляет его асинхронно с timeout, retry и ограничениями безопасности.

Секретные заголовки хранятся зашифрованными и не возвращаются API. URL проверяются против SSRF, redirects повторно валидируются, а private network targets запрещаются или ограничиваются allowlist согласно конфигурации.

В simulation и shadow mode webhook не отправляется.

Иначе проверка нового правила могла бы создать несколько сотен вполне настоящих задач с заголовком TEST PLEASE IGNORE.

Что получилось в итоге

Global Orchestration дала нам единое место, где разнородные факты мониторинга превращаются в объяснимое операционное решение до начала paging:

  • интеграции могут сохранять собственные форматы payload;

  • общие правила нормализации не дублируются по сервисам;

  • ownership выбирается по содержимому события;

  • локальная логика остаётся в service orchestration;

  • каждое решение можно проверить в trace;

  • draft не влияет на production;

  • опубликованные версии неизменяемы;

  • simulation и shadow mode позволяют внедрять изменения без ставки на удачу.

IncidentRelay — open-source и self-hosted. Исходный код доступен на GitHub.

Подробное руководство по условиям, действиям, Simulator и shadow mode находится в документации Event Orchestration, а API — в отдельном руководстве.