Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/logger factory #202

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ The logger needs to be added AFTER the express router(`app.router)`) and BEFORE

To use winston's existing transports, set `transports` to the values (as in key-value) of the `winston.default.transports` object. This may be done, for example, by using underscorejs: `transports: _.values(winston.default.transports)`.

Alternatively, if you're using a winston logger instance elsewhere and have already set up levels and transports, pass the instance into expressWinston with the `winstonInstance` option. The `transports` option is then ignored.
Alternatively, if you're using a winston logger instance elsewhere and have already set up levels and transports, the `winstonInstance` option can be used. It has 2 forms:
+ an actual `winston` logger instance. In the case, the logger is used as is.
+ a factory function that takes parameters `req` and `res` of the middleware. The function is expected to return a `winston` logger instance.

When using any version of `winstonInstance`, the `transports` option is then ignored.

## Examples

Expand Down
46 changes: 34 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,15 @@ exports.errorLogger = function errorLogger(options) {

options.requestWhitelist = options.requestWhitelist || exports.requestWhitelist;
options.requestFilter = options.requestFilter || exports.defaultRequestFilter;
options.winstonInstance = options.winstonInstance || (winston.createLogger({
transports: options.transports,
format: options.format
}));
const getLogger = loggerFactory(options);
options.msg = options.msg || 'middlewareError';
options.baseMeta = options.baseMeta || {};
options.metaField = options.metaField || null;
options.level = options.level || 'error';
options.dynamicMeta = options.dynamicMeta || function(req, res, err) { return null; };
const exceptionHandler = new winston.ExceptionHandler(options.winstonInstance);
// TODO: improve the default exception meta information by not calling a
// constructor than a function that does nothing with the constructor parameter.
const exceptionHandler = new winston.ExceptionHandler({});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain a bit more why this change? ExceptionHandler needs the logger instance

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logger instance is not known at application startup. Which is when this line is executing. The logger instance is known at execution time through the 'getLogger' method.

The only reason why there is an ExceptionHandler is to call getAllInfo on it.

I can think of 2 ways to solve this problem.

  1. Build an exception handler on each requests, passing the logger instance. In other works, call these lines (or something similar) on all requests:
    options.exceptionToMeta = options.exceptionToMeta || exceptionHandler.getAllInfo.bind(exceptionHandler);

  2. Accept and embrace the fact that ExceptionHandler.getAllInfo does not require the logger instance to build the exception metadata. You can view the implementation here: https://github.com/winstonjs/winston/blob/8423c03c59c31f61f57519b4097420a464c90f83/lib/winston/exception-handler.js#L75

I chose option 2 because it seemed a reasonable tradeoff between readability and a small iterative change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bithavoc

Does that answer your question?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@golopot want to throw your two cents here?

Copy link

@jblevins1991 jblevins1991 Mar 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 119 - 122 seems to provide evidence that this can be called with options.winstonInstance initialized. I don't think it is good for performance to create a new instance every time errorLogger is called for everyone. This seems like a niche feature. I think a better solution would be to leave this as it was and find a good place to remove options.winstonInstance (maybe after winston is finished with a single log?) so that a new instance can be created here, only if you need it. Then we could pass an option to express-winston named uniqueInstances or something of that ilk to trigger this behavior.

options.exceptionToMeta = options.exceptionToMeta || exceptionHandler.getAllInfo.bind(exceptionHandler);
options.blacklistedMetaFields = options.blacklistedMetaFields || [];

Expand Down Expand Up @@ -158,7 +157,7 @@ exports.errorLogger = function errorLogger(options) {
options.msg = typeof options.msg === 'function' ? options.msg(req, res) : options.msg;

// This is fire and forget, we don't want logging to hold up the request so don't wait for the callback
options.winstonInstance.log({
getLogger(req, res).log({
level,
message: getTemplate(options.msg, {err: err, req: req, res: res}),
meta: exceptionMeta
Expand All @@ -178,12 +177,35 @@ function levelFromStatus(options) {
}
}

/**
* returns a factory function to get a logger instance.
* The output function expect a request and result as parameters
* @param options - the library options
* @return func
*/
function loggerFactory(options) {

if(options.winstonInstance) {

if(_.isFunction(options.winstonInstance))
return options.winstonInstance;

const originalInstance = options.winstonInstance;
return function() { return originalInstance; };
}

const localLogger = winston.createLogger({
transports: options.transports,
format: options.format
});
return function() { return localLogger;};
}

//
// ### function logger(options)
// #### @options {Object} options to initialize the middleware.
//
exports.logger = function logger(options) {

ensureValidOptions(options);
ensureValidLoggerOptions(options);

Expand All @@ -194,10 +216,8 @@ exports.logger = function logger(options) {
options.requestFilter = options.requestFilter || exports.defaultRequestFilter;
options.responseFilter = options.responseFilter || exports.defaultResponseFilter;
options.ignoredRoutes = options.ignoredRoutes || exports.ignoredRoutes;
options.winstonInstance = options.winstonInstance || (winston.createLogger({
transports: options.transports,
format: options.format
}));

const getLogger = loggerFactory(options);
options.statusLevels = options.statusLevels || false;
options.level = options.statusLevels ? levelFromStatus(options) : (options.level || "info");
options.msg = options.msg || "HTTP {{req.method}} {{req.url}}";
Expand Down Expand Up @@ -346,7 +366,7 @@ exports.logger = function logger(options) {
// This is fire and forget, we don't want logging to hold up the request so don't wait for the callback
if (!options.skip(req, res)) {
var level = _.isFunction(options.level) ? options.level(req, res) : options.level;
options.winstonInstance.log({level, message: msg, meta});
getLogger(req, res).log({level, message: msg, meta});
}
};

Expand All @@ -372,9 +392,11 @@ function bodyToString(body, isJSON) {

function ensureValidOptions(options) {
if(!options) throw new Error("options are required by express-winston middleware");

if(!((options.transports && (options.transports.length > 0)) || options.winstonInstance))
throw new Error("transports or a winstonInstance are required by express-winston middleware");


if (options.dynamicMeta && !_.isFunction(options.dynamicMeta)) {
throw new Error("`dynamicMeta` express-winston option should be a function");
}
Expand Down
169 changes: 165 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ class MockTransport extends Transport {
}
}

class MockWinston {
constructor(test) {
this.invoked = false;
}

log(options) {
this.invoked = true;
}
}

function mockReq(reqMock) {
var reqSpec = _.extend({
method: 'GET',
Expand Down Expand Up @@ -185,9 +195,84 @@ describe('express-winston', function () {
});
});

describe('when middleware function encounters an error in the pipeline', function () {
it('should invoke the transport', function () {
return errorLoggerTestHelper().then(function (result) {
describe('when providing logger instance', function() {

it('should use the winstonInstance', function() {

const winstonMock = new MockWinston();
const localOptions = {
loggerOptions: {
winstonInstance: winstonMock,
transports: null
}
};

return errorLoggerTestHelper(localOptions).then(function(result) {
winstonMock.invoked.should.eql(true);
});
});
it('should use the winstonInstance in favor of transport specification', function() {

const winstonMock = new MockWinston();
const localOptions = {
loggerOptions: {
winstonInstance: winstonMock,
transports: [new MockTransport({})]
}
};


return errorLoggerTestHelper(localOptions).then(function(result) {
winstonMock.invoked.should.eql(true);
});
});

it('should invoke the winstonInstance factory if provided', function() {
const mockWinston = new MockWinston();

const loggerFactory = function() {
return mockWinston;
}
const localOptions = {
loggerOptions: {
winstonInstance: loggerFactory,
transports: null,
}
};
return errorLoggerTestHelper(localOptions).then(function(result) {
mockWinston.invoked.should.eql(true);
});
});

it('should have the req and res as parameters when invoking the winstonInstance factory', function() {

const mockWinston = new MockWinston();
let factoryReq;
let factoryRes;
const loggerFactory = function(req, res) {
factoryReq = req;
factoryRes = res;
return mockWinston;
}
const localOptions = {
loggerOptions: {
winstonInstance: loggerFactory,
transports: null,
}
};

return errorLoggerTestHelper(localOptions).then(function(result) {
mockWinston.invoked.should.eql(true);
factoryReq.should.be.deepEqual(result.req);
factoryRes.should.be.deepEqual(result.res);
});
});

});

describe('when middleware function encounters an error in the pipeline', function() {
it('should invoke the transport', function() {
return errorLoggerTestHelper().then(function(result) {
result.transportInvoked.should.eql(true);
});
});
Expand Down Expand Up @@ -608,7 +693,83 @@ describe('express-winston', function () {
});
});

describe('when middleware function is invoked on a route', function () {
describe('when providing logger instance', function() {

it('should use the winstonInstance', function() {

const winstonMock = new MockWinston();
const localOptions = {
loggerOptions: {
winstonInstance: winstonMock,
transports: null
}
};

return loggerTestHelper(localOptions).then(function(result) {
winstonMock.invoked.should.eql(true);
});
});
it('should use the winstonInstance in favor of transport specification', function() {

const winstonMock = new MockWinston();
const localOptions = {
loggerOptions: {
winstonInstance: winstonMock,
transports: [new MockTransport({})]
}
};


return loggerTestHelper(localOptions).then(function(result) {
winstonMock.invoked.should.eql(true);
});
});

it('should invoke the winstonInstance factory if provided', function() {
const mockWinston = new MockWinston();

const loggerFactory = function() {
return mockWinston;
}
const localOptions = {
loggerOptions: {
winstonInstance: loggerFactory,
transports: null,
}
};
return loggerTestHelper(localOptions).then(function(result) {
mockWinston.invoked.should.eql(true);
});
});

it('should have the req and res as parameters when invoking the winstonInstance factory', function() {

const mockWinston = new MockWinston();
let factoryReq;
let factoryRes;
const loggerFactory = function(req, res) {
factoryReq = req;
factoryRes = res;
return mockWinston;
}
const localOptions = {
loggerOptions: {
winstonInstance: loggerFactory,
transports: null,
}
};

return loggerTestHelper(localOptions).then(function(result) {
mockWinston.invoked.should.eql(true);
factoryReq.should.be.deepEqual(result.req);
factoryRes.should.be.deepEqual(result.res);
});
});

});


describe('when middleware function is invoked on a route', function() {
function next(req, res, next) {
req._startTime = (new Date()) - 125;

Expand Down