const winston = require("winston"); require("winston-daily-rotate-file"); require("winston-kafka-connect"); export const { createLogger, transports, format } = winston; const { combine, splat, colorize, label, json, simple, printf } = format; import { patchLogger } from "./patchLogger"; import { loadConfig } from "my-nconf"; const dateFormat = require("dateformat"), path = require("path"), fs = require("fs-extra"); const LogFolder = path.resolve(process.cwd(), "logs/"); //log to same dir of caller script const Config = loadConfig(); //i.e the directory from which you invoked the node command fs.mkdirsSync(LogFolder); export class AppLogger { private static _instance; static get Instance() { if (!this._instance) { let opts = {}; if ( Config.has("logger.pipe_to_kafka") && !!Config.get("logger.pipe_to_kafka") && Config.has("kafka_cluster.kafkaHost") ) { /** centralize logs to kafka */ opts["kafka_transport"] = { //localstore: [], kafkaClient: { kafkaHost: Config.get("kafka_cluster.kafkaHost"), clientId: "lucy-logger" }, topic: "app-logs", level: "debug", format: DefaultLogFormat, meta: {} }; } this._instance = new AppLogger(opts, { patchConSole: true }); } return this._instance; } /** * [constructor description] * @param {Object} options */ constructor( options?, { patchConSole = false }: { patchConSole?: boolean } = {} ) { let loggy = patchLogger( createLogger( Object.assign( { format: SafeJson(), transports: [ new transports.Console({ format: ConsoleLogFormat, level: "silly", //[{ error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 }] handleExceptions: true, humanReadableUnhandledException: true }) ] }, options ) ), { patchConSole } ); //global settings loggy.emitErrs = false; loggy.exitOnError = false; //default transports //filter out errors loggy.add( new transports.DailyRotateFile({ name: "error-file", level: "error", format: DefaultLogFormat, handleExceptions: true, dirname: LogFolder, filename: "error-%DATE%.log", datePattern: "YYYY-MM-DD-HH", zippedArchive: true, // whether or not to gzip archived log files. (default 'false') maxSize: "100m", //mb maxFiles: "14d" //14 days }) ); //everything else >=debug loggy.add( new transports.DailyRotateFile({ name: "debug-file", level: "debug", format: DefaultLogFormat, handleExceptions: true, dirname: LogFolder, filename: "debug-%DATE%.log", datePattern: "YYYY-MM-DD-HH", zippedArchive: true, // whether or not to gzip archived log files. (default 'false') maxSize: "100m", //mb maxFiles: "14d" //14 days }) ); if (options && options.kafka_transport) { loggy.debug( "Logger: adding kafka transport", options.kafka_transport ); loggy.add( new transports.WinstonKafkaTransport(options.kafka_transport) ); } return loggy; } } /** Below are custom Formatters */ const timestamp = format(info => { //if (opts.timestamp) { info.timestamp = dateFormat( new Date(), "ddd, mmm dS yy, h:MM:ss.l TT Z(o)", false //use local time, UTC if true ); //} return info; }); const toUppserCaseLevel = format(info => { info.level = info.level.toUpperCase(); return info; }); /** * add trace to each logs * similar to console.trace * @TODO */ //const stackTrace = require('stack-trace'); const trace = format((info, opts) => { if (opts) { } //if (info.level !== "trace") return info; // var err = new Error('something went wrong'); // var trace = stackTrace.parse(err); // info.trace = trace; return info; }); //https://github.com/winstonjs/winston/issues/1338 const enumerateErrorFormat = format(info => { let err = info.message instanceof Error ? info.message : info instanceof Error ? info : undefined; if (!err) return info; return Object.assign(info, { stack: err.stack, //not enumerateable in node >8.9... ...err }); }); //==============================================\\ // BASE Format \\ // Create a format to decycle the object const { MESSAGE } = require("triple-beam"); const CircularJSON = require("circular-json"); /* * function replacer (key, value) * Handles proper stringification of Buffer output. */ function replacer(__, value) { return value instanceof Buffer ? value.toString("base64") : value; } const SafeJson = format((info, opts?) => { info[MESSAGE] = CircularJSON.stringify( info, opts.replacer || replacer, opts.space ); return info; }); function _checkandwrapObject(obj) { if (typeof obj === "object" && obj !== null) { return CircularJSON.stringify(obj, null, 2); } if (obj === undefined) return ""; return obj; } export const HumanReadableFormat = printf(info => { // let msg = `${info.level} ${info.timestamp} [${ info.label ? info.label : "" }] (${info.caller ? info.caller : ""}): ${_checkandwrapObject( info.message )} ${_checkandwrapObject(info.meta)}`; if (info.stack) { msg += `\n ${info.stack}`; } return msg; }); export const DefaultLogFormat = combine( splat(), //string interpolation splat for %d %s-style messages timestamp(), enumerateErrorFormat(), trace(), SafeJson() ); export const ConsoleLogFormat = combine( toUppserCaseLevel(), colorize(), DefaultLogFormat, HumanReadableFormat ); /** * Default singeleton * * @type {[type]} */ export const logger = AppLogger.Instance;