egg-logger
Version:
297 lines (267 loc) • 8.71 kB
JavaScript
;
const os = require('os');
const util = require('util');
const { performance } = require('perf_hooks');
const chalk = require('chalk');
const utility = require('utility');
const iconv = require('iconv-lite');
const circularJSON = require('circular-json-for-egg');
const { FrameworkBaseError, FrameworkErrorFormater } = require('egg-errors');
const levels = require('./level');
const hostname = os.hostname();
const durationRegexp = /([0-9]+ms)/g;
// eslint-disable-next-line no-useless-escape
const categoryRegexp = /(\[[\w\-_.:]+\])/g;
const httpMethodRegexp = /(GET|POST|PUT|PATH|HEAD|DELETE) /g;
/**
* @class LoggerUtils
*/
module.exports = {
normalizeLevel(level) {
if (typeof level === 'number') {
return level;
}
// 'WARN' => level.warn
if (typeof level === 'string' && level) {
return levels[level.toUpperCase()];
}
},
defaultContextPaddingMessage(ctx) {
// Auto record necessary request context infomation, e.g.: user id, request spend time
// format: '[$userId/$ip/$traceId/$use_ms $method $url]'
const userId = ctx.userId || '-';
const traceId = ctx.tracer?.traceId || '-';
let use = 0;
if (ctx.performanceStarttime) {
use = Math.floor((performance.now() - ctx.performanceStarttime) * 1000) / 1000;
} else if (ctx.starttime) {
use = Date.now() - ctx.starttime;
}
return '[' +
userId + '/' +
ctx.ip + '/' +
traceId + '/' +
use + 'ms ' +
ctx.method + ' ' +
ctx.url +
']';
},
// format with ctx and ctx.tracer
// Auto record necessary request context infomation, e.g.: user id, request spend time
// format: '[$userId/$ip/$traceId/$use_ms $method $url]'
defaultFormatter(meta) {
let paddingMessage = ' ';
// try to use the exists paddingMessage first
if (meta.paddingMessage) {
paddingMessage = ` ${meta.paddingMessage} `;
} else {
// gen the default ctx paddingMessage
const ctx = meta.ctx;
if (ctx) {
paddingMessage = ` ${module.exports.defaultContextPaddingMessage(ctx)} `;
}
}
return meta.date + ' ' + meta.level + ' ' + meta.pid + paddingMessage + meta.message;
},
// output to Terminal format
consoleFormatter(meta) {
let paddingMessage = ' ';
// try to use the exists paddingMessage first
if (meta.paddingMessage) {
paddingMessage = ` ${meta.paddingMessage} `;
}
let msg = meta.date + ' ' + meta.level + ' ' + meta.pid + paddingMessage + meta.message;
if (!chalk.supportsColor) {
return msg;
}
if (meta.level === 'ERROR') {
return chalk.red(msg);
} else if (meta.level === 'WARN') {
return chalk.yellow(msg);
}
msg = msg.replace(durationRegexp, chalk.green('$1'));
msg = msg.replace(categoryRegexp, chalk.blue('$1'));
msg = msg.replace(httpMethodRegexp, chalk.cyan('$1 '));
return msg;
},
/**
* Get final formated log string buffer
*
* Invoke link: {@Link Logger#log} -> {@link Transport#log} -> LoggerUtils.format
* @function LoggerUtils#format
* @param {String} level - log level
* @param {Array} args - format arguments
* @param {Object} meta - loging behaviour meta infomation
* - {String} level
* - {Boolean} raw
* - {Function} formatter
* - {Error} error
* - {String} message
* - {Number} pid
* - {String} hostname
* - {String} date
* @param {Object} options - {@link Transport}'s options
* - {String} encoding
* - {Boolean} json
* - {Boolean} dateISOFormat
* - {Function} [formatter]
* - {Function} [contextFormatter]
* - {Function} [paddingMessageFormatter]
* - {number} [maxCauseChainLength]
* @return {String|Buffer} formatted log string buffer
*/
format(level, args, meta, options) {
meta = meta || {};
let message;
let output;
let formatter = meta.formatter || options.formatter;
if (meta.ctx) {
if (options.contextFormatter) {
formatter = options.contextFormatter;
if (!meta.paddingMessage) {
// should add paddingMessage to meta on custom contextFormatter
meta.paddingMessage = options.paddingMessageFormatter ?
options.paddingMessageFormatter(meta.ctx) : module.exports.defaultContextPaddingMessage(meta.ctx);
}
} else if (options.paddingMessageFormatter && !meta.paddingMessage) {
// use custom paddingMessageFormatter to format paddingMessage
meta.paddingMessage = options.paddingMessageFormatter(meta.ctx);
}
}
if (args[0] instanceof Error) {
message = formatError(args[0], options);
} else {
message = util.format.apply(util, args);
}
if (meta.raw === true) {
output = message;
} else if (options.json === true || formatter) {
meta.level = level;
meta.date = options.dateISOFormat ? new Date().toISOString() : utility.logDate(',');
meta.pid = process.pid;
meta.hostname = hostname;
meta.message = message;
if (options.json === true) {
const outputMeta = { ...meta };
outputMeta.ctx = undefined;
output = JSON.stringify(outputMeta);
} else {
output = formatter(meta);
}
} else {
output = message;
}
if (!output) return Buffer.from('');
output += options.eol;
// convert string to buffer when encoding is not utf8
return options.encoding === 'utf8' ? output : iconv.encode(output, options.encoding);
},
// Like `Object.assign`, but don't copy `undefined` value
assign(target) {
if (!target) {
return {};
}
const sources = Array.prototype.slice.call(arguments, 1);
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
if (!source) continue;
const keys = Object.keys(source);
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
if (source[key] !== undefined && source[key] !== null) {
target[key] = source[key];
}
}
}
return target;
},
formatError,
};
function errorToString(err, options, causeLength) {
options = options || {};
const maxCauseChainLength = options.maxCauseChainLength || 10;
causeLength = causeLength || 0;
if (causeLength > maxCauseChainLength) {
return 'too long cause chain';
}
let errName = err.name || 'no_name';
if (err.name === 'Error' && typeof err.code === 'string') {
errName = err.code + errName;
}
let errMessage = err.message || 'no_message';
if (err.host) {
errMessage += ` (${err.host})`;
}
// name and stack could not be change on node 0.11+
const errStack = err.stack || 'no_stack';
const errProperties = Object.keys(err).map(key => inspect(key, err[key])).join('\n');
let errorString = util.format('nodejs.%s: %s\n%s\n%s',
errName,
errMessage,
errStack.substring(errStack.indexOf('\n') + 1),
errProperties
);
if (err.name === 'AggregateError' && err.errors) {
for (let i = 0; i < err.errors.length; i++) {
const subError = err.errors[i];
const subErrorMsg = errorToString(subError, options, causeLength + 1);
errorString = util.format('%s\n[error-%d]:\n\n%s',
errorString,
i,
subErrorMsg
);
}
}
if (err.cause) {
const causeMsg = errorToString(err.cause, options, causeLength + 1);
errorString = util.format('%s\ncause:\n\n%s',
errorString,
causeMsg
);
}
return errorString;
}
function formatError(err, options, causeLength) {
if (FrameworkBaseError.isFrameworkError(err)) {
return FrameworkErrorFormater.format(err);
}
const msg = errorToString(err, options, causeLength);
return util.format('%s\npid: %s\nhostname: %s\n',
msg,
process.pid,
hostname
);
}
function inspect(key, value) {
return `${key}: ${formatObject(value)}`;
}
function formatString(str) {
if (str.length > 10000) {
return `${str.substr(0, 10000)}...(${str.length})`;
}
return str;
}
function formatBuffer(buf) {
const tail = buf.data.length > 50 ? ` ...(${buf.data.length}) ` : '';
const bufStr = buf.data.slice(0, 50).map(i => {
i = i.toString(16);
if (i.length === 1) i = `0${i}`;
return i;
}).join(' ');
return `<Buffer ${bufStr}${tail}>`;
}
function formatObject(obj) {
try {
return circularJSON.stringify(obj, (key, v) => {
if (typeof v === 'string') return formatString(v);
if (v && v.type === 'Buffer' && Array.isArray(v.data)) {
return formatBuffer(v);
}
if (v instanceof RegExp) return util.inspect(v);
return v;
});
} catch (_) {
/* istanbul ignore next */
return String(obj);
}
}