Как приготовить тосты и заодно визуализировать ИТ-системы
Давайте вообразим себе следующее: вы работаете в организации (она может быть и вашей), в которой есть бизнес-процесс, поддерживаемый одной или несколькими ИТ-системами. Я точно знаю, что есть система мониторинга. Дальше видение немного расплывается, и непонятно: промышленная это система или бесплатная Open Source. Однако у вас случаются ситуации, когда все датчики на ней зеленые, но сам бизнес-процесс дает необъяснимые сбои, демонстрируя снижение ключевых показателей. Как вспышка электрошокера представителя правопорядка на несанкционированном митинге, в вашей голове проскакивает мысль о том, что ситуация вышла из-под контроля и нужно немедленно действовать. Только вот непонятно как. Скажу вам, это достаточно распространенный кейс, от проявлений которого желательно поскорее отделаться. Системный подход к решению данной проблемы обеспечит составление модели сервиса.
Том Вуджек, который оказывает услуги по визуализации процессов, происходящих в компаниях (не только ИТ-), провел одно любопытное исследование. Он попросил разных людей нарисовать процесс приготовления тостов. Ниже приведены некоторые результаты этой работы.
Что же мы видим на многих картинках? Правильно! Объекты и связи, присутствующие в любой системе. Чем их больше, тем более системным будет подход. Правильная степень гранулярности позволит более точно отслеживать «здоровье» бизнес-системы. Для построения схемы вы можете попробовать использовать Visio, но гораздо интереснее взять маркер для отрисовки связей, клейкие листочки для объектов и изобразить вашу систему на маркерной доске. Чем большее количество желтых листочков, тем больше связей, тем выше шанс определить максимальное количество точек мониторинга для точного определения источника проблемы.
А теперь пришло время рассказать о наших наработках в области расширения стандартного функционала Zabbix и применения системного подхода, описанного выше. Их ровно две.
Первая — составление карты систем и создание тепловой карты сервисов. Учитывая наш серьезный опыт в части мониторинга банковских бизнес-процессов, пример мы приведем именно из этой области. Рассмотрим три самые типичные банковские системы. Если в вашем банке есть более типичные системы, прошу меня извинить — их мы рассматривать тут не будем.
Система дистанционного банковского обслуживания (ДБО):
Корпоративная шина передачи данных (ESB):
Система принятия решений (СПР):
В нашем приложении это будет выглядеть следующим образом (да, структура несколько нарушается, но при этом наглядность остается):
В случае необходимости можно перейти на уровень ниже. И, что не менее важно, при наведении на объект всплывает pop-up окно с описанием события и ссылкой на график в Zabbix:
Благодаря такому подходу и заданной степени детализации, на выходе мы получаем удобный дашборд, а заодно простой инструмент локализации проблемы. Несколько слов о функциональных возможностях нашей системы:
— визуализация зависимостей между корпоративными системами;
— настройка степени влияния компонентов друг на друга (вес связи);
— интеграция с Zabbix (объекты на тепловой карте связаны с триггерами);
— всплывающие окна с текстом события при наведении на объект;
— визуальный интерфейс настройки связей объектов;
— визуальный интерфейс настройки связи объектов с триггерами Zabbix.
В качестве примера приведу несколько уже разработанных интерфейсов.
Добавление объектов на тепловую карту:
Добавление интеграций с Zabbix:
Подключение триггеров Zabbix к объектам на тепловой карте:
Система работает на базе Google Charts и Bootstrap. Пока это альфа-версия, мы планируем ее развивать и дальше, добавляя полезные фишки из промышленных систем, с которыми успешно работаем уже много лет. Постараюсь держать вас в курсе и публиковать посты по мере накопления вороха новых возможностей.
Вторая наработка — это интеграция с Zabbix и тепловой картой функционала синтетических транзакций. Фактически это продолжение тепловой карты, но взгляд с другой стороны. Однозначно, контролируя систему только со стороны самого приложения и инфраструктуры, вы не будете обладать необходимой полнотой информации. Синтетические транзакции позволят посмотреть на это дело со стороны пользователя и локализовать проблему еще до первых обращений пользователей в Help Desk.
Синтетические транзакции построены на базе фреймворка phantom.js (но вам ничего не мешает перейти на casper.js, на чистый selenium или на что-то другое на ваш вкус). В нашей тестовой лаборатории выполнение тестового сценария настроено через cron и далее полученные данные передаются в Zabbix посредством zabbix_trapper. В качестве примера тестового сценария взят логин в личный кабинет МТС и получение остатка денег на счету и трафика в интернет-пакете. Ниже — листинг скрипта. В банковской среде наиболее вероятным применением этого инструмента может быть, например, ДБО. Никто же вам не мешает производить логин в систему и перекидывать 1 рубль со счета на счет.
/**
* Скрипт получения метрик по балансу абонента МТС
* 1.0
*
* Параметры запуска:
* phantomjs --web-security=no getMtsBalance.js "<папка для вывода результатов>" "телефон в формате (XXX) XXX-XX-XX" "<пароль>"
* Пример:
* phantomjs --web-security=no getMtsBalance.js "/tmp/getMtsBalance" "(916) 123-45-67" "P@ssw0rd"
*
* (c) Jet/ДСУ 2016
*/
// PhantomJs stuff
var fs = require('fs');
var system = require('system');
var webpage = require('webpage');
var args = system.args;
var TRAFFIC_REGEX = /Доступно.{1,100}?([0-9.]+).{0,50}?(ГБ|МБ).{0,50}?на (\d+) дн/i;
var DATE_REGEX = /Дата обновления интернет-пакета ([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{2}) ([0-2][0-9]):([0-5][0-9])/;
var SCRIPT_TIMEOUT = 40000;
var config = {
lkUrl : 'https://lk.ssl.mts.ru/',
lkLogin: null,
lkPass : null,
outDir : null,
debugDir: null,
formattedStartTime: null
};
var timer = {
lastActionStartTime: null,
lastActionTimeMs : null,
currentTimeMillis: function() {
return Date.now();
},
startAction: function() {
this.lastActionStartTime = this.currentTimeMillis();
this.lastActionTimeMs = null;
},
stopAction: function() {
lastActionTimeMs = this.currentTimeMillis() - this.lastActionStartTime;
},
getLastActionTimeMs: function() {
return lastActionTimeMs;
},
getLastActionTimeSec: function() {
return lastActionTimeMs === null ? null : lastActionTimeMs/1000;
}
}
var metrics = {
trafLeftMb: null,
daysLeft : null,
balance : null,
pages: {
login: {
availability : null,
responseTimeSec: null
},
lk: {
availability : null,
responseTimeSec: null
}
}
};
////////// HELPER FUNCTIONS //////////
var func = {
log: function(s) {
console.log(this.formatDateTimeForLog(new Date()) + " " + s);
},
roundToTwo: function(num) {
return +(Math.round(num + "e+2") + "e-2");
},
zero: function(i) {
return i < 10 ? '0' + i : i;
},
formatDateTimeForLog: function(date) {
var dd = date.getDate();
var mm = date.getMonth() + 1;
var yy = date.getFullYear();
var hh = date.getHours();
var min = date.getMinutes();
var ss = date.getSeconds();
var ms = date.getMilliseconds();
ms = ('00' + ms).slice(-3);
return yy + '-' + this.zero(mm) + '-' + this.zero(dd) + ' ' + this.zero(hh) + ':' + this.zero(min) + ':' + this.zero(ss) + '.' + ms;
},
formatDateTimeForFileName: function(date) {
return date.getFullYear()
+ this.zero(date.getMonth() + 1)
+ this.zero(date.getDate())
+ '-'
+ this.zero(date.getHours())
+ this.zero(date.getMinutes())
;
},
writeMetricToFileAndLog: function(filePrefix, metricName, metricValue) {
if ( metricValue == null ) {
metricValue = 0;
}
fs.write(config.outDir + filePrefix + config.formattedStartTime + '.log', this.roundToTwo(metricValue), 'w');
this.log(' ' + metricName + ' = ' + metricValue);
}
}
// Разбираем аргументы запуска скрипта
config.outDir = args[1] + '/';
config.lkLogin = args[2];
config.lkPass = args[3];
config.debugDir = config.outDir + 'debug/';
fs.makeDirectory(config.debugDir);
func.log("Папка с результатами работы: " + config.outDir);
// Таймаут - чтобы процесс навечно не завис
setTimeout(function() {
func.log("Сработал таймаут после " + SCRIPT_TIMEOUT + " мс");
if ( metrics.pages.login.availability == null ) {
metrics.pages.login.availability = 0;
metrics.pages.login.responseTimeSec = 0;
}
if ( metrics.pages.lk.availability == null ) {
metrics.pages.lk.availability = 0;
metrics.pages.lk.responseTimeSec = 0;
}
outMetricsAndExit();
}, SCRIPT_TIMEOUT);
// Настраиваем наш "браузер"
var page = webpage.create();
page.settings.userAgent = 'Mozilla/4.0';
// Отмечаем время начала процесса для логов
config.formattedStartTime = func.formatDateTimeForFileName(new Date());
// Открываем страницу личного кабинета
func.log("Загружаем " + config.lkUrl);
timer.startAction();
page.open(config.lkUrl, function (status) {
timer.stopAction();
metrics.pages.login.responseTimeSec = timer.getLastActionTimeSec();
if (status !== "success" ) {
func.log("Страница " + config.lkUrl + " недоступна");
metrics.pages.login.availability = 0;
outMetricsAndExit();
} else {
func.log("Страница " + config.lkUrl + " успешно получена");
metrics.pages.login.availability = 1;
page.render(config.debugDir + 'login.png');
// Страница будет подгружать iframe'ы, будем их обрабатывать
var contentN = 0;
page.onLoadFinished = function(status) {
// Останавливаем таймер, чтобы замерять время получения страницы личного кабинета
// Если мы будем получать несколько страниц (iframe'ов),
// то время таймера просто будет расти, т.к. мы его стартанули только один раз,
// перед отправкой логина-пароля. Личный кабинет открывается не сразу -
// сначала он открывает страницу "Подождите", и только через некоторое время
// показывает контент. Соотв. парсер корректно замеряет время от отправки логина до
// получения страницы с реальными данными
timer.stopAction();
contentN++;
func.log('Загружен контент N' + contentN + ':' + status);
page.render(config.debugDir + contentN + '.png');
fs.write(config.debugDir + contentN + '.html', page.content, 'w');
if ( status === 'success') {
getMtsMetrics(page, contentN);
}
};
func.log("Заполняем поля формы, логин: " + config.lkLogin);
timer.startAction();
page.evaluate(function(config) {
var form = document.forms[0];
form.phone.value = config.lkLogin;
form.password.value = config.lkPass;
form.elements[2].click();
}, config);
}
});
function getMtsMetrics(page, contentN) {
if ( page.content.match('подозрительную активность') ) {
func.log("Ответ страницы МТС: замечена подозрительная активность. Слишком частое обращение к странице личного кабинета");
metrics.pages.lk.availability = 0;
metrics.pages.lk.responseTimeSec = 0;
outMetricsAndExit();
}
// Ищем информацию о балансе внутри iFrame'ов
findBalanceInPage(page, contentN);
// Ищем остаток трафика и кол-во дней до оплаты
findTrafficInfoInPage(page);
// Если собрали все метрики, заканчиваем скрипт
if ( checkGotMetricsAlready() ) {
outMetricsAndExit();
}
}
/**
* Ищет информацию о балансе на полученной странице
*/
function findBalanceInPage(page, contentN) {
// Информация о балансе находится в iframe'ах
if ( page.framesCount == 0 ) {
return;
}
func.log("Анализируем полученные iframe'ы");
var balanceResult = page.evaluate(function() {
var result = {
iframes: [],
balance: null
};
$("iframe").each(function(i, iframe) {
var iframeBody = $(iframe).contents().find('body');
if ( iframeBody.size() > 0 ) {
result.iframes.push( iframeBody.html() );
// Вариант поиска баланса 1 - через DOM
if ( result.balance === null ) {
iframeBody.find(".b-header_balance").each(function() {
var m = $(this).text().match(/([0-9.]+) руб/i);
if ( m ) {
result.balance = m[1];
}
});
}
// Вариант поиска баланса 2 - через regex
if ( result.balance === null ) {
var m = iframeBody.text().match(/баланс\s*:\s*-?([0-9.]+)\s*руб/i);
if ( m ) {
result.balance = m[1];
}
}
}
});
return result;
});
var iframesAnalyzed = balanceResult.iframes.length;
func.log("Проанализировано iframe'ов:" + iframesAnalyzed);
if ( iframesAnalyzed > 0 ) {
// Сохраняем iFrame'ы на диск
for (var i = 0; i < iframesAnalyzed; i++) {
var iframeContent = balanceResult.iframes[i];
func.log(" Сохраняем iframe " + config.debugDir + contentN + '_iframe' + i + '.html');
fs.write(config.debugDir + contentN + '_iframe' + i + '.html', iframeContent, 'w');
}
// Проверяем, была ли найдена информация о балансе
if ( balanceResult.balance !== null ) {
if ( metrics.pages.lk.availability === null ) {
// Если мы получили баланс, то страница личного кабинета корректно загрузилась
metrics.pages.lk.availability = 1;
metrics.pages.lk.responseTimeSec = timer.getLastActionTimeSec();
}
func.log("Найдена информация о балансе: " + balanceResult.balance);
metrics.balance = balanceResult.balance;
}
}
}
/**
* Ищет информацию об интернет-трафике на странице
*/
function findTrafficInfoInPage(page) {
var traf = page.content.match(TRAFFIC_REGEX);
if ( traf ) {
func.log("Остаток трафика - строка найдена: " + traf);
metrics.trafLeftMb = traf[1];
var trafUnits = traf[2];
if ( trafUnits.toLowerCase() == 'гб' ) {
metrics.trafLeftMb *= 1024;
}
metrics.daysLeft = traf[3];
}
else if (page.content.match("ревышена квота трафика") ) {
func.log("Найдена строка: превышена квота трафика");
metrics.trafLeftMb = 0;
if ( page.injectJs("jquery.min.js") ) {
metrics.daysLeft = page.evaluate(function() {
var p = $("p:contains('Интернет-пакет будет обновлен')");
var pText = p.find("b").text();
console.log( "Найден текст: " + pText);
return pText.replace(/\D/g, '');
});
}
}
}
/**
* Проверяет, собраны ли уже все метрики
*/
function checkGotMetricsAlready() {
if ( metrics.pages.login.availability == 0 || metrics.pages.lk.availability == 0 ) {
// Мы определили недоступность одной из страниц (логин или страница ЛК)
// В любом из этих случаев метрики собрать не удастся, инициируем окончание скрипта
return true;
}
if ( metrics.balance != null && metrics.daysLeft != null && metrics.trafLeftMb != null ) {
// Все метрики собраны, инициируем окончание скрипта
return true;
}
return false;
}
/**
* Выводит значения всех метрик на консоль и в файлы,
* инициирует завершение скрипта
*/
function outMetricsAndExit() {
func.log("Метрики:");
func.writeMetricToFileAndLog('traffic', 'metrics.trafLeftMb', metrics.trafLeftMb);
func.writeMetricToFileAndLog('money', 'metrics.balance', metrics.balance);
func.writeMetricToFileAndLog('daysLeft', 'metrics.daysLeft', metrics.daysLeft);
func.writeMetricToFileAndLog('status-initialpageload', 'metrics.pages.login.availability', metrics.pages.login.availability);
func.writeMetricToFileAndLog('time-initialpageload', 'metrics.pages.login.responseTimeSec', metrics.pages.login.responseTimeSec);
func.writeMetricToFileAndLog('status-lkpageload', 'metrics.pages.lk.availability', metrics.pages.lk.availability);
func.writeMetricToFileAndLog('time-lkpageload', 'metrics.pages.lk.responseTimeSec', metrics.pages.lk.responseTimeSec);
phantom.exit();
}
Собираемые items выглядят следующим образом:
Каждому соответствует свой график.
Ни в коем случае не хочу сказать, что применение в мониторинге Open Source решений является таблеткой от всех неприятностей. Открою секрет Полишинеля: как и в физике, тут действует закон сохранения денег и трудозатрат. Чем больше денег вы вольете в готовый продукт, тем меньше трудозатрат на доработку, и наоборот. Всегда стоит руководствоваться здравым смыслом, имеющимся бюджетом и человеческим фактором: готова ли будет ваша команда броситься на амбразуру бизнес-мониторинга по первому зову?
Особо интересующимся технологиями мониторинга предлагаю ознакомиться с нашей предыдущей статьей по этой теме «Принципы мониторинга бизнес-приложений».
Автор статьи: Антон Касимов