diff --git a/README.md b/README.md index 37006617c..e78ec0261 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ simplest way to do this is using `winston.createLogger`: const logger = winston.createLogger({ level: 'info', format: winston.format.json(), + defaultMeta: {service: 'user-service'}, transports: [ // // - Write to all logs with level `info` and below to `combined.log` @@ -191,6 +192,20 @@ logger.configure({ }); ``` +### Creating child loggers + +You can create child loggers from existing loggers to pass metadata overrides: + +``` js +const logger = winston.createLogger({ + transports: [ + new winston.transports.Console(), + ] +}); + +const childLogger = logger.child({ req_id: '451' }); +``` + ### Streams, `objectMode`, and `info` objects In `winston`, both `Logger` and `Transport` instances are treated as diff --git a/lib/winston/create-logger.js b/lib/winston/create-logger.js index fedfadaa4..aeae6948d 100644 --- a/lib/winston/create-logger.js +++ b/lib/winston/create-logger.js @@ -52,6 +52,7 @@ class DerivedLogger extends Logger { const [msg] = args; const info = msg && msg.message && msg || { message: msg }; info.level = info[LEVEL] = level; + this._addDefaultMeta(info); this.write(info); return this; } diff --git a/lib/winston/logger.js b/lib/winston/logger.js index e06556033..e0164a51b 100644 --- a/lib/winston/logger.js +++ b/lib/winston/logger.js @@ -24,10 +24,10 @@ const config = require('./config'); */ class Logger extends Transform { /** - * Constructor function for the Logger object responsible for persisting log - * messages and metadata to one or more transports. - * @param {!Object} options - foo - */ + * Constructor function for the Logger object responsible for persisting log + * messages and metadata to one or more transports. + * @param {!Object} options - foo + */ constructor(options) { super({ objectMode: true @@ -35,6 +35,29 @@ class Logger extends Transform { this.configure(options); } + child(defaultRequestMetadata) { + const logger = this; + return Object.create(logger, { + write: { + value: function (info) { + const infoClone = Object.assign( + {}, + defaultRequestMetadata, + info + ); + + // Object.assign doesn't copy inherited Error properties so we have to do that explicitly + if (info instanceof Error) { + infoClone.stack = info.stack; + infoClone.message = info.message; + } + + logger.write(infoClone); + } + } + }); + } + /** * This will wholesale reconfigure this instance by: * 1. Resetting all transports. Older transports will be removed implicitly. @@ -46,6 +69,7 @@ class Logger extends Transform { configure({ silent, format, + defaultMeta, levels, level = 'info', exitOnError = true, @@ -66,6 +90,7 @@ class Logger extends Transform { this.silent = silent; this.format = format || this.format || require('logform/json')(); + this.defaultMeta = defaultMeta || null; // Hoist other options onto this instance. this.levels = levels || this.levels || config.npm.levels; this.level = level; @@ -153,6 +178,7 @@ class Logger extends Transform { // In this context the LHS `level` here is actually the `info` so read // this as: info[LEVEL] = info.level; level[LEVEL] = level.level; + this._addDefaultMeta(level); this.write(level); return this; } @@ -161,6 +187,7 @@ class Logger extends Transform { if (arguments.length === 2) { if (msg && typeof msg === 'object') { msg[LEVEL] = msg.level = level; + this._addDefaultMeta(msg); this.write(msg); return this; } @@ -176,14 +203,16 @@ class Logger extends Transform { [SPLAT]: splat.slice(1), level, message: msg - })); + }, + this.defaultMeta)); } else { this.write(Object.assign({}, { [LEVEL]: level, [SPLAT]: splat, level, message: msg - })); + }, + this.defaultMeta)); } return this; @@ -535,6 +564,12 @@ class Logger extends Transform { transport.on(event, transport['__winston' + event]); } } + + _addDefaultMeta(msg) { + if (this.defaultMeta) { + Object.assign(msg, this.defaultMeta); + } + } } function getLevelValue(levels, level) { diff --git a/package-lock.json b/package-lock.json index fd17d490f..b1ef6693c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2412,12 +2412,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2432,17 +2434,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2559,7 +2564,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2571,6 +2577,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2585,6 +2592,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2592,12 +2600,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -2616,6 +2626,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2696,7 +2707,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2708,6 +2720,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2829,6 +2842,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/test/helpers/index.js b/test/helpers/index.js index 5351bf673..07cc862e7 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -6,14 +6,15 @@ * */ -var assume = require('assume'), +const assume = require('assume'), fs = require('fs'), path = require('path'), through = require('through2'), spawn = require('child_process').spawn, stream = require('stream'), util = require('util'), - winston = require('../../lib/winston'); + winston = require('../../lib/winston'), + mockTransport = require('./mocks/mock-transport'); var helpers = exports; @@ -25,15 +26,10 @@ var helpers = exports; * @returns {Logger} A winston.Logger instance */ helpers.createLogger = function (write, format) { - var writeable = new stream.Writable({ - objectMode: true, - write: write - }); - return winston.createLogger({ format, transports: [ - new winston.transports.Stream({ stream: writeable }) + mockTransport.createMockTransport(write) ] }); }; diff --git a/test/helpers/mocks/mock-transport.js b/test/helpers/mocks/mock-transport.js new file mode 100644 index 000000000..47a61f210 --- /dev/null +++ b/test/helpers/mocks/mock-transport.js @@ -0,0 +1,22 @@ +const stream = require('stream') +const winston = require('../../../lib/winston'); + +/** + * Returns a new Winston transport instance which will invoke + * the `write` method onĀ each call to `.log` + * + * @param {function} write Write function for the specified stream + * @returns {StreamTransportInstance} A transport instance + */ +function createMockTransport(write) { + const writeable = new stream.Writable({ + objectMode: true, + write: write + }); + + return new winston.transports.Stream({stream: writeable}) +} + +module.exports = { + createMockTransport +}; diff --git a/test/logger.test.js b/test/logger.test.js index da0beca28..13eede777 100755 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -20,6 +20,7 @@ const winston = require('../lib/winston'); const TransportStream = require('winston-transport'); const format = require('../lib/winston').format; const helpers = require('./helpers'); +const mockTransport = require('./helpers/mocks/mock-transport'); describe('Logger', function () { it('new Logger()', function () { @@ -892,3 +893,101 @@ describe('Should bubble transport events', () => { consoleTransport.emit('warn', new Error()); }); }); + +describe('Should support child loggers', () => { + it('sets default meta for text messages correctly', (done) => { + const assertFn = ((msg) => { + assume(msg.level).equals('info'); + assume(msg.message).equals('dummy message'); + assume(msg.req_id).equals('451'); + done(); + }); + + const logger = winston.createLogger({ + transports: [ + mockTransport.createMockTransport(assertFn) + ] + }); + + const childLogger = logger.child({ req_id: '451' }); + childLogger.info('dummy message'); + }); + + it('sets default meta for json messages correctly', (done) => { + const assertFn = ((msg) => { + assume(msg.level).equals('info'); + assume(msg.message.text).equals('dummy'); + assume(msg.req_id).equals('451'); + done(); + }); + + const logger = winston.createLogger({ + transports: [ + mockTransport.createMockTransport(assertFn) + ] + }); + + const childLogger = logger.child({ req_id: '451' }); + childLogger.info({text: 'dummy'}); + }); + + it('merges default and non-default meta correctly', (done) => { + const assertFn = ((msg) => { + assume(msg.level).equals('info'); + assume(msg.message).equals('dummy message'); + assume(msg.service).equals('user-service'); + assume(msg.req_id).equals('451'); + done(); + }); + + const logger = winston.createLogger({ + transports: [ + mockTransport.createMockTransport(assertFn) + ] + }); + + const childLogger = logger.child({ service: 'user-service' }); + childLogger.info('dummy message', {req_id: '451'}); + }); + + it('non-default take precedence over default meta', (done) => { + const assertFn = ((msg) => { + assume(msg.level).equals('info'); + assume(msg.message).equals('dummy message'); + assume(msg.service).equals('audit-service'); + assume(msg.req_id).equals('451'); + done(); + }); + + const logger = winston.createLogger({ + transports: [ + mockTransport.createMockTransport(assertFn) + ] + }); + + const childLogger = logger.child({ service: 'user-service' }); + childLogger.info('dummy message', { + req_id: '451', + service: 'audit-service' + }); + }); + + it('handles error stacktraces in child loggers correctly', (done) => { + const assertFn = ((msg) => { + assume(msg.level).equals('error'); + assume(msg.message).equals('dummy error'); + assume(msg.stack).includes('logger.test.js'); + assume(msg.service).equals('user-service'); + done(); + }); + + const logger = winston.createLogger({ + transports: [ + mockTransport.createMockTransport(assertFn) + ] + }); + + const childLogger = logger.child({ service: 'user-service' }); + childLogger.error(Error('dummy error')); + }); +});