Привет, Habr! Меня зовут Сергей, я инженер в ДОМ.РФ, и одна из моих задач — развитие ИТ-мониторинга. В этой статье я расскажу, как мы самостоятельно и с минимальными затратами пришли от идеи к рабочему инструменту, с помощью которого в одном месте в простой и понятной форме можно определить текущее здоровье наших систем.
В идеале итоговый результат должен выглядеть как-то так:

При этом информация должна собираться из разных источников.
Вводные:
-
единый интерфейс, отображающий все ИТ-системы в виде иконок со статусами,
-
визуально понятные статусы,
-
звуковое оповещение в случае изменения статуса работоспособности в разрезе каждой ИТ-системы,
-
возможность просмотра расширенной информации, а именно — причин, повлиявших на статус работоспособности.
Чем подобный инструмент должен помочь:
-
уменьшить время реакции дежурной смены при массовых сбоях,
-
поможет коррелировать события между зависимыми ИТ-системами,
-
прозрачно укажет очаги возгорания в ИТ для людей как углубленных, так и не углубленных в ИТ.
Звучит удобно? Удобно!
Идея на самом деле не нова и на первый взгляд нам могла бы подойти какая-либо система зонтичного мониторинга, коих, благо, на рынке сейчас достаточное количество, есть из чего выбирать. Плюс надо отталкиваться от того, что у нас уже есть, в частности, с чем этот инструмент интегрировать.
А это минимум Zabbix, являющейся у нас основной системой мониторинга, в котором уже заведены все ИТ-системы. Он же занимается агрегацией событий из смежных систем мониторинга. Он же отвечает за визуализацию событий дежурной смене.
Не забываем про существование Grafana, которая визуализирует дашборды команд. Тут появляется еще одно требование к инструменту: возможность добавлять к сущностям ИТ-сервисов линки на дашборды Grafana.
К большим enterprise душа сразу не лежала, и вот почему:
-
муки выбора: быстро найти подходящий инструмент непросто, а это означает, что нас ждут длительные пилоты,
-
закупки — пилот → бюрократия → промышленная эксплуатация, путь далеко не быстрый,
-
доработки: в случае возникновения каких‑либо дополнительных потребностей мы зависимы от поставляемого продукта. Мало того, что надо договориться с вендором о возможности кастомизации, так еще и 90% вероятность, что это приведет к пункту закупок,
-
интеграции — очень каверзный пункт. У нас есть два варианта развития событий: как правило, многие системы зонтичного мониторинга являются самостоятельным продуктом или агрегатором событий/данных из нижнеуровненвых систем мониторинга.
Это означает, что все уже имеющиеся сущности ИТ-сервисов из Zabbix придётся дублировать в новую систему. Одно дело — скопипастить названия, если предусмотрена тонкая интеграция, другое дело — выстроить зависимости (как событие из системы мониторинга «А» должно отобразиться в верхнеуровневой системе «Б»), перенести правила оповещений, корреляцию событий и взаимосвязи систем.
Далее необходимо запустить процесс обработки событий. Из чего появляются требования к обработке событий. В привычном Zabbix есть возможности проставлении статусов обработки событий, временного подавления события и снижения критичности.
Необходимо обучить персонал дежурной смены и администраторов/владельцев ИТ-систем пользоваться новым продуктом, в частности обработки событий и самостоятельной корректировки параметров мониторящихся ИТ-систем.
Необходим продукт с тонкой интеграцией с Zabbix. Который будет только подхватывать активные события и визуализировать их, то есть вся логика останется на стороне Zabbix.
Был один вариант (название умолчим), который мог собирать и обрабатывать события из Zabbix по API, но делал он это минут 15 на одну итерацию. Согласитесь, «такой себе» оперативный мониторинг. Вишенкой стало отсутствие возможности корреляции событий между смежными ИТ-системами.
Подходим к разработке
А раз уж это в любом случае в каком-то виде разработка, можно подумать и об обслуживании инструмента. Если это самостоятельное приложение, оно обязательно должно быть построено на HA-архитектуре, что несет за собой дополнительные трудозатраты на разработку + дальнейшее обслуживание.
А если нет? У нас же есть Grafana! Grafana умеет в HA, легко обслуживается, она используется как основной инструмент визуализации метрик для команд. Используя именно её как инструмент отображения статусов ИТ-систем, мы избавляемся от лишних переходов между инструментами и лишнего балласта, который бы нам пришлось в последующем обслуживать.
А может, получится обойтись малой кровью без разработки?
Посмотрим на возможности, предоставленные из коробки:
-
Grafana имеет интеграцию с Zabbix. — Хорошо
-
Grafana из системы Zabbix может визуализировать метрики, количество проблем по какой-либо хост группе, статус «Услуга». — Вроде хорошо, но нет. Все это не подходит. Сущности «Услуга» — это заготовка от Zabbix для визуализаций зонтичного типа, здесь боль в том, что услуги заполняются вручную. Вручную, Карл! Не наш путь. Остальное совсем мимо.
-
У Grafana есть различные плагины для визуализаций, наиболее подходящая из коробки это «Stat». — Вроде хорошо, но нет. «Stat» — отображает красивую панельку, можно убрать тайтл, отображать имена значений, а не само значение, но нет визуального и звукового оповещения + не вставить два значения в одну плашку. Не подходит.

Значит, от разработки не уйти
Ок. Настало время структурировать требования, дополнить их, и собрать в единое ТЗ. Которое вкратце выглядит так:
-
единый дашборд отображающий все ИТ-системы в виде плашек,
-
возможность перемещения, изменения размеров плашек для визуального обозначения значимости ИТ-системы,
-
визуальное отображение статусов работоспособности в разрезе каждой ИТ-системы /плашки в виде цветового индикатора,
-
визуальное (blink — мигать) и звуковое оповещение в случае изменения статуса работоспособности в разрезе каждой ИТ-системы/плашки,
-
возможность добавления своих ссылок/линков к сущностям ИТ-системы /плашки,
-
отображать статусы работоспособности ИТ-систем, основываясь на событиях в системе Zabbix,
-
оперативно отображать изменения статусов с задержкой не более 1 минуты от времени возникновения событий,
-
при отображении статусов учитывать обработку событий в системе Zabbix (периоды технического обслуживания, подавление событий, снижение уровней критичности, а так же статус обработки),
-
отображение на сущностях ИТ-системы /плашки количества не обработанных событий если таковые имеются,
-
второй уровень дашборда , возможностью просмотра расширенной информации (текущий статус, события, повлиявшие на статус работоспособности ИТ-системы /плашки, список текущих активных проблем, отображение сущностей мониторинга, входящих в ИТ-систему в виде плашек с отображением статусов работоспособности сущности).
Рассмотрим техническую составляющую
Определимся, каким образом будем получать из Zabbix статусы ИТ-систем и количество необработанных событий.
Какие есть варианты:
-
собирать статусы активных событий из Zabbix по API, структурировать и агрегировать их, далее перекладывать их в какую-то систему хранения, из которой Grafana будет брать статусы,
-
собирать лог событий, складывать в Elastic, структурировать и агрегировать их, далее перекладывать их в какую-то систему хранения, из которой Grafana будет брать статусы,
-
собирать лог событий, складывать в Elastic, собирать статус Grafana из Elastic,
-
направлять события в виде оповещения стандартными средствами Zabbix в обработчик, которым складывать в какую-то систему хранения, из которой Grafana будет брать статусы.
Минусы везде схожи. А именно: возможна длительная обработка событий, дополнительное ПО — инструмент нуждающийся в периодическом обслуживании.
В итоге мы решили брать статусы событий напрямую из БД Zabbix, структурировать и агрегировать их на уровне SELECT.
Минусы данного подхода: дополнительная нагрузка на БД. Но нагрузка незначительная + она все равно была при использовании любого другого метода, меньше, но была бы.
Для визуализации в Grafana нужен новый плагин, который будет учитывать все требования, описанные выше. Какие есть варианты:
-
писать плагин с нуля. — Боль, подрядчиков нанимать не хочется из соображений описанных про enterprise, у своих разработчиков и без нас дедлайны горят.
-
поискать исходники плагина «Stat» или любого другого подходящего по смыслу стороннего плагина и дописать необходимый функционал. — и такой плагин есть! Status Panel, нам подходит.
Status Panel — плагин, способный отображать визуально статус какой-либо сущности, основываясь на нескольких показателях сущности + отображать эти показатели на этой же плашке (в отличие от того же коробочного «Stat»). А самое главное — у него открытый исходный код!
Фактически мы получаем весь нужный функционал, а недостающий можем дописать. Да, есть минусы — в отсутствии поддержки (автор его забросил два года назад) и количестве поддерживаемых источников данных.
Приступаем к применению напильника
Далее мы разберем, как прорабатывался верхнеуровневый дашборд с отображением статусов работоспособности ИТ-систем. Второй слой с расширенной информацией рассматривать не будем. Так как в Zabbix уровней severity шесть, а в Status Panel из коробки всего два, оперировать будем матчингом Zabbix High → Status Panel Warning, Zabbix Disaster → Status Panel Critical. Для нас достаточно отображения последних двух уровней severity из Zabbix. В случае необходимости добавить в Status Panel дополнительные severity можно путем правки statis_ctrl.js — то есть добавить в код недостающие severity по аналогии с уже имеющимися.
Начнем с запросов. Сейчас стоит задача собрать по одной ИТ-системе статус и количество необработанных событий. Для выборки данных по ИТ-системе можно отталкиваться от хост-групп или тегов, тут кому как удобнее. Мы пошли по пути тегов, так как теги можно раскидать где угодно, тем самым покрыть зависимости систем от друг друга. Для начала отобразим в стандартном плагине Stat, дабы убедиться, что все работает:

Пример одного из запросов:
SELECT CASE WHEN (COALESCE(max(severity), '-1') = 4) THEN 'High' WHEN (COALESCE(max(severity), '-1') = 5) THEN 'Disaster' END as "Severity" FROM ( SELECT DISTINCT e.severity, e.acknowledged, e.name, t.triggerid from triggers t LEFT JOIN functions f ON ( f.triggerid = t.triggerid ) LEFT JOIN items i ON ( i.itemid = f.itemid ) LEFT JOIN events e ON ( e.objectid = t.triggerid ) LEFT JOIN problem_tag pt ON ( pt.eventid = e.eventid ) LEFT JOIN problem p ON ( p.eventid = e.eventid ) LEFT JOIN hosts ON ( i.hostid = hosts.hostid ) left join trigger_depends td on t.triggerid = td.triggerid_down where pt.tag='IT-SYSTEM' AND pt.value='SYSTEM-NAME' AND i.status =0 AND t.status =0 AND hosts.status =0 AND (e.object-0)=0 AND e.severity > 3 AND p.r_eventid IS NULL AND hosts.maintenance_status = 0 AND e.eventid NOT IN (select eventid from event_suppress where userid IS not NULL) ) AS row
Далее плагин. Для начала запустим AS IS с одним запросом выборки статусов. Заранее известно, что запрос должен вернуть High. Указываем матчинг с нашей метрикой Severity, текстовые трешхолды. Но трешхолд не работает, хотя плашка должна была изменить цвет и показать, по какому параметру засветилась.

Почему ничего не изменилось? Есть такой пункт Use ‘Disable’ color if no data, который в явном виде дал понять, что скрипт данных не видит. В принципе, ожидаемо, в доке есть упоминание:
Currently the plugin was tested with influxDB and Graphite. Support for other data sources could be added by demand
Благо это js, а значит путем просмотра кода и DevTools браузера сможем разобраться, что к чему. Основной код плагина находиться в файле statis_ctrl.js, за рендер отвечает функция onRender(), т.к. у нас нет данных по мнению этой функции, посмотрим, какие данные в неё приходят. За передачу данных рендеру отвечает функция onDataReceived(), которая уже берет данные из запроса, консолидирует данные в seriesHandler и возвращает обратно на рендер.
onDataRecevied()
key: "onDataReceived", value: function onDataReceived(dataList) { this.series = dataList.map(StatusPluginCtrl.seriesHandler.bind(this)); this.render(); seriesHandler key: "seriesHandler", value: function seriesHandler(seriesData) { var series = new TimeSeries({ datapoints: seriesData.datapoints, alias: seriesData.target }); series.flotpairs = series.getFlotPairs("connected"); return series;
Как видно из кода, от Grafana ожидаются данные target и datapoints. Посмотрим, что мы получаем фактически. Для этого выведем в лог данные из переменной dataList (console.log(dataList);):
Как видим, при запросе к БД вида table мы не получаем данные в необходимом для нас виде. Два варианта, либо написать обработчик, либо переделать запрос возвращающий TimeSeries. Изначально мы пошли по неправильному пути и написали обработчик.
onDataRecevied()
key: "onDataReceived", value: function onDataReceived(dataList) { try { this.series = dataList.map(StatusPluginCtrl.seriesHandler.bind(this)); this.render(); } catch (e) { var newserie2 = new Array; for (var j = 0; j < dataList.length; j++) { for (var i = 0; i < dataList[j].rows.length; i++) { var newserie = { "alias": `${dataList[j].columns[i]['text']}`, "target": `${dataList[j].columns[i]['text']}`, "datapoints": [[dataList[j].rows[i][0],0],[dataList[j].rows[i][0],1]] }; newserie2[j] = newserie; } } this.series = newserie2.map(StatusPluginCtrl.seriesHandler.bind(this)); this.render();
Да, работает, но есть вариант проще: добавляем в запрос выборку time, чтобы Grafana возвращала TimeSeries.
Пример одного из запросов:
………….
END as «Severity», extract(epoch from now()) as time FROM (
………….

И о, чудо! Можно приступать к остальным пунктам тз.
Начнем с отображения данных. Из коробки доступны варианты: выводить метрику по достижению какого-либо трешхолда либо выводить только текст/значение метрики, не отталкиваясь на трешхолды. Нам не подходит. Надо сделать/дописать две вариации вывода:
-
не отображать метрику совсем, при этом использовать трешхолд (используем для Severity, в случае изменения статуса работоспособности ИТ-системы у нас должен поменяться цвет, зачем нам еще текстом это выводить, тем самым занимая место?)
Поступим немного по-варварски: в файле module.html, есть блоки, отвечающие за выводы метрик, наподобие:
module.html
<div ng-style="ctrl.panel.colorMode === 'Metric' && {'color':ctrl.panel.colors.crit}" ng-class="{'boldAlertMetric': ctrl.panel.fontFormat === 'Bold', 'italicAlertMetric': ctrl.panel.fontFormat === 'Italic'}" ng-repeat="crit in ctrl.crit"> <a ng-show="crit.url" ng-href="{{ crit.url | interpolateTemplateVars:this }}" target="_blank" style="color:inherit">{{ crit.alias }}</a> <span ng-hide="crit.url">{{ crit.alias }}</span> <span ng-show="crit.isDisplayValue"> - {{ crit.display_value | numberOrTextWithRegex : crit.valueDisplayRegex }}</span> </div>
Упростим его до:
module.html
<div ng-style="ctrl.panel.colorMode === 'Metric' && {'color':ctrl.panel.colors.crit}" ng-class="{'boldAlertMetric': ctrl.panel.fontFormat === 'Bold', 'italicAlertMetric': ctrl.panel.fontFormat === 'Italic'}" ng-repeat="crit in ctrl.crit"> <span ng-show="crit.isDisplayValue">{{ crit.alias }} - {{ crit.display_value | numberOrTextWithRegex : crit.valueDisplayRegex }}</span> </div>
Тем самым мы убрали привязку к выбору меню Display Alias и оставили только Display Value, зависящую от переменной isDisplayValue. Соглашусь, не самый хороший ход — вырезать часть имеющегося функционала из коробки, но под наши нужды сойдет.
Отображать метрику только в случае прохождения порога, но не учитывать в состоянии самой плашки (используем для Not acknowledged — отображения не обработанных событий).
Добавим свой Hendler Type. Так как нам необходимо отображать метрику только в случае превышения трешхолда, и у нас он будет неизменный, то зададим его в явном виде в коде и назовем Hendler Type, соответственно, ‘More Zero’.
statis_ctrl.js
----- < _this.valueHandlers = ['Number Threshold', 'String Threshold', 'Date Threshold', 'Disable Criteria', 'Text Only']; -- > _this.valueHandlers = ['Number Threshold', 'String Threshold', 'Date Threshold', 'Disable Criteria', 'Text Only', 'More Zero']; ----- <} else if (target.valueHandler == "Text Only") { <_this5.handleTextOnly(s, target); -- >} else if (target.valueHandler == "Text Only") { >_this5.handleTextOnly(s, target); >} else if (target.valueHandler == "More Zero") { >_this5.handleMoreZero(s, target); ----- < }, { <key: "updatePanelState", -- >}, { >key: "handleMoreZero", >value: function handleMoreZero(series, target) { >if (series.displayType == "Annotation") { >this.annotation.push(series); >} else { >if (series.display_value == 0 ) { >delete series.isDisplayValue; >} >this.display.push(series); >} >} >}, { >key: "updatePanelState", -----
Уже неплохо, осталось добавить визуальное и звуковое оповещение. Для визуального оповещения воспользуемся эффектом анимации, применяемым к плашке. В данном случае нам необходимо учесть, что анимация должна появляться, когда изменяется статус и пропадать если статус сохраняется. Отталкиваться будем от обновления данных на странице. Так как встраивать все это дело будем в js, нужно как-то между собой различать плашки.
CSS по линку выше добавляем к нашим CSS плагина (сразу не много поправим, чтобы эффект не вызывал эпилепсию):
/css/status_panel.css .blink { animation-name: blinker; animation-iteration-count: infinite; animation-timing-function: cubic-bezier(1.0,2.0,0,1.0); animation-duration: 1s; animation-play-state: running; -webkit-animation-name: blinker; -webkit-animation-iteration-count: infinite; -webkit-animation-play-state: running; -webkit-animation-timing-function: cubic-bezier(1.0,2.0,0,1.0); -webkit-animation-duration: 1s; } @keyframes blinker { from { opacity: 1.0; } 50% { opacity: 0.5; } to { opacity: 1.0; } } @-webkit-keyframes blinker { from { opacity: 1.0; } 50% { opacity: 0.5; } to { opacity: 1.0; } }
Далее строим логику в js, чтобы эффекты применялись когда нужно. Для начала объявим переменную массив для хранения статусов, где-нибудь в начале кода:
statis_ctrl.js
----- <}(); <panelDefaults = { -- >}(); >var GlobalpanelState = []; >panelDefaults = { -----
Ну и сама логика, за обновление статусов отвечает функция updatePanelState(), до и после описывать не будем, конечный результат выглядит вот так:
updatePanelState()
key: "updatePanelState", value: function updatePanelState() { this.$panelContainer.removeClass('blink'); if (this.duplicates) { this.panelState = 'error-state'; } else if (this.disabled.length > 0) { this.panelState = 'disabled-state'; } else if (this.crit.length > 0) { this.panelState = 'error-state'; } else if (this.warn.length > 0) { this.panelState = 'warn-state'; } else if ((this.series == undefined || this.series.length == 0) && this.panel.isGrayOnNoData) { this.panelState = 'no-data-state'; } else { this.panelState = 'ok-state'; } if ((GlobalpanelState[this.panel.id] != 'notplaycrit') & this.panelState === 'error-state') { const music = new Audio('https://zabbix/audio/alarm_disaster.mp3').play(); if (typeof GlobalpanelState[this.panel.id] != "undefined") { this.$panelContainer.addClass('blink'); } GlobalpanelState[this.panel.id] = 'notplaycrit'; } else if ((GlobalpanelState[this.panel.id] != 'notplayok') & this.panelState === 'ok-state') { const music = new Audio('https://zabbix/audio/alarm_ok.mp3').play(); if (typeof GlobalpanelState[this.panel.id] != "undefined") { this.$panelContainer.addClass('blink'); } GlobalpanelState[this.panel.id] = 'notplayok'; } else if ((GlobalpanelState[this.panel.id] != 'notplayhigh') & this.panelState === 'warn-state') { const music = new Audio('https://zabbix/audio/alarm_high.mp3').play(); if (typeof GlobalpanelState[this.panel.id] != "undefined") { this.$panelContainer.addClass('blink'); } GlobalpanelState[this.panel.id] = 'notplayhigh'; } }
Некоторые особенности:
В процессе добавления функционала все правки плагина были сделаны “на живую”, непосредственно в коде самого установленного плагина. С восьмой версии Grafana появилась проверка валидации плагина. В случае, если в коде какого-либо плагина были какие-то изменения, Grafana не подхватит его при последующей перезагрузке. Чтобы этого не произошло, плагин надо пересобрать и переподписать. Сходу пересобрать не получилось, но переподписать то, что получилось, нам это не мешает. Для этого регистрируемся на сайте Grafana, получаем API-key роли PluginPublisher. Задаем переменную окружения GRAFANA_API_KEY. Устанавливаем nodejs с npx. Устанавливаем @grafana/toolkit. Качаем исходники плагина, заменяем файлы в директории dict нашими. И запускаем из корня исходников приватное подписывание плагина: «npx @grafana/toolkit plugin:sign —rootUrls http://ваш_url_grafana/».
После чего будет получен заветный файл MANIFEST.txt, который поможет нам пройти валидацию, для этого не обходимо в директории плагина заменить файл на полученный.
Планы на будущее:
-
переписать плагин полностью под актуальный фреймворк использующейся в Grafana,
-
пересобрать и переподписать плагин уровнем community, а не private,
-
вернуть недостающий функционал, все возможные переменные добавить в поля редактирования,
-
поглотить наработки по плагину «Status By Group Panel».
ссылка на оригинал статьи https://habr.com/ru/articles/726924/
Добавить комментарий