Skip to content

Commit 59cfdd3

Browse files
committed
lib: implement node:logger
A simpler attempt to add a structured logging API. Uses a provider model similar to VFS. This implements two providers out of the box, ConsoleProvider and ReadableProvider. ConsoleProvider is the default and uses Utf8Stream to emit to either stdout or stderr. ```js const { create } = require('node:logger'); const logger = create(); // default logger to console logger.info('foo'); // ... const als = new AsyncLocalStorage(); const logger2 = create(new ConsoleProvider({ pid: true }), { name: 'foo', bindings: { 'abc': 'included in every log line', 'xyz': als, // current als.getStore() included in // every log line } }); logger2.info('foo', { baz: 1 }); ``` The `Logger` keeps things as simple as possible, leaving actual handling of the log events to the providers, which can be fully customized. Easy to adapt to other loggers like pino or extend capabilities without directly touching the facade. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
1 parent 0e92441 commit 59cfdd3

25 files changed

Lines changed: 3752 additions & 2 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
// End-to-end cost of logging through ConsoleProvider with the default JSON
4+
// serializer. The provider's stream write() is replaced to avoid I/O latency.
5+
6+
const common = require('../common.js');
7+
8+
const bench = common.createBenchmark(main, {
9+
n: [5e5],
10+
variant: ['json', 'flatten-pid', 'error'],
11+
}, {
12+
flags: ['--experimental-logger', '--no-warnings'],
13+
});
14+
15+
function main({ n, variant }) {
16+
const { ConsoleProvider, create } = require('node:logger');
17+
18+
const provider = new ConsoleProvider({
19+
flatten: variant === 'flatten-pid',
20+
pid: variant === 'flatten-pid',
21+
});
22+
let bytes = 0;
23+
provider.stream.write = (data) => {
24+
bytes += data.length;
25+
return true;
26+
};
27+
const logger = create(provider, {
28+
name: 'bench',
29+
bindings: { service: 'api' },
30+
});
31+
32+
if (variant === 'error') {
33+
const err = new Error('boom');
34+
err.code = 'E_BOOM';
35+
bench.start();
36+
for (let i = 0; i < n; i++) logger.error('failed', { err });
37+
bench.end(n);
38+
} else {
39+
bench.start();
40+
for (let i = 0; i < n; i++) logger.info('hello', { i, user: 'x' });
41+
bench.end(n);
42+
}
43+
44+
if (bytes === 0) throw new Error('nothing was serialized');
45+
}

‎benchmark/logger/creation.js‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
// Cost of creating loggers and child loggers.
4+
5+
const common = require('../common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e6],
9+
type: ['create', 'child'],
10+
}, {
11+
flags: ['--experimental-logger', '--no-warnings'],
12+
});
13+
14+
function main({ n, type }) {
15+
const { ConsoleProvider, create } = require('node:logger');
16+
17+
const provider = new ConsoleProvider();
18+
const parent = create(provider, { name: 'bench' });
19+
const loggers = new Array(1024);
20+
21+
if (type === 'child') {
22+
bench.start();
23+
for (let i = 0; i < n; i++) {
24+
loggers[i & 1023] = parent.child({ requestId: i });
25+
}
26+
bench.end(n);
27+
} else {
28+
bench.start();
29+
for (let i = 0; i < n; i++) {
30+
loggers[i & 1023] = create(provider, { name: 'bench' });
31+
}
32+
bench.end(n);
33+
}
34+
}

‎benchmark/logger/disabled.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
// Calls to level methods for a level the provider has disabled.
4+
5+
const common = require('../common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e8],
9+
provider: ['console', 'custom'],
10+
attributes: ['none', 'object'],
11+
child: [0, 1],
12+
}, {
13+
flags: ['--experimental-logger', '--no-warnings'],
14+
});
15+
16+
function main({ n, provider, attributes, child }) {
17+
const { ConsoleProvider, create } = require('node:logger');
18+
19+
let logger = create(provider === 'console' ?
20+
new ConsoleProvider({ level: 'info' }) :
21+
{
22+
isEnabled(level) { return level.value >= 30; },
23+
log() {},
24+
});
25+
if (child) logger = logger.child({ requestId: 1 });
26+
27+
if (attributes === 'object') {
28+
bench.start();
29+
for (let i = 0; i < n; i++) logger.debug('hello', { i });
30+
bench.end(n);
31+
} else {
32+
bench.start();
33+
for (let i = 0; i < n; i++) logger.debug('hello');
34+
bench.end(n);
35+
}
36+
}

‎benchmark/logger/enabled.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
// Cost of composing an enabled log event, with a provider that does no work.
4+
5+
const common = require('../common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e6],
9+
provider: ['minimal', 'custom'],
10+
variant: ['no-attributes', 'attributes', 'function-binding'],
11+
}, {
12+
flags: ['--experimental-logger', '--no-warnings'],
13+
});
14+
15+
function main({ n, provider, variant }) {
16+
const { create } = require('node:logger');
17+
const { AsyncLocalStorage } = require('node:async_hooks');
18+
19+
let last;
20+
const log = (event) => { last = event; };
21+
const als = new AsyncLocalStorage();
22+
const bindings = variant === 'function-binding' ?
23+
{ service: 'api', request: () => als.getStore() } :
24+
{ service: 'api' };
25+
const logger = create(provider === 'minimal' ? { log } : {
26+
isEnabled(level) { return level.value >= 30; },
27+
log,
28+
}, { name: 'bench', bindings });
29+
30+
switch (variant) {
31+
case 'no-attributes':
32+
bench.start();
33+
for (let i = 0; i < n; i++) logger.info('hello');
34+
bench.end(n);
35+
break;
36+
case 'attributes':
37+
bench.start();
38+
for (let i = 0; i < n; i++) logger.info('hello', { i, user: 'x' });
39+
bench.end(n);
40+
break;
41+
case 'function-binding':
42+
als.enterWith(1);
43+
bench.start();
44+
for (let i = 0; i < n; i++) logger.info('hello', { i });
45+
bench.end(n);
46+
break;
47+
}
48+
49+
if (last === undefined) throw new Error('no event was logged');
50+
}

‎benchmark/logger/request.js‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
// A child logger per request, logging a few lines through ConsoleProvider.
4+
// The provider's stream write() is replaced to avoid I/O latency.
5+
6+
const common = require('../common.js');
7+
8+
const bench = common.createBenchmark(main, {
9+
n: [5e5],
10+
lines: [0, 1, 3],
11+
level: ['info', 'debug'],
12+
}, {
13+
flags: ['--experimental-logger', '--no-warnings'],
14+
});
15+
16+
function main({ n, lines, level }) {
17+
const { ConsoleProvider, create } = require('node:logger');
18+
19+
// With level 'debug' the lines are disabled (the provider is at 'info').
20+
const provider = new ConsoleProvider({ level: 'info' });
21+
let bytes = 0;
22+
provider.stream.write = (data) => {
23+
bytes += data.length;
24+
return true;
25+
};
26+
const logger = create(provider, {
27+
name: 'bench',
28+
bindings: { service: 'api' },
29+
});
30+
31+
bench.start();
32+
for (let i = 0; i < n; i++) {
33+
const child = logger.child({ requestId: i });
34+
for (let j = 0; j < lines; j++) {
35+
child[level]('handled', { step: j });
36+
}
37+
}
38+
bench.end(n);
39+
40+
if (lines > 0 && level === 'info' && bytes === 0) {
41+
throw new Error('nothing was serialized');
42+
}
43+
}

‎doc/api/cli.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,16 @@ Specify the `module` containing exported [asynchronous module customization hook
15451545

15461546
This feature requires `--allow-worker` if used with the [Permission Model][].
15471547

1548+
### `--experimental-logger`
1549+
1550+
<!-- YAML
1551+
added: REPLACEME
1552+
-->
1553+
1554+
> Stability: 1.1 - Active development
1555+
1556+
Enable the experimental [`node:logger`][] module.
1557+
15481558
### `--experimental-network-inspection`
15491559

15501560
<!-- YAML
@@ -4276,6 +4286,7 @@ one is included in the list below.
42764286
* `--experimental-import-text`
42774287
* `--experimental-json-modules`
42784288
* `--experimental-loader`
4289+
* `--experimental-logger`
42794290
* `--experimental-modules`
42804291
* `--experimental-package-map`
42814292
* `--experimental-print-required-tla`
@@ -4961,6 +4972,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
49614972
[`inspector.open()`]: inspector.md#inspectoropenport-host-wait
49624973
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: net.md#netgetdefaultautoselectfamilyattempttimeout
49634974
[`node:ffi`]: ffi.md
4975+
[`node:logger`]: logger.md
49644976
[`node:sqlite`]: sqlite.md
49654977
[`node:stream/iter`]: stream_iter.md
49664978
[`node:vfs`]: vfs.md

‎doc/api/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* [Inspector](inspector.md)
3737
* [Internationalization](intl.md)
3838
* [Iterable Streams API](stream_iter.md)
39+
* [Logger](logger.md)
3940
* [Modules: CommonJS modules](modules.md)
4041
* [Modules: ECMAScript modules](esm.md)
4142
* [Modules: `node:module` API](module.md)

0 commit comments

Comments
 (0)