UNPKG

@villedemontreal/logger

Version:
516 lines 20.6 kB
"use strict"; 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