Телепатія на стероїдах в js/node.js

imageЕтап підтримки продуктів забирає багато сил і нервів. Шлях від «я натискаю а воно не працює» до вирішення проблеми, навіть у першокласного телепата, може займати багато часу. Часу, протягом якого клієнт/начальник буде злий і невдоволений.

Щоб скоротити час рішення проблеми потрібно оперативно дізнаватися про помилки, мати як можна більш точну інформацію про те, що до неї призвело і, бажано, збирати всі разом.

Про своє рішення я і розповім під катом.

1. Завдання
Після обговорення було прийнято рішення створити механізм, який збирає з клієнта і сервера інформацію про помилки, і дозволяє передавати або обробляти дані для подальшого реагування. Механізм повинен давати можливість у майбутньому додавати способи роботи з даними без зайвого переписування коду і дозволяти з конфига змінювати способи роботи, порядок і т. п.

Ключові точки:
  • Ловити помилки як на frontend так і на backend
  • Можливість додати кілька обробників подій у т. ч. в майбутньому
  • Великий обсяг налагоджувальної інформації
  • Гнучке налаштування для кожного проекту
  • Висока надійність
2. Рішення
Було вирішено при запуску сервера проводити завантаження спеціальних обробників подій — драйверів, порядок і пріоритет яких буде завантажений з конфига. Помилки на frontend будуть надсилатися на сервер, де будуть оброблятися разом з іншими.

Основна ідея в тому, що при виникненні помилки, і по мірі відмотування стека до глобальної галузі, в клас помилки буде додаватися налагоджувальна інформація за допомогою розставлених збирачів. При випаданні в глобальну область, помилка буде перехоплюватися і оброблятися за допомогою драйвера помилки.

2.1 Клас помилки
Був написаний свій клас помилки, успадкований від стандартного. З конструктором, які беруть помилку, можливістю вказати «рівень тривоги» і додаванням налагоджувальних даних. Клас розташований в єдиному для front — і backend файлі інструментів.

Тут і далі, в коді використані бібліотеки co, socket.io і sugar.js
Повний код класу
app.Error = function Помилка(error,lastFn){
if(error && error.name && error.message && error.stack){у випадку, якщо в конструктор передана інша помилка
this.name=error.name;
this.message=error.message;
this.stack=error.stack;
this.clueData=error.clueData||[];
this._alarmLvl=error._alarmLvl||'trivial';
this._side=error._side || (module ? "backend" : "frontend");//визначення сторони
return;
}
if(!app.isString(error)) error='unknown error';
this.name='Error';
this.message=error;
this._alarmLvl='trivial';
this._side=module ? "backend" : "frontend";
this.clueData=[];

if (Error.captureStackTrace) {
Error.captureStackTrace(this, app.isFunction(lastFn)? lastFn : this.constructor);
} else {
this.stack = (new Error()).stack.split('\n').removeAt(1).join();//видалення з стека виклику конструктора класу помилки
}

};
app.Error.prototype = Object.create(Error.prototype);
app.Error.prototype.constructor = app.Error;
app.Error.prototype.setFatal = function () {//getter/setters для рівня тривоги
this._alarmLvl='fatal';
return this;
};
app.Error.prototype.setTrivial = function () {
this._alarmLvl='trivial';
return this;
};
app.Error.prototype.setWarning = function () {
this._alarmLvl='warning';
return this;
};
app.Error.prototype.getAlarmLevel = function () {
return this._alarmLvl;
};
app.Error.prototype.addClueData = function(name,data){//додавання налагоджувальної інформації
var dataObj={};
dataObj[name]=data;
this.clueData.push(dataObj);
return this;
};


І відразу приклад використання для promise:

socket.on(fullName, function (values) {
<...>
method(values)//Виконуємо функцію api
.then(<...>)
.catch(function (error) {//Ловимо помилку
throw new app.Помилка(error)//Обертаємо в наш клас і пробрасываем далі по стеку
.setFatal()//Вказуємо "рівень тривоги"
.addClueData('api', {//Додаємо дані налагодження
fullName,
values,
handshake: socket.handshake
})
});
});

Для try-catch поступаємо аналогічно.

2.2 Frontend
Для frontend заковика в тому, що помилка може статися ще до того, як завантажиться бібліотека транспорту (socket.io в даному випадку).

Обходимо цю проблему, збираючи помилки під тимчасову змінну. Для перехоплення помилок з глобальної області використовуємо window.onerror:

app.errorForSending=[];
app.sendError = function (error) {//Функція відправки помилки на сервер
app.io.emit('server error send', new app.Помилка(error));
};

window.onerror = function (message, source, lineno, colno, error) {//Перехватываем помилку глобальної області
app.errorForSending.push(//Записуємо в масив для помилок. 
new app.Помилка(error)
.setFatal());//присвоюємо високий рівень тривоги, адже помилка сталася під час завантаження
};
app.events.on('socket.io ready', ()=> {//Після готовності транспортної бібліотеки
window.onerror = function (message, source, lineno, colno, error) {//Перезаписуємо коллбек
app.sendError(new app.Помилка(error).setFatal());
};

app.errorForSending.forEach((error)=> {//Відправляємо всі помилки, зібрані раніше
app.sendError(error);
});
delete app.errorForSending;
});
app.events.on('client ready', ()=> {//після завантаження записуємо остаточну версію обробника
window.onerror = function (message, source, lineno, colno, error) {
app.sendError(error);
};
});

Залишається проблема в тому, що деякі бібліотеки люблять не викидати помилки, а просто гаді виводити в консоль. Перезапишем функції консолі для перехоплення даних.

function wrapConsole(name, action) {
console['$' + name] = console[name];//зберігаємо вихідний метод
console[name] = function () {
console['$' + name](...arguments);//викликаємо метод вихідний
app.sendError(
new app.Error(`From console.${name}: ` + [].join.call(arguments, " ),//запишемо в повідомлення помилки консольний висновок
console[name])//Скоротимо стек до виклику цієї функції(буде працювати тільки в движку v8)
.addClueData('console', {//додамо дані про імені консолі і вихідних аргументах
consoleMethod: name,
arg : Array.create(arguments)
})[action]());//викличемо відповідний рівню сетер
};
}
wrapConsole('error', 'setTrivial');
wrapConsole('warn', 'setWarning');
wrapConsole('info', 'setWarning');

2.3 Server
Нам залишилося найцікавіше, для всіх, хто дочитав до цього моменту і не помер від втоми. Адже залишилось реалізувати не просто ініціалізацію та виконання драйверів, які отримують помилки,
  • Все повинно працювати якомога швидше, навіть якщо кожному драйверу в процесі ініціалізації/обробки помилки, потрібно «поговорити по душам» з іншим сервером або обчислити відповідь на головне питання життя всесвіту і всього такого;
  • Гнучка система запасних і дублюючих драйверів;
  • Динамічно запускати запасні драйвера, у разі відмови попередніх;
  • Виключення, що виникли під час роботи драйверів, відправляти по працюючим драйверам;
  • Ловити і обробляти помилки з frontend, а також випадають в глобальну область node.js.
Весь код можна подивитися на гітхабі (посилання внизу), а зараз пройдемося по основних завдань:

  1. Паралельний запуск для швидкості
    Для цих цілей використовуємо yield [...](або Promise.all(...)) з урахуванням того, що кожна функція з масиву не повинна викидати помилку інакше, якщо функцій з помилками кілька, ми не зможемо обробити їх усі
  2. Гнучка конфігурація
    Всі драйвера знаходяться в «пакет драйверів», які розташовуються у масиві по пріоритету. Помилка розсилається відразу на весь пакет драйверів, якщо весь пакет не працює, система переходить до наступного і т. д.
  3. Динамічний запуск
    При ініціалізації помічаємо всі драйвера як «not started».
    При запуску перший пакет драйверів помічаємо або як «started», або як «bad».
    При відправці, у поточному пакеті пропускаємо «bad», відправляємо в «started» і запускаємо «not started». Драйвера, выкинувшие помилку, помічаємо як bad і йдемо далі. Якщо все драйвера в поточному пакеті помічені як bad переходимо до наступного пакету.
  4. Відправка помилок драйверів ще живих драйвери
    При виникненні помилок в драйверах помилок(трохи тавтології), записуємо їх в спеціальний масив. Після знаходження першого живого драйвера, відправляємо через нього помилки драйверів і саму помилку(якщо драйвера падали при відправці помилки і помилки драйверів.
  5. Ловимо помилки з front/backend
    Створюємо спеціальний api для frontend і ловимо виключення node.js через process.on('uncaughtException',fn) і process.on('unhandledRejection',fn)
3. Висновок
Викладений механізм збору і відправки повідомлень про помилки дозволить миттєво реагувати на помилки, ще до того, як кінцевий користувач, і обійтися без допиту кінцевого користувача на предмет останніх натиснутих кнопок.

Якщо задуматися про розвиток, то в майбутньому можна додати кілька корисних функцій:
  • Зміна політики відключення непрацюючих драйверів
    Наприклад, додати можливість повторної перевірки драйвера на працездатність через деякий час.
  • Можливість вставки коду драйверів на frontend
    Можна використовувати для збору додаткової інформації.
  • Пресет логгирования
    DRY повторюваних функцій збору загальної інформації(останні завантажені сторінки, останні використані api)


Робочий приклад можна подивитися на гітхабі. За архітектуру прошу не лаяти, приклад робився методом видалити з проекту все-непотрібне.

Буду радий коментарям.
Джерело: Хабрахабр

0 коментарів

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