Skip to content

Commit

Permalink
refactor: refactor the logger factory
Browse files Browse the repository at this point in the history
  • Loading branch information
simplymichael committed Jun 17, 2024
1 parent ccd1d75 commit 54cc1d3
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 85 deletions.
2 changes: 1 addition & 1 deletion src/framework/factory/logger/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
*.scoped.logs
*.logs
159 changes: 78 additions & 81 deletions src/framework/factory/logger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,6 @@ require("winston-daily-rotate-file");


module.exports = class LoggerFactory {
/* Features:
* It should let the user customize the level colours
* It should let the user add custom log transports [Array/List of transports].
* It should let the user specify if they want it to log uncaught exceptions
* and what transports to use (or whether to use same transport as for regular logging)
* It should let the user specify if they want it to log uncaught promise rejections
* and what transports to use
* (or whether to use same transport as for regular logging or uncaught exceptions)
*/

/*
* Events on rotating transport
* // fired when a log file is created
* fileRotateTransport.on('new', (filename) => {});
* // fired when a log file is rotated
* fileRotateTransport.on('rotate', (oldFilename, newFilename) => {});
* // fired when a log file is archived
* fileRotateTransport.on('archive', (zipFilename) => {});
* // fired when a log file is deleted
* fileRotateTransport.on('logRemoved', (removedFilename) => {});
*/

/**
* @param {Object} options
* @param {String} [options.label]
Expand All @@ -41,7 +19,7 @@ module.exports = class LoggerFactory {
*/
static createLogger(options) {
const {
label = "FrameworkLogger",
label,
levels,
logDir,
logToFile,
Expand All @@ -51,8 +29,11 @@ module.exports = class LoggerFactory {
transports: customTransports = [],
} = options || {};

let { exceptionHandlers, rejectionHandlers } = options || {};

let exceptionHandlers;
let rejectionHandlers;
const consoleTransport = new winston.transports.Console({
format: winston.format.simple(),
});
const transports = [].concat(customTransports);

if(logToFile) {
Expand Down Expand Up @@ -82,14 +63,22 @@ module.exports = class LoggerFactory {
}

if(logExceptions) {
if(!Array.isArray(exceptionHandlers)) {
if(!disableConsoleLogs) {
// Cf. https://github.com/winstonjs/winston/issues/1289#issuecomment-396527779
consoleTransport.handleExceptions = true;
exceptionHandlers = transports.concat([consoleTransport]);
} else {
exceptionHandlers = transports;
}
}

if(logRejections) {
if(!Array.isArray(rejectionHandlers)) {
rejectionHandlers = transports;
if(!disableConsoleLogs) {
// Cf. https://github.com/winstonjs/winston/issues/1289#issuecomment-396527779
consoleTransport.handleRejections = true;
exceptionHandlers = transports.concat([consoleTransport]);
} else {
exceptionHandlers = transports;
}
}

Expand All @@ -109,73 +98,30 @@ module.exports = class LoggerFactory {
transports,
exceptionHandlers,
rejectionHandlers,
defaultMeta: { service: label },
defaultMeta: { service: label ?? "FrameworkLogger" },
exitOnError: false,
});

winston.addColors({
debug : "blue",
error : "red",
http : "gray",
info : "green",
warn : "yellow",
});

/*
* Log to the `console` with the format:
* `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
*/
if(!disableConsoleLogs) {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
logger.add(consoleTransport);
}

winston.addColors({
debug: "blue",
error: "red",
http: "gray",
info: "green",
warn: "yellow"
});

return logger;


// Private Helper functions
/**
* A common need that Winston does not enable by default
* is the ability to log each level into different files (or transports)
* so that only info messages go to, for example, an `app-info.log` file,
* debug messages into an `app-debug.log file`, and so on.
* To get around this, we create a custom format on the transport to filter the messages by level.
*
* USAGE EXAMPLE:
* transports: [
* new winston.transports.File({
* filename: 'app-error.log',
* level: 'error',
* format: combine(levelFilter("error")(), timestamp(), json()),
* }),
*
* new winston.transports.File({
* filename: 'app-others.log',
* level: 'error',
* format: combine(levelFilter(["debug", "info", ...])(), timestamp(), json()),
* }),
* ]
*/
/*function createLevelFilter(infoLevels, availableLevels = npm.levels) {
let levels;
if(typeof infoLevels === "string") {
levels = infoLevels.split(/\s+,\s+/).map(Boolean);
}
if(!Array.isArray(levels)) {
throw new TypeError(
`Invalid log level ${levels} specified. ` +
`Supported log levesl are ${availableLevels.join(", ")}`
);
}
return winston.format((info, opts) => {
return levels.includes(info.level) ? info : false;
});
}*/

function customFormatter(info) {
return JSON.stringify({
service: label,
Expand All @@ -186,4 +132,55 @@ module.exports = class LoggerFactory {
});
}
}

/**
* A common need that Winston does not enable by default
* is the ability to log each level into different files (or transports)
* such that only info messages go to, for example, an `app-info.log` file,
* debug messages go into an `app-debug.log file`, and so on.
* To get around this, we can create a custom format on the transport
* to filter the messages by level.
*
* @param {Array|String} logLevels: An array or comma-separated list
* of the log priority levels.
* @param {Object} logPriorityProtocol: The protocol that defines the
* available log levels. The default is `winston.npm.levels`.
*
* USAGE EXAMPLE:
* transports: [
* new winston.transports.File({
* filename: 'app-error.log',
* level: 'error',
* format: combine(levelFilter("error")(), timestamp(), json()),
* }),
*
* new winston.transports.File({
* filename: 'app-debug.log',
* level: 'error',
* format: combine(levelFilter("debug")(), timestamp(), json()),
* }),
*
* new winston.transports.File({
* filename: 'app-others.log',
* level: 'error',
* format: combine(levelFilter(["info", ...])(), timestamp(), json()),
* }),
* ]
*/
static levelFilter(logLevels, availableLevels = winston.npm.levels) {
let levels;

if(typeof logLevels === "string") {
levels = logLevels.split(/\s+,\s+/).map(Boolean);
}

if(!Array.isArray(levels)) {
throw new TypeError(
`Invalid log level ${levels} specified. ` +
`Supported log levesl are ${availableLevels.join(", ")}`
);
}

return winston.format((info) => levels.includes(info.level) ? info : false);
}
};
51 changes: 48 additions & 3 deletions src/framework/factory/logger/logger.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const os = require("node:os");
const path = require("node:path");
const util = require("node:util");
const sinon = require("sinon");
const winston = require("winston");
const { chai } = require("../../lib/test-helper");
const LoggerFactory = require(".");

Expand All @@ -26,7 +27,9 @@ function spyOnConsoleOutput(object = "stdout") {
// Overwrite the console._stdout.write used by winston internally.
// So that it doesn't write to the actual console, cluttering our screen.
// Instead, it writes to an output file.
console[object].write = () => fs.appendFileSync(logFile, util.inspect(arguments, { depth: 12 }));
console[object].write = function() {
fs.appendFileSync(logFile, util.inspect(arguments, { depth: 12 }));
};

// spy on the overwritten console method
const consoleSpy = sinon.spy(console[object], "write");
Expand All @@ -45,7 +48,7 @@ function spyOnConsoleOutput(object = "stdout") {

module.exports = {
createLogger() {
describe("LoggerFactory", function() {
describe("Factory", function() {
let expect;

before(async function() {
Expand Down Expand Up @@ -157,6 +160,7 @@ module.exports = {

it("should let the user specify the option to log to file", function() {
const scopedLogDir = path.join(__dirname, ".scoped.logs");

fs.rmSync(scopedLogDir, { recursive: true, force: true });

LoggerFactory.createLogger();
Expand All @@ -176,9 +180,50 @@ module.exports = {
expect(thrower).to.throw("The 'logToFile' option requires a 'logDir' options to be specified");
});

describe("logger instance", function() {
it("should let the user add an array of transports", function(done) {
this.timeout(5000);

const filename = "custom.logs";
const filepath = path.join(__dirname, filename);

expect(fs.existsSync(filepath)).to.equal(false);

LoggerFactory.createLogger({
transports: [new winston.transports.File({ filename: filepath })]
});

setTimeout(() => {
expect(fs.existsSync(filepath)).to.equal(true);

fs.rmSync(filepath, { force: true });
done();
}, 2000);
});

/*it("should let the user specify if they want to log uncaught exceptions", function(done) {
this.timeout(5000);
const logger = LoggerFactory.createLogger({ logExceptions: true });
const { sinonSpy, restore } = spyOnConsoleOutput("stdout");
process.on("uncaughtException", function() {
restore();
const expected = /Error: Uncaught exception/;
// Since we are spying on/monkey-patching stdout,
// the first output (call[0]) is the one that
// displays the test title: "should let the user specify if..."
// The error output is captured in the second (call[1]).
// Why spying on stdout instead of stderr?
// Probably because the setting of createLogger exitOnError is false,
// so the error output is delivered via the stdout rather than stderr.
expect(sinonSpy.getCall(1).args[0]).to.match(expected);
expect(sinonSpy.calledWith(sinon.match(expected))).to.equal(true);
});
// Cf. https://github.com/winstonjs/winston/issues/1289#issuecomment-396527779
setTimeout(() => { throw new Error("Uncaught exception"); }, 1000);
});*/
});
});
}
Expand Down

0 comments on commit 54cc1d3

Please sign in to comment.