bifocal/node_modules/@tryghost/pretty-stream/lib/PrettyStream.js

211 lines
6.6 KiB
JavaScript

const moment = require('moment');
const Transform = require('stream').Transform;
const format = require('util').format;
const prettyjson = require('prettyjson');
const each = require('lodash/each');
const omit = require('lodash/omit');
const get = require('lodash/get');
const isArray = require('lodash/isArray');
const isEmpty = require('lodash/isEmpty');
const isObject = require('lodash/isObject');
const isString = require('lodash/isString');
const _private = {
levelFromName: {
10: 'trace',
20: 'debug',
30: 'info',
40: 'warn',
50: 'error',
60: 'fatal'
},
colorForLevel: {
10: 'grey',
20: 'grey',
30: 'cyan',
40: 'magenta',
50: 'red',
60: 'inverse'
},
colors: {
default: [39, 39],
bold: [1, 22],
italic: [3, 23],
underline: [4, 24],
inverse: [7, 27],
white: [37, 39],
grey: [90, 39],
black: [30, 39],
blue: [34, 39],
cyan: [36, 39],
green: [32, 39],
magenta: [35, 39],
red: [31, 39],
yellow: [33, 39]
}
};
function colorize(colors, value) {
if (isArray(colors)) {
return colors.reduce((acc, color) => colorize(color, acc), value);
} else {
return '\x1B[' + _private.colors[colors][0] + 'm' + value + '\x1B[' + _private.colors[colors][1] + 'm';
}
}
function statusCode(status) {
/* eslint-disable indent */
const color = status >= 500 ? 'red'
: status >= 400 ? 'yellow'
: status >= 300 ? 'cyan'
: status >= 200 ? 'green'
: 'default'; // no color
/* eslint-enable indent */
return colorize(color, status);
}
class PrettyStream extends Transform {
constructor(options) {
options = options || {};
super(options);
this.mode = options.mode || 'short';
}
write(data, enc, cb) {
// Bunyan sometimes passes things as objects. Because of this, we need to make sure
// the data is converted to JSON
if (isObject(data) && !(data instanceof Buffer)) {
data = JSON.stringify(data);
}
super.write(data, enc, cb);
}
_transform(data, enc, cb) {
if (!isString(data)) {
data = data.toString();
}
// Remove trailing newline if any
data = data.replace(/\\n$/, '');
try {
data = JSON.parse(data);
} catch (err) {
cb(err);
// If data is not JSON we don't want to continue processing as if it is
return;
}
let output = '';
const time = moment(data.time).format('YYYY-MM-DD HH:mm:ss');
let logLevel = _private.levelFromName[data.level].toUpperCase();
const codes = _private.colors[_private.colorForLevel[data.level]];
let bodyPretty = '';
logLevel = '\x1B[' + codes[0] + 'm' + logLevel + '\x1B[' + codes[1] + 'm';
if (data.req) {
output += format('[%s] %s "%s %s" %s %s\n',
time,
logLevel,
data.req.method.toUpperCase(),
get(data, 'req.originalUrl'),
statusCode(get(data, 'res.statusCode')),
get(data, 'res.responseTime')
);
} else if (data.msg === undefined) {
output += format('[%s] %s\n',
time,
logLevel
);
} else {
bodyPretty += data.msg;
output += format('[%s] %s %s\n', time, logLevel, bodyPretty);
}
each(omit(data, ['time', 'level', 'name', 'hostname', 'pid', 'v', 'msg']), function (value, key) {
// we always output errors for now
if (isObject(value) && value.message && value.stack) {
let error = '\n';
if (value.errorType) {
error += colorize(_private.colorForLevel[data.level], 'Type: ' + value.errorType) + '\n';
}
error += colorize(_private.colorForLevel[data.level], value.message) + '\n\n';
if (value.context) {
error += colorize('white', value.context) + '\n';
}
if (value.help) {
error += colorize('yellow', value.help) + '\n';
}
if (value.context || value.help) {
error += '\n';
}
if (value.id) {
error += colorize(['white', 'bold'], 'Error ID:') + '\n';
error += ' ' + colorize('grey', value.id) + '\n\n';
}
if (value.code) {
error += colorize(['white', 'bold'], 'Error Code: ') + '\n';
error += ' ' + colorize('grey', value.code) + '\n\n';
}
if (value.errorDetails) {
let details = value.errorDetails;
try {
const jsonDetails = JSON.parse(value.errorDetails);
details = isArray(jsonDetails) ? jsonDetails[0] : jsonDetails;
} catch (err) {
// no need for special handling as we default to unparsed 'errorDetails'
}
const pretty = prettyjson.render(details, {
noColor: true
}, 4);
error += colorize(['white', 'bold'], 'Details:') + '\n';
error += colorize('grey', pretty) + '\n\n';
}
if (value.stack && !value.hideStack) {
error += colorize('grey', '----------------------------------------') + '\n\n';
error += colorize('grey', value.stack) + '\n';
}
output += format('%s\n', colorize(_private.colorForLevel[data.level], error));
} else if (isObject(value)) {
bodyPretty += '\n' + colorize('yellow', key.toUpperCase()) + '\n';
let sanitized = {};
each(value, function (innerValue, innerKey) {
if (!isEmpty(innerValue)) {
sanitized[innerKey] = innerValue;
}
});
bodyPretty += prettyjson.render(sanitized, {}) + '\n';
} else {
bodyPretty += prettyjson.render(value, {}) + '\n';
}
});
if (this.mode !== 'short' && (bodyPretty !== data.msg)) {
output += format('%s\n', colorize('grey', bodyPretty));
}
cb(null, output);
}
}
module.exports = PrettyStream;