Merge pull request #1316 from l0ner/patternLayout-class-function-alias-names

* FEAT: patternLayout function name, class name and function alias

Added to patternLayout the following fields:

- %M - function name of the caller issuing the logging request
- %C - class name of the caller issuing the logging request
- %A - function alias of the caller issuing the logging request

The first two come from log4j, and use the same field specifiers.

When called from method bar of a Class foo:

- %M will be replaced with bar
- %C will be replaced with Foo
- %A will be empty

When called from function foo:

- %M will be replaced with foo
- %C will be empty
- %A will be empty

%A will be non empty only if the call stack parsed to obtain the values
contains string [as foo].
This commit is contained in:
Lam Wei Li 2022-09-01 19:50:40 +08:00 committed by GitHub
commit be433f26c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 228 additions and 3 deletions

View File

@ -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{<tokenname>}` add dynamic tokens to your log. Tokens are specified in the tokens parameter.
- `%X{<tokenname>}` 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`)

View File

@ -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,

View File

@ -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{<tokenname>} add dynamic tokens to your log. Tokens are specified in the tokens parameter
* - %X{<tokenname>} 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) {

View File

@ -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 {

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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() {