Skip to content

Commit d0a6f6f

Browse files
committed
lib: address review feedback for logger API
2 parents 6c38bab + e2b603f commit d0a6f6f

4 files changed

Lines changed: 276 additions & 28 deletions

File tree

doc/api/logger.md

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# Logger
22

3-
<!--introduced_in=v26.0.0-->
3+
<!--introduced_in=REPLACEME-->
44

5-
> Stability: 1.0 - Early Development
5+
> Stability: 1.1 - Active Development
66
77
<!-- source_link=lib/logger.js -->
88

9+
The `node:logger` module is available when Node.js is started with the
10+
`--experimental-logger` flag.
11+
912
The `node:logger` module provides high-performance structured logging
1013
capabilities for Node.js applications. It uses `diagnostics_channel` internally
1114
to dispatch log events to consumers, allowing multiple consumers to receive
@@ -100,10 +103,15 @@ const logger = new Logger({
100103
added: REPLACEME
101104
-->
102105

103-
* `msg` {string} Log message.
104-
* `obj` {Object} Object containing `msg` property and additional fields.
105-
* `error` {Error} Error object to log.
106-
* `fields` {Object} Additional fields to include in the log record.
106+
* `msg` {string} The log message. When passed as the first argument, any second
107+
argument is treated as `fields`.
108+
* `obj` {Object} When passed as the first argument, `obj.msg` is used as the
109+
log message and all other properties are merged into the log record as fields.
110+
* `error` {Error} When passed as the first argument, the error is serialized
111+
into log fields including `type`, `message`, and `stack`. An optional `fields`
112+
argument may follow.
113+
* `fields` {Object} Additional fields to merge into the log record. Only
114+
applicable alongside `msg` or `error`, not with `obj`.
107115

108116
Logs a message at the `trace` level.
109117

@@ -123,10 +131,15 @@ logger.trace({ msg: 'Object format', requestId: 'abc123' });
123131
added: REPLACEME
124132
-->
125133

126-
* `msg` {string} Log message.
127-
* `obj` {Object} Object containing `msg` property and additional fields.
128-
* `error` {Error} Error object to log.
129-
* `fields` {Object} Additional fields to include in the log record.
134+
* `msg` {string} The log message. When passed as the first argument, any second
135+
argument is treated as `fields`.
136+
* `obj` {Object} When passed as the first argument, `obj.msg` is used as the
137+
log message and all other properties are merged into the log record as fields.
138+
* `error` {Error} When passed as the first argument, the error is serialized
139+
into log fields including `type`, `message`, and `stack`. An optional `fields`
140+
argument may follow.
141+
* `fields` {Object} Additional fields to merge into the log record. Only
142+
applicable alongside `msg` or `error`, not with `obj`.
130143

131144
Logs a message at the `debug` level.
132145

@@ -145,10 +158,15 @@ logger.debug('Processing request', { requestId: 'abc123' });
145158
added: REPLACEME
146159
-->
147160

148-
* `msg` {string} Log message.
149-
* `obj` {Object} Object containing `msg` property and additional fields.
150-
* `error` {Error} Error object to log.
151-
* `fields` {Object} Additional fields to include in the log record.
161+
* `msg` {string} The log message. When passed as the first argument, any second
162+
argument is treated as `fields`.
163+
* `obj` {Object} When passed as the first argument, `obj.msg` is used as the
164+
log message and all other properties are merged into the log record as fields.
165+
* `error` {Error} When passed as the first argument, the error is serialized
166+
into log fields including `type`, `message`, and `stack`. An optional `fields`
167+
argument may follow.
168+
* `fields` {Object} Additional fields to merge into the log record. Only
169+
applicable alongside `msg` or `error`, not with `obj`.
152170

153171
Logs a message at the `info` level.
154172

@@ -168,10 +186,15 @@ logger.info({ msg: 'User logged in', userId: 123 });
168186
added: REPLACEME
169187
-->
170188

171-
* `msg` {string} Log message.
172-
* `obj` {Object} Object containing `msg` property and additional fields.
173-
* `error` {Error} Error object to log.
174-
* `fields` {Object} Additional fields to include in the log record.
189+
* `msg` {string} The log message. When passed as the first argument, any second
190+
argument is treated as `fields`.
191+
* `obj` {Object} When passed as the first argument, `obj.msg` is used as the
192+
log message and all other properties are merged into the log record as fields.
193+
* `error` {Error} When passed as the first argument, the error is serialized
194+
into log fields including `type`, `message`, and `stack`. An optional `fields`
195+
argument may follow.
196+
* `fields` {Object} Additional fields to merge into the log record. Only
197+
applicable alongside `msg` or `error`, not with `obj`.
175198

176199
Logs a message at the `warn` level.
177200

@@ -190,10 +213,15 @@ logger.warn('High memory usage', { memoryUsage: process.memoryUsage() });
190213
added: REPLACEME
191214
-->
192215

193-
* `msg` {string} Log message.
194-
* `obj` {Object} Object containing `msg` property and additional fields.
195-
* `error` {Error} Error object to log.
196-
* `fields` {Object} Additional fields to include in the log record.
216+
* `msg` {string} The log message. When passed as the first argument, any second
217+
argument is treated as `fields`.
218+
* `obj` {Object} When passed as the first argument, `obj.msg` is used as the
219+
log message and all other properties are merged into the log record as fields.
220+
* `error` {Error} When passed as the first argument, the error is serialized
221+
into log fields including `type`, `message`, and `stack`. An optional `fields`
222+
argument may follow.
223+
* `fields` {Object} Additional fields to merge into the log record. Only
224+
applicable alongside `msg` or `error`, not with `obj`.
197225

198226
Logs a message at the `error` level.
199227

@@ -213,10 +241,15 @@ logger.error(new Error('Request failed'), { requestId: 'abc123' });
213241
added: REPLACEME
214242
-->
215243

216-
* `msg` {string} Log message.
217-
* `obj` {Object} Object containing `msg` property and additional fields.
218-
* `error` {Error} Error object to log.
219-
* `fields` {Object} Additional fields to include in the log record.
244+
* `msg` {string} The log message. When passed as the first argument, any second
245+
argument is treated as `fields`.
246+
* `obj` {Object} When passed as the first argument, `obj.msg` is used as the
247+
log message and all other properties are merged into the log record as fields.
248+
* `error` {Error} When passed as the first argument, the error is serialized
249+
into log fields including `type`, `message`, and `stack`. An optional `fields`
250+
argument may follow.
251+
* `fields` {Object} Additional fields to merge into the log record. Only
252+
applicable alongside `msg` or `error`, not with `obj`.
220253

221254
Logs a message at the `fatal` level.
222255

@@ -231,6 +264,8 @@ logger.fatal(new Error('Unrecoverable error'));
231264
added: REPLACEME
232265
-->
233266

267+
> Stability: 1.1 - Active Development
268+
234269
* `bindings` {Object} Additional context fields for the child logger.
235270
* `options` {Object}
236271
* `level` {string} Log level for the child logger.
@@ -613,6 +648,17 @@ added: REPLACEME
613648
An object containing `diagnostics_channel` instances for each log level.
614649
Advanced users can subscribe directly to these channels.
615650
651+
The following channel names are used:
652+
653+
| Channel | Log Level |
654+
| ----------- | --------- |
655+
| `log:trace` | `trace` |
656+
| `log:debug` | `debug` |
657+
| `log:info` | `info` |
658+
| `log:warn` | `warn` |
659+
| `log:error` | `error` |
660+
| `log:fatal` | `fatal` |
661+
616662
```js
617663
const { channels } = require('node:logger');
618664

lib/internal/logger/serializers.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
'use strict';
22
const {
33
ObjectKeys,
4+
SafeSet,
45
} = primordials;
56

67
const { isNativeError } = require('internal/util/types');
78

89
/**
910
* Serializes an Error object
1011
* @param {Error} error
12+
* @param {Set} [seen] Set of already-visited errors to detect circular references
1113
* @returns {{ type: string, message: string, stack: string, code?: any, cause?: any } | Error }
1214
*/
13-
function serializeErr(error) {
15+
function serializeErr(error, seen) {
1416
if (!isNativeError(error)) {
17+
// Pass through non-Error values unchanged to allow this serializer
18+
// to be used even when the value may not always be an Error instance.
1519
return error;
1620
}
1721

22+
if (seen === undefined) {
23+
seen = new SafeSet();
24+
}
25+
26+
if (seen.has(error)) {
27+
return '[Circular]';
28+
}
29+
seen.add(error);
30+
1831
const obj = {
1932
__proto__: null,
2033
type: error.constructor.name,
@@ -34,7 +47,7 @@ function serializeErr(error) {
3447
// Handle error cause recursively
3548
if (error.cause !== undefined) {
3649
obj.cause = typeof error.cause === 'object' && error.cause !== null ?
37-
serializeErr(error.cause) :
50+
serializeErr(error.cause, seen) :
3851
error.cause;
3952
}
4053

test/parallel/test-log-basic.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,3 +474,151 @@ describe('channels export', () => {
474474
assert.strictEqual(typeof channels.fatal, 'object');
475475
});
476476
});
477+
478+
describe('Logger constructor validation', () => {
479+
it('should throw when bindings is not an object', () => {
480+
assert.throws(() => {
481+
new Logger({ bindings: 'not-an-object' });
482+
}, { code: 'ERR_INVALID_ARG_TYPE' });
483+
});
484+
485+
it('should throw when a serializer value is not a function', () => {
486+
assert.throws(() => {
487+
new Logger({ serializers: { myField: 'not-a-function' } });
488+
}, { code: 'ERR_INVALID_ARG_TYPE' });
489+
});
490+
491+
it('should include constructor bindings in every log record', () => {
492+
const stream = new TestStream();
493+
const consumer = new JSONConsumer({ stream, level: 'info' });
494+
consumer.attach();
495+
496+
const logger = new Logger({
497+
level: 'info',
498+
bindings: { service: 'api', version: '2.0' },
499+
});
500+
501+
logger.info('startup');
502+
consumer.flushSync();
503+
504+
const log = stream.logs[0];
505+
assert.strictEqual(log.service, 'api');
506+
assert.strictEqual(log.version, '2.0');
507+
});
508+
});
509+
510+
describe('Logger all log levels', () => {
511+
it('should log at trace, debug, warn, and fatal levels', () => {
512+
const stream = new TestStream();
513+
const consumer = new JSONConsumer({ stream, level: 'trace' });
514+
consumer.attach();
515+
516+
const logger = new Logger({ level: 'trace' });
517+
518+
logger.trace('trace msg');
519+
logger.debug('debug msg');
520+
logger.warn('warn msg');
521+
logger.fatal('fatal msg');
522+
consumer.flushSync();
523+
524+
assert.strictEqual(stream.logs.length, 4);
525+
assert.strictEqual(stream.logs[0].level, 'trace');
526+
assert.strictEqual(stream.logs[1].level, 'debug');
527+
assert.strictEqual(stream.logs[2].level, 'warn');
528+
assert.strictEqual(stream.logs[3].level, 'fatal');
529+
});
530+
});
531+
532+
describe('Error logging with additional fields', () => {
533+
it('should merge extra fields alongside serialized error', () => {
534+
const stream = new TestStream();
535+
const consumer = new JSONConsumer({ stream, level: 'info' });
536+
consumer.attach();
537+
538+
const logger = new Logger({ level: 'info' });
539+
const err = new Error('request failed');
540+
logger.error(err, { requestId: 'abc-123', retries: 3 });
541+
consumer.flushSync();
542+
543+
const log = stream.logs[0];
544+
assert.strictEqual(log.msg, 'request failed');
545+
assert.ok(log.err.stack);
546+
assert.strictEqual(log.requestId, 'abc-123');
547+
assert.strictEqual(log.retries, 3);
548+
});
549+
});
550+
551+
describe('child() validation', () => {
552+
it('should throw when options.serializers is not an object', () => {
553+
const logger = new Logger();
554+
assert.throws(() => {
555+
logger.child({}, { serializers: 'not-an-object' });
556+
}, { code: 'ERR_INVALID_ARG_TYPE' });
557+
});
558+
559+
it('should throw when a child serializer value is not a function', () => {
560+
const logger = new Logger();
561+
assert.throws(() => {
562+
logger.child({}, { serializers: { field: 99 } });
563+
}, { code: 'ERR_INVALID_ARG_TYPE' });
564+
});
565+
});
566+
567+
describe('LogConsumer validation', () => {
568+
it('should throw for invalid log level', () => {
569+
assert.throws(() => {
570+
new LogConsumer({ level: 'verbose' });
571+
}, { code: 'ERR_INVALID_ARG_VALUE' });
572+
});
573+
574+
it('should have correct enabled values for all six levels', () => {
575+
const consumer = new LogConsumer({ level: 'warn' });
576+
assert.strictEqual(consumer.trace.enabled, false);
577+
assert.strictEqual(consumer.debug.enabled, false);
578+
assert.strictEqual(consumer.info.enabled, false);
579+
assert.strictEqual(consumer.warn.enabled, true);
580+
assert.strictEqual(consumer.error.enabled, true);
581+
assert.strictEqual(consumer.fatal.enabled, true);
582+
});
583+
});
584+
585+
describe('JSONConsumer stream options', () => {
586+
it('should throw for an invalid stream type', () => {
587+
assert.throws(() => {
588+
new JSONConsumer({ stream: true });
589+
}, { code: 'ERR_INVALID_ARG_TYPE' });
590+
});
591+
592+
it('should accept a file descriptor number as stream', () => {
593+
// fd 2 = stderr; use fatal-only to avoid noise in test output
594+
// Should not throw
595+
new JSONConsumer({ stream: 2, level: 'fatal' });
596+
});
597+
598+
it('should throw when options.fields is not an object', () => {
599+
assert.throws(() => {
600+
new JSONConsumer({ fields: 'not-an-object' });
601+
}, { code: 'ERR_INVALID_ARG_TYPE' });
602+
});
603+
604+
it('should call the callback passed to flush()', () => {
605+
const stream = new TestStream();
606+
const consumer = new JSONConsumer({ stream, level: 'info' });
607+
let called = false;
608+
consumer.flush(() => { called = true; });
609+
assert.strictEqual(called, true);
610+
});
611+
612+
it('should close the underlying stream on end()', () => {
613+
let ended = false;
614+
const mockStream = {
615+
write() {},
616+
flush(cb) { if (cb) cb(); },
617+
flushSync() {},
618+
end() { ended = true; },
619+
};
620+
const consumer = new JSONConsumer({ stream: mockStream, level: 'info' });
621+
consumer.end();
622+
assert.strictEqual(ended, true);
623+
});
624+
});

0 commit comments

Comments
 (0)