diff --git a/index.js b/index.js index 46531bd..c149c7f 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ const TransportStream = module.exports = function TransportStream(options = {}) this.format = options.format; this.level = options.level; this.handleExceptions = options.handleExceptions; + this.exceptionsLevel = options.exceptionsLevel; this.handleRejections = options.handleRejections; this.silent = options.silent; @@ -71,13 +72,20 @@ TransportStream.prototype._write = function _write(info, enc, callback) { return callback(null); } + // Reset logging level if handling exception + if (info.exception === true && this.exceptionsLevel) { + // Clone info so that the logging level is only changed for this transport + info = cloneObj(info); + info.level = info[LEVEL] = this.exceptionsLevel; + } + // Remark: This has to be handled in the base transport now because we // cannot conditionally write to our pipe targets as stream. We always // prefer any explicit level set on the Transport itself falling back to // any level set on the parent. const level = this.level || (this.parent && this.parent.level); - if (!level || this.levels[level] >= this.levels[info[LEVEL]]) { + if (info.exception === true || !level || this.levels[level] >= this.levels[info[LEVEL]]) { if (info && !this.format) { return this.log(info, callback); } @@ -115,6 +123,18 @@ TransportStream.prototype._write = function _write(info, enc, callback) { * @private */ TransportStream.prototype._writev = function _writev(chunks, callback) { + // Clone chunks so any change made will apply to this transport only + chunks = cloneObjArray(chunks); + + chunks.forEach((chunkEntry) => { + // Reset logging level of chunk (info) if handling exception + if (chunkEntry.chunk.exception === true && this.exceptionsLevel) { + // Clone info so that the logging level is only changed for this transport + chunkEntry.chunk = cloneObj(chunkEntry.chunk); + chunkEntry.chunk.level = chunkEntry.chunk[LEVEL] = this.exceptionsLevel; + } + }); + if (this.logv) { const infos = chunks.filter(this._accept, this); if (!infos.length) { @@ -210,6 +230,27 @@ TransportStream.prototype._nop = function _nop() { return void undefined; }; +function cloneObj(obj) { + const clonedObj = {}; + + Object.getOwnPropertyNames(obj).forEach((propName) => { + Object.defineProperty(clonedObj, propName, Object.getOwnPropertyDescriptor(obj, propName)); + }); + + Object.setPrototypeOf(clonedObj, Object.getPrototypeOf(obj)); + + return clonedObj; +} + +function cloneObjArray(arr) { + if (typeof arr === 'object') { + if (Array.isArray(arr)) { + return arr.map(cloneObjArray); + } + + return cloneObj(arr); + } +} // Expose legacy stream module.exports.LegacyTransportStream = require('./legacy'); diff --git a/legacy.js b/legacy.js index 8025bc9..c825c1f 100644 --- a/legacy.js +++ b/legacy.js @@ -21,6 +21,7 @@ const LegacyTransportStream = module.exports = function LegacyTransportStream(op this.transport = options.transport; this.level = this.level || options.transport.level; this.handleExceptions = this.handleExceptions || options.transport.handleExceptions; + this.exceptionsLevel = this.exceptionsLevel || options.transport.exceptionsLevel; // Display our deprecation notice. this._deprecated(); @@ -58,8 +59,13 @@ LegacyTransportStream.prototype._write = function _write(info, enc, callback) { // Remark: This has to be handled in the base transport now because we // cannot conditionally write to our pipe targets as stream. - if (!this.level || this.levels[this.level] >= this.levels[info[LEVEL]]) { - this.transport.log(info[LEVEL], info.message, info, this._nop); + if (info.exception === true || !this.level || this.levels[this.level] >= this.levels[info[LEVEL]]) { + this.transport.log( + info.exception === true && this.exceptionsLevel ? this.exceptionsLevel : info[LEVEL], + info.message, + info, + this._nop + ); } callback(null); @@ -77,7 +83,7 @@ LegacyTransportStream.prototype._writev = function _writev(chunks, callback) { for (let i = 0; i < chunks.length; i++) { if (this._accept(chunks[i])) { this.transport.log( - chunks[i].chunk[LEVEL], + chunks[i].chunk.exception === true && this.exceptionsLevel ? this.exceptionsLevel : chunks[i].chunk[LEVEL], chunks[i].chunk.message, chunks[i].chunk, this._nop diff --git a/test/index.test.js b/test/index.test.js index ba1b78e..3964c13 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -121,7 +121,7 @@ describe('TransportStream', () => { expected.forEach(transport.write.bind(transport)); }); - it('{ level } should be ignored when { handleExceptions: true }', () => { + it('{ level } should be ignored when { handleExceptions: true }', done => { const expected = testOrder.map(levelAndMessage).map(info => { info.exception = true; return info; @@ -129,11 +129,15 @@ describe('TransportStream', () => { const transport = new TransportStream({ level: 'info', + handleExceptions: true, log: logFor(testOrder.length, (err, infos) => { - // eslint-disable-next-line no-undefined - assume(err).equals(undefined); + if (err) { + return done(err); + } + assume(infos.length).equals(expected.length); assume(infos).deep.equals(expected); + done(); }) }); @@ -192,6 +196,52 @@ describe('TransportStream', () => { expected.forEach(transport.write.bind(transport)); }); + + it('should log specified level when { handleExceptions: true, exceptionsLevel: "exception_level" }', done => { + const actual = []; + const expected = [{ + exception: true, + [LEVEL]: 'error', + level: 'error', + message: 'Test exception handling' + }, { + [LEVEL]: 'info', + level: 'info', + message: 'Testing ... 1 2 3.' + }]; + + const transport = new TransportStream({ + level: 'info', + handleExceptions: true, + exceptionsLevel: 'fatal', + log(info, next) { + actual.push(info); + + if (info.exception) { + assume(info.level).equals(transport.exceptionsLevel); + assume(info[LEVEL]).equals(transport.exceptionsLevel); + } + else { + assume(info).deep.equals(expected[actual.length - 1]); + } + + if (actual.length === expected.length) { + return done(); + } + + next(); + } + }); + + transport.levels = { + 'fatal': 0, + 'error': 1, + 'warn': 2, + 'info': 3 + }; + + expected.forEach(transport.write.bind(transport)); + }); }); }); @@ -326,6 +376,68 @@ describe('TransportStream', () => { expected.forEach(transport.write.bind(transport)); transport.uncork(); }); + + describe('when { exception: true } in info', () => { + it('should log specified level when { handleExceptions: true, exceptionsLevel: "exception_level" }', done => { + const actual = []; + const expected = [{ + exception: true, + [LEVEL]: 'error', + level: 'error', + message: 'Test exception handling #1' + }, { + [LEVEL]: 'info', + level: 'info', + message: 'Testing ... 1 2 3.' + }, { + exception: true, + [LEVEL]: 'error', + level: 'error', + message: 'Test exception handling #2' + }]; + + const transport = new TransportStream({ + level: 'info', + handleExceptions: true, + exceptionsLevel: 'fatal', + log(info, next) { + actual.push(info); + + if (info.exception) { + assume(info.level).equals(transport.exceptionsLevel); + assume(info[LEVEL]).equals(transport.exceptionsLevel); + } + else { + assume(info).deep.equals(expected[actual.length - 1]); + } + + if (actual.length === expected.length) { + return done(); + } + + next(); + } + }); + + transport.levels = { + 'fatal': 0, + 'error': 1, + 'warn': 2, + 'info': 3 + }; + + // + // Make the standard _write throw to ensure that _writev is called. + // + transport._write = () => { + throw new Error('TransportStream.prototype._write should never be called.'); + }; + + transport.cork(); + expected.forEach(transport.write.bind(transport)); + transport.uncork(); + }); + }); }); describe('parent (i.e. "logger") ["pipe", "unpipe"]', () => { diff --git a/test/legacy.test.js b/test/legacy.test.js index a564cd6..afdd873 100644 --- a/test/legacy.test.js +++ b/test/legacy.test.js @@ -127,21 +127,38 @@ describe('LegacyTransportStream', () => { expected.forEach(transport.write.bind(transport)); }); - it('{ level } should be ignored when { handleExceptions: true }', () => { + it('{ level } should be ignored when { handleExceptions: true }', done => { const expected = testOrder.map(levelAndMessage).map(info => { info.exception = true; return info; }); transport = new LegacyTransportStream({ transport: legacy, - level: 'info' + level: 'info', + handleExceptions: true }); legacy.on('logged', logFor(testOrder.length, (err, infos) => { - // eslint-disable-next-line no-undefined - assume(err).equals(undefined); + if (err) { + return done(err); + } + assume(infos.length).equals(expected.length); - assume(infos).deep.equals(expected); + + // Adjust log level for comparison if required for comparison + let compExpected; + if (transport.exceptionsLevel) { + compExpected = expected.map(info => { + info.level = transport.exceptionsLevel; + return info; + }); + } + else { + compExpected = expected; + } + + assume(infos).deep.equals(compExpected); + done(); })); transport.levels = testLevels; @@ -198,6 +215,51 @@ describe('LegacyTransportStream', () => { expected.forEach(transport.write.bind(transport)); }); + + it('should log specified level when { handleExceptions: true, exceptionsLevel: "exception_level" }', done => { + const actual = []; + const expected = [{ + exception: true, + [LEVEL]: 'error', + level: 'error', + message: 'Test exception handling' + }, { + [LEVEL]: 'info', + level: 'info', + message: 'Testing ... 1 2 3.' + }]; + + transport = new LegacyTransportStream({ + transport: legacy, + level: 'info', + handleExceptions: true, + exceptionsLevel: 'fatal' + }); + + legacy.on('logged', info => { + actual.push(info); + + if (info.exception) { + assume(info.level).equals(transport.exceptionsLevel); + } + else { + assume(info).deep.equals(expected[actual.length - 1]); + } + + if (actual.length === expected.length) { + return done(); + } + }); + + transport.levels = { + 'fatal': 0, + 'error': 1, + 'warn': 2, + 'info': 3 + }; + + expected.forEach(transport.write.bind(transport)); + }); }); }); @@ -229,6 +291,67 @@ describe('LegacyTransportStream', () => { expected.forEach(transport.write.bind(transport)); transport.uncork(); }); + + describe('when { exception: true } in info', () => { + it('should log specified level when { handleExceptions: true, exceptionsLevel: "exception_level" }', done => { + const actual = []; + const expected = [{ + exception: true, + [LEVEL]: 'error', + level: 'error', + message: 'Test exception handling #1' + }, { + [LEVEL]: 'info', + level: 'info', + message: 'Testing ... 1 2 3.' + }, { + exception: true, + [LEVEL]: 'error', + level: 'error', + message: 'Test exception handling #2' + }]; + + transport = new LegacyTransportStream({ + transport: legacy, + level: 'info', + handleExceptions: true, + exceptionsLevel: 'fatal' + }); + + legacy.on('logged', info => { + actual.push(info); + + if (info.exception) { + assume(info.level).equals(transport.exceptionsLevel); + } + else { + assume(info).deep.equals(expected[actual.length - 1]); + } + + if (actual.length === expected.length) { + return done(); + } + }); + + transport.levels = { + 'fatal': 0, + 'error': 1, + 'warn': 2, + 'info': 3 + }; + + // + // Make the standard _write throw to ensure that _writev is called. + // + transport._write = () => { + throw new Error('TransportStream.prototype._write should never be called.'); + }; + + transport.cork(); + expected.forEach(transport.write.bind(transport)); + transport.uncork(); + }); + }); }); describe('close()', () => {