интеграция zabbix и planfix

Интеграция Zabbix и ПланФикс: жизненный цикл задачи из алерта мониторинга

Связали Zabbix 7.0 (около 260 хостов) с таск-трекером ПланФикс так, что событие мониторинга автоматически проходит весь жизненный цикл задачи: проблема создаёт задачу, нажатие Acknowledge переводит её «В работе», восстановление триггера закрывает, отмена эскалации отменяет.

Всё держится на штатном механизме Zabbix: media type типа Webhook (скрипт на JavaScript) и действия с эскалацией, без промежуточного сервиса и внешней базы соответствий. Ниже разбираем архитектуру, полный код webhook на JavaScript и тонкости настройки медиатипа, которые экономят часы при переносе на свою инсталляцию.

Интеграция Zabbix и ПланФикс: жизненный цикл задачи из алерта мониторинга

Мы настроили интеграцию Zabbix и ПланФикс так, что алерт автоматически проходит полный жизненный цикл: от создания задачи до её закрытия при восстановлении сервиса, исключая ручной труд дежурных инженеров.

Содержание

Задача: алерты Zabbix вручную превращаются в задачи ПланФикс

Интеграция Zabbix и ПланФикс закрывает разрыв между мониторингом и учётом работ: пока проблема видна только в Zabbix, задача для дежурной смены заводится в ПланФикс руками. На наших проектах это означало потерянные инциденты, отсутствие истории «проблема ↔ задача» и ручную рутину на каждом срабатывании триггера.

Дано: боевая инсталляция Zabbix 7.0, около 260 хостов разных клиентов и порядка 150 шаблонов. Заявки и работа дежурных у нас ведутся в ПланФикс, поэтому именно туда должны попадать значимые события мониторинга. До автоматизации связка держалась на внимательности оператора: увидел алерт, скопировал, создал задачу, не забыл закрыть. На потоке из сотен хостов такая схема даёт сбои ежедневно.

Мы поставили цель: двусторонняя автоматическая связка «проблема Zabbix ↔ задача ПланФикс» без ручного труда и без отдельного промежуточного сервиса. Принципиальное ограничение: никаких внешних демонов и сторонних шин, только то, что умеет сам Zabbix. Любой дополнительный сервис пришлось бы мониторить отдельно, а это новая точка отказа в системе, которая как раз и следит за отказами.

К решению мы предъявили четыре требования. Во-первых, реакция не на каждый «чих»: проблема, мигнувшая на 30 секунд, не должна плодить задачу. Во-вторых, полный жизненный цикл: создание, перевод в работу, закрытие и отмена. В-третьих, идемпотентность по возможности, чтобы повторная доставка не порождала дубли. В-четвёртых, выборочность: задачи только по значимым проблемам и не по всем группам хостов, потому что часть парка составляют внутренние и партнёрские группы, которым место в трекере не нужно.

Не пытайтесь решить это «уведомлением на почту с парсером»: мы сталкивались с тем, что почтовый канал не возвращает в Zabbix идентификатор задачи, и связать восстановление триггера с конкретной строкой в трекере потом нечем. Именно требование вернуть task id обратно в событие определило всю дальнейшую архитектуру.

Задача: алерты Zabbix вручную превращаются в задачи ПланФикс

Архитектура: webhook-медиатип, действия и теги события как «память»

Интеграция Zabbix и ПланФикс держится на двух штатных сущностях Zabbix и одном приёме: тип оповещения media type (webhook), действия (actions) с фильтрами и эскалацией, а в роли «памяти» о созданной задаче выступают теги события. Внешней базы соответствий между проблемами и задачами нет, и это сознательное решение.

Поток данных простой. Триггер переходит в проблему, событие попадает в одно из действий (у нас это действия с внутренними номерами 87, 88 и 89), где фильтр проверяет severity, группы хостов и флаг suppressed. Если проблема прожила дольше одного периода эскалации и её не подтвердили, на шаге эскалации 2 операция-сообщение уходит служебному пользователю ПланФикс, а тот доставляет его через media type «Planfix Tasks». Скрипт дёргает эндпоинт zabbix-new, ПланФикс отдаёт task id.

Дальнейшие переходы используют те же теги. Когда триггер возвращается в OK, recovery-операция (тип 11) закрывает задачу; когда инженер нажимает Acknowledge, update-операция (тип 12) переводит её в работу; при отмене эскалации уведомление приходит благодаря notify_if_canceled. Во всех случаях скрипт берёт taskId из тега, а не из внешнего справочника. Для удобства дежурного включён show_event_menu: прямо из карточки проблемы в Zabbix открывается связанная задача по ссылке __zbx_planfix_link. Подробности механизма — в документации Zabbix: media type webhook и действия и эскалации.

Ключевая идея — теги как память. При создании задачи webhook-скрипт возвращает Zabbix два тега: __zbx_planfix_taskid с идентификатором задачи и __zbx_planfix_link со ссылкой на неё, прикрепляет их к событию (process_tags=1), и все последующие операции (закрытие, acknowledge, отмена) находят ту же задачу по этим тегам — внешняя СУБД соответствий не нужна. Вот что скрипт отдаёт обратно в Zabbix:

				
					{
  "tags": {
    "__zbx_planfix_taskid": "12345",
    "__zbx_planfix_link": "https://company.planfix.ru/task/12345"
  }
}
				
			

Настройка media type и действий: пошагово

Вся интеграция Zabbix и ПланФикс собирается из штатных средств Zabbix — отдельный сервис не нужен. Ниже порядок настройки от media type до действий; полный код скрипта приведён отдельным блоком ниже.

Шаг 1. Создайте media type. В Zabbix откройте Оповещения → Способы оповещений → Создать способ оповещения, тип — Webhook, имя — например «Planfix Tasks». Это контейнер для скрипта, параметров и шаблонов сообщений.

Шаг 2. Вставьте webhook-скрипт в поле Скрипт — полный код в разделе «Полный код webhook-скрипта» ниже. Скрипт получает все данные события одним JSON в переменной value и сам собирает HTTP-запрос к ПланФикс.

Шаг 3. Задайте параметры в разделе Параметры — пары «имя → макрос Zabbix». Скрипт читает их из value через JSON.parse(value); адрес ПланФикс и проект тоже передаются параметрами, поэтому код переносится между инсталляциями без правок:

ПараметрЗначение (макрос Zabbix)
alert_message{ALERT.MESSAGE}
alert_subject{ALERT.SUBJECT}
event_id{EVENT.ID}
event_value{EVENT.VALUE}
event_source{EVENT.SOURCE}
event_update_status{EVENT.UPDATE.STATUS}
severity{EVENT.SEVERITY}
host_name{HOST.NAME}
host_ip{HOST.IP}
event_time{EVENT.DATE} {EVENT.TIME}
trigger_id{TRIGGER.ID}
planfix_urlhttps://company.planfix.ru
planfix_projectZABBIX
planfix_acknowledge_user{USER.FULLNAME}
planfix_taskid{EVENT.TAGS.__zbx_planfix_taskid}
planfix_default_useridСотрудники ТП

Шаг 4. Настройте шаблоны сообщений для трёх состояний события: проблема (recovery=0), восстановление (recovery=1) и обновление/acknowledge (recovery=2). Без шаблона {ALERT.MESSAGE} придёт пустым и webhook упадёт на валидации. В шаблоне проблемы передайте как минимум {EVENT.NAME}, {HOST.NAME}, {EVENT.SEVERITY}, {EVENT.ID} и ссылку на событие. Acknowledge и его снятие скрипт распознаёт по первой строке шаблона обновления, куда {EVENT.UPDATE.ACTION} разворачивается в acknowledged или unacknowledged.

Шаг 5. Задайте параметры доставки — они определяют порядок и надёжность отправки:

ПараметрЗначениеЗачем
maxsessions1строгий порядок: create обязан уйти раньше, чем edit или close по той же задаче
maxattempts3пережить кратковременную недоступность ПланФикс
attempt_interval30sпауза между повторами
timeout30sлимит исполнения скрипта
notify_if_canceled1без него не придёт уведомление об отмене эскалации

Значение maxsessions=1 принципиально: повышать нельзя, иначе закрытие задачи обгонит её создание и edit придёт по ещё не существующей задаче. Включите также process_tags=1 (скрипт пишет теги обратно в событие) и show_event_menu со ссылкой {EVENT.TAGS.__zbx_planfix_link} — тогда из карточки проблемы в Zabbix открывается связанная задача.

Шаг 6. Поднимите четыре эндпоинта на стороне ПланФикс. Каждый отвечает за свой переход жизненного цикла задачи — их и вызывает скрипт:

ЭндпоинтКогда вызываетсяЧто делает в ПланФикс
/webhook/get/zabbix-newновая проблемасоздаёт задачу
/webhook/get/zabbix-editвосстановление или отменаменяет статус задачи
/webhook/get/zabbix-ackacknowledgeназначает исполнителя, «В работе»
/webhook/get/zabbix-unackснятие acknowledgeвозвращает исполнителя по умолчанию

На стороне ПланФикс каждый эндпоинт — это входящий вебхук: zabbix-new создаёт задачу из переданных полей (name, description, project) и возвращает {"task": id}; zabbix-edit, zabbix-ack и zabbix-unack находят задачу по параметру task и меняют статус, исполнителя или добавляют комментарий. Поэтому скрипт после создания и сохраняет task id в теге события — чтобы остальные вызовы нашли ту же задачу. Формат входящих запросов — в справке ПланФикс по входящим вебхукам.

Шаг 7. Создайте действия (actions), которые маршрутизируют проблемы в ПланФикс по уровню важности. Три действия с одинаковым фильтром, отличающиеся только severity:

ДействиеSeverityПериод эскалацииЗадача создаётся на шаге
PlanFix | Report disasterDisaster (5)2 мин2
PlanFix | Report highHigh (4)5 мин2
PlanFix | Report averageAverage (3)5 мин2

Условия фильтра соединены по AND: совпадение severity, хост не входит в служебные группы ГРУППА-A, ГРУППА-B и ГРУППА-C, проблема не подавлена (не suppressed). Операция-сообщение стоит на шаге эскалации 2 с условием «Event acknowledged = No» — это шумовой фильтр: задача заведётся, только если проблема прожила дольше одного периода эскалации и её не подтвердили. Recovery- и update-операции (тип Notify all involved) обрабатывают восстановление и acknowledge, а notify_if_canceled=1 обязателен, иначе webhook не получит уведомление об отмене эскалации. Все действия шлют сообщения служебному пользователю ПланФикс, у которого единственный канал — этот медиатип.

Шаг 8. Настройте обратный сценарий ПланФикс → Zabbix. Связка двусторонняя: когда оператор принимает задачу в ПланФикс, событие в Zabbix должно подтвердиться (acknowledge) — с ответственным и комментарием. В ПланФикс это автоматический сценарий: событие — «Задача принята»; условия — статус ≠ «Завершенная» и проект = ZABBIX; операция — послать HTTP-запрос POST на адрес вашего Zabbix — https://zabbix.example.com/api_jsonrpc.php (Content-Type: application/json):

{
  "jsonrpc": "2.0",
  "method": "event.acknowledge",
  "params": {
    "eventids": ["{{Задача.Event ID}}"],
    "action": 6,
    "message": "ACK from Planfix by {{Текущий пользователь.ФИО}}"
  },
  "auth": "API_TOKEN",
  "id": 1
}

Здесь action: 6 — битовая маска Zabbix (2 «подтвердить» + 4 «добавить сообщение»): событие переходит в acknowledged и получает комментарий. eventids берётся из поля задачи {{Задача.Event ID}} — того самого event_id, что скрипт передал при создании, поэтому ПланФикс всегда знает, какое именно событие подтверждать. Адрес https://zabbix.example.com/api_jsonrpc.php — это JSON-RPC API вашего сервера Zabbix (подставьте свой хост). В итоге инженер работает там, где удобнее: нажал «принял» в Zabbix — задача в ПланФикс уходит «В работе»; взял задачу в ПланФикс — подтверждается событие в Zabbix. Токен Zabbix API храните в защищённом поле, а не открытым текстом в теле вебхука.

Полный жизненный цикл задачи: создание → в работе → закрытие → отмена

В интеграции Zabbix и ПланФикс полный жизненный цикл задачи означает, что одно событие мониторинга проходит путь от создания до закрытия или отмены без участия человека. Скрипт работает как конечный автомат: по флагам события он определяет тип перехода и выбирает нужный эндпоинт. Порядок проверки веток важен, и отмена эскалации проверяется первой — почему именно так, видно из кода диспетчера ниже.

Состояние ZabbixКак распознаётсяЭндпоинтСтатус задачи
Эскалация отмененатекст Escalation canceledzabbix-editОтмененная
Восстановление (OK)event_value = 0zabbix-editЗавершенная
Acknowledgeпервая строка содержит acknowledgedzabbix-ackВ работе
Снятие acknowledgeпервая строка содержит unacknowledgedzabbix-unackисполнитель по умолчанию
Новая проблемаevent_value=1 , event_update_status=0zabbix-newсоздание
Прочее обновлениени одно из условий вышеzabbix-editбез смены статуса

Разберём типовой сценарий. Триггер сработал, проблема прожила дольше одного периода эскалации, на шаге 2 уходит оповещение. Скрипт видит event_value=1 и event_update_status=0, попадает в ветку новой проблемы и вызывает zabbix-new. ПланФикс создаёт задачу и возвращает её идентификатор, который скрипт кладёт в теги события. С этого момента задача и проблема связаны.

Дальше инженер берёт проблему в работу и нажимает Acknowledge. Приходит обновление, первая строка которого содержит acknowledged, скрипт вызывает zabbix-ack, передаёт исполнителя и переводит задачу в статус «В работе». Если acknowledge сняли, отрабатывает zabbix-unack и исполнитель возвращается к значению по умолчанию (у нас это группа сотрудников техподдержки).

Когда триггер восстанавливается, событие приходит с event_value=0, скрипт распознаёт восстановление и через zabbix-edit переводит задачу в «Завершенная». Отдельный случай — отмена эскалации: проблема ещё активна, но эскалацию погасили, отключив хост или триггер. Такое событие маскируется под новую проблему, поэтому его проверка стоит первой, и задача уходит в «Отмененная», а не дублируется.

Везде, кроме создания, скрипт берёт идентификатор задачи из тега __zbx_planfix_taskid. Если тега нет (например, проблема случилась раньше, чем мы развернули интеграцию), ветка молча завершается без вызова ПланФикс: закрывать или комментировать нечего. На наших проектах это спасает от ошибок на «исторических» событиях, которые ещё живут в системе после включения связки.

Полный код webhook-скрипта

Скрипт целиком — три логические части в одном файле: объект Planfix (валидация параметров и четыре метода-обёртки над эндпоинтами), helper clean() для очистки значений и блок MAIN с конечным автоматом, который по флагам события выбирает нужную ветку. В движке Zabbix всё это исполняется как единый скрипт, поэтому приводим его одним блоком.

Куда вставить: в Zabbix откройте Оповещения → Способы оповещений, создайте media type типа Webhook и вставьте код в поле Скрипт. Параметры из таблицы выше добавьте в разделе Параметры того же медиатипа — скрипт читает их из value через JSON.parse(value).

				
					// Zabbix → Planfix webhook (адаптация для Zabbix 7)

var Planfix = {
  params: {},
  logEnabled: false,

  setParams: function (params) {
    if (typeof params !== 'object') throw 'Planfix params must be object.';
    Planfix.params = params;
  },

  log: function (level, msg) {
    if (Planfix.logEnabled) Zabbix.log(3, '[Planfix Webhook] ' + msg);
  },

  validateParams: function () {
    var required = [
      'alert_message', 'alert_subject', 'event_id',
      'event_source', 'event_update_status', 'event_value',
      'planfix_url', 'planfix_project'
    ];
    required.forEach(function (key) {
      if (!Planfix.params[key] || Planfix.params[key].toString().trim() === '')
        throw 'Missing parameter: ' + key;
    });
    if (Planfix.params.planfix_url.indexOf('http') !== 0)
      throw 'planfix_url must start with http/https';
  },

  httpGet: function (url) {
    Planfix.log(3, 'GET: ' + url);
    var request = new HttpRequest();
    var response = request.get(url);
    Planfix.log(3, 'Raw response: ' + response);
    if (request.getStatus() < 200 || request.getStatus() >= 300)
      throw 'HTTP GET failed: ' + request.getStatus() + ' ' + response;
    try { return JSON.parse(response); }
    catch (e) { throw 'JSON parse error: ' + response; }
  },

  createTask: function () {
    var name       = encodeURIComponent(clean(Planfix.params.alert_subject));
    var desc       = encodeURIComponent(clean(Planfix.params.alert_message).replace(/\r?\n/g, '<br>').replace(/(<br>\s*)+$/g, ''));
    var project    = encodeURIComponent(clean(Planfix.params.planfix_project));
    var event_id   = encodeURIComponent(clean(Planfix.params.event_id));
    var trigger_id = encodeURIComponent(clean(Planfix.params.trigger_id));
    var host_name  = encodeURIComponent(clean(Planfix.params.host_name));
    var host_ip    = encodeURIComponent(clean(Planfix.params.host_ip));
    var severity   = encodeURIComponent(clean(Planfix.params.severity));
    var event_time = encodeURIComponent(clean(Planfix.params.event_time));

    var url = Planfix.params.planfix_url
      + '/webhook/get/zabbix-new'
      + '?name=' + name
      + '&description=' + desc
      + '&project=' + project
      + '&event_id=' + event_id
      + '&trigger_id=' + trigger_id
      + '&host_name=' + host_name
      + '&host_ip=' + host_ip
      + '&severity=' + severity
      + '&event_time=' + event_time;

    Planfix.log(3, 'CreateTask URL: ' + url);
    var data = Planfix.httpGet(url);
    if (!data.task) throw 'No task id in Planfix response: ' + JSON.stringify(data);
    return data.task;
  },

  updateTask: function (taskId, comment, status, userfullname) {
    comment = clean(comment).replace(/\r?\n/g, '<br>').replace(/(<br>\s*)+$/g, '');
    var url = Planfix.params.planfix_url + '/webhook/get/zabbix-edit?task=' + encodeURIComponent(taskId);
    if (comment)      url += '&comment=' + encodeURIComponent(comment);
    if (status)       url += '&status=' + encodeURIComponent(status);
    if (userfullname) url += '&userfullname=' + encodeURIComponent(userfullname);
    Planfix.log(3, 'UpdateTask URL: ' + url);
    var data = Planfix.httpGet(url);
    if (!data.task) throw 'Update: No task id in Planfix response: ' + JSON.stringify(data);
    return data.task;
  },

  ackTask: function (taskId, comment, status, userfullname) {
    comment = clean(comment).replace(/\r?\n/g, '<br>').replace(/(<br>\s*)+$/g, '');
    var url = Planfix.params.planfix_url + '/webhook/get/zabbix-ack'
      + '?task=' + encodeURIComponent(taskId);
    if (comment)      url += '&comment=' + encodeURIComponent(comment);
    if (status)       url += '&status=' + encodeURIComponent(status);
    if (userfullname) url += '&userfullname=' + encodeURIComponent(userfullname);
    Planfix.log(3, 'ACKTask URL: ' + url);
    var data = Planfix.httpGet(url);
    if (!data.task) throw 'ACK: No task id in Planfix response: ' + JSON.stringify(data);
    return data.task;
  },

  unAckTask: function (taskId, comment, userfullname) {
    comment = clean(comment).replace(/\r?\n/g, '<br>').replace(/(<br>\s*)+$/g, '');
    var url = Planfix.params.planfix_url + '/webhook/get/zabbix-unack'
      + '?task=' + encodeURIComponent(taskId);
    if (comment)      url += '&comment=' + encodeURIComponent(comment);
    if (userfullname) url += '&userfullname=' + encodeURIComponent(userfullname);
    Planfix.log(3, 'UNACKTask URL: ' + url);
    var data = Planfix.httpGet(url);
    if (!data.task) throw 'UNACK: No task id in Planfix response: ' + JSON.stringify(data);
    return data.task;
  },

  getTaskLink: function (taskId) {
    return Planfix.params.planfix_url + '/task/' + taskId;
  }
};

// helpers
function clean(v) {
  if (v === null || v === undefined) return '';
  v = v.toString();
  if (v === '*UNKNOWN*') return '';
  if (/^\{.*\}$/.test(v)) return ''; // голый неразвёрнутый макрос {SOMETHING}
  return v;
}
function isUnset(v) { return clean(v) === ''; }

// статусы задач в Planfix
var statusWork   = 'В работе';
var statusClose  = 'Завершенная';
var statusCancel = 'Отмененная';

// MAIN
Planfix.logEnabled = false;
Planfix.log(3, 'Webhook started');

try {
  var params = JSON.parse(value);
  Planfix.log(3, 'Params: ' + JSON.stringify(params));

  var planfix = {};
  Object.keys(params).forEach(function (k) { planfix[k] = params[k]; });

  Planfix.setParams(planfix);
  Planfix.validateParams();

  var result  = { tags: {} };

  var taskId  = clean(params.planfix_taskid);
  var comment = clean(params.alert_message || '');
  var ackUser = clean(params.planfix_acknowledge_user || '');
  var userId  = clean(params.planfix_default_userid || '');

  // вытащить user@domain из скобок в {USER.FULLNAME}
  var ackUserUsername = '';
  var m = ackUser.match(/\(([^)]+)\)/);
  if (m && m[1]) { ackUserUsername = m[1]; Planfix.log(3, 'ackUserUsername extracted: ' + ackUserUsername); }

  // флаги состояния
  var actionLine = (comment.split('\n')[0] || '').toLowerCase();
  var isUnAcknowledge = actionLine.indexOf('unacknowledged') !== -1;
  var isAcknowledge   = !isUnAcknowledge && actionLine.indexOf('acknowledged') !== -1;

  var ua = clean(params.update_action || '');
  if (ua !== '') {
    isAcknowledge   = ua === '1';
    isUnAcknowledge = ua === '2';
  }

  var isRecovery   = (params.event_source === '0' && params.event_value === '0');
  var isProblemNew = (params.event_source === '0' && params.event_value === '1' && params.event_update_status === '0');
  var isEscalationCanceled = /escalation\s+cancell?ed/i.test(comment);
  var haveTask     = !isUnset(taskId);

  Planfix.log(3, 'Flags: isAck=' + isAcknowledge + ', isUnAck=' + isUnAcknowledge +
    ', isRecovery=' + isRecovery + ', isProblemNew=' + isProblemNew +
    ', isCancel=' + isEscalationCanceled + ', taskId=' + (taskId || '*empty*'));

  // выбор действия (порядок важен: отмена эскалации проверяется первой)
  if (isEscalationCanceled) {
    if (!haveTask) { Planfix.log(3, 'skip cancel: no taskId'); return JSON.stringify(result); }
    Planfix.log(3, 'Escalation canceled → cancel task');
    Planfix.updateTask(taskId, comment, statusCancel, ackUserUsername || '');

  } else if (isRecovery) {
    if (!haveTask) { Planfix.log(3, 'skip recovery: no taskId'); return JSON.stringify(result); }
    Planfix.log(3, 'Recovery → close task');
    Planfix.updateTask(taskId, comment, statusClose, ackUserUsername || '');

  } else if (isAcknowledge) {
    if (!haveTask) { Planfix.log(3, 'skip ack: no taskId'); return JSON.stringify(result); }
    Planfix.log(3, 'ACK → assign executor');
    Planfix.ackTask(taskId, comment, statusWork, ackUserUsername || '');

  } else if (isUnAcknowledge) {
    if (!haveTask) { Planfix.log(3, 'skip unack: no taskId'); return JSON.stringify(result); }
    Planfix.log(3, 'UNACK → return to default');
    Planfix.unAckTask(taskId, comment, userId || '');

  } else if (isProblemNew) {
    Planfix.log(3, 'New PROBLEM → create task');
    var newTaskId = Planfix.createTask();
    result.tags['__zbx_planfix_taskid'] = newTaskId;
    result.tags['__zbx_planfix_link']   = Planfix.getTaskLink(newTaskId);

  } else {
    if (!haveTask) { Planfix.log(3, 'skip update: no taskId'); return JSON.stringify(result); }
    Planfix.log(3, 'Regular update → add comment');
    Planfix.updateTask(taskId, comment, null, ackUserUsername || ackUser || '');
  }

  Planfix.log(3, 'Result: ' + JSON.stringify(result));
  return JSON.stringify(result);

} catch (error) {
  Planfix.log(3, '[ Planfix Webhook ] ERROR: ' + error);
  throw 'Sending failed: ' + error;
}
				
			

Чек-лист и проверка перед запуском

Вся схема работает на штатных средствах Zabbix 7.0, без промежуточного сервиса; перенос на другую инсталляцию — это правка макросов медиатипа и четырёх эндпоинтов на стороне ПланФикс. Финальный чек-лист, собранный из того, что мы переписывали по ходу проекта:

  • Media type = Webhook: скрипт-автомат на JavaScript, статусы ПланФикс вынесены в переменные.
  • Теги события как память: возвращайте __zbx_* теги при создании и ищите задачу по ним в остальных ветках; включите process_tags=1 и show_event_menu для ссылки из карточки.
  • Шумовой фильтр через эскалацию: операция создания на шаге не ниже 2 плюс условие «Event acknowledged = No», чтобы короткие мигания и быстро подтверждённые проблемы не плодили задачи.
  • Шаблоны для всех состояний: problem, recovery и update, иначе пустой {ALERT.MESSAGE} уронит webhook.
  • Отмена эскалации первой веткой: notify_if_canceled=1 плюс отдельная проверка баннера до восстановления.
  • Порядок против дублей: maxsessions=1, а повторы включайте только при идемпотентном создании с дедупликацией по event_id на приёмнике.
  • Исключайте служебные группы из маршрутизации и фильтруйте suppressed.
  • Сервисный пользователь с единственным каналом даёт чистую схему «действие → пользователь → media type».
  • Проверяйте доставку по каждому действию отдельно, а не по факту «что-то приходит»: иначе легко не заметить, что целый класс алертов (например, disaster) вообще не доходит до трекера.

И последнее — перед первым реальным алертом проверьте эндпоинт вручную, одним запросом curl, имитирующим zabbix-new. Если в ответе нет поля task, скрипт намеренно бросит исключение, поэтому такую проверку стоит сделать заранее:

				
					curl -G 'https://company.planfix.ru/webhook/get/zabbix-new' \
  --data-urlencode 'name=[TEST] проверка интеграции' \
  --data-urlencode 'description=тестовая задача из Zabbix' \
  --data-urlencode 'project=ZABBIX' \
  --data-urlencode 'event_id=999001' \
  --data-urlencode 'host_name=host-07' \
  --data-urlencode 'severity=High'
# ожидаемый ответ: {"task": 12345}
				
			

Итог

Часто задаваемые вопросы

Ответы на часто задаваемые вопросы по теме статьи.

Мы используем встроенный механизм Media Type Webhook в Zabbix. Скрипт на JavaScript отправляет HTTP-запросы к API ПланФикс. Для связи применяется сервисный пользователь Zabbix, который получает уведомления от действий (Actions) и передаёт их в webhook. Это позволяет реализовать двустороннюю интеграцию Zabbix и ПланФикс, используя только штатные средства мониторинга.
Задача создаётся при срабатывании триггера severity Average и выше. Действие (Action) в Zabbix отправляет уведомление на шаге эскалации 2, если проблема не подтверждена (acknowledged). Webhook-скрипт формирует GET-запрос к эндпоинту /webhook/get/zabbix-new, передавая имя хоста, описание проблемы и severity. ПланФикс создаёт задачу и возвращает её ID, который сохраняется в тегах события Zabbix.
Media Type Webhook — это тип канала уведомлений, выполняющий пользовательский JavaScript-код вместо отправки email или SMS. В нашем случае он формирует запросы к REST API ПланФикс. Это гибкий инструмент, позволяющий не просто отправлять текст, а выполнять сложную логику: создавать задачи, менять их статусы и назначать исполнителей в зависимости от контекста события (проблема, восстановление, подтверждение).
Мы устанавливаем maxsessions=1 для строгой последовательности запросов. При создании задачи Zabbix сохраняет её ID в тегах события. Если происходит повторная отправка (retry), скрипт проверяет наличие тега __zbx_planfix_taskid. Если задача уже создана, повторный вызов zabbix-new блокируется или обрабатывается как обновление. Полная защита от дублей требует дедупликации по event_id на стороне ПланФикс.

Константин Тютюнник — ведущий инженер IT For Prof. Специализируется на системах мониторинга и автоматизации IT-процессов: внедрение Zabbix, интеграции с таск-трекерами, webhook-автоматизация инцидентов.

Наши инженеры имеют глубокий опыт настройки сложных систем мониторинга. Если вам требуется надёжная интеграция Zabbix и ПланФикс для автоматизации работы техподдержки, оставьте заявку на консультацию.