Подключение Zabbix

Принимайте алерты Zabbix в Incident Garden через webhook.

Zabbix отправляет события в Incident Garden через media type типа Webhook: это скрипт оповещения, который собирает тело запроса из макросов события и отправляет его на адрес интеграции.

Создайте интеграцию типа Zabbix, как описано в обзоре интеграций, и сохраните API key. Адрес приёма у неё /webhook:

https://api.incidentgarden.ru/api/v1/integrations/<API_KEY>/webhook

Настройка media type в Zabbix

Создайте media type

В Zabbix откройте Alerts → Media types → Create media type и заполните:

  • Name: Incident Garden
  • Type: Webhook

В таблице Parameters задайте параметры. Значения справа это макросы Zabbix, а в url подставьте API key вашей интеграции:

ПараметрЗначение
event_id{EVENT.ID}
event_name{EVENT.NAME}
event_opdata{EVENT.OPDATA}
event_severity{EVENT.SEVERITY}
event_tagsjson{EVENT.TAGSJSON}
event_value{EVENT.VALUE}
host_name{HOST.HOST}
urlhttps://api.incidentgarden.ru/api/v1/integrations/<API_KEY>/webhook

Остальные поля формы оставьте по умолчанию, кроме вкладки Options (о ней в разделе Повторная отправка при сбое).

Вставьте скрипт

В поле Script вставьте код целиком:

// Incident Garden — Zabbix webhook media type script.
// Формирует generic-webhook payload и POST'ит на
//   {url}/api/v1/integrations/<API_KEY>/webhook
//
// Параметры медиатайпа (передаются в `value` как JSON после раскрытия макросов):
//   url            — базовый URL приёма, вкл. API key
//   event_id       — {EVENT.ID}       (стабилен между проблемой и восстановлением → dedup_key)
//   event_value    — {EVENT.VALUE}    (1 = проблема, 0 = восстановление)
//   event_severity — {EVENT.SEVERITY} (имя важности триггера: Disaster/High/Average/Warning/…)
//   event_name     — {EVENT.NAME}     (имя проблемы)
//   host_name      — {HOST.HOST}      (техническое имя хоста)
//   event_tagsjson — {EVENT.TAGSJSON} (теги события → лейбл алерта)
//   event_opdata   — {EVENT.OPDATA}   (operational data триггера → описание алерта)
//
// Возврат строки = успех. throw = ошибка → Zabbix повторит по maxattempts/attempt_interval.
 
try {
    var p = JSON.parse(value);
 
    // firing / resolved определяется значением события, НЕ severity.
    var status = (String(p.event_value) === '0') ? 'resolved' : 'firing';
 
    // Zabbix отдаёт severity как "Disaster"/"Not classified"; наш zabbix-пресет
    // ждёт "disaster"/"not_classified" (source_field=severity).
    var severity = String(p.event_severity || '').toLowerCase().replace(/ /g, '_');
 
    // Теги события приезжают отдельным JSON внутри параметра, поэтому им нужен
    // свой JSON.parse.
    //
    // Аккумулятор это объект БЕЗ прототипа. У обычного {}
    // унаследованы toString, valueOf, hasOwnProperty и прочее, поэтому проверка
    // "=== undefined" ниже приняла бы одноимённый тег за уже виденный, а запись
    // в __proto__ у {} вообще не создаёт ключ — такие теги молча терялись бы.
    // Не «упрощать» обратно в {}.
    var labels = Object.create(null);
    try {
        var tags = JSON.parse(p.event_tagsjson || '[]');
        for (var i = 0; i < tags.length; i++) {
            // Имя и значение приводятся к строке явно: приём отбрасывает ключ
            // с нестроковым значением, а тип после раскрытия макроса не гарантирован.
            var tagName = String(tags[i].tag);
            var tagValue = String(tags[i].value || '');
            // Одно имя может встретиться на событии несколько раз, а порядок
            // элементов Zabbix не гарантирует. Берём первое по алфавиту, чтобы
            // результат от этого порядка не зависел.
            if (labels[tagName] === undefined || tagValue < labels[tagName]) {
                labels[tagName] = tagValue;
            }
        }
    } catch (tagsErr) {
        Zabbix.log(3, '[IncidentGarden] event tags unusable, sending without them: ' + tagsErr);
        labels = Object.create(null);
    }
 
    // severity и host пишутся ПОВЕРХ тегов: severity — вход маппинга важности,
    // от него зависит эскалация и отдавать его произвольному тегу нельзя.
    labels.severity = severity;
    labels.host = String(p.host_name || '');
 
    var payload = {
        title: p.event_name,
        status: status,
        // dedup_key связывает firing и resolved одного триггера — обязателен,
        // иначе закрывающее событие не найдёт открытый алерт (group_key разойдётся).
        dedup_key: String(p.event_id),
        labels: labels
    };
 
    // Operational data — человекочитаемое описание в теле уведомления.
    // Пустое значение не отправляем вовсе, чтобы не слать пустую строку.
    var opdata = String(p.event_opdata || '');
    if (opdata !== '') {
        payload.description = opdata;
    }
 
    var body = JSON.stringify(payload);
 
    var req = new HttpRequest();
    req.addHeader('Content-Type: application/json');
    var response = req.post(p.url, body);
    var code = req.getStatus();
 
    if (code < 200 || code >= 300) {
        throw 'API returned HTTP ' + code + ': ' + response;
    }
    return 'OK';
} catch (err) {
    Zabbix.log(3, '[IncidentGarden] webhook failed: ' + err);
    throw 'Sending failed: ' + err;
}

Готовая форма выглядит так:

Форма создания media type «Incident Garden» в Zabbix

Привяжите media type к пользователю

Откройте Users → <ваш пользователь> → Media → Add и выберите Incident Garden. Без этой привязки Zabbix не будет отправлять оповещения через media type.

Создайте trigger action

Заведите trigger action, который на срабатывание и на восстановление триггера вызывает media type Incident Garden. Тогда одно и то же событие пришлёт и открытие, и закрытие алерта. В условиях action ограничьте важность, например Trigger severity is greater than or equals High, чтобы в on-call не уходили информационные события (почему это важно, объясняет антипаттерн Все уровни мониторинга в on-call).

Как макросы ложатся в тело запроса

  • {EVENT.VALUE} (1 проблема, 0 восстановление) → status (firing или resolved).
  • {EVENT.ID} → dedup_key. Идентификатор события одинаков у проблемы и её восстановления, поэтому восстановление закрывает тот же алерт, а не заводит новый.
  • {EVENT.SEVERITY} → labels.severity. Скрипт переводит значение в нижний регистр и меняет пробел на _, чтобы оно совпало со шкалой пресета (disaster, high, average, warning, information, not_classified).
  • {EVENT.NAME} → title.
  • {HOST.HOST} → labels.host.
  • {EVENT.TAGSJSON} → остальные labels (подробнее в следующем разделе).
  • {EVENT.OPDATA} → description, который дежурный видит в уведомлении. Если Operational data у триггера пустое, Zabbix подставит значения элементов данных из выражения триггера.

Теги события в лейблах алерта

Каждый тег события становится лейблом алерта. Например тег env со значением stage даёт лейбл env=stage. Если тег встречается на событии несколько раз, берётся первое по алфавиту значение.

Имена severity и host зарезервированы: их заполняет скрипт, и одноимённый тег Zabbix в лейбл не попадёт (иначе тег мог бы подменить важность).

Тег без значения даёт лейбл с пустой строкой. Условия политик «существует» и «отсутствует» проверяют только наличие ключа, поэтому такой лейбл для них полноценный. Сайленсы же сравнивают значение и для них нужен тег со значением.

Теги шаблона в события не попадают. До алерта доезжают теги триггера, элемента данных и хоста, хотя интерфейс Zabbix показывает тег шаблона на хосте как унаследованный. Если лейбл нужен в условиях, задавайте тег на триггере, элементе данных или хосте.

Zabbix собирает все параметры media type в одно тело и обрезает его на 65 535 символах. Обрезанное тело перестаёт быть корректным JSON и событие не отправляется вовсе (ни открытие алерта, ни закрытие).

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

Если событие потерялось по дороге, например из-за сетевого сбоя, и Zabbix его не повторил, алерта не будет. В отличие от AlertManager, Zabbix повторяет отправку только тогда, когда это задано в media type.

Проверьте повторные попытки. Откройте media type Incident Garden, вкладку Options, и задайте Attempts больше 1 и ненулевой Attempt interval. Разумная отправная точка: три попытки с интервалом в десяток секунд.

Скрипт из шага выше под это рассчитан: при неуспешном ответе он завершается ошибкой и пишет строку в журнал Zabbix. Фактические отправки видны в Reports → Action log.

Важность

Пресет Zabbix переводит disaster и high в critical, а остальные уровни в warning. Изменить соответствие можно в настройке важности.