Как приготовить тосты и заодно визуализировать ИТ-системы

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

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

    Автор статьи: Антон Касимов
    • +10
    • 6,9k
    • 5
    Инфосистемы Джет
    616,00
    Системный интегратор
    Поделиться публикацией

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

      0
      Ваша статья напомнила мне о моих потугах визуализировать свою инфраструктуру — статья на Хабре о deedoo
        0
        С момента написания статься прошло уже больше года, вы как-то продвинулись в вашем вопросе? Все-таки закупили промышленное решение или доработали свое, например?
          0
          Нет, именно в описании взаимосвязей на макроуровне нет. Некоторые бизнес-сервисы переписаны и используют docker. Где можно в docker-compose файлах описать взаимосвязи ближайших сервисов: бд, приложения, очереди. Но пока не более того.
          В будущем планирую внедрить какое-нибудь решение оркестрации для контейнеров, где, надеюсь, будет визуальное представление зависимостей.
        0
        Очень хорошо расписан подход к построению структуры перед началом мониторинга. Если кто-то только начинает мониторинг, можно прям как учебник использовать :) Из того, что видел я, люди сначала просто начинают мониторить все подряд и когда что-то краснеет, не знают что делать и не знают влияет ли это вообще на что-то.

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

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

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое