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

imageЭтап поддержки продуктов отнимает много сил и нервов. Путь от «я нажимаю а оно не работает» до решения проблемы, даже у первоклассного телепата, может занимать много времени. Времени, в течение которого клиент/начальник будет зол и недоволен.

Чтобы сократить время решения проблемы, нужно оперативно узнавать об ошибках, иметь как можно более точную информацию о том, что к ней привело и, желательно, собирать всё вместе.

О своём решении я и расскажу под катом.

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.

Весь код можно посмотреть на гитхабе (ссылка внизу), а сейчас пройдёмся по основным задачам:

  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)


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

Буду рад комментариям.
Поделиться публикацией

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

    0
    Тоже используем подобную самописную штуку, хотя у нас она даже больше похоже на отдельно стоящую библиотеку, чем у вас. Вашу внедрять не захочется, потому что слишком много махинаций нужно делать.
    И, кстати, есть сервисы готовые по сбору ошибок и логов, в основном платные, конечно.
      +1
      В нашем случае это часть ядра, что даёт больше возможностей для сбора отладочной информации.
      +1
      А что происходит с отловленными ошибками дальше? Загорается какая-то лампочка в админке, и какие-то дежурные разбираются в ситуации?
      И что будет, если в релизе выкатится фатальная ошибка какой-нибудь жутко популярной ручки, не заддосит ли ваш механизм сбора ошибок?
        0
        У нас построена таким образом, что все ошибки хранятся на отдельном сервере, который не жалко положить. А сама система сбора ошибок учитывает что сервер может лежать и просто не пишет в него (потому что если начался ддос ошибками, то значит саму ошибку уже записали). PS: я к либе из данного поста отношения не имею, я про свои решения)
          +1
          Лампочка в админке. Только без дежурных программистов, поднятых по тревоге.

          Вся система в целом не направлена на высокие нагрузки, однако это хорошая идея, при большом потоке ошибок складывать их в пачки или сверять с последними отправленными и не слать дубли.
            0
            И что будет, если в релизе выкатится фатальная ошибка какой-нибудь жутко популярной ручки, не заддосит ли ваш механизм сбора ошибок?

            Поддерживаю, вот эта тема гораздо интереснее самого отлова ошибок. Как сделать редукцию и определять однотипные ошибки от разных пользователей? Что бы писать в базу не весь поток а тупо первый десяток инцидентов, а потом лишь собирать статистику если она нужна (число инцидентов, версии пользователей и все такое)

            +1
            Мне кажется как-то жирно для ошибок, которые могут ни когда не произойти, держать постоянно открытое вебсокет соединение и тянуть не маленькую библиотеку socket.io.
              0
              Всё, конечно, зависит от загруженности сервиса. Возникает идея сделать возможным сбор ошибок по запросу (в т.ч. на продакшне).
                0
                Скорее всего вебсокеты используется не только для передачи ошибок, но и в основном «цикле» работы приложения.
                0
                socket.io используется нами как основной канал для api сервера, т.к. мы пишем RIA Отсюда и выбор.
                При желании можно спокойно переписать на динамику.
                  +2

                  А для каких конкретно браузеров сейчас есть необходимость в socket.io? Чем плох стандартный WebSocket?

                    0
                    Эммм вы о чем?! А socket.io это что марсианские почтовые голуби? Он его и использует + пердоставляет прозрачный даунгрейд канала в случае если его использование невозможно к примеру браузер не умеет WebSocket
                      +1
                      Он его и использует

                      Я в курсе. Но socket.io, кроме этого, тянет ещё килограмм зависимостей и строит фаллбэки для поддержки того, что давно умерло.


                      к примеру браузер не умеет WebSocket

                      Пример браузера приведите, пожалуйста.


                      Как раз суть в том, что для этого тянуть большой и страшный сокетио уже не надо — всё работает и так, и более стабильно.

                        0

                        caniuse гляньте, Вам лень что ли? Ослы, как обычно. И как обычно — их поддержка обязательна у корпоративных заказчиков.

                          +1

                          Мне не лень, я хочу, чтобы вы своими глазами посмотрели и убедились, что там только ие9 из десктопных. Который уже не поддерживается MS, если у вас не Vista. Плюс очень старые мобильные времён iOS 5.

                            0
                            Ага, и не очень старые и гораздо более (чем iOS5) распространенные андройды 4.3 и ниже. Но вообще думается вы правы, привычка она такая.
                              +1

                              4.3 это не все андроиды. Это штатный андроид браузер версии 4.3. У всех версий штатного андроид браузера целиком — 9% в русскоязычном сегменте, всё остальное — хром. Причём надо ещё смотреть, какая часть из этого — 4.3, ими вообще скорее как звонилками пользуются.

                              +1

                              Если к Вам заказчик придет с XP-ой — Вы его заказ будете выполнять или рассказывать ему что он дебил недальновидный человек и ему как минимум надо поменять безопасника а заодно обновить систему? На паре тысяч компов во всех офисах.

                                +1
                                Я откажусь работать. А если у них пни первые и NovellNetware или OS/2 или табуляторы? Мне что работать с ними? Зачем? XP это сим-сим ей уже 15 лет так-то. XP в 2016 — это шиза и для внедрения и разработки нового продукта требуется иметь подходящую инфраструктуру. Это техничесоке требование типа мощности электросети или предельной нагрузки на перекрытия, это же не просто хотелка.
                                  0

                                  Это сермяжная правда жизни. Настоящей жизни, которая за окном а не за экраном монитора. Можно отказаться от проекта если Вы — владелец компании (студии, команды интеграторов). Или фрилансер (как я). Во всех остальных случаях — Вы не отказываетесь от проекта — Вы пишете заявление (хорошо если по собственному). В некоторых ситуациях это может быть расценено как проявления непрофессионализма. Более того — в некотором подмножестве этих ситуаций это именно так и будет. А еще есть подмножество ситуаций в котором об этом станет известно не только Вам и Вашему работодателю, но и некоторому множеству других потенциальных работодателей. Жизнь, она такая — не всегда понятная и не всегда приятная )

                                    +1
                                    Позвольте не согласиться (да я руководитель).
                                    Опыт и практика показывают, что при таком подходе проект заранее провальный — т.к. всем наплевать на проект причем и заказчику и исполнителю (продажникам и руководству как минимум).
                                    Заказчику, потомучто сейчас не 90ые, и если с IT все настолько плохо и нет вариантов изменить ситуацию, то и с другим такаже все печально(а значит будет — постоянное недофинансирование, нарушения договоров, «получилось как всегда», невозможность реализации эффективной работы на имеющемся и прочее, прочее, прочее, и виноват будет всегда исполнитель — тыж программист, профессионал).
                                    Исполнителю, потомучто если ваш руководитель настолько алчный идиот, во-первых потомучто поддержка настолько устаревших систем это ДОРОГО (т.к. требует более редких спецов и гораздо больше человекочасов), а это значит компания меньше заработает, во-вторых он не понимает то что я написал про заказчика а значи сидит не на своем месте.
                                    А когда все наплевать ожидать успешной реализации проекта не стоит + куча дополнительных факторов (трудоемкость, неинтересность, сложность, высокая вероятность аварий, неспособность переварить объемы информации и тп).
                                    В любых дургих конфигурациях такой проект просто не начнется.

                                    Ок — давайте понятнее, если перекрытия в здании не расчитано на на новое оборудование то он рухнут. Если подключать станок 380V/50A к сети 220V/20A ничего не заработает. Тут тоже самое, winXP это слишком древняя система. Если у предприятия для нового проекта IT-инфраструктура не годится для реализации проекта ничего хорошего тоже можно не ждать — не взлетит.

                                    Более того если нет возможности изменить ситцуациию или не уволиться, а остаться в таком чудо проекте то есть очень серьезный шанс что обвинят в провале именно вас, и шанс этого и степень порчи репутации на ПОРЯДОК больше чем в ситуации которую описали вы.
                            +1
                            Кроме старых браузеров бывают еще прокси…
                              –1
                              Но socket.io, кроме этого, тянет ещё килограмм зависимостей и строит фаллбэки для поддержки того, что давно умерло.

                              В скотио с выхода версии 1.0 произошло разделение на модули, при желании можно не тащить транспорты для фалбеков и использовать только вебсокеты.

                              А используют сокетио, главным образом, из-за реализованных механизмов комнат, нэмспесов, решенных вопросов с масштабированностью и тому подобному.
                                +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

                                Это всё обязательно поставится при установке socket.io текущей версии. Включая две версии ws. Часть из этого любит ломаться при обновлении, потому что привязано к нативным модулям. Причём они это чинят далеко не сразу, и это даже почему-то иногда в issues Node.js приносят:



                                Часть из этого — из-за старого ws, который они долго обновляли по своей цепочке зависимостей — посмотрите время между фиксом руками и реальным исправлением тикета.


                                Плюс оно в каки-то версиях при установке тянуло блобы (и в один чудесный момент установка сломалась так как сервер ой), не знаю, как сейчас:



                                Кроме этого, у сокетио есть некоторые проблемы, которые сказываются в т.ч. на других проектах (например, karma — https://github.com/karma-runner/karma/issues/1788).


                                А используют сокетио, главным образом, из-за реализованных механизмов комнат, нэмспесов, решенных вопросов с масштабированностью и тому подобному.

                                Первые два — тривиальны и нет смысл ради этого тащить кило чёртовщины, а про масштабированность можно поподробнее? Что он делает, что невозможно на вебсокетах? Хотелось бы узнать, так как внутри он использует именно вебсокеты.

                                  0
                                  Что он делает, что невозможно на вебсокетах? Хотелось бы узнать, так как внутри он использует именно вебсокеты.

                                  А я разве утверждал, что он делает что-то исключительное. что нельзя сделать самому на веб-сокетах? Сокетио дает уже готовые решения.

                                  Первые два — тривиальны и нет смысл ради этого тащить кило чёртовщины, а про масштабированность можно поподробнее?

                                  Комнаты тривилальны пока нет необходимости масштабировать, но вряд ли миллион ваших пользователей будут комфортно себя чувствовать на одном ядре, или даже на одном сервере. Когда сообщениями через ваше приложение будут обмениваться пользователи, которых волею судьбы занесло на разные процессы или сервера, то придется решать этот момент. Не сказать, что это сложная задача, иметь готовые решения весьма не плохо.
                            +1
                            Для устаревших. Специфика проекта предполагает возможность работы на устаревших браузерах, в т.ч. мобильных(медленно, глючно, но если человек хочет — нам не жалко). И это не говоря о том что socket.io одна из самых популярных библиотек по работе с WebSocket, даже если понижения канала не происходит.
                              +1
                              И это не говоря о том что socket.io одна из самых популярных библиотек по работе с WebSocket, даже если понижения канала не происходит.

                              Ага. глючная и постоянно отваливающаяся при обновлениях.


                              по работе с WebSocket

                              Неа. Внутри она всё равно ws использует, по работе с вебсокетами самый популярный — ws. А сокетио предоставляет фаллбэк, который не особо-то и нужен в большинстве случаев, плюс за некоторую цену.


                              Специфика проекта предполагает возможность работы на устаревших браузерах,

                              Хорошо, с этим соглашусь, если у вас действительно такие требования — есть смысл. Но ие9 совсем-совсем умер, вообще-то.

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

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