Webhook Zabbix под капотом: куда пропадают теги и почему не приходят события восстановления
Почему событие из Zabbix приходит в webhook без тега команды, ручной JSON ломается и в каком случае за проблемой не придёт восстановление.
Вы пишете webhook, который передаёт проблемы из Zabbix в трекер задач, чат или сервис дежурств. Получатель распределяет события по командам с помощью тегов team и env, показывает значение метрики и закрывает инцидент после восстановления. Кажется, что теги с шаблонов и хоста придут вместе с событием, числа останутся числами, а за каждой проблемой рано или поздно придёт восстановление. Zabbix может нарушить каждое из этих ожиданий, и вы об этом не узнаете.
Разберём, что приходит на самом деле, где это увидеть в Zabbix и как настроить webhook с учётом такого поведения.
TL;DR: Если нужны только рекомендации, переходите к чек-листу. Для каждого пункта там есть пример.
Как проверяли
Мы подняли Zabbix 7.0 и 6.0 и расставили теги на триггере, элементе данных, хосте, шаблоне и вложенном шаблоне. В значения добавили кавычки, запятые, переводы строки, кириллицу и эмодзи. Webhook пересылал всё полученное на тестовый приёмник, где мы сравнивали данные с настройками Zabbix. Эскалации проверили отдельно: закрывали проблемы после последнего шага и выключали действие посреди эскалации.
Теги для маршрутизации задавайте на хосте и читайте из {EVENT.TAGSJSON}
Тег в Zabbix задаётся парой имени и значения, например team=db. По смыслу это похоже на метку в Prometheus. Теги можно задать на триггере, элементе данных, хосте и шаблоне. Шаблон содержит элементы данных и триггеры; его привязывают к хосту или другому шаблону. В последнем случае он становится вложенным. Событие собирает теги с этих уровней. Если получатель распределяет события по командам, потерянный тег team помешает маршрутизации.
Тег для маршрутизации ставьте на хост, а не на шаблон
Шаблон Base DB с тегом team=db вложен в PostgreSQL, что видно в столбце Linked to templates. Шаблон PostgreSQL привязан к хосту db-01:

Откроем триггер «Replication lag is too high» из шаблона PostgreSQL на хосте db-01. В Data collection → Hosts → Triggers на вкладке Tags выберем Inherited and trigger tags. Здесь видны теги, которые получит событие: env=prod с хоста, service=postgresql с шаблона PostgreSQL и scope=availability с самого триггера. Тега team=db среди них нет:

Тег team=db будет только у события триггера «Too many connections» из Base DB. У событий триггеров из PostgreSQL его не будет: тег шаблона получают только события его собственных триггеров. Поэтому:
- тег вложенного шаблона не получат события триггеров внешнего шаблона, как в примере выше;
- триггер, созданный прямо на хосте, не получит ни одного тега шаблонов.
Часть событий хоста придёт получателю без team и не попадёт ни к одной команде. Тег хоста присутствует в каждом его событии, поэтому для маршрутизации лучше задать team на хосте. Если задаёте его в шаблонах ради централизованного управления, проверяйте наличие тега у каждого нужного триггера.
Теги читайте из {EVENT.TAGSJSON}, а {EVENT.TAGS} не разбирайте
{EVENT.TAGSJSON} возвращает массив объектов {"tag": ..., "value": ...}. Значения тегов в нём корректно разбираются даже с кавычками, обратной косой чертой, переводом строки, кириллицей и эмодзи. {EVENT.TAGS} возвращает одну строку: пары имя:значение разделены запятой с пробелом и отсортированы по имени. Запятые и двоеточия внутри значений не экранируются. Например:
Начало строки выглядит как два тега, хотя это один тег commas со значением a, b: c. После разделения строки по запятым восстановить его уже не получится.
Одно имя тега может встретиться несколько раз. Тег dup с разными значениями на триггере, элементе данных, хосте и шаблоне дал четыре элемента массива. А одинаковая пара имени и значения с нескольких уровней слилась в один элемент. Чтобы сохранить все значения, собирайте их в список по имени:
Для этого фрагмента нужен параметр типа оповещения tagsjson={EVENT.TAGSJSON}. Если параметры обрежутся из-за предела длины, JSON.parse выбросит исключение и оповещение получит статус Failed. О пределе длины расскажем в следующей части. Код написан на ES5: скрипты выполняет движок Duktape, имя которого видно в стеке ошибок.
Не полагайтесь на порядок тегов. Zabbix выдаёт их группами: сначала теги триггера, затем элемента данных, хоста и шаблона. Внутри группы алфавитного порядка нет. Изменение важности триггера и повторная отправка тех же тегов порядок не меняли. После изменения набора тегов триггера порядок менялся, причём в 6.0 и 7.0 по-разному. Поэтому выбор первого или последнего значения одноимённого тега ненадёжен. Передавайте получателю весь список или задавайте нужный тег только на одном уровне.
{EVENT.TAGS.имя} без тега: *UNKNOWN* на 7.0 и сам макрос на 6.0
Макрос {EVENT.TAGS.env} удобен, если у события есть тег env. Если тега нет, скрипт получит не пустую строку. Zabbix 7.0 подставит *UNKNOWN*, а Zabbix 6.0 оставит текст макроса. Вот значения одного поля с двух стендов:
Если получатель не учитывает это поведение, он может создать окружение с именем *UNKNOWN* или {EVENT.TAGS.env}. Проверить наличие тега можно в колонке Tags раздела Monitoring → Problems. С другими макросами происходит похожее. На 7.0 {EVENT.RECOVERY.ID} остаётся текстом вне восстановления, а {EVENT.UPDATE.TIME} вне обновления. Макрос {ESC.STEP} остаётся текстом всегда. Номер шага эскалации не передаётся в параметрах webhook; о том, как различать шаги, мы писали в первой части.
Необязательные теги удобнее брать из {EVENT.TAGSJSON}: отсутствующего тега просто не будет в массиве. Если нужен отдельный макрос, проверяйте обе формы отсутствия:
Проверка первого символа на { даст ложный результат для параметра, в котором вы намеренно передаёте JSON. Отдельный макрос также не подходит для одноимённых тегов.
Все параметры webhook приходят строками, даже {EVENT.ID} и {ITEM.VALUE}
Параметры типа оповещения задаются в таблице Parameters на вкладке Media type в разделе Alerts → Media types → ваш тип. Значение каждого параметра приходит в скрипт строкой, даже если в ячейке стоит число 500. После JSON.parse(value) тип string будет и у {EVENT.ID}, и у {ITEM.VALUE} числового элемента данных, и у параметра с JSON внутри. При вычислениях эта разница сразу заметна:
| Выражение в скрипте | Что ушло получателю |
|---|---|
p.item_value | "500" |
p.item_value + 1 | "5001" |
Number(p.item_value) | 500 |
Number(p.item_value) + 1 | 501 |
JSON.parse(p.jobj) при jobj={"n":500,"s":"500"} | {"n":500,"s":"500"} |
Получатель увидит число, только если скрипт преобразует значение через Number или разберёт вложенный JSON через JSON.parse. Если переслать параметры как есть, получатель со строгой схемой может отвергнуть поле. А получатель, который складывает значения без преобразования в числа, сложит строки вместо чисел.
Преобразуйте в число только те поля, где оно ожидается. Для текста, например *UNKNOWN* или значения текстового элемента данных, Number вернёт NaN. При сериализации через JSON.stringify он превратится в null. Если вместо этого выбросить исключение, оповещение получит статус Failed, а повторные попытки столкнутся с тем же значением. Передайте получателю и результат преобразования, и исходную строку:
Если получатель сам преобразует строки в числа, менять скрипт не нужно.
JSON, собранный из макросов вручную, ломается на кавычке и переводе строки
Шаблон {"title":"{EVENT.NAME}"} можно указать в параметре или в тексте сообщения на вкладке Message templates. Пока в имени триггера нет двойной кавычки или перевода строки, он выглядит рабочим. Zabbix подставляет значение без экранирования, поэтому JSON с такими символами ломается. Кириллица и эмодзи не мешают разбору. Например, для триггера q"uote back\slash Привет 😀 тело, собранное вручную, выглядело так:
А после JSON.stringify в скрипте так:
Python json.loads не смог разобрать вручную собранный JSON: в примере выше из-за кавычки, в отдельной проверке из-за перевода строки. Сообщения об ошибках:
Zabbix отправит такое оповещение как обычно: он только подставляет текст и не проверяет готовый JSON. Ошибку обнаружит получатель при разборе. Передавайте каждый макрос отдельным параметром, например name={EVENT.NAME} и weird={EVENT.TAGS.weird}, а тело собирайте в скрипте через JSON.stringify. На обеих версиях оно оставалось корректным при всех проверенных именах. Значение тега с кавычкой ломало ручную сборку точно так же.
Макросы раскрываются при создании оповещения: {ITEM.LASTVALUE} на шаге 2 эскалации уже новое
Zabbix раскрывает макросы при создании оповещения. На 7.0 все повторы одного оповещения содержали одинаковые значения: третья попытка через 10 секунд после первой сохранила исходные {TIME} и возраст события. Каждый шаг эскалации создаёт новое оповещение, поэтому макросы раскрываются заново. В Reports → Action log шаг виден отдельной записью. Разницу хорошо показывают значения элемента данных. Когда он выдал 500, открылась проблема. Через 6 секунд значение выросло до 900:
| Оповещение | {ITEM.VALUE} | {ITEM.LASTVALUE} |
|---|---|---|
| шаг 1 | 500 | 500 |
| шаг 2, через минуту | 500 | 900 |
На обеих версиях {ITEM.VALUE} сохраняет значение, при котором сработал триггер. {ITEM.LASTVALUE} берёт текущее значение на момент шага. Передавайте оба, если на следующем шаге нужно видеть, изменилась ли ситуация. Время получения и возраст события на момент доставки лучше вычислять у получателя: при повторах значения в теле остаются прежними.
Это же свойство помогает составить ключ для отсева дублей, о котором мы писали в первой части.
Recovery приходит и после завершённой эскалации, но не после отменённой
Если у действия настроена операция восстановления, recovery придёт и после завершения всех шагов эскалации. Мы закрыли проблему через 2 минуты 38 секунд после первого оповещения. Единственный шаг длился минуту, но recovery ушёл на обеих версиях. Так работали и уведомление всех участников, и отправка конкретному пользователю.
Если действие выключить посреди эскалации, recovery не придёт. Даже после повторного включения действия и закрытия проблемы инцидент у получателя останется открытым. Вместо recovery придёт лишнее оповещение с темой из шаблона проблемы и значением {EVENT.VALUE} равным 1. Оно отправляется не сразу после выключения, а в момент следующего шага. В наших проверках это происходило через минуту при шаге в минуту и через пять минут при шаге в пять минут. В Reports → Action log оно выглядит как ещё одна отправка по той же проблеме. Отличить его можно по началу сообщения:
Чтобы не создать у получателя новую проблему, передайте в скрипт параметр message={ALERT.MESSAGE} и проверяйте префикс NOTE: Escalation canceled. Он сохранялся на 6.0 и 7.0 как с собственным шаблоном типа оповещения, так и с сообщением, заданным в операции действия.
Пометьте такую проблему у получателя. После сверки с Zabbix её должен закрыть человек, поскольку recovery уже не придёт. Автоматическое закрытие может скрыть проблему, которая всё ещё существует.
Чек-лист: что проверить в скрипте webhook
- Читайте теги из
{EVENT.TAGSJSON}черезJSON.parse. Строку{EVENT.TAGS}не разбирайте: запятые и двоеточия в значениях не экранируются. Параметр:tagsjson={EVENT.TAGSJSON}. - Собирайте значения одноимённых тегов в список. На их порядок не опирайтесь:
tags[list[i].tag].push(list[i].value). - Теги для маршрутизации ставьте на хост. Тег шаблона получают только события его триггеров. Триггеры внешнего шаблона и созданные на хосте его не получат. Проверяйте теги триггера в Tags → Inherited and trigger tags.
- Берите необязательные теги из
{EVENT.TAGSJSON}. Если используете{EVENT.TAGS.<имя>}, проверяйте обе формы отсутствия:v !== '{EVENT.TAGS.env}' && v !== '*UNKNOWN*'. - Преобразуйте числа явно и сохраняйте исходное значение:
body.value = isNaN(n) ? null : n; body.value_raw = p.item_value;. - Передавайте каждый макрос отдельным параметром и собирайте тело через
JSON.stringify:name={EVENT.NAME}вместо{"title":"{EVENT.NAME}"}в одном параметре. - Передавайте значение срабатывания и текущее значение:
trigger_value={ITEM.VALUE},current_value={ITEM.LASTVALUE}. - Время доставки определяйте у получателя. Например, он сам заполнит поле
received_at. Значения{TIME}и возраста события фиксируются при создании оповещения и при повторах не обновляются. - Распознавайте отмену эскалации по префиксу в
{ALERT.MESSAGE}и помечайте такие проблемы. Recovery не придёт, поэтому после сверки с Zabbix их закрывает человек. Параметр:message={ALERT.MESSAGE}. Проверка:p.message.indexOf('NOTE: Escalation canceled') === 0.
Что дальше
Скрипт, собранный по этому чек-листу, отдаёт получателю теги, числа и тексты ровно такими, какими их видит Zabbix, и не принимает отмену эскалации за новую проблему. Но о доставленном событии ещё нужно сообщить человеку. Если вам нужны эскалации и графики дежурств поверх Zabbix, Incident Garden принимает его события напрямую. Скрипт из инструкции по подключению Zabbix уже читает теги из {EVENT.TAGSJSON}. Если ночное сообщение в чате можно пропустить, настройте голосовые звонки: робот позвонит дежурному на телефон.
Перед использованием webhook проверьте в своём окружении сценарии с тегами, значениями и восстановлением. Если понадобится помощь, обратитесь в Telegram-чат DevOps, SRE, Infra.