Тихон Усков, инженер команды интеграции Zabbix
Zabbix — кастомизируемая платформа, которая используется для мониторинга любых данных. С самых ранних версий Zabbix у администраторов мониторинга была возможность запускать различные скрипты через Actions для проверок на целевых узлах сети. При этом запуск скриптов приводил к возникновению ряда сложностей, в том числе таких, как необходимость поддержки скриптов, их доставки на узлы связи и прокси, а также поддержки разных версий.
JavaScript для Zabbix
В апреле 2019 года был представлен Zabbix 4.2 с функцией предобработки на JavaScript. Многие загорелись идеей отказаться от написания скриптов, которые где-то забирают данные, переваривают их и предоставляют уже в понятном для Zabbix формате, а выполнять простые проверки, которые будут получать неготовые для хранения и обработки Zabbix данные, а потом обрабатывать этот поток данных с использованием средств Zabbix и JavaScript. В связке с низкоуровневым обнаружением и зависимыми элементами данных, которые появились в Zabbix 3.4, получилось достаточно гибкая концепция для сортировки и управления полученными данными.
В Zabbix 4.4, как логическое продолжение предобработки на JavaScript, появился новый способ оповещения — Webhook, который можно использовать для простой интеграции оповещений Zabbix со сторонними приложениями.
JavaScript и Duktape
Почему были выбраны именно JavaScript и Duktape? Рассматривались различные варианты языков и движков:
- Lua – Lua 5.1
- Lua – LuaJIT
- Javascript – Duktape
- Javascript – JerryScript
- Embedded Python
- Embedded Perl
Основными критериями выбора были распространенность, простота интеграции движка в продукт, низкое потребление ресурсов и общая производительность движка, и безопасность внедрения кода на этом языке в мониторинг. По совокупности показателей победил JavaScript на движке Duktape.
Критерии выбора и performance testing
Особенности Duktape:
— Стандарт ECMAScript E5/E5.1
— Модули Zabbix для Duktape:
- Zabbix.log() — позволяет вписать непосредственно в лог Zabbix Server сообщения с различным уровень детализации, что обеспечивает возможность сопоставлять ошибки, например, в Webhook с состоянием сервера.
- CurlHttpRequest() — позволяет делать HTTP-запросы в сеть, на чем основано применение Webhook.
- atob() и btoa() — позволяет кодировать и декодировать строки в формат Base64.
ПРИМЕЧАНИЕ. Duktape соответствует стандартам ACME. В Zabbix используется версия скрипта 2015 года. Последующие изменения незначительны, поэтому их можно игнорировать.
Магия JavaScript
Вся магия JavaScript заключена в динамической типизации и приведении типов: строковых, числовых и логических.
Это означает, что не нужно заранее объявлять какого типа переменная должна возвращать значение.
При математических операциях значения, которые возвращаются операторами-функциями, преобразуются в числа. Исключение из таких операций — сложение, поскольку, если хотя бы одно из слагаемых является строкой, ко всем слагаемым применяется строковое преобразование.
ПРИМЕЧАНИЕ. Методы, отвечающие за такие преобразования, как правило, реализованы в родительских прототипах объектов, valueOf и toString. valueOf вызывается при численном преобразовании и всегда перед методом toString. Метод valueOf обязан возвращать примитивные значения, иначе его результат игнорируется.
Для объекта вызывается метод valueOF. Если он не найден или не возвращает примитивное значение, вызывается метод toString. Если метод toString не найден, производится поиск valueOf в прототипе объекта, и все повторяется до завершения обработки значения и приведения всех значений в выражении к одному типу. Если для объекта реализован метод toString, который возвращает примитивное значение, то именно он используется для строкового преобразования. При этом результатом применения этого метода не обязательно является строка.
Например, если для для объекта 'obj' определяется метод toString,
`var obj = { toString() { return "200" }}`
метод toString возвращает именно строку, и при сложении строки с числом мы получаем склеенную строку:
`obj + 1 // '2001'`
`obj + 'a' // ‘200a'`
Но если переписать toString, чтобы метод возвращал число, при сложении объекта будет выполняться математическая операция с числовым преобразованием и получается результат математического сложения.
`var obj = { toString() { return 200 }}`
`obj + 1 // '2001'`
При этом, если мы выполняем сложение со строкой, выполняется строковое преобразование, и мы получаем склеенную строку.
`obj + 'a' // ‘200a'`
Именно в этом кроется причина большого количества ошибок начинающих пользователей JavaScript.
В метод toString можно вписать функцию, которая будет увеличивать текущее значения объекта на 1.
Выполнение скрипта при условии, что переменная равна 3, и она же равна 4.
При сравнении с приведением типов (==) каждый раз выполняется метод toString с функцией увеличения значения. Соответственно, при каждом последующем сравнении значение увеличивается. Этого можно избежать путем использования сравнения без приведения типов (===).
Сравнение без приведения типов
ПРИМЕЧАНИЕ. Не используйте сравнение с приведением типов без необходимости.
Для сложных скриптов, например, Webhook со сложной логикой, в которых необходимо сравнение с приведением типов, рекомендуется предварительно написать проверки для значений, которые возвращают переменные и обработать несоответствия и ошибки.
Webhook Media
В конце 2019 года и в начале 2020 года команда интеграции Zabbix занималась активной разработкой Webhooks и интеграций «из коробки», которые поставляются в дистрибутиве Zabbix.
- Discord
- Jira
- Jira Service Desk
- Mattermost
- Microsoft Teams
- Opsgenie
- OTRS
- Pagerduty
- Pushover
- Redmine
- ServiceNow
- SINGL4
- Slack
- Telegram
- Zammad
- Zendesk
Ссылка на документацию
Preprocessing
- Появление предобработки на JavaScript позволило отказаться от большинства внешних скриптов, и в настоящее время в Zabbix можно получить любое значение и преобразовать его в совершенно другое любое значение.
- Предобработка в Zabbix реализована кодом на JavaScript, который при компиляции в байт-код преобразуется в функцию, принимающую единственное значение в виде параметра value в виде строки (в строке может быть и цифра, и число).
- Поскольку на выходе получается функция, в конце скрипта обязателен return.
- Возможно использование пользовательских макросов в коде.
- Ресурсы можно ограничить не только на уровне операционной системы, но и программно. Для шага предобработки выделяется максимум 10 мегабайт оперативной памяти и лимит времени выполнения в 10 секунд.
ПРИМЕЧАНИЕ. Значения тайм-аута в 10 секунд достаточно много, потому что сбор условных тысяч элементов данных за 1 секунду по достаточно «тяжелому» сценарию предобработки может замедлить работу Zabbix. Поэтому не рекомендуется использовать предобработку для выполнения полноценных скриптов на JavaScript через так называемые теневые элементы данных (dummy items), которые запускаются только для выполнения предобработки.
Проверить свой код можно через тест предобработки или с помощью утилиты zabbix_js:
`zabbix_js -s *script-file -p *input-param* [-l log-level] [-t timeout]`
`zabbix_js -s script-file -i input-file [-l log-level] [-t timeout]`
`zabbix_js -h`
`zabbix_js -V`
Практические задачи
Задача 1
Заменить вычисляемый элемент данных предобработкой.
Условие: получаем с датчика температуру в градусах по Фаренгейту для хранения в градусах по Цельсию.
Раньше мы создали бы элемент данных, который собирает температуру в градусах по Фаренгейту. После этого — еще один элемент данных (вычисляемый), который по формуле преобразовывал бы градусы по Фаренгейту в градусы по Цельсию.
Проблемы:
- Необходимо дублировать элементы данных и хранить все значения в базе.
- Необходимо согласовать интервалы для «родительского» элемента данных, который вычисляется и используется в формуле, и для вычисляемого элемента данных. В противном случае вычисляемый элемент данных может перейти в неподдерживаемое состояние или посчитать предыдущее значение, что скажется на надежности результатов мониторинга.
Одним из решений был отказ от гибких интервалов проверок в пользу фиксированных интервалов, чтобы гарантированно вычислять вычисляемый элемент данных после элемента данных, получающего данные (в нашем случае — температура в градусах по Фаренгейту).
Но если, например, шаблон мы используем для проверки большого количества устройств, и проверка выполняется раз в 30 секунд, 29 секунд Zabbix «халтурит», а в последнюю секунду начинает проверки и вычисления. Это приводит к созданию очереди и влияет на производительность. Поэтому рекомендуется использовать фиксированные интервалы, только если это действительно необходимо.
В данной задаче оптимальное решение — предобработка из одной строки на JavaScript, которая конвертирует градусы по Фаренгейту в градусы по Цельсию:
`return (value - 32) * 5 / 9;`
Это быстро и просто, не нужно создавать лишних элементов данных и хранить по ним историю, а также можно использовать для проверок гибкие интервалы.
`return (parseInt(value) + parseInt("{$EXAMPLE.MACRO}"));`
Но, если в гипотетической ситуации необходимо полученный элемент данных сложить, например, с какой-либо константой, определенной в макросе, необходимо учитывать, что параметр value раскрывается в строку. При операции сложения строк, две строки просто объединяются в одну.
`return (value + "{$EXAMPLE.MACRO}");`
Для получения результата математического действия, необходимо привести типы полученных значений в числовой формат. Для этого можно использовать функцию parseInt(), которая выдает целое число, функцию parseFloat(), которая выдает десятичную дробь, или функцию number, которая выдает целое число или десятичную дробь.
Задача 2
Получить время в секундах до окончания сертификата.
Условие: некий сервис выдает дату окончания сертификата в формате "Feb 12 12:33:56 2022 GMT".
В ECMAScript5 Date.parse() принимает дату в формате ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ). Необходимо привести к нему строку в формате MMM DD YYYY HH:mm:ss ZZ
Проблема: значение месяца выражено текстом, а не числом. Данные в таком формате не принимаются Duktape.
Пример решения:
В первую очередь объявляется переменная, которая принимает значение (весь скрипт — объявление переменных, которые перечислены через запятую).
В первой строке мы получаем дату в параметре value и разделяем ее пробелами методом split. Таким образом, мы получаем массив, где каждому элементу массива, начиная с индекса 0, соответствует один элемент даты до и после пробела. split(0) — месяц, split(1) — число, split(2) — строка с временем и т. д. После этого к каждому элементу даты можно обращаться по индексу в массиве.
`var split = value.split(' '),`
- Каждому месяцу (в хронологическом порядке) соответствует индекс его положения в массиве (с 0 до 11). Чтобы сконвертировать текстовое значение в числовое, к индексу месяца прибавляется единица (потому что нумерация месяцев начинается с 1). При этом выражение с прибавлением единицы взято в скобки, потому что в противном случае будет получена строка, а не число. В конце мы выполняем slice() — срез массива с конца, чтобы оставить только два символа (что важно для месяцев с двузначным номером).
`MONTHS_LIST = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],`
`month_index = ('0' + (MONTHS_LIST.indexOf(split[0]) + 1)).slice(-2),`
- Формируем из полученных значений строку в формате ISO обычным сложением строк в соответствующем порядке.
`ISOdate = split[3] + '-' + month_index + '-' + split[1] + 'T' + split[2],`
Данные в полученном формате — количество секунд с 1970 года до какого-то момента в будущем. Использовать данные в полученном формате в триггерах практически невозможно, потому что Zabbix позволяет оперировать только макросами {Date} и {Time}, которые возвращают дату и время в понятном для пользователя формате.
- После этого мы можем получить в JavaScript текущую дату в формате Unix Timestamp и вычесть ее из полученного значения даты окончания сертификата, чтобы получить количество миллисекунд с текущего момента до момента окончания сертификата.
`now = Date.now();`
- Делим полученное значение на тысячу, чтобы получить секунды в Zabbix.
`return parseInt((Date.parse(ISOdate) - now) / 1000);`
В триггере можно указать выражение 'last<' и набор цифр, который соответствует количеству секунд в периоде, на которое необходимо отреагировать, например, в неделях. Таким образом, триггер будет оповещать о том, что срок действия сертификата заканчивается через неделю.
ПРИМЕЧАНИЕ. Обратите внимание на использование parseInt() в функции return, чтобы сконвертировать дробное число, полученное в результате деления миллисекунд, в целое число. Также можно использовать parseFloat() и хранить дробные данные.