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

Мы настроили интеграцию Zabbix и ПланФикс так, что алерт автоматически проходит полный жизненный цикл: от создания задачи до её закрытия при восстановлении сервиса, исключая ручной труд дежурных инженеров.
Содержание
Задача: алерты Zabbix вручную превращаются в задачи ПланФикс
Интеграция Zabbix и ПланФикс закрывает разрыв между мониторингом и учётом работ: пока проблема видна только в Zabbix, задача для дежурной смены заводится в ПланФикс руками. На наших проектах это означало потерянные инциденты, отсутствие истории «проблема ↔ задача» и ручную рутину на каждом срабатывании триггера.
Дано: боевая инсталляция Zabbix 7.0, около 260 хостов разных клиентов и порядка 150 шаблонов. Заявки и работа дежурных у нас ведутся в ПланФикс, поэтому именно туда должны попадать значимые события мониторинга. До автоматизации связка держалась на внимательности оператора: увидел алерт, скопировал, создал задачу, не забыл закрыть. На потоке из сотен хостов такая схема даёт сбои ежедневно.
Мы поставили цель: двусторонняя автоматическая связка «проблема Zabbix ↔ задача ПланФикс» без ручного труда и без отдельного промежуточного сервиса. Принципиальное ограничение: никаких внешних демонов и сторонних шин, только то, что умеет сам Zabbix. Любой дополнительный сервис пришлось бы мониторить отдельно, а это новая точка отказа в системе, которая как раз и следит за отказами.
К решению мы предъявили четыре требования. Во-первых, реакция не на каждый «чих»: проблема, мигнувшая на 30 секунд, не должна плодить задачу. Во-вторых, полный жизненный цикл: создание, перевод в работу, закрытие и отмена. В-третьих, идемпотентность по возможности, чтобы повторная доставка не порождала дубли. В-четвёртых, выборочность: задачи только по значимым проблемам и не по всем группам хостов, потому что часть парка составляют внутренние и партнёрские группы, которым место в трекере не нужно.
Не пытайтесь решить это «уведомлением на почту с парсером»: мы сталкивались с тем, что почтовый канал не возвращает в Zabbix идентификатор задачи, и связать восстановление триггера с конкретной строкой в трекере потом нечем. Именно требование вернуть task id обратно в событие определило всю дальнейшую архитектуру.

Архитектура: 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_url | https://company.planfix.ru |
| planfix_project | ZABBIX |
| 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. Задайте параметры доставки — они определяют порядок и надёжность отправки:
| Параметр | Значение | Зачем |
|---|---|---|
| maxsessions | 1 | строгий порядок: create обязан уйти раньше, чем edit или close по той же задаче |
| maxattempts | 3 | пережить кратковременную недоступность ПланФикс |
| attempt_interval | 30s | пауза между повторами |
| timeout | 30s | лимит исполнения скрипта |
| notify_if_canceled | 1 | без него не придёт уведомление об отмене эскалации |
Значение 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-ack | acknowledge | назначает исполнителя, «В работе» |
| /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 disaster | Disaster (5) | 2 мин | 2 |
| PlanFix | Report high | High (4) | 5 мин | 2 |
| PlanFix | Report average | Average (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 canceled | zabbix-edit | Отмененная |
| Восстановление (OK) | event_value = 0 | zabbix-edit | Завершенная |
| Acknowledge | первая строка содержит acknowledged | zabbix-ack | В работе |
| Снятие acknowledge | первая строка содержит unacknowledged | zabbix-unack | исполнитель по умолчанию |
| Новая проблема | event_value=1 , event_update_status=0 | zabbix-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, '
').replace(/(
\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, '
').replace(/(
\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, '
').replace(/(
\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, '
').replace(/(
\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}
Итог
- Интеграция Zabbix и ПланФикс реализована через штатный Media Type Webhook без внешних демонов-посредников.
- Использование тегов события (__zbx_planfix_taskid) позволяет хранить ID задачи прямо в Zabbix, обеспечивая идемпотентность операций.
- Фильтрация шума достигается настройкой эскалации на шаг 2 с условием Event acknowledged = No, отсекая кратковременные сбои.
- Обработка отмены эскалации (notify_if_canceled=1) предотвращает появление задач-дублей и «висящих» инцидентов при отключении хостов.
- Строгий порядок доставки (maxsessions=1) гарантирует, что операция закрытия задачи не выполнится раньше её создания.
Часто задаваемые вопросы
Ответы на часто задаваемые вопросы по теме статьи.
Константин Тютюнник — ведущий инженер IT For Prof. Специализируется на системах мониторинга и автоматизации IT-процессов: внедрение Zabbix, интеграции с таск-трекерами, webhook-автоматизация инцидентов.
Наши инженеры имеют глубокий опыт настройки сложных систем мониторинга. Если вам требуется надёжная интеграция Zabbix и ПланФикс для автоматизации работы техподдержки, оставьте заявку на консультацию.




