@villedemontreal/logger
Version:
Logger and logging utilities
516 lines • 20.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = exports.setGlobalLogLevel = exports.initLogger = exports.convertLogLevelToPinoNumberLevel = exports.convertLogLevelToPinoLabelLevel = exports.convertPinoLevelToNumber = exports.LogLevel = void 0;
exports.createLogger = createLogger;
exports.isInited = isInited;
const general_utils_1 = require("@villedemontreal/general-utils");
const fs = require("fs");
const http = require("http");
const _ = require("lodash");
const path = require("path");
const pino_1 = require("pino");
const pino_pretty_1 = require("pino-pretty");
const constants_1 = require("./config/constants");
const createRotatingFileStream = require('rotating-file-stream').createStream;
// ==========================================
// We export the LogLevel
// ==========================================
var general_utils_2 = require("@villedemontreal/general-utils");
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return general_utils_2.LogLevel; } });
// ==========================================
// This allows us to get the *TypeScript*
// informations instead of the ones from the
// transpiled Javascript file.
// ==========================================
require('source-map-support').install({
environment: 'node',
});
// ==========================================
// App infos
// ==========================================
const packageJson = require(`${constants_1.constants.appRoot}/package.json`);
const appName = packageJson.name;
const appVersion = packageJson.version;
let loggerInstance;
let loggerConfigs;
let libIsInited = false;
// Keeping track of all created loggers
const loggerChildren = [];
let multistream;
/**
* Converts a Pino level to its number value.
*/
const convertPinoLevelToNumber = (pinoLogLevel) => {
return pino_1.pino.levels.values[pinoLogLevel];
};
exports.convertPinoLevelToNumber = convertPinoLevelToNumber;
/**
* Converts a local LogLevel to a Pino label level.
*/
const convertLogLevelToPinoLabelLevel = (logLevel) => {
let pinoLevel = 'error';
if (logLevel !== undefined) {
if (logLevel === general_utils_1.LogLevel.TRACE) {
pinoLevel = 'trace';
}
else if (logLevel === general_utils_1.LogLevel.DEBUG) {
pinoLevel = 'debug';
}
else if (logLevel === general_utils_1.LogLevel.INFO) {
pinoLevel = 'info';
}
else if (logLevel === general_utils_1.LogLevel.WARNING) {
pinoLevel = 'warn';
}
else if (logLevel === general_utils_1.LogLevel.ERROR) {
pinoLevel = 'error';
}
}
return pinoLevel;
};
exports.convertLogLevelToPinoLabelLevel = convertLogLevelToPinoLabelLevel;
/**
* Converts a local LogLevel to a Pino number level.
*/
const convertLogLevelToPinoNumberLevel = (logLevel) => {
return (0, exports.convertPinoLevelToNumber)((0, exports.convertLogLevelToPinoLabelLevel)(logLevel));
};
exports.convertLogLevelToPinoNumberLevel = convertLogLevelToPinoNumberLevel;
/**
* Gets the path to the directory where to log, if required
*/
const getLogDirPath = (loggerConfig) => {
let logDir = loggerConfig.getLogDirectory();
if (!path.isAbsolute(logDir)) {
logDir = path.join(process.cwd(), logDir);
}
logDir = path.normalize(logDir);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
return logDir;
};
/**
* Initialize the logger with the config given in parameter
* This function must be used before using createLogger or Logger Class
* @param {LoggerConfigs} loggerConfig
* @param {string} [name='default']
* @param force if `true`, the logger will be initialized
* again even if it already is.
*/
const initLogger = (loggerConfig, name = 'default', force = false) => {
if (loggerInstance && !force) {
return;
}
const streams = [];
loggerConfigs = loggerConfig;
// ==========================================
// Logs to stdout, potentially in a human friendly
// format...
// ==========================================
if (loggerConfig.isLogHumanReadableinConsole()) {
streams.push({
level: (0, exports.convertLogLevelToPinoLabelLevel)(loggerConfig.getLogLevel()),
stream: (0, pino_pretty_1.PinoPretty)(),
});
}
else {
streams.push({
level: (0, exports.convertLogLevelToPinoLabelLevel)(loggerConfig.getLogLevel()),
stream: process.stdout,
});
}
// ==========================================
// Logs in a file too?
// ==========================================
if (loggerConfig.isLogToFile()) {
const rotatingFilesStream = createRotatingFileStream('application.log', {
path: getLogDirPath(loggerConfig),
size: `${loggerConfig.getLogRotateThresholdMB()}M`,
maxSize: `${loggerConfig.getLogRotateMaxTotalSizeMB()}M`,
maxFiles: loggerConfig.getLogRotateFilesNbr(),
});
// ==========================================
// TODO
// Temp console logs, to help debug this issue:
// https://github.com/iccicci/rotating-file-stream/issues/17#issuecomment-384423230
// ==========================================
rotatingFilesStream.on('error', (err) => {
// tslint:disable-next-line:no-console
console.log('Rotating File Stream error: ', err);
});
rotatingFilesStream.on('warning', (err) => {
// tslint:disable-next-line:no-console
console.log('Rotating File Stream warning: ', err);
});
streams.push({
level: (0, exports.convertLogLevelToPinoLabelLevel)(loggerConfig.getLogLevel()),
stream: rotatingFilesStream,
});
}
multistream = pino_1.pino.multistream(streams);
loggerInstance = (0, pino_1.pino)({
name,
safe: true,
timestamp: pino_1.pino.stdTimeFunctions.isoTime, // ISO-8601 timestamps
messageKey: 'msg',
level: (0, exports.convertLogLevelToPinoLabelLevel)(loggerConfig.getLogLevel()),
}, multistream);
libIsInited = true;
};
exports.initLogger = initLogger;
/**
* Change the global log level of the application. Useful to change dynamically
* the log level of something that is already started.
* @param level The log level to set for the application
*/
const setGlobalLogLevel = (level) => {
if (!loggerInstance) {
throw new Error('You must use "initLogger" function in @villedemontreal/logger package before making new instance of Logger.');
}
// Change the log level and update children accordingly
loggerInstance.level = (0, exports.convertLogLevelToPinoLabelLevel)(level);
for (const logger of loggerChildren) {
logger.update();
}
// ==========================================
// The streams's levels need to be modified too.
// ==========================================
if (multistream && multistream.streams) {
for (const stream of multistream.streams) {
// We need to use the *numerical* level value here
stream.level = (0, exports.convertLogLevelToPinoNumberLevel)(level);
}
}
};
exports.setGlobalLogLevel = setGlobalLogLevel;
/**
* Shorthands function that return a new logger instance
* Internally, we use the same logger instance but with different context like the name given in parameter
* and this context is kept in this new instance returned.
* @export
* @param {string} name
* @returns {ILogger}
*/
function createLogger(name) {
return new Logger(name);
}
function isInited() {
return libIsInited;
}
/**
* Logger implementation.
*/
class Logger {
/**
* Creates a logger.
*
* @param the logger name. This name should be related
* to the file the logger is created in. On a production
* environment, it's possible that only this name will
* be available to locate the source of the log.
* Streams will be created after the first call to the logger
*/
constructor(name) {
if (!loggerInstance) {
throw new Error('You must use "initLogger" function in @villedemontreal/logger package before making new instance of Logger.');
}
this.pino = loggerInstance.child({ name });
loggerChildren.push(this);
}
/**
* Logs a DEBUG level message object.
*
* If the extra "txtMsg" parameter is set, it is
* going to be added to messageObj as a ".msg"
* property (if messageObj is an object) or
* concatenated to messageObj (if it's not an
* object).
*
* Those types of logs are possible :
*
* - log.debug("a simple text message");
* - log.debug({"name": "an object"});
* - log.debug({"name": "an object..."}, "... and an extra text message");
* - log.debug(err, "a catched error and an explanation message");
*/
debug(messageObj, txtMsg) {
this.log(general_utils_1.LogLevel.DEBUG, messageObj, txtMsg);
}
/**
* Logs an INFO level message.
*
* If the extra "txtMsg" parameter is set, it is
* going to be added to messageObj as a ".msg"
* property (if messageObj is an object) or
* concatenated to messageObj (if it's not an
* object).
*
* Those types of logs are possible :
*
* - log.info("a simple text message");
* - log.info({"name": "an object"});
* - log.info({"name": "an object..."}, "... and an extra text message");
* - log.info(err, "a catched error and an explanation message");public
*/
info(messageObj, txtMsg) {
this.log(general_utils_1.LogLevel.INFO, messageObj, txtMsg);
}
/**
* Logs a WARNING level message.
*
* If the extra "txtMsg" parameter is set, it is
* going to be added to messageObj as a ".msg"
* property (if messageObj is an object) or
* concatenated to messageObj (if it's not an
* object).
*
* Those types of logs are possible :
*
* - log.warning("a simple text message");
* - log.warning({"name": "an object"});
* - log.warning({"name": "an object..."}, "... and an extra text message");
* - log.warning(err, "a catched error and an explanation mespublic sage");
*/
warning(messageObj, txtMsg) {
this.log(general_utils_1.LogLevel.WARNING, messageObj, txtMsg);
}
/**
* Logs an ERROR level message.
*
* If the extra "txtMsg" parameter is set, it is
* going to be added to messageObj as a ".msg"
* property (if messageObj is an object) or
* concatenated to messageObj (if it's not an
* object).
*
* Those types of logs are possible :
*
* - log.error("a simple text message");
* - log.error({"name": "an object"});
* - log.error({"name": "an object..."}, "... and an extra text message");
* - log.error(err, "a catched error and an explanatpublic ion message");
*/
error(messageObj, txtMsg) {
this.log(general_utils_1.LogLevel.ERROR, messageObj, txtMsg);
}
/**
* Logs a level specific message.
*
* If the extra "txtMsg" parameter is set, it is
* going to be added to messageObj as a ".msg"
* property (if messageObj is an object) or
* concatenated to messageObj (if it's not an
* object).
*
* Those types of logs are possible :
*
* - log(LogLevel.XXXXX, "a simple text message");
* - log({"name": "an object"});
* - log({"name": "an object..."}, "... and an extra text message");
* - log(err, "a catched error and an epublic xplanation message");
*/
// tslint:disable-next-line:cyclomatic-complexity
log(level, messageObj, txtMsg) {
let messageObjClean = messageObj;
const txtMsgClean = txtMsg;
if (messageObjClean === null || messageObjClean === undefined) {
messageObjClean = {};
}
else if (_.isArray(messageObjClean)) {
try {
loggerInstance.error(`The message object to log can't be an array. An object will be used instead and` +
`the content of the array will be moved to an "_arrayMsg" property on it : ${messageObjClean}`);
}
catch (err) {
// too bad
}
messageObjClean = {
_arrayMsg: _.cloneDeep(messageObjClean),
};
}
if (general_utils_1.utils.isObjectStrict(messageObjClean)) {
// ==========================================
// The underlying logger may ignore all fields
// except "message" if
// the message object is an instance of the
// native "Error" class. But we may want to use
// that Error class to log more fields. For example :
//
// let error: any = new Error("my message");
// error.customKey1 = "value1";
// error.customKey2 = "value2";
// throw error;
//
// This is useful if we need a *stackTrace*, which
// the Error class allows.
//
// This is why we create a plain object from that Error
// object.
// ==========================================
if (messageObjClean instanceof Error) {
const messageObjNew = {};
messageObjNew.name = messageObj.name;
messageObjNew.msg = messageObj.message;
messageObjNew.stack = messageObj.stack;
// Some extra custom properties?
messageObjClean = _.assignIn(messageObjNew, messageObj);
}
else if (messageObjClean instanceof http.IncomingMessage && messageObjClean.socket) {
// ==========================================
// This is a weird case!
// When logging an Express Request, Pino transforms
// it first: https://github.com/pinojs/pino-std-serializers/blob/master/lib/req.js#L65
// But doing so it accesses the `connection.remoteAddress` prpperty
// and, in some contexts, the simple fact to access this property
// throws an error:
// "TypeError: Illegal invocation\n at Socket._getpeername (net.js:712:30)"
//
// The workaround is to access this property in a try/catch
// and, if an error occures, force its value to
// a simple string.
// ==========================================
messageObjClean = _.cloneDeep(messageObjClean);
try {
// tslint:disable-next-line:no-unused-expression
messageObjClean.socket.remoteAddress;
}
catch (err) {
messageObjClean.socket = {
...messageObjClean.socket,
remoteAddress: '[not available]',
};
}
}
else {
messageObjClean = _.cloneDeep(messageObjClean);
}
// ==========================================
// Pino will always use the "msg" preoperty of
// the object if it exists, even if we pass a
// second parameter consisting in the message.
// ==========================================
if (txtMsgClean) {
messageObjClean.msg =
(messageObjClean.msg ? `${messageObjClean.msg} - ` : '') + txtMsgClean;
}
}
else {
const suffix = txtMsgClean ? ` - ${txtMsgClean}` : '';
messageObjClean = {
msg: `${messageObjClean}${suffix}`,
};
}
if (level === general_utils_1.LogLevel.TRACE) {
this.pino.trace(this.enhanceLog(messageObjClean));
}
else if (level === general_utils_1.LogLevel.DEBUG) {
this.pino.debug(this.enhanceLog(messageObjClean));
}
else if (level === general_utils_1.LogLevel.INFO) {
this.pino.info(this.enhanceLog(messageObjClean));
}
else if (level === general_utils_1.LogLevel.WARNING) {
this.pino.warn(this.enhanceLog(messageObjClean));
}
else if (level === general_utils_1.LogLevel.ERROR) {
this.pino.error(this.enhanceLog(messageObjClean));
}
else {
try {
loggerInstance.error(`UNMANAGED LEVEL "${level}"`);
}
catch (err) {
// too bad
}
this.pino.error(this.enhanceLog(messageObjClean));
}
}
/**
* Update the logger based on the parent changes.
* Could use something more precise to handle specific event but
* people could use it to update the child independently from the parent,
* which is not what is intended.
*/
update() {
// Set new level
this.pino.level = loggerInstance.level;
}
/**
* Adds the file and line number where the log occures.
* This particular code is required since our custom Logger
* is a layer over Pino and therefore adds an extra level
* to the error stack. Without this code, the file and line number
* are not the right ones.
*
* Based by http://stackoverflow.com/a/38197778/843699
*/
enhanceLog(messageObj) {
// ==========================================
// Adds a property to indicate this is a
// Montreal type of log entry.
//
// TODO validate this + adds standardized
// properties.
// ==========================================
if (!(constants_1.constants.logging.properties.LOG_TYPE in messageObj)) {
messageObj[constants_1.constants.logging.properties.LOG_TYPE] = constants_1.constants.logging.logType.MONTREAL;
// ==========================================
// TO UPDATE when the properties added to the
// log change!
//
// 1 : first version with Bunyan
// 2 : With Pino
// ==========================================
messageObj[constants_1.constants.logging.properties.LOG_TYPE_VERSION] = '2';
}
// ==========================================
// cid : correlation id
// ==========================================
const cid = loggerConfigs.correlationId;
if (cid) {
messageObj[constants_1.constants.logging.properties.CORRELATION_ID] = cid;
}
// ==========================================
// "app" and "version"
// @see https://sticonfluence.interne.montreal.ca/pages/viewpage.action?pageId=43530740
// ==========================================
messageObj[constants_1.constants.logging.properties.APP_NAME] = appName;
messageObj[constants_1.constants.logging.properties.APP_VERSION] = appVersion;
if (!loggerConfigs.isLogSource()) {
return messageObj;
}
let stackLine;
const stackLines = new Error().stack.split('\n');
stackLines.shift();
for (const stackLineTry of stackLines) {
if (stackLineTry.indexOf(`at ${Logger.name}.`) <= 0) {
stackLine = stackLineTry;
break;
}
}
if (!stackLine) {
return messageObj;
}
let callerLine = '';
if (stackLine.indexOf(')') >= 0) {
callerLine = stackLine.slice(stackLine.lastIndexOf('/'), stackLine.lastIndexOf(')'));
if (callerLine.length === 0) {
callerLine = stackLine.slice(stackLine.lastIndexOf('('), stackLine.lastIndexOf(')'));
}
}
else {
callerLine = stackLine.slice(stackLine.lastIndexOf('at ') + 2);
}
const firstCommaPos = callerLine.lastIndexOf(':', callerLine.lastIndexOf(':') - 1);
const filename = callerLine.slice(1, firstCommaPos);
const lineNo = callerLine.slice(firstCommaPos + 1, callerLine.indexOf(':', firstCommaPos + 1));
messageObj.src = {
file: filename,
line: lineNo,
};
return messageObj;
}
}
exports.Logger = Logger;
//# sourceMappingURL=logger.js.map