Як приготувати тости і заодно візуалізувати ІТ-системи

Вітаю, шановні читачі! У цьому матеріалі я хочу поділитися історією про приготування тостів і розповісти, яким чином ми розширили функціонал 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"
*
* © 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(data) {
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(data) { 
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 рішень є таблеткою від усіх неприємностей. Відкрию секрет Полішинеля: як і у фізиці, тут діє закон збереження грошей і трудовитрат. Чим більше грошей ви увіллєте в готовий продукт, тим менше трудовитрат на доопрацювання, і навпаки. Завжди варто керуватися здоровим глуздом, наявним бюджетом та людським фактором: чи готова ваша команда кинутися на амбразуру бізнес-моніторингу за першим покликом?

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

Автор статті: Антон Касимов
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.