const path = require("path");
const bunyan = require("bunyan");
const bunyanDebugStream = require("bunyan-debug-stream");
const {Config} = require("./Config");
let baseLogger;
let mainLogger;
let stdoutStream;
let fileStream;
let origConsoleLog = console.log;
let origConsoleTrace = console.trace;
let origConsoleDebug = console.debug;
let origConsoleInfo = console.info;
let origConsoleWarn = console.warn;
let origConsoleError = console.error;
Error.stackTraceLimit = Config.get("log-error-stack-length");
/**
* Application logging utilities
*/
class Log {
/**
* Initializes the Logger
*/
static init() {
stdoutStream = bunyanDebugStream({
basepath: path.join(__dirname, ".."),
prefixers: {
component: function(val) {
return val;
},
},
colors: {
fatal: "bgRed",
trace: "white",
},
forceColor: Config.get("log-force-color"),
showPid: false,
});
baseLogger = bunyan.createLogger({
name: Config.get("app-name"),
stream: stdoutStream,
level: Config.get("log-level"),
src: Config.get("log-src"),
});
if (Config.get("log-file-enabled")) {
let numericDateTime = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
};
let d = new Intl.DateTimeFormat("default", numericDateTime)
.formatToParts()
.reduce((acc, v) => Object({... acc, [v.type]: v.value}), {});
fileStream = {
path: `${Config.get("log-file-path")}/${Config.get("log-file-prefix")}-${d.year}${d.month}${d.day}-${d.hour}${d.minute}${d.second}${Config.get("log-file-suffix")}`,
level: "trace",
};
baseLogger.addStream(fileStream);
}
if (Config.get("log-patch-console")) {
Log.patch();
}
mainLogger = new Log("main");
return mainLogger;
}
/**
* Patches `console.log` (and kin) to use Log
*/
static patch() {
console.log = Log.debug;
console.trace = Log.trace;
console.debug = Log.debug;
console.info = Log.info;
console.warn = Log.warn;
console.error = Log.error;
console.print = (... args) => origConsoleLog(... args);
}
/**
* After calling {@link Log.patch}, returns `console.log` (and kin) to its original state
*/
static unpatch() {
console.log = origConsoleLog;
console.trace = origConsoleTrace;
console.debug = origConsoleDebug;
console.info = origConsoleInfo;
console.warn = origConsoleWarn;
console.error = origConsoleError;
delete console.print;
}
/**
* Constructs a child logger with the same attributes as the default logger.
*
* @param {string} name - The name of this module, which will be logged with each line.
*/
constructor(name) {
this.logger = baseLogger.child({component: name});
}
/**
* Log a message at the `fatal` level.
*
* @function
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
get fatal() {
return this._fatal.bind(this);
}
// eslint-disable-next-line jsdoc/require-jsdoc
_fatal(... args) {
this.logger.fatal(... args);
if (Config.get("log-error-stack")) {
let t = {};
Error.captureStackTrace(t);
this.logger.fatal(t.stack);
}
}
/**
* Log a message at the `error` level.
*
* @function
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
get error() {
return this._error.bind(this);
}
// eslint-disable-next-line jsdoc/require-jsdoc
_error(... args) {
this.logger.error(... args);
if (Config.get("log-error-stack")) {
let t = {};
Error.captureStackTrace(t);
this.logger.error(t.stack);
}
}
/**
* Log a message at the `warn` level.
*
* @function
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
get warn() {
return this._warn.bind(this);
}
// eslint-disable-next-line jsdoc/require-jsdoc
_warn(... args) {
this.logger.warn(... args);
}
/**
* Log a message at the `info` level.
*
* @function
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
get info() {
return this._info.bind(this);
}
// eslint-disable-next-line jsdoc/require-jsdoc
_info(... args) {
this.logger.info(... args);
}
/**
* Log a message at the `debug` level.
*
* @function
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
get debug() {
return this._debug.bind(this);
}
// eslint-disable-next-line jsdoc/require-jsdoc
_debug(... args) {
this.logger.debug(... args);
}
/**
* Log a message at the `trace` level.
*
* @function
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
get trace() {
return this._trace.bind(this);
}
// eslint-disable-next-line jsdoc/require-jsdoc
_trace(... args) {
this.logger.trace(... args);
}
/**
* Returns the logger object created by {@link https://github.com/trentm/node-bunyan|Bunyan}
*
* @returns {object} The {@link https://github.com/trentm/node-bunyan#constructor-api|object created by} `createLogger`
*/
static getBaseLogger() {
return baseLogger;
}
/**
* Returns the child logger object named `main`
*
* @returns {object} The {@link https://github.com/trentm/node-bunyan#constructor-api|object created by} `createLogger`
*/
static getMainLogger() {
return mainLogger.logger;
}
/**
* Sets the logging level of messages. All messages at or above this level will be logged.
*
* @param {"trace" | "debug" | "info" | "warn" | "error" | "fatal" | number} level A recognized log level string or a number associated with the {@link https://github.com/trentm/node-bunyan#levels|log level}.
*/
static setStdoutLevel(level) {
if (typeof level === "string") {
level = Log.nameToLevel(level);
}
if ((typeof level !== "number") ||
(level > 60) ||
(level < 10) ||
((level % 10) !== 0)) {
throw new TypeError("setStdoutLevel expected level to be a number and a multiple of 10 between 0 and 61");
}
getStdoutStream().level = level;
}
/**
* Gets the current logging level for the default stdout stream.
*
* @returns {object} An object describing the current logging level. Properties are `levelName`, a string describing the current log level name
* and `levelValue` a corresponding number for the current log level.
*/
static getStdoutLevel() {
let s = getStdoutStream();
return {
levelName: Log.levelToName(s.level),
levelValue: s.level,
};
}
/**
* Adds a stream to the logger. See {@link https://www.npmjs.com/package/bunyan#streams|Bunyan documentation} for details.
* Note that {@link https://www.npmjs.com/|NPM} has logging streams for nearly any logging service (syslog, CloudWatch, Slack, Logstash, etc.)
*
* @param {object} obj The stream object to be added.
*/
static addStream(obj) {
mainLogger.logger.addStream(obj);
}
/**
* Uses the original `console.log` to write the args to `process.stdout`
*
* @function
* @param {...*} args See {@link https://nodejs.org/api/console.html|Console} for a description of arguments
* @returns {undefined} No return
*/
static print(... args) {
return origConsoleLog(... args);
}
/**
* Uses the original `console.error` to write the args to `process.stderr`
*
* @function
* @param {...*} args See {@link https://nodejs.org/api/console.html|Console} for a description of arguments
* @returns {undefined} No return
*/
static printErr(... args) {
return origConsoleError(... args);
}
/**
* Log a message at the `fatal` level.
*
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
static fatal(... args) {
mainLogger.fatal(... args);
}
/**
* Log a message at the `error` level.
*
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
static error(... args) {
mainLogger.error(... args);
}
/**
* Log a message at the `warn` level.
*
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
static warn(... args) {
mainLogger.warn(... args);
}
/**
* Log a message at the `info` level.
*
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
static info(... args) {
mainLogger.info(... args);
}
/**
* Log a message at the `debug` level.
*
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
static debug(... args) {
mainLogger.debug(... args);
}
/**
* Log a message at the `trace` level.
*
* @param {...*} args See {@link https://github.com/trentm/node-bunyan|Bunyan} documentation for a description of possible args
* @returns {undefined} No return
*/
static trace(... args) {
mainLogger.trace(... args);
}
/**
* Convert name to a logging level number (e.g. "error" -> 50)
*
* @param {string} name The logging level name to convert
* @returns {number} The logging level number
*/
static nameToLevel(name) {
switch (name) {
case "trace": return 10;
case "debug": return 20;
case "info": return 30;
case "warn": return 40;
case "error": return 50;
case "fatal": return 60;
default: throw new TypeError(`unknown log level name: ${name}`);
}
}
/**
* Convert logging level number to a name (e.g. 50 -> "error")
*
* @param {number} level The logging level number to convert
* @returns {number} The logging level name
*/
static levelToName(level) {
switch (level) {
case 10: return "trace";
case 20: return "debug";
case 30: return "info";
case 40: return "warn";
case 50: return "error";
case 60: return "fatal";
default: throw new TypeError(`unknown level: ${level}`);
}
}
}
function getStdoutStream() {
let {0: s} = mainLogger.logger.streams.filter((s) => s.stream === stdoutStream);
return s;
}
module.exports = Log;