Перейти к основному содержимому
Версия: 8.5

Сигналы

Рекомендации по проектированию сценариев обработки событий и генерации Сигналов
Доступные примеры сценариев обработки событий
Проектирование сценария
Добавление сценария

В данном разделе документации рассматриваются общие принципы и инструменты обработки событий в Сигналы функционалом Автоматизации.

Полезная информация

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

Рекомендации по проектированию сценариев обработки событий и генерации Сигналов

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

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

    • Какие в событии поля обязательны?
    • Какие опциональны?
    • Какие значения могут принимать?
    • Могут ли быть вложенные сообщения и потребуется ли обрабатывать единичные объекты и/или массивы?
    • Есть ли в событии идентификаторы, позволяющие связать событие с конкретной КЕ?
  • Изучите или разработайте требования к работе с сигналами конечными пользователями. Необходимо ответить на вопросы:

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

  • Составление схемы. Перед непосредственной разработкой сценария будет полезно задокументировать в схематичном виде шаги сценария (Visio, PowerPoint и др.). Это поможет выявить до начала разработки возможные развилки сценария и предусмотреть их корректное разрешение. А также при необходимости доработки сценария иметь отправную точку от "AS IS" на пути к новому "TO BE".

  • Проведите негативное тестирование сценария. Подайте недопустимые входные данные, чтобы посмотреть, как сценарий справится с этими данными. Это позволит выявить все ли возможные ошибки будут корректно обработаны и в том числе не "уйдет ли сценарий в цикл?".

  • Проведите нагрузочное тестирование. Справляется ли сценарий с поступающей нагрузкой? Накапливаются ли очереди? Устраивает ли вас время выполнения полученного сценария? Если ответ на один из вопросов выше "Да", то сценарий необходимо оптимизировать (рассмотреть использование batch-функций, кеширование части данных) или спланировать увеличивать количества обработчиков. Также стоит обратить внимание на потребление памяти обработчиком при выполнении сценария - есть ли риски возникновения OOM?

  • Соберите обратную связь от конечных пользователей. Достаточно ли данных о проблеме в Сигнале? Нет ли дублирующих или пропущенных событий и лишнего шума? Что можно улучшить?

Доступные примеры сценариев обработки событий

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

Все сценарии доступны в публичном репозитории Monq на платформе GitHub

Проектирование сценария

Давайте рассмотрим следующий алгоритм обработки событий в Сигналы и создадим по нему наш сценарий:

image

Далее перейдем к добавлению сценария в системе.

Информация

Перед непосредственным созданием вашего первого сценария обработки событий рекомендуется ознакомиться с терминологией и общими понятиями о функционале Автоматизации:

Добавление сценария

  1. Перейдите через основное меню в раздел Автоматизация - Сценарии - откроется экран управления сценариями.

  2. Для добавления в систему нового сценария нажмите кнопку ➕ Создать сценарий в правом верхнем углу экрана.

  3. Заполните основные параметры создаваемого сценария:

    • Владелец сценария - Рабочая группа, которой будет принадлежать сценарий.
    • Название - логически понятное название сценария.
    • Тип - Signals Processor.
    • Описание (опционально).
    • Импорт сценария (опционально).

    image

  4. Нажмите кнопку Создать - сценарий будет создан и откроется конструктор сценария.

  5. Каждый сценарий начинает работу с События запуска - блока 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 - используем для указания названия сигнала

Следующим действием необходимо произвести фильтрацию поступающих событий в наш сценарий.

Фильтрация событий и определение перемененных

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

  1. Добавьте функцию FilterByStreamId в сценарий (справка):

    Функция позволяет осуществлять фильтрацию принимаемого объекта по полю id структуры _stream.

    image

  2. Соедините пины Out и In блоков OnLogEvent и FilterByStreamId

    image

    Данным действием, после получения события передаем управление функции FilterByStreamId

  3. Чтобы получить значение идентификатора потока, по которому пришло событие необходимо разложить содержимое пина Value на составляющие. Добавьте в сценарий функцию BreakStruct и соедините её с пином Value

    image

    Служебная информация о потоке содержится в поле _stream соответствующего исходящего пина функции BreakStruct.

  4. Соедините пин _stream функции BreakStruct с пином Stream функции FilterByStreamId. Таким образом вы передадите информацию о потоке данных, по которому пришло событие в функцию фильтрации.

    image

  5. Укажите в функции FilterByStreamId значение пина StreamId равным идентификатору потока, события которого планируется обрабатывать в сценарии.

    В случае соответствия идентификатора Потока данных идентификатору, который содержится в первичном событии, дальнейшее управление пойдет по управляющему пину Ok, иначе - по пину Failed.

    image

Как мы условились ранее, нам понадобятся данные из полей первичного события: status, fingerprint, labels.severity, labels.namespace, labels.daemonset и annotations.description. Определим переменные в сценарии и запишем значения данных полей в них:

  1. Слева, в "Диспетчере объектов" нажмите на кнопку "+" в секции "Переменные" - "Локальные", чтобы добавить переменную "Variable"

    image

  2. Кликните курсором по добавленной переменной "Variable" и справа, в "Инспекторе объектов" задайте ей название, например srcStatus

    image

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

    image

  3. Определите значение переменных, добавив на холст сценария переменные через функцию "SET"

    image

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

    image

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

    image

    Дважды кликните по функции "BreakDynamic" и в "Инспекторе объектов" определите поля которые нужно извлечь из объекта source первичного события (используйте кнопку "+ Добавить пин"):

    image

    Подобным образом нужно извлечь данные из объекта labels и соединить соответствующие пины с назначаемыми переменными:

    image

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

  5. Добавим еще одну переменную srcEventс типом Dynamic, в которую запишем всё событие целиком, чтобы прикрепить потом слепок этого события к Сигналу

    image

Поиск открытых сигналов по первичному событию

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

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

  1. Добавьте на холст функцию FilterSignalsExpanded и сразу задайте параметры функции:

    image

    • Scenario - системная переменная Scenario
    • CreatedAt - не используем, передаем null
    • ClosedAt - не используем, передаем null
    • DurationMilliseconds - не используем, передаем null
    • OwnerWorkGroupIds - системная переменная OwnerWorkGroupId через функцию ArrayCreate для преобразования в массив

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

    Для дедупликации нам не нужна информация:

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

    • Statuses - Open

    • Labels - объект, содержащий информацию:

      "fingerprint": "идентификатор события"

      Для определения модели labels понадобиться создать локальную структуру через "Диспетчер объектов" и определить ее свойства:

      image

      Добавьте на холст функцию ArrayCreate и соедините ее с пином Labels функции FilterSignalsExpanded. В качестве передаваемого элемента массива (пин a) является системная структура LabelsFilter. При помощи функции MakeStruct в значение пина Value необходимо передать нашу заполненную локальную структуру SignalFilter, пин Key остается пустым.

      Чтобы заполнить нашу структуру и преобразовать в тип Dynamic добавьте функцию ConvertToDynamic, в "Инспекторе объекта" этой функции укажите тип - локальную структуру SignalFilter:

      image.

      Соедините все соответствующие пины между собой:

      image.

      Добавьте на холст переменную srcFingerprint через функцию "GET" и передайте ее значение через еще один MakeStruct в пин Value функции ConvertToDynamic

      image.

    Таким образом, при вызове функции, она вернет нам только открытые сигналы, в метках которых есть поле fingerprint, равное значению из первичного события.

Проверка результатов поиска сигнала

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

  1. Добавьте на холст блок ArrayAny и соедините его с пином Signals функции фильтрации сигналов.

    image.

    В зависимости от размера массива на входе функции устанавливается логическое значение пина Result. Если размер массива равен нулю - False, и наоборот - True

  2. Добавьте на холст функцию ветвления Branch и соедините входящий пин Condition с пином Result блока ArrayAny.

    Если сигналов нет, управление передается по пину False - рассматриваем сейчас. Если найден сигнал, управление передается по пину True - рассмотрим в следующем подразделе.

    image.

  3. Перед тем как создать сигнал, нужно определить статус пришедшего события (мы сохранили его в переменной srcStatus). Если значение переменной равно firing - будем создавать сигнал, если resolve - сигнал нужно закрыть, но закрывать нечего (открытый сигнал мы не нашли), значит просто выходим из сценария.

    Воспользуемся блоком Switch и передадим в него значение переменной srcStatus:

    image

    В зависимости от возможных значений, через "Инспектор объектов" добавим исходящие Exec пины - resolved и firing:

    image

    Если srcStatus = resolved - выходим из сценария, так как открытый сигнал по закрывающему событию не был найден.

    Если srcStatus = firing - продолжаем выполнение сценария и переходим к поиску связанной КЕ.

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

Поиск связанных КЕ

Чтобы при создании сигнала его можно было привязать к КЕ, ее сперва нужно найти. Подробнее прочитать о методике поиска КЕ можно в этом разделе документации.

В данной статье рассмотрим использование функции GetConfigItemByUniqueKey и предположим, что атрибуты namespace и daemonset являются ключевыми для некоторого типа КЕ.

В качестве входных параметров функция принимает:

  • ConfigItemTypeId - идентификатор типа КЕ
  • Attributes - модель атрибута

Допустим, что мы используем тип КЕ по умолчанию, и оставим ConfigItemTypeId=0. А для поля Attribute, по аналогии с фильтрацией сигналов по меткам, создадим локальную структуру - ConfigItemFilter:

image

Обратите внимание на поле @namespace. Так как слово namespace является зарезервированным в C# мы должны добавить в начало символ - @.

При помощи функций ConvertToDynamic и MakeStruct передаем в качестве атрибутов значения переменных srcNamespace и srcDaemonset:

image

В функции ConvertToDynamic не забудьте выбрать тип входящего пина - локальную структуру ConfigItemFilter.

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

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

Создание сигнала

Добавляем на холст сценария функцию CreateSignalExpanded и формируем входные параметры функции:

image

  • Пин Scenario - подключаем системную переменную Scenario

  • Пин Name - будущее название Сигнала, подтягиваем из переменной srcDescription

  • Пин Description является опциональным, пропускаем

  • Пин Labels - метки Сигнала, которые будут использоваться для дедупликации Сигналов

    • Нужно подготовить локальную структуру, например SignalLabels с обязательным свойством fingerprint, которое мы используем для поиска открытых Сигналов в начале сценария

      image

    • Сформировать модель меток (json) через функции ConvertToDynamic и MakeStruct

    • Передать значение fingerprint из заранее определенной переменной srcFingerprint

  • Пин OwnerWorkGroupId - владелец Сигнала, системная переменная OwnerWorkGroupId (текущий владелец сценария)

  • Пин Severity - критичность сигнала.

    Так как критичность в первичном событии указана в виде строки (может иметь значения: critical, warning), а функция CreateSignalExpanded требует значение с типом Integer, необходимо преобразовать это значение. Простым способом преобразования является прием с функцией Switch:

    image

    в зависимости от значения переменной srcSeverity устанавливаем значение новой переменной srcSeverityInt (critical=2, warning=4)

  • Пин ConfigItemsIds - массив идентификаторов КЕ, которые будут связаны с созданным Сигналом. При помощи функций ArrayCreate сформируем массив из идентификатора КЕ, которую заберем из исходящего пина ConfigItem функции GetConfigItemByUniqueKey, добравшись до Id при помощи блока BreakStruct

  • Пин Events - массив событий, которые будут связаны с будущим сигналом.

    Воспользуемся блоками ArrayCreate и MakeStruct для определения массива из одного элемента, в котором будет содержаться наше первичное событие.

    image

    • Пин StartEventId соединяем с системной переменной StartEventId
    • Пин Type заполняем вручную значением Opening (открытие сигнала)
    • Пин Body соединяем с ранее созданной переменной srcEvent

    image

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

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

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

Проверка статуса и критичности первичного события

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

  1. Воспользуемся уже имеющимися на холсте блоками ArrayAny и Branch, которые расположены после функции FilterSignalExpanded, и продолжим настройку сценария по пину True

    image

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

  3. Добавьте на холст:

    • переменную srcStatus (метод GET)
    • функцию Branch
    • функцию Equal

    При помощи данных блоков сравним значение переменной srcStatus с константой resolved и в случае совпадения закроем сигнал (пин True), иначе выполним операцию подтверждения сигнала (пин False).

    image

Закрытие сигнала

Добавьте на холст функцию CloseSignal с помощью которой закроем найденный сигнал. Основным входным параметром функции является идентификатор сигнала - SignalId.

Достать идентификатор сигнала можно из массива найденных сигналов Signals при помощи блоков ArrayFirst и BreakStruct

image

В данном случае мы получаем только первый элемент массива, только потому-что мы уверены - в ответе будет только один сигнал, так как фильтруем по уникальному полю fingerprint.

Подтверждение сигнала

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

Для обогащения Сигнала новым событием нам понадобится функция BindEventsToSignal. Добавьте ее на холст сценария и соедините с пином False блока Branch.

image

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

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

image

С пином Events поступим также, как и при создании Сигнала. Воспользуемся блоками ArrayCreate и MakeStruct для определения массива из одного элемента, в котором будет содержаться наше первичное событие.

  • Пин StartEventId соединяем с системной переменной StartEventId
  • Пин Type заполняем вручную значением Confirming (подтверждение сигнала)
  • Пин Body соединяем с ранее созданной переменной srcEvent

image

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