parse-logs
Version:
Parse and validate logs to adhere to the message and meta standards from Lad and Cabin
151 lines (127 loc) • 4.53 kB
JavaScript
const ErrorStackParser = require('error-stack-parser');
const defaultsDeep = require('lodash/defaultsDeep');
const includes = require('lodash/includes');
const isEmpty = require('lodash/isEmpty');
const isError = require('iserror');
const isObject = require('lodash/isObject');
const isString = require('lodash/isString');
const isWhitespace = require('is-whitespace');
const parseRequest = require('parse-request');
const pick = require('lodash/pick');
const prepareStackTrace = require('prepare-stack-trace');
const rfdc = require('rfdc');
const clone = rfdc();
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
function parseError(error) {
const err = new Error(error.message);
const { stack } = err;
for (const prop in error) {
if (Object.prototype.hasOwnProperty.call(error, prop))
err[prop] = error[prop];
}
if (!err.name && err.constructor.name) err.name = err.constructor.name;
// <https://github.com/tjmehta/error-to-json/blob/f8c293395c71d157ac96eb83657da9c47d7dadb2/src/index.ts#L55-L56>
if (err.stack === stack) err.stack = stack.slice(0, stack.indexOf('\n'));
//
// Note we could use `stackTrace.parse(err)`
// however we shouldn't assume that everyone
// will be sending us Node-like stack traces
// (e.g. ones converted using StackTrace.JS `fromError`)
//
// const stackTrace = require('stack-trace');
//
err.stack = prepareStackTrace(err, ErrorStackParser.parse(err));
return err;
}
// req = ctx.request (Koa)
// req = req (Express)
// eslint-disable-next-line complexity
const parseLogs = (req, userFields = ['ip_address'], allowEmpty = false) => {
// ensure that `req` is an object
if (!isObject(req))
throw new Error('Request object was missing or not an object');
const isBodyAnObject = isObject(req.body);
// ensure that a 'body' exists in the request
if (!allowEmpty && !isBodyAnObject)
throw new Error('Log request is missing parsed `body` object property');
// parse the request body for `err`, `message`, and `meta` object
const log = {};
if (isBodyAnObject) {
if (isObject(req.body.err) && !isEmpty(req.body.err))
log.err = clone(req.body.err);
if (isString(req.body.message) && !isWhitespace(req.body.message))
log.message = clone(req.body.message);
if (isObject(req.body.meta) && !isEmpty(req.body.meta))
log.meta = clone(req.body.meta);
}
// ensure that we have something sent from the client otherwise throw error
if (!allowEmpty && isEmpty(log))
throw new Error(
'Log is missing `message` and/or `meta` properties (at least one is required)'
);
// if `log.meta` is not an object then make it one
if (!isObject(log.meta)) log.meta = {};
// parse the request (will populate user and IP if they do not already exist)
log.meta = defaultsDeep(log.meta, parseRequest({ req, userFields }));
// parse the app info
if (isObject(log.meta.app))
log.meta.app = pick(log.meta.app, [
'name',
'version',
'node',
'hash',
'tag',
'environment',
'hostname',
'pid'
]);
// ensure log level is a String if it was passed in request
if (!allowEmpty && !isString(log.meta.level))
throw new Error('Log meta `level` must be a String');
// ensure it is a valid log level
if (!allowEmpty && !includes(levels, log.meta.level))
throw new Error(
`Log \`level\` of "${
log.meta.level
}" was invalid, it must be one of: ${levels.join(', ')}`
);
// parse the error if it has one in `log.meta.original_err`
if (
isObject(log.meta) &&
isObject(log.meta.original_err) &&
isString(log.meta.original_err.message) &&
!isError(log.meta.original_err)
) {
try {
log.meta.original_err = parseError(log.meta.original_err);
} catch (err) {
log.meta.err = err;
}
}
// parse the error if it has one in `log.err`
if (isObject(log.err) && isString(log.err.message) && !isError(log.err)) {
try {
log.err = parseError(log.err);
} catch (err) {
log.original_err = log.err;
log.err = err;
}
}
// parse the error if it has one in `log.meta.err`
if (
isObject(log.meta) &&
isObject(log.meta.err) &&
isString(log.meta.err.message) &&
!isError(log.meta.err)
) {
try {
log.meta.err = parseError(log.meta.err);
} catch (err) {
log.meta.original_err = log.meta.err;
log.meta.err = err;
}
}
// return the log
return log;
};
module.exports = parseLogs;