'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;