307 lines
8.1 KiB
JavaScript
307 lines
8.1 KiB
JavaScript
'use strict';
|
|
|
|
// external modules
|
|
var assert = require('assert-plus');
|
|
var EventEmitter = require('events');
|
|
var _ = require('lodash');
|
|
var nerror = require('@netflix/nerror');
|
|
var MultiError = nerror.MultiError;
|
|
var VError = nerror.VError;
|
|
var safeJsonStringify;
|
|
|
|
// We load the domain module lazily to avoid performance regression on Node.js
|
|
// v12.
|
|
var domain;
|
|
|
|
// try to require optional dependency
|
|
try {
|
|
// eslint-disable-next-line global-require
|
|
safeJsonStringify = require('safe-json-stringify');
|
|
} catch (e) {
|
|
safeJsonStringify = null;
|
|
}
|
|
|
|
|
|
/**
|
|
* @class ErrorSerializer
|
|
* @param {Object} opts an options object
|
|
*/
|
|
function ErrorSerializer(opts) {
|
|
assert.object(opts, 'opts');
|
|
assert.bool(opts.topLevelFields, 'opts.topLevelFields');
|
|
|
|
/**
|
|
* when true, serialize all top level fields found on the Error object
|
|
* @type {Bool}
|
|
*/
|
|
this._serializeTopLevelFields = opts.topLevelFields;
|
|
|
|
/**
|
|
* find known fields we don't want to serialize
|
|
* @type {Array}
|
|
*/
|
|
this._knownFields = this._findKnownFields();
|
|
}
|
|
|
|
|
|
/**
|
|
* loop through all errors() in a MultiError and build a stack trace
|
|
* output.
|
|
* @private
|
|
* @method _getMultiErrorStack
|
|
* @param {Object} err an error object
|
|
* @returns {String} stack trace string
|
|
*/
|
|
ErrorSerializer.prototype._getMultiErrorStack =
|
|
function _getMultiErrorStack(err) {
|
|
|
|
var self = this;
|
|
var out = '';
|
|
|
|
_.forEach(err.errors(), function(e, idx, errs) {
|
|
out += 'MultiError ' + (idx + 1) + ' of ' + errs.length + ': ';
|
|
out += self._getFullErrorStack(e) + '\n';
|
|
});
|
|
|
|
// remove last new line char
|
|
out = out.slice(0, -1);
|
|
|
|
return out;
|
|
};
|
|
|
|
|
|
/**
|
|
* loop through all cause() errors and build a stack trace output
|
|
* @private
|
|
* @method _getFullErrorStack
|
|
* @param {Object} err an error object
|
|
* @returns {String} stack trace string
|
|
*/
|
|
ErrorSerializer.prototype._getFullErrorStack =
|
|
function _getFullErrorStack(err) {
|
|
var self = this;
|
|
var e = err;
|
|
var out = '';
|
|
var first = true;
|
|
|
|
do {
|
|
if (first !== true) {
|
|
out += '\nCaused by: ';
|
|
}
|
|
|
|
// parse out first new line of stack trace, append context there.
|
|
var stackString = (e.stack || e.toString()).split('\n');
|
|
|
|
out += stackString.shift() + self._getSerializedContext(e);
|
|
out += stackString.join('\n');
|
|
e = (typeof e.cause === 'function') ? e.cause() : null;
|
|
first = false;
|
|
} while (e);
|
|
|
|
return out;
|
|
};
|
|
|
|
|
|
/* eslint-disable max-len */
|
|
/* jscs:disable maximumLineLength */
|
|
/**
|
|
* serialize the error context object into a string. borrows liberally from
|
|
* bunyan's serializer:
|
|
* https://github.com/trentm/node-bunyan/blob/6fdc5ff20965b81ab15f8f408fe11917e06306f6/lib/bunyan.js#L865
|
|
* @private
|
|
* @method _getSerializedContext
|
|
* @param {Object} err an error object
|
|
* @return {String} serialized context obj
|
|
*/
|
|
/* jscs:enable maximumLineLength */
|
|
/* eslint-enable max-len */
|
|
ErrorSerializer.prototype._getSerializedContext =
|
|
function _getSerializedContext(err) {
|
|
|
|
/**
|
|
* serialize a POJO into a string of the format:
|
|
* (key="valString", key2=valInteger, key3={a:valPojo})
|
|
* @param {Object} obj a POJO to serialize
|
|
* @return {String}
|
|
*/
|
|
function serializeIntoEqualString(obj) {
|
|
|
|
var out = '';
|
|
|
|
_.forEach(obj, function(val, key) {
|
|
var stringVal;
|
|
|
|
try {
|
|
stringVal = JSON.stringify(val, safeCycles());
|
|
} catch (e) {
|
|
if (safeJsonStringify) {
|
|
stringVal = safeJsonStringify(val);
|
|
} else {
|
|
stringVal = 'unserializable! you can install ' +
|
|
'"safe-json"stringify" module for safer ' +
|
|
'stringification';
|
|
}
|
|
}
|
|
|
|
out += key + '=' + stringVal + ', ';
|
|
});
|
|
// remove last comma
|
|
return out.slice(0, -2);
|
|
}
|
|
|
|
var self = this;
|
|
var ret = '';
|
|
|
|
// look for error context in 3 places, in ascending order of precedence:
|
|
// 1) raw fields on the error object that are not known verror or
|
|
// restify-error fields
|
|
// 2) restify-error context fields (restify-errors@ <= 5.x)
|
|
// 3) verror info field
|
|
var topLevelFields = (self._serializeTopLevelFields === true) ?
|
|
_.omit(err, self._knownFields) :
|
|
{};
|
|
|
|
// We don't want to load domains just to check if topLevelFields.domain is
|
|
// a Domain instance, so first we make sure domains are already loaded.
|
|
if (EventEmitter.usingDomains) {
|
|
if (!domain) {
|
|
// eslint-disable-next-line global-require
|
|
domain = require('domain');
|
|
}
|
|
|
|
if (topLevelFields.domain instanceof domain.Domain) {
|
|
topLevelFields = _.omit(topLevelFields, [ 'domain' ]);
|
|
}
|
|
}
|
|
|
|
// combine all fields into a pojo, and serialize
|
|
var allFields = _.assign({}, topLevelFields, err.context, nerror.info(err));
|
|
|
|
if (!_.isEmpty(allFields)) {
|
|
ret = ' (' + serializeIntoEqualString(allFields) + ')';
|
|
}
|
|
|
|
return ret + '\n';
|
|
};
|
|
|
|
|
|
/**
|
|
* find a list of known error fields that we don't want to serialize. create
|
|
* verror instances to programatically build that list.
|
|
* @private
|
|
* @method _findKnownFields
|
|
* @return {Array}
|
|
*/
|
|
ErrorSerializer.prototype._findKnownFields = function _findKnownFields() {
|
|
// when looping through arbitrary fields attached to the error object, cross
|
|
// reference them against this known list of fields.
|
|
var fields = [
|
|
// known Error fields
|
|
'message',
|
|
'name',
|
|
'toJSON',
|
|
// known restify-error fields
|
|
'toString',
|
|
'body'
|
|
];
|
|
|
|
// make a verror and multierror and find expected fields
|
|
var verr = new VError();
|
|
var multiErr = new MultiError([ verr ]);
|
|
fields.push(_.keys(verr));
|
|
fields.push(_.keys(Object.getPrototypeOf(verr)));
|
|
fields.push(_.keys(multiErr));
|
|
fields.push(_.keys(Object.getPrototypeOf(multiErr)));
|
|
|
|
return _(fields).flatten().uniq().value();
|
|
};
|
|
|
|
|
|
/**
|
|
* built in bunyan serializer for restify errors. it's more or less the
|
|
* standard bunyan serializer with support for the context property.
|
|
* @private
|
|
* @method serialize
|
|
* @param {Object} err an error object
|
|
* @returns {Object} serialized object for bunyan output
|
|
*/
|
|
ErrorSerializer.prototype.serialize = function serialize(err) {
|
|
if (!err || !err.stack) {
|
|
return err;
|
|
}
|
|
|
|
var self = this;
|
|
var multiErr = (err.errors && _.isFunction(err.errors));
|
|
|
|
return {
|
|
message: err.message,
|
|
name: err.name,
|
|
stack: (multiErr === true) ?
|
|
self._getMultiErrorStack(err) :
|
|
self._getFullErrorStack(err),
|
|
code: err.code,
|
|
signal: err.signal
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* copy pasta-ed from bunyan.
|
|
* A JSON stringifier that handles cycles safely.
|
|
* Usage: JSON.stringify(obj, safeCycles())
|
|
* @returns {Function}
|
|
*/
|
|
function safeCycles() {
|
|
|
|
var seen = [];
|
|
|
|
return function(key, val) {
|
|
if (!val || typeof (val) !== 'object') {
|
|
return val;
|
|
}
|
|
|
|
if (seen.indexOf(val) !== -1) {
|
|
return '[Circular]';
|
|
}
|
|
seen.push(val);
|
|
return val;
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* factory function to create customized serializers.
|
|
* @public
|
|
* @param {Object} options an options object
|
|
* @return {Function} serializer function
|
|
*/
|
|
function factory(options) {
|
|
assert.optionalObject(options, 'options');
|
|
|
|
var opts = _.assign({
|
|
topLevelFields: false
|
|
}, options);
|
|
|
|
var serializer = new ErrorSerializer(opts);
|
|
// rebind the serialize function since this will be lost when we export it
|
|
// as a POJO
|
|
serializer.serialize = serializer.serialize.bind(serializer);
|
|
|
|
return serializer;
|
|
}
|
|
|
|
|
|
// we should be exporting this create function, but to refrain from making it a
|
|
// breaking change, let's attach the create to the existing function export. we
|
|
// can make the change in next major version.
|
|
var defaultSerializer = factory();
|
|
defaultSerializer.serialize.create = function create(opts) {
|
|
var serializer = factory(opts);
|
|
return {
|
|
err: serializer.serialize
|
|
};
|
|
};
|
|
|
|
|
|
module.exports = defaultSerializer.serialize;
|