Чтобы сократить время решения проблемы, нужно оперативно узнавать об ошибках, иметь как можно более точную информацию о том, что к ней привело и, желательно, собирать всё вместе.
О своём решении я и расскажу под катом.
1. Задачи
После обсуждения было принято решение создать механизм, собирающий с клиента и сервера информацию об ошибках, и позволяющий передавать или обрабатывать данные для последующего реагирования. Механизм должен давать возможность в будущем добавлять способы работы с данными без лишнего переписывания кода и позволять из конфига менять способы работы, порядок и т.п.
Ключевые точки:
- Ловить ошибки как на frontend так и на backend
- Возможность добавить несколько обработчиков ошибок в т.ч. в будущем
- Большой объем отладочной информации
- Гибкая настройка для каждого проекта
- Высокая надёжность
2. Решение
Было решено при запуске сервера производить загрузку специальных обработчиков ошибок — драйверов, порядок и приоритет которых будет загружен из конфига. Ошибки на frontend будут посылаться на сервер, где будут обрабатываться вместе с остальными.
Основная идея в том, что при возникновении ошибки, и по мере отмотки стека до глобальной области, в класс ошибки будет добавляться отладочная информация при помощи расставленных сборщиков. При выпадении в глобальную область, ошибка будет перехватываться и обрабатываться при помощи драйвера ошибки.
2.1 Класс ошибки
Был написан свой класс ошибки, наследуемый от стандартного. С конструктором, принимающим ошибку, возможностью указать «уровень тревоги» и добавлением отладочных данных. Класс расположен в едином для front- и backend файле инструментов.
Здесь и далее, в коде использованы библиотеки co, socket.io и sugar.js
app.Error = function Error(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(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(error));
};
window.onerror = function (message, source, lineno, colno, error) {//Перехватываем ошибку из глобальной области
app.errorForSending.push(//Записываем в массив для ошибок.
new app.Error(error)
.setFatal());//Сразу присваиваем высокий уровень тревоги, ведь ошибка произошла во время загрузки
};
app.events.on('socket.io ready', ()=> {//После готовности транспортной библиотеки
window.onerror = function (message, source, lineno, colno, error) {//Перезаписываем коллбек
app.sendError(new app.Error(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.
Весь код можно посмотреть на гитхабе (ссылка внизу), а сейчас пройдёмся по основным задачам:
- Параллельный запуск для скорости
Для этих целей используем yield [...](или Promise.all(...)) с учётом того, что каждая функция из массива не должна выбрасывать ошибку иначе, если функций с ошибками несколько, мы не сможем обработать их все - Гибкая конфигурация
Все драйвера находятся в «пакете драйверов», которые располагаются в массиве по приоритету. Ошибка рассылается сразу на весь пакет драйверов, если весь пакет не работает, система переходит к следующему и т.д. - Динамический запуск
При инициализации помечаем все драйвера как «not started».
При запуске первый пакет драйверов помечаем либо как «started», либо как «bad».
При отправке, в текущем пакете пропускаем «bad», отправляем в «started» и запускаем «not started». Драйвера, выкинувшие ошибку, помечаем как bad и идём дальше. Если все драйвера в текущем пакете помечены как bad переходим к следующему пакету. - Отправка ошибок драйверов в ещё живых драйверах
При возникновении ошибок в самих драйверах ошибок(немного тавтологии), записываем их в специальный массив. После нахождения первого живого драйвера, отправляем через него ошибки драйверов и саму ошибку(если драйвера падали при отправке ошибки) и ошибки драйверов. - Ловим ошибки с front/backend
Создаем специальный api для frontend и ловим исключения node.js через process.on('uncaughtException',fn) и process.on('unhandledRejection',fn)
3. Заключение
Изложенный механизм сбора и отправки сообщений об ошибках позволит мгновенно реагировать на ошибки, ещё до того, как конечный пользователь, и обойтись без допроса конечного пользователя на предмет последних нажатых кнопок.
Если задуматься о развитии, то в будущем можно добавить несколько полезных фич:
- Изменение политики отключения неработающих драйверов
Например, добавить возможность повторной проверки драйвера на работоспособность через некоторое время. - Возможность вставки кода драйверов на frontend
Можно использовать для сбора дополнительной информации. - Пресет логгирования
DRY для повторяющихся функций сбора общей информации(последние загруженные страницы, последние использованные api)
Рабочий пример можно посмотреть на гитхабе. За архитектуру прошу не ругать, пример делался методом удалить-из-проекта-всё-ненужное.
Буду рад комментариям.
Комментарии (27)
aezhko
10.08.2016 11:25+1А что происходит с отловленными ошибками дальше? Загорается какая-то лампочка в админке, и какие-то дежурные разбираются в ситуации?
И что будет, если в релизе выкатится фатальная ошибка какой-нибудь жутко популярной ручки, не заддосит ли ваш механизм сбора ошибок?affka
10.08.2016 11:28У нас построена таким образом, что все ошибки хранятся на отдельном сервере, который не жалко положить. А сама система сбора ошибок учитывает что сервер может лежать и просто не пишет в него (потому что если начался ддос ошибками, то значит саму ошибку уже записали). PS: я к либе из данного поста отношения не имею, я про свои решения)
Kot_DaVinchi
10.08.2016 11:40+1Лампочка в админке. Только без дежурных программистов, поднятых по тревоге.
Вся система в целом не направлена на высокие нагрузки, однако это хорошая идея, при большом потоке ошибок складывать их в пачки или сверять с последними отправленными и не слать дубли.
gearbox
10.08.2016 15:12И что будет, если в релизе выкатится фатальная ошибка какой-нибудь жутко популярной ручки, не заддосит ли ваш механизм сбора ошибок?
Поддерживаю, вот эта тема гораздо интереснее самого отлова ошибок. Как сделать редукцию и определять однотипные ошибки от разных пользователей? Что бы писать в базу не весь поток а тупо первый десяток инцидентов, а потом лишь собирать статистику если она нужна (число инцидентов, версии пользователей и все такое)
lusever
10.08.2016 13:02+1Мне кажется как-то жирно для ошибок, которые могут ни когда не произойти, держать постоянно открытое вебсокет соединение и тянуть не маленькую библиотеку socket.io.
uoziod
10.08.2016 13:17Всё, конечно, зависит от загруженности сервиса. Возникает идея сделать возможным сбор ошибок по запросу (в т.ч. на продакшне).
CrazyNiger
10.08.2016 13:54Скорее всего вебсокеты используется не только для передачи ошибок, но и в основном «цикле» работы приложения.
Kot_DaVinchi
10.08.2016 15:34socket.io используется нами как основной канал для api сервера, т.к. мы пишем RIA Отсюда и выбор.
При желании можно спокойно переписать на динамику.ChALkeRx
10.08.2016 16:08+2А для каких конкретно браузеров сейчас есть необходимость в socket.io? Чем плох стандартный WebSocket?
VasiliyIsaichkin
10.08.2016 16:49Эммм вы о чем?! А socket.io это что марсианские почтовые голуби? Он его и использует + пердоставляет прозрачный даунгрейд канала в случае если его использование невозможно к примеру браузер не умеет WebSocket
ChALkeRx
10.08.2016 17:34+1Он его и использует
Я в курсе. Но socket.io, кроме этого, тянет ещё килограмм зависимостей и строит фаллбэки для поддержки того, что давно умерло.
к примеру браузер не умеет WebSocket
Пример браузера приведите, пожалуйста.
Как раз суть в том, что для этого тянуть большой и страшный сокетио уже не надо — всё работает и так, и более стабильно.
gearbox
10.08.2016 17:38caniuse гляньте, Вам лень что ли? Ослы, как обычно. И как обычно — их поддержка обязательна у корпоративных заказчиков.
ChALkeRx
10.08.2016 17:43+1Мне не лень, я хочу, чтобы вы своими глазами посмотрели и убедились, что там только ие9 из десктопных. Который уже не поддерживается MS, если у вас не Vista. Плюс очень старые мобильные времён iOS 5.
VasiliyIsaichkin
10.08.2016 18:09Ага, и не очень старые и гораздо более (чем iOS5) распространенные андройды 4.3 и ниже. Но вообще думается вы правы, привычка она такая.
ChALkeRx
10.08.2016 18:17+14.3 это не все андроиды. Это штатный андроид браузер версии 4.3. У всех версий штатного андроид браузера целиком — 9% в русскоязычном сегменте, всё остальное — хром. Причём надо ещё смотреть, какая часть из этого — 4.3, ими вообще скорее как звонилками пользуются.
gearbox
10.08.2016 19:07+1Если к Вам заказчик придет с XP-ой — Вы его заказ будете выполнять или рассказывать ему что он
дебилнедальновидный человек и ему как минимум надо поменять безопасника а заодно обновить систему? На паре тысяч компов во всех офисах.VasiliyIsaichkin
10.08.2016 21:01+1Я откажусь работать. А если у них пни первые и NovellNetware или OS/2 или табуляторы? Мне что работать с ними? Зачем? XP это сим-сим ей уже 15 лет так-то. XP в 2016 — это шиза и для внедрения и разработки нового продукта требуется иметь подходящую инфраструктуру. Это техничесоке требование типа мощности электросети или предельной нагрузки на перекрытия, это же не просто хотелка.
gearbox
10.08.2016 21:30Это сермяжная правда жизни. Настоящей жизни, которая за окном а не за экраном монитора. Можно отказаться от проекта если Вы — владелец компании (студии, команды интеграторов). Или фрилансер (как я). Во всех остальных случаях — Вы не отказываетесь от проекта — Вы пишете заявление (хорошо если по собственному). В некоторых ситуациях это может быть расценено как проявления непрофессионализма. Более того — в некотором подмножестве этих ситуаций это именно так и будет. А еще есть подмножество ситуаций в котором об этом станет известно не только Вам и Вашему работодателю, но и некоторому множеству других потенциальных работодателей. Жизнь, она такая — не всегда понятная и не всегда приятная )
VasiliyIsaichkin
10.08.2016 22:09+1Позвольте не согласиться (да я руководитель).
Опыт и практика показывают, что при таком подходе проект заранее провальный — т.к. всем наплевать на проект причем и заказчику и исполнителю (продажникам и руководству как минимум).
Заказчику, потомучто сейчас не 90ые, и если с IT все настолько плохо и нет вариантов изменить ситуацию, то и с другим такаже все печально(а значит будет — постоянное недофинансирование, нарушения договоров, «получилось как всегда», невозможность реализации эффективной работы на имеющемся и прочее, прочее, прочее, и виноват будет всегда исполнитель — тыж программист, профессионал).
Исполнителю, потомучто если ваш руководитель настолько алчный идиот, во-первых потомучто поддержка настолько устаревших систем это ДОРОГО (т.к. требует более редких спецов и гораздо больше человекочасов), а это значит компания меньше заработает, во-вторых он не понимает то что я написал про заказчика а значи сидит не на своем месте.
А когда все наплевать ожидать успешной реализации проекта не стоит + куча дополнительных факторов (трудоемкость, неинтересность, сложность, высокая вероятность аварий, неспособность переварить объемы информации и тп).
В любых дургих конфигурациях такой проект просто не начнется.
Ок — давайте понятнее, если перекрытия в здании не расчитано на на новое оборудование то он рухнут. Если подключать станок 380V/50A к сети 220V/20A ничего не заработает. Тут тоже самое, winXP это слишком древняя система. Если у предприятия для нового проекта IT-инфраструктура не годится для реализации проекта ничего хорошего тоже можно не ждать — не взлетит.
Более того если нет возможности изменить ситцуациию или не уволиться, а остаться в таком чудо проекте то есть очень серьезный шанс что обвинят в провале именно вас, и шанс этого и степень порчи репутации на ПОРЯДОК больше чем в ситуации которую описали вы.
CrazyNiger
11.08.2016 08:15-1Но socket.io, кроме этого, тянет ещё килограмм зависимостей и строит фаллбэки для поддержки того, что давно умерло.
В скотио с выхода версии 1.0 произошло разделение на модули, при желании можно не тащить транспорты для фалбеков и использовать только вебсокеты.
А используют сокетио, главным образом, из-за реализованных механизмов комнат, нэмспесов, решенных вопросов с масштабированностью и тому подобному.ChALkeRx
11.08.2016 08:44+1В скотио с выхода версии 1.0 произошло разделение на модули, при желании можно не тащить транспорты для фалбеков и использовать только вебсокеты.
Большой Список Обязательных Зависимостейaccepts@1.1.4 after@0.8.1 arraybuffer.slice@0.0.6 backo2@1.0.2 base64-arraybuffer@0.1.2 base64id@0.1.0 benchmark@1.0.0 better-assert@1.0.2 blob@0.0.4 callsite@1.0.0 component-bind@1.0.0 component-emitter@1.1.2 component-emitter@1.2.0 component-inherit@0.0.3 debug@0.7.4 debug@2.2.0 engine.io-client@1.6.11 engine.io-parser@1.2.4 engine.io@1.6.11 has-binary@0.1.6 has-binary@0.1.7 has-cors@1.1.0 indexof@0.0.1 isarray@0.0.1 json3@3.2.6 json3@3.3.2 mime-db@1.12.0 mime-types@2.0.14 ms@0.7.1 negotiator@0.4.9 object-component@0.0.3 options@0.0.6 parsejson@0.0.1 parseqs@0.0.2 parseuri@0.0.4 socket.io-adapter@0.4.0 socket.io-client@1.4.8 socket.io-parser@2.2.2 socket.io-parser@2.2.6 socket.io@1.4.8 to-array@0.1.4 ultron@1.0.2 utf8@2.1.0 ws@1.0.1 ws@1.1.0 xmlhttprequest-ssl@1.5.1 yeast@0.1.2
CrazyNiger
11.08.2016 09:26Что он делает, что невозможно на вебсокетах? Хотелось бы узнать, так как внутри он использует именно вебсокеты.
А я разве утверждал, что он делает что-то исключительное. что нельзя сделать самому на веб-сокетах? Сокетио дает уже готовые решения.
Первые два — тривиальны и нет смысл ради этого тащить кило чёртовщины, а про масштабированность можно поподробнее?
Комнаты тривилальны пока нет необходимости масштабировать, но вряд ли миллион ваших пользователей будут комфортно себя чувствовать на одном ядре, или даже на одном сервере. Когда сообщениями через ваше приложение будут обмениваться пользователи, которых волею судьбы занесло на разные процессы или сервера, то придется решать этот момент. Не сказать, что это сложная задача, иметь готовые решения весьма не плохо.
Kot_DaVinchi
10.08.2016 16:49+1Для устаревших. Специфика проекта предполагает возможность работы на устаревших браузерах, в т.ч. мобильных(медленно, глючно, но если человек хочет — нам не жалко). И это не говоря о том что socket.io одна из самых популярных библиотек по работе с WebSocket, даже если понижения канала не происходит.
ChALkeRx
10.08.2016 17:42+1И это не говоря о том что socket.io одна из самых популярных библиотек по работе с WebSocket, даже если понижения канала не происходит.
Ага. глючная и постоянно отваливающаяся при обновлениях.
по работе с WebSocket
Неа. Внутри она всё равно ws использует, по работе с вебсокетами самый популярный — ws. А сокетио предоставляет фаллбэк, который не особо-то и нужен в большинстве случаев, плюс за некоторую цену.
Специфика проекта предполагает возможность работы на устаревших браузерах,
Хорошо, с этим соглашусь, если у вас действительно такие требования — есть смысл. Но ие9 совсем-совсем умер, вообще-то.
affka
Тоже используем подобную самописную штуку, хотя у нас она даже больше похоже на отдельно стоящую библиотеку, чем у вас. Вашу внедрять не захочется, потому что слишком много махинаций нужно делать.
И, кстати, есть сервисы готовые по сбору ошибок и логов, в основном платные, конечно.
Kot_DaVinchi
В нашем случае это часть ядра, что даёт больше возможностей для сбора отладочной информации.