Skip to content

Commit f2aa86d

Browse files
committed
lib: add logger module with diagnostics_channel
1 parent 7f45ee4 commit f2aa86d

File tree

2 files changed

+88
-13
lines changed

2 files changed

+88
-13
lines changed

lib/logger.js

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
const {
44
DateNow,
5-
ErrorIsError,
5+
Error,
66
JSONStringify,
77
ObjectAssign,
88
ObjectKeys,
9+
ObjectPrototypeToString,
910
SafeSet,
1011
} = primordials;
1112

@@ -129,21 +130,32 @@ class JSONConsumer extends LogConsumer {
129130
}
130131

131132
_createStream(stream) {
133+
// Fast path: already a Utf8Stream
132134
if (stream instanceof Utf8Stream) {
133135
return stream;
134136
}
135137

138+
// Number: file descriptor
136139
if (typeof stream === 'number') {
137140
return new Utf8Stream({ fd: stream });
138141
}
139142

143+
// String: file path
140144
if (typeof stream === 'string') {
141145
return new Utf8Stream({ dest: stream });
142146
}
143147

144-
throw new ERR_INVALID_ARG_TYPE('options.stream',
145-
['number', 'string', 'Utf8Stream'],
146-
stream);
148+
// Object: custom stream with write method
149+
if (typeof stream === 'object' && stream !== null &&
150+
typeof stream.write === 'function') {
151+
return stream;
152+
}
153+
154+
throw new ERR_INVALID_ARG_TYPE(
155+
'options.stream',
156+
['number', 'string', 'Utf8Stream', 'object with write method'],
157+
stream,
158+
);
147159
}
148160

149161
handle(record) {
@@ -189,15 +201,15 @@ class JSONConsumer extends LogConsumer {
189201
class Logger {
190202
constructor(options = kEmptyObject) {
191203
validateObject(options, 'options');
192-
const {
193-
level = 'info',
194-
bindings = kEmptyObject,
195-
} = options;
204+
const { level = 'info', bindings = kEmptyObject } = options;
196205

197206
validateString(level, 'options.level');
198207
if (!LEVELS[level]) {
199-
throw new ERR_INVALID_ARG_VALUE('options.level', level,
200-
`must be one of: ${LEVEL_NAMES.join(', ')}`);
208+
throw new ERR_INVALID_ARG_VALUE(
209+
'options.level',
210+
level,
211+
`must be one of: ${LEVEL_NAMES.join(', ')}`,
212+
);
201213
}
202214

203215
validateObject(bindings, 'options.bindings');
@@ -206,7 +218,6 @@ class Logger {
206218
this._levelValue = LEVELS[level];
207219
this._bindings = bindings;
208220

209-
// Optimize: replace disabled log methods with noop
210221
this._setLogMethods();
211222
}
212223

@@ -263,7 +274,13 @@ class Logger {
263274
* @private
264275
*/
265276
_isError(value) {
266-
return ErrorIsError(value);
277+
// TODO(@mertcanaltin): Use ErrorIsError from primordials when available
278+
// For now, use manual check until ErrorIsError is added to primordials
279+
280+
return value !== null &&
281+
typeof value === 'object' &&
282+
(ObjectPrototypeToString(value) === '[object Error]' ||
283+
value instanceof Error);
267284
}
268285

269286
/**
@@ -303,7 +320,7 @@ class Logger {
303320
};
304321
} else if (typeof msgOrObj === 'string') {
305322
msg = msgOrObj;
306-
logFields = fields || kEmptyObject;
323+
logFields = fields ?? kEmptyObject;
307324
} else {
308325
const { msg: extractedMsg, ...restFields } = msgOrObj;
309326
msg = extractedMsg;
@@ -312,6 +329,10 @@ class Logger {
312329
if (logFields.err && this._isError(logFields.err)) {
313330
logFields.err = this._serializeError(logFields.err);
314331
}
332+
333+
if (logFields.error && this._isError(logFields.error)) {
334+
logFields.error = this._serializeError(logFields.error);
335+
}
315336
}
316337

317338
const record = {

test/parallel/test-log-basic.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,57 @@ const path = require('path');
444444
assert.strictEqual(typeof channels.error, 'object');
445445
assert.strictEqual(typeof channels.fatal, 'object');
446446
}
447+
448+
// Test: Support both 'err' and 'error' fields
449+
{
450+
const logs = [];
451+
const consumer = new JSONConsumer({
452+
stream: {
453+
write(data) { logs.push(JSON.parse(data)); },
454+
flush() {},
455+
flushSync() {},
456+
end() {},
457+
},
458+
level: 'info',
459+
});
460+
consumer.attach();
461+
462+
const logger = new Logger({ level: 'info' });
463+
464+
const err = new Error('Error 1');
465+
const error = new Error('Error 2');
466+
467+
logger.error({ msg: 'Multiple errors', err, error });
468+
469+
assert.strictEqual(logs.length, 1);
470+
assert.strictEqual(logs[0].err.message, 'Error 1');
471+
assert.strictEqual(logs[0].error.message, 'Error 2');
472+
assert.ok(logs[0].err.stack);
473+
assert.ok(logs[0].error.stack);
474+
}
475+
476+
// Test: 'error' field serialization
477+
{
478+
const logs = [];
479+
const consumer = new JSONConsumer({
480+
stream: {
481+
write(data) { logs.push(JSON.parse(data)); },
482+
flush() {},
483+
flushSync() {},
484+
end() {},
485+
},
486+
level: 'info',
487+
});
488+
consumer.attach();
489+
490+
const logger = new Logger({ level: 'info' });
491+
const error = new Error('Test error');
492+
error.code = 'TEST_CODE';
493+
494+
logger.error({ msg: 'Operation failed', error });
495+
496+
assert.strictEqual(logs.length, 1);
497+
assert.strictEqual(logs[0].error.message, 'Test error');
498+
assert.strictEqual(logs[0].error.code, 'TEST_CODE');
499+
assert.ok(logs[0].error.stack);
500+
}

0 commit comments

Comments
 (0)