UNPKG

log4bro

Version:

easy ELK logger with compliant output format, based on bunyan.

338 lines (257 loc) 11.5 kB
"use strict"; const fs = require("fs"); const bunyan = require("bunyan"); let googleBunyan = null; const RawStream = require("./LoggerRawStream.js"); const Middlewares = require("./ExpressMiddlewares.js"); const LOG_LEVELS = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"]; class ServiceLogger { constructor(loggerName, silence, logDir, productionMode, dockerMode, varKey, logFieldOptions, level, serviceName, caller, stackdriver) { if(typeof loggerName === "object" && arguments.length === 1){ productionMode = loggerName.production || loggerName.productionMode; //support fallback logDir = loggerName.logDir; silence = loggerName.silence; dockerMode = loggerName.docker || loggerName.dockerMode; //support fallback varKey = loggerName.varKey; logFieldOptions = loggerName.logFieldOptions; level = loggerName.level || loggerName.logLevel; //support fallback to older key named "logLevel" serviceName = loggerName.serviceName; stackdriver = loggerName.stackdriver; caller = loggerName.caller; stackdriver = loggerName.stackdriver; loggerName = loggerName.name; //last } if(level && LOG_LEVELS.indexOf(level) === -1){ console.log("[log4bro] level is not a supported logLevel: " + level + ", defaulting to INFO."); level = "INFO"; } this.productionMode = productionMode || false; this.varKey = varKey || "LOG"; this.dockerMode = dockerMode || false; this.logFieldOptions = logFieldOptions || null; this.silence = silence || false; this.logDir = logDir || "logs"; this.logLevel = level || (productionMode ? "WARN" : "DEBUG"); //level -> logLevel (dockerconfig cannot set camelcase) this.serviceName = serviceName || "undefined"; this.stackdriver = stackdriver; this.caller = caller || false; this.skipDebug = this.silence || (this.productionMode && !(this.logLevel === "TRACE" || this.logLevel === "DEBUG")); if (!loggerName && !this.productionMode) { this.loggerName = loggerName || "dev"; } else { this.loggerName = loggerName || "prod"; } this._streams = null; this.LOG = this._createLogger(); let sdInitText = ""; if (this.stackdriver) { // Not pretty, but the way to catch authentication error is rewriting google bunyan stream lib // Apparently they create a callback from native code which there's no error handling process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at:', p, 'reason:', reason); }); switch(this.stackdriver.scope) { case "only": // Make the logger streams to only Google Stackdriver this.LOG = this._createGoogleLogger(); break; case "both": // Make the logger streams to both to default stream and Google Stackdriver this.GLOG = this._createGoogleLogger(); break; default: // Return default stream when there's no scope is written // this.LOG is still the same this.stackdriver.scope = "default"; } sdInitText = ", stackdriver-log=" + Boolean(this.stackdriver) + ", stream-scope=" + this.stackdriver.scope; } this.LOG.info("[log4bro] Logger is: in-prod=" + this.productionMode + sdInitText + ", in-docker:" + this.dockerMode + ", level=" + this.logLevel + ", skipDebug=" + this.skipDebug); this.setGlobal(); } _createGoogleLogger() { let logger; try { if(!googleBunyan){ googleBunyan = require("@google-cloud/logging-bunyan"); } logger = bunyan.createLogger({ "name": this.loggerName, "streams": [ googleBunyan( Object.assign({}, this.stackdriver, {logName: this.loggerName}) ) .stream(this.logLevel) ], "src": false }); } catch(e) { this.LOG.error("Unable to create Stackdriver Stream, switching to default. Reason:", e); logger = this._createLogger(); } return logger; } _createLogger() { this._streams = []; //clear this._streams.push( { "type": "raw", "level": this.logLevel, "stream": new RawStream(null, this.logFieldOptions, this.dockerMode) //will only write to console/stdout } ); if(!this.dockerMode){ //console.log("[log4bro] Logger is not in docker mode."); this.createLoggingDir(); this._streams.push({ "type": "raw", "level": this.logLevel, "stream": new RawStream(this.logDir + "/service-log.json", this.logFieldOptions) //will only write to logfile }); } return bunyan.createLogger({ "name": this.loggerName, "streams": this._streams, "src": false }); } createChild(defaultAdditionalFields = {}) { const self = this; defaultAdditionalFields.child = true; return { trace: (message, additionalFields) => self.trace(message, Object.assign({}, additionalFields, defaultAdditionalFields)), debug: (message, additionalFields) => self.debug(message, Object.assign({}, additionalFields, defaultAdditionalFields)), info: (message, additionalFields) => self.info(message, Object.assign({}, additionalFields, defaultAdditionalFields)), warn: (message, additionalFields) => self.warn(message, Object.assign({}, additionalFields, defaultAdditionalFields)), error: (message, additionalFields) => self.error(message, Object.assign({}, additionalFields, defaultAdditionalFields)), fatal: (message, additionalFields) => self.fatal(message, Object.assign({}, additionalFields, defaultAdditionalFields)) } } changeLogLevel(level) { if(level && LOG_LEVELS.indexOf(level) === -1){ this.LOG.error("[log4bro] level is not a supported logLevel: " + level + ", defaulting to INFO."); return; } this.skipDebug = !(level === "DEBUG" || level === "TRACE"); this.LOG.info("[log4bro] changing loglevel from " + this.logLevel + " to " + level + "."); this.logLevel = level; this.LOG = this._createLogger(); } createLoggingDir() { if (!fs.existsSync(this.logDir)) { //console.log("[log4bro] Logs folder does not exists creating " + this.logDir + " make sure to set path in blammo.xml."); fs.mkdirSync(this.logDir); return; } //console.log("[log4bro] Logs folder exists, clearing " + this.logDir); let files = null; try { files = fs.readdirSync(this.logDir); } catch (e) { return; } if (files.length > 0) for (let i = 0; i < files.length; i++) { const filePath = this.logDir + "/" + files[i]; if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath); else fs.rmDir(filePath); } } applyMiddlewareAccessLog(expressApp, customTokens, accessLogOptions) { if(!expressApp || typeof expressApp !== "function"){ throw new Error("[log4bro] ExpressApp is null or not an object, make sure you pass an instance of express() to applyMiddleware."); } expressApp.use(Middlewares.accessLogMiddleware(this.serviceName, this.dockerMode, customTokens, accessLogOptions)); return expressApp; } applyMiddlewareAccessLogFile(expressApp, logFilePath) { if(!expressApp || typeof expressApp !== "function"){ throw new Error("[log4bro] ExpressApp is null or not an object, make sure you pass an instance of express() to applyMiddleware."); } if(!logFilePath){ throw new Error("[log4bro] logFilePath is empty on applyMiddlewareAccessLogFile."); } expressApp.use(Middlewares.accessLogMiddlewareFile(logFilePath)); return expressApp; } setGlobal() { global[this.varKey] = this; } trace(message, additionalFields) { if (this.skipDebug) return; //save memory & cpu this._moveMsgObject("trace", message, additionalFields); } debug(message, additionalFields) { if (this.skipDebug) return; //save memory & cpu this._moveMsgObject("debug", message, additionalFields); } info(message, additionalFields) { if (this.silence) return; this._moveMsgObject("info", message, additionalFields); } warn(message, additionalFields) { if (this.silence) return; this._moveMsgObject("warn", message, additionalFields); } error(message, additionalFields) { if (this.silence) return; if (message instanceof Error) { this._moveMsgObject("error", message.message, Object.assign({}, message, {stack: message.stack}, additionalFields)); return; } this._moveMsgObject("error", message, additionalFields); } fatal(message, additionalFields) { if (this.silence) return; this._moveMsgObject("fatal", message, additionalFields); } _moveMsgObject(level, message, additionalFields = {}) { // identify if dealing with an object or string message // move an object to the msg_json field that can be index by ELK // do nothing if dealing with a string // it is important to run this step before the message touches bunyan const {child, wrapped} = additionalFields; if (this.caller) { try { const index = 3 + (child ? 1 : 0) + (wrapped ? 1: 0); additionalFields.caller = new Error().stack.split("\n")[index].trim(); } catch(err) { additionalFields.caller = "error"; } } child && delete additionalFields.child; wrapped && delete additionalFields.wrapped; if(typeof message === "object"){ message = { msg_json: message }; Object.assign(message, additionalFields); if (this.GLOG) this.GLOG[level](message); return this.LOG[level](message); } if (this.GLOG) this.GLOG[level](additionalFields, message); return this.LOG[level](additionalFields, message); } raw(messageObject, support) { if(typeof messageObject !== "object"){ throw new Error("Logger.raw(obj) must be called with an object."); } if (this.silence) return; support = support || false; this._streams.forEach(function(stream){ if(stream && stream.stream){ stream.stream.write(messageObject, support ? RawStream.OVERWRITE_MODES.ADAPT : RawStream.OVERWRITE_MODES.NONE); } }); } } module.exports = ServiceLogger;