Приветствую, уважаемые читатели! В этом материале я хочу поделиться историей о приготовлении тостов и рассказать, каким образом мы расширили функционал Zabbix с помощью мотка провода и интегральной микросхемы Open Source решений. Обо всем по порядку, прошу под кат.

Image1.jpg

Давайте вообразим себе следующее: вы работаете в организации (она может быть и вашей), в которой есть бизнес-процесс, поддерживаемый одной или несколькими ИТ-системами. Я точно знаю, что есть система мониторинга. Дальше видение немного расплывается, и непонятно: промышленная это система или бесплатная 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 рубль со счета на счет.

Тестовый сценарий проверки баланса и остатка трафика (Javascript)
/**
 * Скрипт получения метрик по балансу абонента МТС
 * 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 решений является таблеткой от всех неприятностей. Открою секрет Полишинеля: как и в физике, тут действует закон сохранения денег и трудозатрат. Чем больше денег вы вольете в готовый продукт, тем меньше трудозатрат на доработку, и наоборот. Всегда стоит руководствоваться здравым смыслом, имеющимся бюджетом и человеческим фактором: готова ли будет ваша команда броситься на амбразуру бизнес-мониторинга по первому зову?

Особо интересующимся технологиями мониторинга предлагаю ознакомиться с нашей предыдущей статьей по этой теме «Принципы мониторинга бизнес-приложений».

Автор статьи: Антон Касимов
Поделиться с друзьями
-->

Комментарии (5)


  1. antirek
    05.10.2016 10:17

    Ваша статья напомнила мне о моих потугах визуализировать свою инфраструктуру — статья на Хабре о deedoo


    1. ITSystemsManagement
      05.10.2016 11:19

      С момента написания статься прошло уже больше года, вы как-то продвинулись в вашем вопросе? Все-таки закупили промышленное решение или доработали свое, например?


      1. antirek
        05.10.2016 13:16

        Нет, именно в описании взаимосвязей на макроуровне нет. Некоторые бизнес-сервисы переписаны и используют docker. Где можно в docker-compose файлах описать взаимосвязи ближайших сервисов: бд, приложения, очереди. Но пока не более того.
        В будущем планирую внедрить какое-нибудь решение оркестрации для контейнеров, где, надеюсь, будет визуальное представление зависимостей.


  1. shep
    05.10.2016 10:31

    Очень хорошо расписан подход к построению структуры перед началом мониторинга. Если кто-то только начинает мониторинг, можно прям как учебник использовать :) Из того, что видел я, люди сначала просто начинают мониторить все подряд и когда что-то краснеет, не знают что делать и не знают влияет ли это вообще на что-то.

    А вы пробовали Dashing: «Как запилить свой дешборд на все случаи жизни?»? На мой взгляд, можно получить более информативные вещи.


    1. ITSystemsManagement
      05.10.2016 11:35

      Согласен, Dashing клевая штука. Прост как тапок и мощный как космический шаттл. Используем его у себя в Сервисном центре в связке с Zabbix и системой инцидент-менеджмента.

      Однако, Dashing он немного про другое, в нем не создать связанную многоуровневую структуру с влиянием одного компонента на другой