Skip to content

Commit 2e64414

Browse files
committed
use single diagnostics channel with consumer-side filtering
Signed-off-by: Mert Can Altin <mertgold60@gmail.com>
1 parent d4f75da commit 2e64414

2 files changed

Lines changed: 73 additions & 56 deletions

File tree

lib/logger.js

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
ObjectAssign,
66
ObjectDefineProperty,
77
ObjectKeys,
8+
SafeWeakMap,
89
SymbolFor,
910
} = primordials;
1011

@@ -38,17 +39,6 @@ const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
3839
emitExperimentalWarning('Logger');
3940
const stdSerializers = require('internal/logger/serializers');
4041

41-
// Create channels for each log level
42-
const channels = {
43-
__proto__: null,
44-
trace: diagnosticsChannel.channel('log:trace'),
45-
debug: diagnosticsChannel.channel('log:debug'),
46-
info: diagnosticsChannel.channel('log:info'),
47-
warn: diagnosticsChannel.channel('log:warn'),
48-
error: diagnosticsChannel.channel('log:error'),
49-
fatal: diagnosticsChannel.channel('log:fatal'),
50-
};
51-
5242
/*
5343
* RFC5424 numerical ordering + log4j interface
5444
* @link https://www.rfc-editor.org/rfc/rfc5424.html
@@ -63,6 +53,8 @@ const LEVELS = {
6353
fatal: 60,
6454
};
6555

56+
const channel = diagnosticsChannel.channel('log');
57+
6658
const LEVEL_NAMES = ObjectKeys(LEVELS);
6759

6860
function isReservedKey(key) {
@@ -79,15 +71,15 @@ function noop() {}
7971
class LogConsumer {
8072
/** @type {number} */
8173
#levelValue;
82-
#handlers;
74+
#handler;
8375
#attached;
8476

8577
constructor(options = kEmptyObject) {
8678
validateObject(options, 'options');
8779
const { level = 'info' } = options;
8880
validateOneOf(level, 'options.level', LEVEL_NAMES);
8981
this.#levelValue = LEVELS[level];
90-
this.#handlers = { __proto__: null };
82+
this.#handler = undefined;
9183
this.#attached = false;
9284

9385
// Setup level-specific enabled properties for typo safety
@@ -100,20 +92,20 @@ class LogConsumer {
10092
}
10193

10294
/**
103-
* Attach this consumer to log channels
95+
* Attach this consumer to log channel
10496
*/
10597
attach() {
10698
if (this.#attached) {
10799
return;
108100
}
109101
this.#attached = true;
110-
for (const level of LEVEL_NAMES) {
111-
if (this[level].enabled) {
112-
const handler = this.handle.bind(this);
113-
this.#handlers[level] = handler;
114-
channels[level].subscribe(handler);
102+
this.#handler = (record) => {
103+
if (record.levelValue < this.#levelValue) {
104+
return;
115105
}
116-
}
106+
this.handle(record);
107+
};
108+
channel.subscribe(this.#handler);
117109
}
118110

119111
/**
@@ -124,12 +116,8 @@ class LogConsumer {
124116
return;
125117
}
126118
this.#attached = false;
127-
for (const level of LEVEL_NAMES) {
128-
if (this.#handlers[level]) {
129-
channels[level].unsubscribe(this.#handlers[level]);
130-
this.#handlers[level] = undefined;
131-
}
132-
}
119+
channel.unsubscribe(this.#handler);
120+
this.#handler = undefined;
133121
}
134122

135123
/**
@@ -147,6 +135,7 @@ class LogConsumer {
147135
class JSONConsumer extends LogConsumer {
148136
#stream;
149137
#fields;
138+
#bindingsCache;
150139

151140
constructor(options = kEmptyObject) {
152141
super(options);
@@ -160,6 +149,7 @@ class JSONConsumer extends LogConsumer {
160149
this.#stream = stream ? this.#createStream(stream) :
161150
new Utf8Stream({ fd: 1 });
162151
this.#fields = fields;
152+
this.#bindingsCache = new SafeWeakMap();
163153
}
164154

165155
#createStream(stream) {
@@ -199,6 +189,10 @@ class JSONConsumer extends LogConsumer {
199189
// record.level is trusted internal value, record.msg is user input and must be escaped
200190
let json = `{"level":"${record.level}","time":${record.time},"msg":${JSONStringify(record.msg)}`;
201191

192+
if (record.name !== undefined) {
193+
json += `,${JSONStringify('name')}:${JSONStringify(record.name)}`;
194+
}
195+
202196
const consumerFields = this.#fields;
203197
const consumerKeys = ObjectKeys(consumerFields);
204198
for (let i = 0; i < consumerKeys.length; i++) {
@@ -209,8 +203,8 @@ class JSONConsumer extends LogConsumer {
209203
json += `,${JSONStringify(key)}:${JSONStringify(value)}`;
210204
}
211205

212-
// Add pre-serialized bindings
213-
json += record.bindingsStr;
206+
// Add bindings, cached per bindings object to avoid rebuilding on every log
207+
json += this.#serializeBindings(record.bindings);
214208

215209
// Add log fields
216210
const fields = record.fields;
@@ -227,6 +221,30 @@ class JSONConsumer extends LogConsumer {
227221
this.#stream.write(json);
228222
}
229223

224+
#serializeBindings(bindings) {
225+
if (!bindings || typeof bindings !== 'object') {
226+
return '';
227+
}
228+
229+
const cached = this.#bindingsCache.get(bindings);
230+
if (cached !== undefined) {
231+
return cached;
232+
}
233+
234+
let result = '';
235+
const keys = ObjectKeys(bindings);
236+
for (let i = 0; i < keys.length; i++) {
237+
const key = keys[i];
238+
if (isReservedKey(key)) continue;
239+
const value = bindings[key];
240+
if (value === undefined) continue;
241+
result += `,${JSONStringify(key)}:${JSONStringify(value)}`;
242+
}
243+
244+
this.#bindingsCache.set(bindings, result);
245+
return result;
246+
}
247+
230248
/**
231249
* Flush pending writes
232250
* @param {Function} callback
@@ -259,26 +277,32 @@ class Logger {
259277
#level;
260278
#levelValue;
261279
#bindings;
262-
#bindingsStr; // Pre-serialized bindings JSON string
280+
#name;
263281
#serializers;
264282

265283
/**
266284
* Create a new Logger instance
267285
* @param {object} [options]
268286
* @param {string} [options.level] - Minimum log level (default: 'info')
287+
* @param {string} [options.name] - Optional logger name/namespace
269288
* @param {object} [options.bindings] - Context fields (default: {})
270289
* @param {object} [options.serializers] - Custom serializers (default: {})
271290
*/
272291
constructor(options = kEmptyObject) {
273292
validateObject(options, 'options');
274293
const {
275294
level = 'info',
295+
name,
276296
bindings = kEmptyObject,
277297
serializers = kEmptyObject,
278298
} = options;
279299

280300
validateOneOf(level, 'options.level', LEVEL_NAMES);
281301

302+
if (name !== undefined) {
303+
validateString(name, 'options.name');
304+
}
305+
282306
validateObject(bindings, 'options.bindings');
283307
validateObject(serializers, 'options.serializers');
284308

@@ -290,6 +314,7 @@ class Logger {
290314

291315
this.#level = level;
292316
this.#levelValue = LEVELS[level];
317+
this.#name = name;
293318
this.#bindings = bindings;
294319

295320
// Create serializers object with default err serializer
@@ -302,35 +327,10 @@ class Logger {
302327
this.#serializers[key] = serializers[key];
303328
}
304329

305-
// Pre-serialize bindings
306-
this.#bindingsStr = this.#serializeBindings(bindings);
307330

308331
this.#setLogMethods();
309332
}
310333

311-
/**
312-
* Pre-serialize bindings
313-
* @param {object} bindings - Context fields to serialize
314-
* @returns {string} Pre-serialized bindings as JSON string
315-
* @private
316-
*/
317-
#serializeBindings(bindings) {
318-
if (!bindings || typeof bindings !== 'object') {
319-
return '';
320-
}
321-
322-
let result = '';
323-
const keys = ObjectKeys(bindings);
324-
for (let i = 0; i < keys.length; i++) {
325-
const key = keys[i];
326-
if (isReservedKey(key)) continue;
327-
const serialized = this.#serializeValue(bindings[key], key);
328-
if (serialized === undefined) continue;
329-
result += `,${JSONStringify(key)}:${JSONStringify(serialized)}`;
330-
}
331-
return result;
332-
}
333-
334334
/**
335335
* Setup log methods with enabled getters.
336336
*
@@ -502,7 +502,6 @@ class Logger {
502502
return;
503503
}
504504

505-
const channel = channels[level];
506505
if (!channel.hasSubscribers) {
507506
return;
508507
}
@@ -557,9 +556,11 @@ class Logger {
557556
const record = {
558557
__proto__: null,
559558
level,
559+
levelValue,
560+
name: this.#name,
560561
msg,
561562
time: DateNow(),
562-
bindingsStr: this.#bindingsStr, // Pre-serialized bindings string
563+
bindings: this.#bindings,
563564
fields: logFields,
564565
};
565566

test/parallel/test-log-basic.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,22 @@ describe('JSONConsumer', () => {
291291
assert.strictEqual(log.userId, 123);
292292
assert.strictEqual(typeof log.time, 'number');
293293
});
294+
295+
it('should include logger name when provided', () => {
296+
const stream = new TestStream();
297+
const consumer = new JSONConsumer({ stream, level: 'info' });
298+
consumer.attach();
299+
const logger = new Logger({ level: 'info', name: 'api' });
300+
301+
logger.info({ msg: 'named logger', userId: 123 });
302+
consumer.flushSync();
303+
304+
assert.strictEqual(stream.logs.length, 1);
305+
const log = stream.logs[0];
306+
assert.strictEqual(log.name, 'api');
307+
assert.strictEqual(log.msg, 'named logger');
308+
assert.strictEqual(log.userId, 123);
309+
});
294310
});
295311

296312
describe('additional fields', () => {

0 commit comments

Comments
 (0)