From 2b879d428eac71e7ca8c280e85a05b6ff9e48ee8 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Wed, 29 Mar 2017 08:46:20 +0300 Subject: [PATCH] #315: improve logger errors serialization --- src/services/logger/abbreviate.js | 154 ++++++++++++++++++++++++++++ src/services/logger/index.js | 3 + src/services/{ => logger}/logger.js | 4 + 3 files changed, 161 insertions(+) create mode 100644 src/services/logger/abbreviate.js create mode 100644 src/services/logger/index.js rename src/services/{ => logger}/logger.js (95%) diff --git a/src/services/logger/abbreviate.js b/src/services/logger/abbreviate.js new file mode 100644 index 0000000..5f1413d --- /dev/null +++ b/src/services/logger/abbreviate.js @@ -0,0 +1,154 @@ +const STRING_MAX_LENGTH = 128*1024; + +/** + * Create a copy of any object without non-serializable elements to make result safe for JSON.stringify(). + * Guaranteed to never throw. + * + * @param {any} obj Any data structure + * @param {object} options + * @param {function} options.filter - callback that is called on every object's key with (key,value) and should return + * value to use (may return undefined to remove unwanted keys). See nodeFilter and browserFilter. + * @param {number} options.depth - maximum recursion depth. Elements deeper than that are stringified with util.inspect() + * @param {number} options.maxSize - roughly maximum allowed size of data after JSON serialisation (but it's not guaranteed + * that it won't exceed the limit) + * + * @see https://github.com/ftlabs/js-abbreviate + */ +function abbreviate(obj, options) { + if (!options) options = {}; + + var filter = options.filter || function(k,v){return v;}; + var maxDepth = options.depth || 10; + var maxSize = options.maxSize || 1*1024*1024; + + return abbreviateRecursive(undefined, obj, filter, {sizeLeft: maxSize}, maxDepth); +} + +function limitStringLength(str) { + if (str.length > STRING_MAX_LENGTH) { + return str.substring(0, STRING_MAX_LENGTH/2) + ' … ' + str.substring(str.length - STRING_MAX_LENGTH/2); + } + return str; +} + +function abbreviateRecursive(key, obj, filter, state, maxDepth) { + if (state.sizeLeft < 0) { + return '**skipped**'; + } + + state.sizeLeft -= 5; // rough approximation of JSON overhead + + obj = filter(key, obj); + + try { + switch(typeof obj) { + case 'object': + if (null === obj) { + return null; + } + + if (maxDepth < 0) { + break; // fall back to stringification + } + + var newobj = Array.isArray(obj) ? [] : {}; + for(var i in obj) { + newobj[i] = abbreviateRecursive(i, obj[i], filter, state, maxDepth-1); + if (state.sizeLeft < 0) break; + } + return newobj; + + case 'string': + obj = limitStringLength(obj); + state.sizeLeft -= obj.length; + return obj; + + case 'number': + case 'boolean': + case 'undefined': + return obj; + } + } catch(e) {/* fall back to inspect*/} + + try { + obj = limitStringLength('' + obj); + state.sizeLeft -= obj.length; + return obj; + } catch(e) { + return "**non-serializable**"; + } +} + +function commonFilter(key, val) { + if ('function' === typeof val) { + return undefined; + } + + if (val instanceof Date) { + return "**Date** " + val; + } + + if (val instanceof Error) { + var err = { + // These properties are implemented as magical getters and don't show up in for in + stack: val.stack, + message: val.message, + name: val.name, + }; + for(var i in val) { + err[i] = val[i]; + } + return err; + } + + return val; +} + +function nodeFilter(key, val) { + + // domain objects are huge and have circular references + if (key === 'domain' && 'object' === typeof val && val._events) { + return "**domain ignored**"; + } + if (key === 'domainEmitter') { + return "**domainEmitter ignored**"; + } + + if (val === global) { + return "**global**"; + } + + return commonFilter(key, val); +} + +function browserFilter(key, val) { + if (val === window) { + return "**window**"; + } + + if (val === document) { + return "**document**"; + } + + if (val instanceof HTMLElement) { + var outerHTML = val.outerHTML; + if ('undefined' != typeof outerHTML) { + return "**HTMLElement** " + outerHTML; + } + } + + return commonFilter(key, val); +} + + +export { + abbreviate, + nodeFilter, + browserFilter +}; + +export default function(obj) { + return abbreviate(obj, { + filter: browserFilter + }) +}; diff --git a/src/services/logger/index.js b/src/services/logger/index.js new file mode 100644 index 0000000..0e991df --- /dev/null +++ b/src/services/logger/index.js @@ -0,0 +1,3 @@ +import logger from './logger'; + +export default logger; diff --git a/src/services/logger.js b/src/services/logger/logger.js similarity index 95% rename from src/services/logger.js rename to src/services/logger/logger.js index 5960cdb..a3eada1 100644 --- a/src/services/logger.js +++ b/src/services/logger/logger.js @@ -1,5 +1,7 @@ import Raven from 'raven-js'; +import abbreviate from './abbreviate'; + const isTest = process.env.__TEST__; // eslint-disable-line const isProduction = process.env.__PROD__; // eslint-disable-line @@ -72,6 +74,8 @@ const logger = { }; } + context = abbreviate(context); // prepare data for JSON.stringify + console[method](message, context); // eslint-disable-line Raven.captureException(message, {