2021-12-03 17:58:48 +01:00

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;