Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 9
Call Stack:
0.0001 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0
Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 9
Call Stack:
0.0001 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0
[Из песочницы] Телепатия на стероидах в js/node.js | Хостинг за 90 р. от cyberhost.biz — платный хостинг
Этап поддержки продуктов отнимает много сил и нервов. Путь от «я нажимаю а оно не работает» до решения проблемы, даже у первоклассного телепата, может занимать много времени. Времени, в течение которого клиент/начальник будет зол и недоволен.
Чтобы сократить время решения проблемы, нужно оперативно узнавать об ошибках, иметь как можно более точную информацию о том, что к ней привело и, желательно, собирать всё вместе.
О своём решении я и расскажу под катом.
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)
Рабочий пример можно посмотреть на гитхабе. За архитектуру прошу не ругать, пример делался методом удалить-из-проекта-всё-ненужное.
Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0001 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0001 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0
Свежие комментарии