Сигналы
Рекомендации по проектированию сценариев обработки событий и генерации Сигналов
Доступные примеры сценариев обработки событий
Проектирование сценария
Добавление сценария
В данном разделе документации рассматриваются общие принципы и инструменты обработки событий в Сигналы функционалом Автоматизации.
Вы также можете ознакомиться с нашей статьей на Habr про обработку событий от Zabbix.
Также в статье есть ссылки на видео-версию статьи.
Рекомендации по проектированию сценариев обработки событий и генерации Сигналов
Вот несколько общих рекомендаций, которые помогут разработать сценарий автоматизации для работы с Сигналами:
- 
Изучите первичные события от источника данных. Для закладывания корректной логики работы алгоритма необходимо понимать входные данные. Это позволит избежать возможных ошибок в работе сценария, когда, например, в функцию попадает значение, тип которого она не способна корректно обработать.
- Какие в событии поля обязательны?
 - Какие опциональны?
 - Какие значения могут принимать?
 - Могут ли быть вложенные сообщения и потребуется ли обрабатывать единичные объекты и/или массивы?
 - Есть ли в событии идентификаторы, позволяющие связать событие с конкретной КЕ?
 
 - 
Изучите или разработайте требования к работе с сигналами конечными пользователями. Необходимо ответить на вопросы:
- Сигналы должны закрываться только оператором или это может делать автоматика?
 - Для закрытия обязательно ли ожидание подтверждающих событий и есть ли они? Или допустимо закрытие через какой-то период времени?
 - Нужно ли сигнал помечать какими-либо тегами для их дальнейшей фильтрации или составления отчетности?
 - Должна ли выполняться какая-то корреляция по открытым сигналам?
 - Есть ли события, которые должны игнорироваться?
 - Предполагается ли какая-то автоматизация через бизнес-процессы? Возможно для этого в сигнале потребуется какая-то мета-информация и при их открытии Сигнал необходимо обогатить какими-либо данными (например, из связанной КЕ)?
 
 - 
Оцените требования к нагрузке. Необходимо понимать какое количество событий в единицу времени предполагается обрабатывать и сообщения какого размера будут подаваться на вход сценария. Требования к нагрузке позволят определить какие функции автоматизации стоит использовать: необходимы ли batch функции или они не обязательны.
 - 
Составление схемы. Перед непосредственной разработкой сценария будет полезно задокументировать в схематичном виде шаги сценария (Visio, PowerPoint и др.). Это поможет выявить до начала разработки возможные развилки сценария и предусмотреть их корректное разрешение. А также при необходимости доработки сценария иметь отправную точку от "AS IS" на пути к новому "TO BE".
 - 
Проведите негативное тестирование сценария. Подайте недопустимые входные данные, чтобы посмотреть, как сценарий справится с этими данными. Это позволит выявить все ли возможные ошибки будут корректно обработаны и в том числе не "уйдет ли сценарий в цикл?".
 - 
Проведите нагрузочное тестирование. Справляется ли сценарий с поступающей нагрузкой? Накапливаются ли очереди? Устраивает ли вас время выполнения полученного сценария? Если ответ на один из вопросов выше "Да", то сценарий необходимо оптимизировать (рассмотреть использование batch-функций, кэширование части данных) или спланировать увеличивать количества обработчиков. Также стоит обратить внимание на потребление памяти обработчиком при выполнении сценария - есть ли риски возникновения OOM?
 - 
Соберите обратную связь от конечных пользователей. Достаточно ли данных о проблеме в Сигнале? Нет ли дублирующих или пропущенных событий и лишнего шума? Что можно улучшить?
 
Доступные примеры сценариев обработки событий
В качестве дополнительного контента, команда разработчиков предоставляет доступ к типовым сценариям обработки первичных событий.
Все сценарии доступны в публичном репозитории Monq на платформе GitHub
Проектирование сценария
Давайте рассмотрим следующий алгоритм обработки событий в Сигналы и создадим по нему наш сценарий:

- Первичное событие - анализируем, что есть у нас на входе сценария и с какими данными предстоит работать
 - Фильтрация событий по потоку данных и определение переменных - отбрасываем лишние события из сценария, которые могут вызывать ошибки в работе сценария из-за отличающейся структуры первичного события. А также определяем переменные для удобства работы со сценарием на холсте.
 - Поиск открытых сигналов по первичному событию - производим базовую дедупликацию событий, схлопывая повторяющиеся события в одном Сигнале
 - Есть открытый сигнал? - определяем дальнейший ход в сценарии (создание/закрытие/подтверждение)
 
Далее перейдем к добавлению сценария в системе.
Перед непосредственным созданием вашего первого сценария обработки событий рекомендуется ознакомиться с терминологией и общими понятиями о функционале Автоматизации:
Добавление сценария
- 
Перейдите через основное меню в раздел Автоматизация - Сценарии - откроется экран управления сценариями.
 - 
Для добавления в систему нового сценария нажмите кнопку ➕ Создать сценарий в правом верхнем углу экрана.
 - 
Заполните основные параметры создаваемого сценария:
- Владелец сценария - Рабочая группа, которой будет принадлежать сценарий.
 - Название - логически понятное название сценария.
 - Тип - Signals Processor.
 - Описание (опционально).
 - Импорт сценария (опционально).
 

 - 
Нажмите кнопку Создать - сценарий будет создан и откроется конструктор сценария.
 - 
Каждый сценарий начинает работу с События запуска - блока
OnLogEvent,
автоматически добавляемого при создании каждого сценария.- Удалить или изменить блок 
OnLogEventнельзя. - В исходящем пине 
ValueблокаOnLogEventсодержится значение того самого события, которое мы рассматриваем 
 - Удалить или изменить блок 
 
Пример события из Prometheus
Перед созданием любого сценария обработки первичных событий, нужно ознакомится с телом этого события. Рассмотрим модель события из "Prometheus Alert Manager":
{
  "status": "firing",
  "labels": {
    "alertname": "KubeDaemonSetNotScheduled",
    "container": "kube-state-metrics",
    "daemonset": "fluent-bit",
    "instance": "10.244.4.75:8080",
    "job": "kube-state-metrics",
    "namespace": "kube-system",
    "prometheus": "monitoring/k8s",
    "severity": "warning"
  },
  "annotations": {
    "description": "3 Pods of DaemonSet kube-system/fluent-bit are not scheduled.",
    "runbook_url": "https://runbooks.prometheus-operator.dev/runbooks/kubernetes/kubedaemonsetnotscheduled",
    "summary": "DaemonSet pods are not scheduled."
  },
  "startsAt": "2024-02-01T05:48:08.4Z",
  "endsAt": "0001-01-01T00:00:00Z",
  "generatorURL": "http://prometheus-k8s-0:9090/graph?g0.expr=kube_daemonset_status_desired_number_scheduled%7Bjob%3D%22kube-state-metrics%22%7D+-+kube_daemonset_status_current_number_scheduled%7Bjob%3D%22kube-state-metrics%22%7D+%3E+0&g0.tab=1",
  "fingerprint": "f96a727d3fa9501d"
}
Сперва нужно проанализировать содержимое события и понять, какие данные из него нам понадобятся. Давайте рассмотрим из каких же полей состоит данное событие:
status- информация о статусе события, произошел какой-то сбой или наоборот восстановилась работа какого-либо сервиса. Возможные значения:firing- авария,resolved- восстановление.labels- объект содержащий метки, переданные в событие из метрики. Набор меток может быть произвольным, но должен как минимум иметь набор меток для осуществления привязки к КЕ (например:daemonsetиnamespace). А также полеseverity, отражающее важность данного события.annotations- объект содержащий аннотации из правила Alert Manager.startsAt- время начала аварии.endsAt- время восстановления. Содержит информацию только, еслиstatus=resolved.generatorURL- ссылка на информацию с сырыми данными в Prometheus.fingerprint- "отпечаток пальца", он же идентификатор события об аварии.
Проанализировав информацию, содержащуюся в событии, можно сделать вывод, что нам в первую очередь потребуются поля:
status- в зависимости от значения данного поля мы будем понимать, что нам делать с сигналом - "открывать или закрывать?"fingerprint- данное поле позволит нам осуществить дедупликацию событий и найти уже имеющийся сигнал в системе, если он естьlabels.severity- поле, которое дает нам понять, сигнал какой критичность нужно открытьlabels.namespaceиlabels.daemonset- поля, которые будем использовать для поиска конфигурационных единицannotations.description- используем для указания названия сигнала
Следующим действием необходимо произвести фильтрацию поступающих событий в наш сценарий.
Фильтрация событий и определение перемененных
Процедура фильтрация необходима для того, чтобы не обрабатывать "чужие" события, которые могут отличаться по структуре и мешать работе нашего сценария в будущем.
- 
Добавьте функцию
FilterByStreamIdв сценарий (справка):Функция позволяет осуществлять фильтрацию принимаемого объекта по полю id структуры _stream.

 - 
Соедините пины
OutиInблоковOnLogEventиFilterByStreamId
Данным действием, после получения события передаем управление функции
FilterByStreamId - 
Чтобы получить значение идентификатора потока, по которому пришло событие необходимо разложить содержимое пина
Valueна составляющие. Добавьте в сценарий функциюBreakStructи соедините её с пиномValue
Служебная информация о потоке содержится в поле
_streamсоответствующего исходящего пина функцииBreakStruct. - 
Соедините пин
_streamфункцииBreakStructс пиномStreamфункцииFilterByStreamId. Таким образом вы передадите информацию о потоке данных, по которому пришло событие в функцию фильтрации.
 - 
Укажите в функции
FilterByStreamIdзначение пинаStreamIdравным идентификатору потока, события которого планируется обрабатывать в сценарии.В случае соответствия идентификатора Потока данных идентификатору, который содержится в первичном событии, дальнейшее управление пойдет по управляющему пину
Ok, иначе - по пинуFailed.
 
Как мы условились ранее, нам понадобятся данные из полей первичного события: status, fingerprint, labels.severity, labels.namespace, labels.daemonset и annotations.description. Определим переменные в сценарии и запишем значения данных полей в них:
- 
Слева, в "Диспетчере объектов" нажмите на кнопку "+" в секции "Переменные" - "Локальные", чтобы добавить переменную "Variable"

 - 
Кликните курсором по добавленной переменной "Variable" и справа, в "Инспекторе объектов" задайте ей название, например
srcStatusПроделайте данную операцию, чтобы создать переменные для всех наших полей.
 - 
Определите значение переменных, добавив на холст сценария переменные через функцию "SET"

Проделайте данную операцию для каждой переменной

 - 
Добавьте на холст функцию "BreakDynamic" и соедините ее с пином "source", чтобы извлечь данные из полей первичного события.

Дважды кликните по функции "BreakDynamic" и в "Инспекторе объектов" определите поля которые нужно извлечь из объекта
sourceпервичного события (используйте кнопку "+ Добавить пин"):
Подобным образом нужно извлечь данные из объекта
labelsи соединить соответствующие пины с назначаемыми переменными:
Не забудьте соединить пины управления.
 - 
Добавим еще одну переменную
srcEventс типомDynamic, в которую запишем всё событие целиком, чтобы прикрепить потом слепок этого события к Сигналу
 
Поиск открытых сигналов по первичному событию
Перед тем как продолжить, нужно проверить наличие, возможно уже открытых Сигналов. Это необходимо делать для дедупликации событий и "схлопывания" повторяющихся событий в одном Сигнале.
Чтобы осуществить поиск, необходимо воспользоваться запросом к API Сигналов через функцию автоматизации FilterSignalsExpanded.
- 
Добавьте на холст функцию FilterSignalsExpanded и сразу задайте параметры функции:

- Scenario - системная переменная 
Scenario - CreatedAt - не используем, передаем 
null - ClosedAt -  не используем, передаем 
null - DurationMilliseconds - не используем, передаем 
null - OwnerWorkGroupIds - системная переменная 
OwnerWorkGroupIdчерез функциюArrayCreateдля преобразования в массив 
В таком виде, при вызове функции, результат будет содержать информацию обо всех имеющихся сигналах в системе.
Для дедупликации нам не нужна информация:
- по сигналам, которые уже закрыты
 - по сигналам, которые не относятся к данному первичному событию
 
 - Scenario - системная переменная 
 - 
Добавьте дополнительные параметры фильтрации сигналов:
- 
Statuses -
Open - 
Labels - объект, содержащий информацию:
"fingerprint": "идентификатор события"Для определения модели
labelsпонадобиться создать локальную структуру через "Диспетчер объектов" и определить ее свойства:Добавьте на холст функцию
ArrayCreateи соедините ее с пиномLabelsфункцииFilterSignalsExpanded. В качестве передаваемого элемента массива (пинa) является системная структураLabelsFilter. При помощи функцииMakeStructв значение пинаValueнеобходимо передать нашу заполненную локальную структуруSignalFilter, пинKeyостается пустым.Чтобы заполнить нашу структуру и преобразовать в тип
Dynamicдобавьте функциюConvertToDynamic, в "Инспекторе объекта" этой функции укажите тип - локальную структуруSignalFilter:
.Соедините все соответствующие пины между собой:
.Добавьте на холст переменную
srcFingerprintчерез функцию "GET" и передайте ее значение через еще одинMakeStructв пинValueфункцииConvertToDynamic
. 
Таким образом, при вызове функции, она вернет нам только открытые сигналы, в метках которых есть поле
fingerprint, равное значению из первичного события. - 
 
Проверка результатов поиска сигнала
В зависимости от результата выполнения фильтрации при помощи функции FilterSignalsExpanded мы определяем дальнейший ход выполнения сценария. Если сигнала еще нет и пришло аварийное событие - мы будем создавать сигнал, а если открытый сигнал найден - либо закроем, либо привяжем к нему событие. В зависимости от того какое событие пришло.
- 
Добавьте на холст блок
ArrayAnyи соедините его с пиномSignalsфункции фильтрации сигналов.
.В зависимости от размера массива на входе функции устанавливается логическое значение пина
Result. Если размер массива равен нулю -False, и наоборот -True - 
Добавьте на холст функцию ветвления
Branchи соедините входящий пинConditionс пиномResultблокаArrayAny.Если сигналов нет, управление передается по пину
False- рассматриваем сейчас. Если найден сигнал, управление передается по пинуTrue- рассмотрим в следующем подразделе.
. - 
Перед тем как создать сигнал, нужно определить статус пришедшего события (мы сохранили его в переменной
srcStatus). Если значение переменной равноfiring- будем создавать сигнал, еслиresolve- сигнал нужно закрыть, но закрывать нечего (открытый сигнал мы не нашли), значит просто выходим из сценария.Воспользуемся блоком
Switchи передадим в него значение переменнойsrcStatus:
В зависимости от возможных значений, через "Инспектор объектов" добавим исходящие
Execпины -resolvedиfiring:
Если
srcStatus=resolved- выходим из сценария, так как открытый сигнал по закрывающему событию не был найден.Если
srcStatus=firing- продолжаем выполнение сценария и переходим к поиску связанной КЕ. 
В зависимости от результата выполнения фильтрации функции FilterSignalsExpanded и проверки статуса в первичном событии, мы определили дальнейший ход выполнения сценария. Допустим функция фильтрации ничего не нашла и вернула пустой массив Signals в соответствующем исходящем пине и нам нужно создать сигнал с привязкой к КЕ.
Поиск связанных КЕ
Чтобы при создании сигнала его можно было привязать к КЕ, ее сперва нужно найти. Подробнее прочитать о методике поиска КЕ можно в этом разделе документации.
В данной статье рассмотрим использование функции GetConfigItemByUniqueKey и предположим, что атрибуты namespace и daemonset являются ключевыми для некоторого типа КЕ.
В качестве входных параметров функция принимает:
- ConfigItemTypeId - идентификатор типа КЕ
 - Attributes - модель атрибута
 
Допустим, что мы используем тип КЕ по умолчанию, и оставим ConfigItemTypeId=0. А для поля Attribute, по аналогии с фильтрацией сигналов по меткам, создадим локальную структуру - ConfigItemFilter:

Обратите внимание на поле
@namespace. Так как словоnamespaceявляется зарезервированным в C# мы должны добавить в начало символ -@.
При помощи функций ConvertToDynamic и MakeStruct передаем в качестве атрибутов значения переменных srcNamespace и srcDaemonset:

В функции
ConvertToDynamicне забудьте выбрать тип входящего пина - локальную структуруConfigItemFilter.
Если на предыдущем шаге будет найдена КЕ - можно создавать сигнал, если КЕ не найдена - решать вам, нужен ли сигнал без связи с конфигурационной единицей или нет.
Допустим, что КЕ мы нашли и по пину Ok передаем управление следующей функции CreateSignalExpanded, чтобы создать Сигнал.
Создание сигнала
Добавляем на холст сценария функцию CreateSignalExpanded и формируем входные параметры функции:

- 
Пин
Scenario- подключаем системную переменнуюScenario - 
Пин
Name- будущее название Сигнала, подтягиваем из переменнойsrcDescription - 
Пин
Descriptionявляется опциональным, пропускаем - 
Пин
Labels- метки Сигнала, которые будут использоваться для дедупликации Сигналов- 
Нужно подготовить локальную структуру, например
SignalLabelsс обязательным свойствомfingerprint, которое мы используем для поиска открытых Сигналов в начале сценария - 
Сформировать модель меток (json) через функции
ConvertToDynamicиMakeStruct - 
Передать значение
fingerprintиз заранее определенной переменнойsrcFingerprint 
 - 
 - 
Пин
OwnerWorkGroupId- владелец Сигнала, системная переменнаяOwnerWorkGroupId(текущий владелец сценария) - 
Пин
Severity- критичность сигнала.Так как критичность в первичном событии указана в виде строки (может иметь значения: critical, warning), а функция
CreateSignalExpandedтребует значение с типомInteger, необходимо преобразовать это значение. Простым способом преобразования является прием с функциейSwitch:
в зависимости от значения переменной
srcSeverityустанавливаем значение новой переменнойsrcSeverityInt(critical=2, warning=4) - 
Пин
ConfigItemsIds- массив идентификаторов КЕ, которые будут связаны с созданным Сигналом. При помощи функцийArrayCreateсформируем массив из идентификатора КЕ, которую заберем из исходящего пинаConfigItemфункцииGetConfigItemByUniqueKey, добравшись доIdпри помощи блокаBreakStruct - 
Пин
Events- массив событий, которые будут связаны с будущим сигналом.Воспользуемся блоками
ArrayCreateиMakeStructдля определения массива из одного элемента, в котором будет содержаться наше первичное событие.
- Пин 
StartEventIdсоединяем с системной переменнойStartEventId - Пин 
Typeзаполняем вручную значениемOpening(открытие сигнала) - Пин 
Bodyсоединяем с ранее созданной переменнойsrcEvent 

 - Пин 
 - 
Пины
Tags,ConfigItemComponentIds,ConfigItemComponentNameявляются опциональными и нужны для обогащения Сигналов дополнительными сведениями, прочитать о которых можно в описании функции CreateSignalExpanded 
После подключения всех перечисленных выше пинов, функция должна выглядеть подобным образом:

На этом шаге с созданием сигналов можно закончить. Далее рассмотрим процесс закрытия сигналов.
Проверка статуса и критичности первичного события
Для подтверждения или закрытия сигнала потребуется вернуться к функции FilterSignalExpanded и рассмотреть тот вариант, когда функция находит и возвращает открытые сигналы.
- 
Воспользуемся уже имеющимися на холсте блоками
ArrayAnyиBranch, которые расположены после функцииFilterSignalExpanded, и продолжим настройку сценария по пинуTrue
 - 
Перед тем как закрывать найденный сигнал нужно убедиться в том, что в первичное событие поступило именно "закрывающее" событие
 - 
Добавьте на холст:
- переменную 
srcStatus(методGET) - функцию 
Branch - функцию 
Equal 
При помощи данных блоков сравним значение переменной
srcStatusс константойresolvedи в случае совпадения закроем сигнал (пинTrue), иначе выполним операцию подтверждения сигнала (пинFalse).
 - переменную 
 
Закрытие сигнала
Добавьте на холст функцию CloseSignal с помощью которой закроем найденный сигнал. Основным входным параметром функции является идентификатор сигнала - SignalId.
Достать идентификатор сигнала можно из массива найденных сигналов Signals при помощи блоков ArrayFirst и BreakStruct

В данном случае мы получаем только первый элемент массива, только потому-что мы уверены - в ответе будет только один сигнал, так как фильтруем по уникальному полю
fingerprint.
Подтверждение сигнала
В том случае, если значение переменной srcStatus у нас не равно resolve мы можем прикрепить очередное первичное событие к имеющемуся Сигналу, тем самым подтвердить его.
Для обогащения Сигнала новым событием нам понадобится функция BindEventsToSignal. Добавьте ее на холст сценария и соедините с пином False блока Branch.

В функцию BindEventsToSignal нам нужно передать идентификатор Сигнала и массив событий, которые нужно привязать к нему.
Идентификатор Сигнала можно позаимствовать у уже имеющегося на холсте блока BreakStruct:

С пином Events поступим также, как и при создании Сигнала. Воспользуемся блоками ArrayCreate и MakeStruct для определения массива из одного элемента, в котором будет содержаться наше первичное событие.
- Пин 
StartEventIdсоединяем с системной переменнойStartEventId - Пин 
Typeзаполняем вручную значениемConfirming(подтверждение сигнала) - Пин 
Bodyсоединяем с ранее созданной переменнойsrcEvent 

В данной статье мы рассмотрели базовые моменты по созданию и закрытию сигналов в сценариях автоматизации Monq. Больше информации вы сможете найти в профильных разделах документации.