diff --git a/docs/layouts.md b/docs/layouts.md index fae80d1..4174110 100644 --- a/docs/layouts.md +++ b/docs/layouts.md @@ -143,6 +143,10 @@ Fields can be any of: - `%l` line number (requires `enableCallStack: true` on the category, see [configuration object](api.md)) - `%o` column postion (requires `enableCallStack: true` on the category, see [configuration object](api.md)) - `%s` call stack (requires `enableCallStack: true` on the category, see [configuration object](api.md)) +- `%M` function or method name (requires `enableCallStack: true` on the category, see [configuration object](api.md)) +- `%C` class name (requires `enableCallStack: true` on the category, see [configuration object](api.md)) +- `%A` function or method name alias (requires `enableCallStack: true` on the category, see [configuration object](api.md)) +- `%F` fully qualified caller name (requires `enableCallStack: true` on the category, see [configuration object](api.md)) - `%x{}` add dynamic tokens to your log. Tokens are specified in the tokens parameter. - `%X{}` add values from the Logger context. Tokens are keys into the context values. - `%[` start a coloured block (colour will be taken from the log level, similar to `colouredLayout`) diff --git a/lib/LoggingEvent.js b/lib/LoggingEvent.js index a3dff7a..494da4c 100644 --- a/lib/LoggingEvent.js +++ b/lib/LoggingEvent.js @@ -28,6 +28,9 @@ class LoggingEvent { this.lineNumber = location.lineNumber; this.columnNumber = location.columnNumber; this.callStack = location.callStack; + this.className = location.className; + this.functionAlias = location.functionAlias; + this.callerName = location.callerName; } } @@ -79,6 +82,9 @@ class LoggingEvent { lineNumber: rehydratedEvent.lineNumber, columnNumber: rehydratedEvent.columnNumber, callStack: rehydratedEvent.callStack, + className: rehydratedEvent.className, + functionAlias: rehydratedEvent.functionAlias, + callerName: rehydratedEvent.callerName, }; event = new LoggingEvent( rehydratedEvent.categoryName, diff --git a/lib/layouts.js b/lib/layouts.js index 8884162..2c01d96 100644 --- a/lib/layouts.js +++ b/lib/layouts.js @@ -109,6 +109,10 @@ function dummyLayout(loggingEvent) { * - %l line number * - %o column postion * - %s call stack + * - %M method or function name + * - %C class name + * - %A method or function name + * - %F fully qualified caller name * - %x{} add dynamic tokens to your log. Tokens are specified in the tokens parameter * - %X{} add dynamic tokens to your log. Tokens are specified in logger context * You can use %[ and %] to define a colored block. @@ -130,7 +134,7 @@ function dummyLayout(loggingEvent) { */ function patternLayout(pattern, tokens) { const TTCC_CONVERSION_PATTERN = '%r %p %c - %m%n'; - const regex = /%(-?[0-9]+)?(\.?-?[0-9]+)?([[\]cdhmnprzxXyflos%])(\{([^}]+)\})?|([^%]+)/; + const regex = /%(-?[0-9]+)?(\.?-?[0-9]+)?([[\]cdhmnprzxXyflosMCAF%])(\{([^}]+)\})?|([^%]+)/; pattern = pattern || TTCC_CONVERSION_PATTERN; @@ -322,6 +326,22 @@ function patternLayout(pattern, tokens) { return loggingEvent.callStack || ''; } + function functionName(loggingEvent) { + return loggingEvent.functionName || ''; + } + + function className(loggingEvent) { + return loggingEvent.className || ''; + } + + function functionAlias(loggingEvent) { + return loggingEvent.functionAlias || ''; + } + + function callerName(loggingEvent) { + return loggingEvent.callerName || ''; + } + const replacers = { c: categoryName, d: formatAsDate, @@ -341,6 +361,10 @@ function patternLayout(pattern, tokens) { l: lineNumber, o: columnNumber, s: callStack, + M: functionName, + C: className, + A: functionAlias, + F: callerName, }; function replaceToken(conversionCharacter, loggingEvent, specifier) { diff --git a/lib/logger.js b/lib/logger.js index d979caf..9291e6e 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -15,12 +15,30 @@ function defaultParseCallStack(data, skipIdx = 4) { const lineMatch = stackReg.exec(stacklines[0]); /* istanbul ignore else: failsafe */ if (lineMatch && lineMatch.length === 6) { + // extract class, function and alias names + let className = ''; + let functionName = ''; + let functionAlias = ''; + if (lineMatch[1] && lineMatch[1] !== '') { + // WARN: this will unset alias if alias is not present. + [functionName, functionAlias] = lineMatch[1] + .replace(/[[\]]/g, '') + .split(' as '); + functionAlias = functionAlias || ''; + + if (functionName.includes('.')) + [className, functionName] = functionName.split('.'); + } + return { - functionName: lineMatch[1], + functionName, fileName: lineMatch[2], lineNumber: parseInt(lineMatch[3], 10), columnNumber: parseInt(lineMatch[4], 10), callStack: stacklines.join('\n'), + className, + functionAlias, + callerName: lineMatch[1] || '', }; // eslint-disable-next-line no-else-return } else { diff --git a/test/tap/LoggingEvent-test.js b/test/tap/LoggingEvent-test.js index f3cd49e..ec4df9f 100644 --- a/test/tap/LoggingEvent-test.js +++ b/test/tap/LoggingEvent-test.js @@ -69,11 +69,19 @@ test('LoggingEvent', (batch) => { const fileName = '/log4js-node/test/tap/layouts-test.js'; const lineNumber = 1; const columnNumber = 14; + const className = ''; + const functionName = ''; + const functionAlias = ''; + const callerName = ''; const location = { + functionName, fileName, lineNumber, columnNumber, callStack, + className, + functionAlias, + callerName, }; const event = new LoggingEvent( 'cheese', @@ -82,10 +90,14 @@ test('LoggingEvent', (batch) => { { user: 'bob' }, location ); + t.equal(event.functionName, functionName); t.equal(event.fileName, fileName); t.equal(event.lineNumber, lineNumber); t.equal(event.columnNumber, columnNumber); t.equal(event.callStack, callStack); + t.equal(event.className, className); + t.equal(event.functionAlias, functionAlias); + t.equal(event.callerName, callerName); const event2 = new LoggingEvent('cheese', levels.DEBUG, ['log message'], { user: 'bob', @@ -94,6 +106,49 @@ test('LoggingEvent', (batch) => { t.equal(event2.lineNumber, undefined); t.equal(event2.columnNumber, undefined); t.equal(event2.callStack, undefined); + t.equal(event2.functionName, undefined); + t.equal(event2.className, undefined); + t.equal(event2.functionAlias, undefined); + t.equal(event2.callerName, undefined); + t.end(); + }); + + batch.test('Should contain class, method and alias names', (t) => { + // console.log([Error('123').stack.split('\n').slice(1).join('\n')]) + const callStack = + ' at Foo.bar [as baz] (repl:1:14)\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len + const fileName = '/log4js-node/test/tap/layouts-test.js'; + const lineNumber = 1; + const columnNumber = 14; + const className = 'Foo'; + const functionName = 'bar'; + const functionAlias = 'baz'; + const callerName = 'Foo.bar [as baz]'; + const location = { + functionName, + fileName, + lineNumber, + columnNumber, + callStack, + className, + functionAlias, + callerName, + }; + const event = new LoggingEvent( + 'cheese', + levels.DEBUG, + ['log message'], + { user: 'bob' }, + location + ); + t.equal(event.functionName, functionName); + t.equal(event.fileName, fileName); + t.equal(event.lineNumber, lineNumber); + t.equal(event.columnNumber, columnNumber); + t.equal(event.callStack, callStack); + t.equal(event.className, className); + t.equal(event.functionAlias, functionAlias); + t.equal(event.callerName, callerName); t.end(); }); diff --git a/test/tap/layouts-test.js b/test/tap/layouts-test.js index f240eec..c9987a4 100644 --- a/test/tap/layouts-test.js +++ b/test/tap/layouts-test.js @@ -300,10 +300,14 @@ test('log4js layouts', (batch) => { // console.log([Error('123').stack.split('\n').slice(1).join('\n')]) const callStack = - ' at repl:1:14\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len + ' at Foo.bar [as baz] (repl:1:14)\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len const fileName = path.normalize('/log4js-node/test/tap/layouts-test.js'); const lineNumber = 1; const columnNumber = 14; + const className = 'Foo'; + const functionName = 'bar'; + const functionAlias = 'baz'; + const callerName = 'Foo.bar [as baz]'; const event = { data: ['this is a test'], startTime: new Date('2010-12-05 14:18:30.045'), @@ -321,6 +325,10 @@ test('log4js layouts', (batch) => { fileName, lineNumber, columnNumber, + className, + functionName, + functionAlias, + callerName, }; event.startTime.getTimezoneOffset = () => -600; @@ -886,6 +894,62 @@ test('log4js layouts', (batch) => { assert.end(); }); + t.test('%M should output function name', (assert) => { + testPattern(assert, layout, event, tokens, '%M', functionName); + assert.end(); + }); + + t.test( + '%M should output empty string when functionName not exist', + (assert) => { + delete event.functionName; + testPattern(assert, layout, event, tokens, '%M', ''); + assert.end(); + } + ); + + t.test('%C should output class name', (assert) => { + testPattern(assert, layout, event, tokens, '%C', className); + assert.end(); + }); + + t.test( + '%C should output empty string when className not exist', + (assert) => { + delete event.className; + testPattern(assert, layout, event, tokens, '%C', ''); + assert.end(); + } + ); + + t.test('%A should output function alias', (assert) => { + testPattern(assert, layout, event, tokens, '%A', functionAlias); + assert.end(); + }); + + t.test( + '%A should output empty string when functionAlias not exist', + (assert) => { + delete event.functionAlias; + testPattern(assert, layout, event, tokens, '%A', ''); + assert.end(); + } + ); + + t.test('%F should output fully qualified caller name', (assert) => { + testPattern(assert, layout, event, tokens, '%F', callerName); + assert.end(); + }); + + t.test( + '%F should output empty string when callerName not exist', + (assert) => { + delete event.callerName; + testPattern(assert, layout, event, tokens, '%F', ''); + assert.end(); + } + ); + t.end(); }); diff --git a/test/tap/logger-test.js b/test/tap/logger-test.js index 3baa296..2524a3a 100644 --- a/test/tap/logger-test.js +++ b/test/tap/logger-test.js @@ -253,6 +253,60 @@ test('../../lib/logger', (batch) => { t.end(); }); + batch.test('parseCallStack names extraction', (t) => { + const logger = new Logger('stack'); + logger.useCallStack = true; + + let results; + + const callStack1 = + ' at Foo.bar [as baz] (repl:1:14)\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len + results = logger.parseCallStack({ stack: callStack1 }, 0); + t.ok(results); + t.equal(results.className, 'Foo'); + t.equal(results.functionName, 'bar'); + t.equal(results.functionAlias, 'baz'); + t.equal(results.callerName, 'Foo.bar [as baz]'); + + const callStack2 = + ' at bar [as baz] (repl:1:14)\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len + results = logger.parseCallStack({ stack: callStack2 }, 0); + t.ok(results); + t.equal(results.className, ''); + t.equal(results.functionName, 'bar'); + t.equal(results.functionAlias, 'baz'); + t.equal(results.callerName, 'bar [as baz]'); + + const callStack3 = + ' at bar (repl:1:14)\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len + results = logger.parseCallStack({ stack: callStack3 }, 0); + t.ok(results); + t.equal(results.className, ''); + t.equal(results.functionName, 'bar'); + t.equal(results.functionAlias, ''); + t.equal(results.callerName, 'bar'); + + const callStack4 = + ' at repl:1:14\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len + results = logger.parseCallStack({ stack: callStack4 }, 0); + t.ok(results); + t.equal(results.className, ''); + t.equal(results.functionName, ''); + t.equal(results.functionAlias, ''); + t.equal(results.callerName, ''); + + const callStack5 = + ' at Foo.bar (repl:1:14)\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:280:10)\n at REPLServer.Interface._line (readline.js:629:8)'; // eslint-disable-line max-len + results = logger.parseCallStack({ stack: callStack5 }, 0); + t.ok(results); + t.equal(results.className, 'Foo'); + t.equal(results.functionName, 'bar'); + t.equal(results.functionAlias, ''); + t.equal(results.callerName, 'Foo.bar'); + + t.end(); + }); + batch.test('should correctly change the parseCallStack function', (t) => { const logger = new Logger('stack'); const parseFunction = function() {