From 7f5db559d1dd9462c33cca7d9cdef993fe6df6bf Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Tue, 28 Oct 2025 21:59:20 +0300 Subject: [PATCH 01/28] lib: added logger package in node core --- benchmark/log/basic-json.js | 53 +++++ benchmark/log/vs-pino.js | 115 +++++++++++ lib/logger.js | 349 ++++++++++++++++++++++++++++++++ test/parallel/test-log-basic.js | 210 +++++++++++++++++++ 4 files changed, 727 insertions(+) create mode 100644 benchmark/log/basic-json.js create mode 100644 benchmark/log/vs-pino.js create mode 100644 lib/logger.js create mode 100644 test/parallel/test-log-basic.js diff --git a/benchmark/log/basic-json.js b/benchmark/log/basic-json.js new file mode 100644 index 00000000000000..b3c3f4bc50719a --- /dev/null +++ b/benchmark/log/basic-json.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +const { createLogger, JSONHandler } = require('node:logger'); +const fs = require('fs'); + +const bench = common.createBenchmark(main, { + n: [1e5], + level: ['info', 'debug'], + fields: [0, 5], + type: ['simple', 'child', 'disabled'], +}); + +function main({ n, level, fields, type }) { + // Use /dev/null to avoid I/O overhead in benchmarks + const nullFd = fs.openSync('/dev/null', 'w'); + const handler = new JSONHandler({ stream: nullFd, level: 'info' }); + const logger = createLogger({ handler, level }); + + // Create test data based on fields count + const logData = { msg: 'benchmark test message' }; + for (let i = 0; i < fields; i++) { + logData[`field${i}`] = `value${i}`; + } + + let testLogger; + switch (type) { + case 'simple': + testLogger = logger; + break; + case 'child': + testLogger = logger.child({ requestId: 'bench-123', userId: 456 }); + break; + case 'disabled': { + // When level is debug and handler is info, logs will be disabled + const nullFd2 = fs.openSync('/dev/null', 'w'); + + testLogger = createLogger({ + handler: new JSONHandler({ stream: nullFd2, level: 'warn' }), + level: 'debug', + }); + break; + } + } + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info(logData); + } + bench.end(n); + + handler.end(); +} diff --git a/benchmark/log/vs-pino.js b/benchmark/log/vs-pino.js new file mode 100644 index 00000000000000..4ef967efa9ada4 --- /dev/null +++ b/benchmark/log/vs-pino.js @@ -0,0 +1,115 @@ +'use strict'; + +// Benchmark: Compare node:log vs Pino +// This benchmark compares the performance of the new node:log module +// against Pino, which is the performance target + +const common = require('../common'); +const fs = require('fs'); + +const bench = common.createBenchmark(main, { + n: [1e5], + logger: ['node-log', 'pino'], + scenario: ['simple', 'child', 'disabled', 'fields'], +}); + +function main({ n, logger, scenario }) { + const nullFd = fs.openSync('/dev/null', 'w'); + let testLogger; + let logData; + + if (logger === 'node-log') { + const { createLogger, JSONHandler } = require('node:logger'); + const handler = new JSONHandler({ stream: nullFd, level: 'info' }); + const baseLogger = createLogger({ handler }); + + switch (scenario) { + case 'simple': + testLogger = baseLogger; + logData = { msg: 'benchmark test message' }; + break; + + case 'child': + testLogger = baseLogger.child({ requestId: 'req-123', userId: 456 }); + logData = { msg: 'benchmark test message' }; + break; + + case 'disabled': { + // Debug logs when handler level is 'warn' (disabled) + const warnHandler = new JSONHandler({ stream: nullFd, level: 'warn' }); + testLogger = createLogger({ handler: warnHandler, level: 'debug' }); + logData = { msg: 'benchmark test message' }; + break; + } + + case 'fields': + testLogger = baseLogger; + logData = { + msg: 'benchmark test message', + field1: 'value1', + field2: 'value2', + field3: 'value3', + field4: 'value4', + field5: 'value5', + }; + break; + } + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info(logData); + } + bench.end(n); + + // Don't close FD immediately, let async writes complete + + } else if (logger === 'pino') { + const pino = require('pino'); + const destination = pino.destination({ dest: nullFd, sync: false }); + + switch (scenario) { + case 'simple': + testLogger = pino({ level: 'info' }, destination); + logData = { msg: 'benchmark test message' }; + break; + + case 'child': { + const baseLogger = pino({ level: 'info' }, destination); + testLogger = baseLogger.child({ requestId: 'req-123', userId: 456 }); + logData = { msg: 'benchmark test message' }; + break; + } + + case 'disabled': + testLogger = pino({ level: 'warn' }, destination); + logData = { msg: 'benchmark test message' }; + break; + + case 'fields': + testLogger = pino({ level: 'info' }, destination); + logData = { + msg: 'benchmark test message', + field1: 'value1', + field2: 'value2', + field3: 'value3', + field4: 'value4', + field5: 'value5', + }; + break; + } + + bench.start(); + for (let i = 0; i < n; i++) { + if (scenario === 'disabled') { + // Use debug level to test disabled logging + testLogger.debug(logData); + } else { + testLogger.info(logData); + } + } + bench.end(n); + + // Don't close FD immediately for Pino either + + } +} diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 00000000000000..fc8a2d0ec07b38 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,349 @@ +'use strict'; + +const { + DateNow, + JSONStringify, + ObjectAssign, + ObjectKeys, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_METHOD_NOT_IMPLEMENTED, + }, +} = require('internal/errors'); + +const { + validateObject, + validateString, +} = require('internal/validators'); + +const Utf8Stream = require('internal/streams/fast-utf8-stream'); + +// RFC5424 numerical ordering + log4j interface +const LEVELS = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, +}; + +const LEVEL_NAMES = ObjectKeys(LEVELS); + +// Noop function for disabled log levels +function noop() {} + +class Handler { + constructor(options = {}) { + validateObject(options, 'options'); + const { level = 'info' } = options; + validateString(level, 'options.level'); + + if (!LEVELS[level]) { + throw new ERR_INVALID_ARG_VALUE('options.level', level, + `must be one of: ${LEVEL_NAMES.join(', ')}`); + } + + this._level = level; + this._levelValue = LEVELS[level]; // Cache numeric value + } + + /** + * Check if a level would be logged + * @param {string} level + * @returns {boolean} + */ + enabled(level) { + return LEVELS[level] >= this._levelValue; + } + + /** + * Handle a log record + * @param {object} record + */ + handle(record) { + throw new ERR_METHOD_NOT_IMPLEMENTED('Handler.handle()'); + } +} + +/** + * JSON handler - outputs structured JSON logs + */ +class JSONHandler extends Handler { + constructor(options = {}) { + super(options); + const { + stream, + fields = {}, + } = options; + + validateObject(fields, 'options.fields'); + + // Default to stdout + this._stream = stream ? this._createStream(stream) : + new Utf8Stream({ fd: 1 }); + this._fields = fields; + } + + _createStream(stream) { + // If it's already a Utf8Stream, use it directly + if (stream instanceof Utf8Stream) { + return stream; + } + + // If it's a file descriptor number + if (typeof stream === 'number') { + return new Utf8Stream({ fd: stream }); + } + + // If it's a path string + if (typeof stream === 'string') { + return new Utf8Stream({ dest: stream }); + } + + throw new ERR_INVALID_ARG_TYPE('options.stream', + ['number', 'string', 'Utf8Stream'], + stream); + } + + handle(record) { + // Note: Level check already done in Logger._log() + // No need to check again here + + // Build the JSON log record + const logObj = { + level: record.level, + time: record.time, + msg: record.msg, + ...this._fields, // Additional fields (hostname, pid, etc) + ...record.bindings, // Parent context + ...record.fields, // Log-specific fields + }; + + const json = JSONStringify(logObj) + '\n'; + this._stream.write(json); + } + + /** + * Flush pending writes + * @param {Function} callback + */ + flush(callback) { + this._stream.flush(callback); + } + + /** + * Flush pending writes synchronously + */ + flushSync() { + this._stream.flushSync(); + } + + /** + * Close the handler + */ + end() { + this._stream.end(); + } +} + +/** + * Logger class + */ +class Logger { + constructor(options = {}) { + validateObject(options, 'options'); + const { + handler = new JSONHandler(), + level, + bindings = {}, + } = options; + + if (!(handler instanceof Handler)) { + throw new ERR_INVALID_ARG_TYPE('options.handler', 'Handler', handler); + } + + validateObject(bindings, 'options.bindings'); + + this._handler = handler; + this._bindings = bindings; + + // If level is specified, it overrides handler's level + if (level !== undefined) { + validateString(level, 'options.level'); + if (!LEVELS[level]) { + throw new ERR_INVALID_ARG_VALUE('options.level', level, + `must be one of: ${LEVEL_NAMES.join(', ')}`); + } + this._level = level; + this._levelValue = LEVELS[level]; // Cache numeric value + } else { + this._level = handler._level; + this._levelValue = handler._levelValue; // Use handler's cached value + } + + // Optimize: replace disabled log methods with noop + this._setLogMethods(); + } + + /** + * Replace disabled log methods with noop for performance + * @private + */ + _setLogMethods() { + const levelValue = this._levelValue; + + // Override instance methods for disabled levels + // This avoids the level check on every call + if (levelValue > 10) this.trace = noop; + if (levelValue > 20) this.debug = noop; + if (levelValue > 30) this.info = noop; + if (levelValue > 40) this.warn = noop; + if (levelValue > 50) this.error = noop; + if (levelValue > 60) this.fatal = noop; + } + + /** + * Check if a level would be logged + * @param {string} level + * @returns {boolean} + */ + enabled(level) { + return LEVELS[level] >= this._levelValue; + } + + /** + * Create a child logger with additional context + * @param {object} bindings - Context to add to all child logs + * @param {object} [options] - Optional overrides + * @returns {Logger} + */ + child(bindings, options = {}) { + validateObject(bindings, 'bindings'); + validateObject(options, 'options'); + + // Shallow merge parent and child bindings + const mergedBindings = ObjectAssign( + { __proto__: null }, + this._bindings, + bindings, + ); + + // Create new logger inheriting handler + return new Logger({ + handler: this._handler, + level: options.level || this._level, + bindings: mergedBindings, + }); + } + + /** + * Internal log method + * @param {string} level + * @param {number} levelValue + * @param {object} obj + */ + _log(level, levelValue, obj) { + // Note: Level check is now done at method assignment time (noop) + // So this function is only called for enabled levels + + // Validate required msg field + validateObject(obj, 'obj'); + if (typeof obj.msg !== 'string') { + throw new ERR_INVALID_ARG_TYPE('obj.msg', 'string', obj.msg); + } + + // Extract msg and remaining fields + const { msg, ...fields } = obj; + + // Build log record + const record = { + level, + msg, + time: DateNow(), + bindings: this._bindings, + fields, + }; + + this._handler.handle(record); + } + + /** + * Log at trace level + * @param {object} obj - Object with required msg property + */ + trace(obj) { + this._log('trace', 10, obj); + } + + /** + * Log at debug level + * @param {object} obj - Object with required msg property + */ + debug(obj) { + this._log('debug', 20, obj); + } + + /** + * Log at info level + * @param {object} obj - Object with required msg property + */ + info(obj) { + this._log('info', 30, obj); + } + + /** + * Log at warn level + * @param {object} obj - Object with required msg property + */ + warn(obj) { + this._log('warn', 40, obj); + } + + /** + * Log at error level + * @param {object} obj - Object with required msg property + */ + error(obj) { + this._log('error', 50, obj); + } + + /** + * Log at fatal level (does NOT exit the process) + * @param {object} obj - Object with required msg property + */ + fatal(obj) { + this._log('fatal', 60, obj); + } + + /** + * Flush pending writes + * @param {Function} callback + */ + flush(callback) { + this._handler.flush(callback); + } +} + +/** + * Create a new logger instance + * @param {object} [options] + * @param {Handler} [options.handler] - Output handler (default: JSONHandler) + * @param {string} [options.level] - Minimum log level (default: 'info') + * @returns {Logger} + */ +function createLogger(options) { + return new Logger(options); +} + +module.exports = { + createLogger, + Logger, + Handler, + JSONHandler, + LEVELS, +}; diff --git a/test/parallel/test-log-basic.js b/test/parallel/test-log-basic.js new file mode 100644 index 00000000000000..c4aa123ee58662 --- /dev/null +++ b/test/parallel/test-log-basic.js @@ -0,0 +1,210 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const { createLogger, Logger, Handler, JSONHandler, LEVELS } = require('logger'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +// Test LEVELS constant +{ + assert.strictEqual(typeof LEVELS, 'object'); + assert.strictEqual(LEVELS.trace, 10); + assert.strictEqual(LEVELS.debug, 20); + assert.strictEqual(LEVELS.info, 30); + assert.strictEqual(LEVELS.warn, 40); + assert.strictEqual(LEVELS.error, 50); + assert.strictEqual(LEVELS.fatal, 60); +} + +// Test createLogger returns a Logger instance +{ + const logger = createLogger(); + assert(logger instanceof Logger); +} + +// Test Logger has all log methods +{ + const logger = createLogger(); + assert.strictEqual(typeof logger.trace, 'function'); + assert.strictEqual(typeof logger.debug, 'function'); + assert.strictEqual(typeof logger.info, 'function'); + assert.strictEqual(typeof logger.warn, 'function'); + assert.strictEqual(typeof logger.error, 'function'); + assert.strictEqual(typeof logger.fatal, 'function'); + assert.strictEqual(typeof logger.enabled, 'function'); + assert.strictEqual(typeof logger.child, 'function'); +} + +// Test level filtering +{ + const logger = createLogger({ level: 'warn' }); + assert.strictEqual(logger.enabled('trace'), false); + assert.strictEqual(logger.enabled('debug'), false); + assert.strictEqual(logger.enabled('info'), false); + assert.strictEqual(logger.enabled('warn'), true); + assert.strictEqual(logger.enabled('error'), true); + assert.strictEqual(logger.enabled('fatal'), true); +} + +// Test msg field is required +{ + const logger = createLogger(); + assert.throws(() => { + logger.info({ userId: 123 }); // Missing msg + }, { + code: 'ERR_INVALID_ARG_TYPE', + }); + + assert.throws(() => { + logger.info({ msg: 123 }); // msg is not a string + }, { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +// Test child logger context inheritance +{ + const logger = createLogger({ level: 'info' }); + const childLogger = logger.child({ requestId: 'abc-123' }); + + assert(childLogger instanceof Logger); + assert.notStrictEqual(childLogger, logger); + + // Child should have same level as parent + assert.strictEqual(childLogger.enabled('info'), true); + assert.strictEqual(childLogger.enabled('debug'), false); + + // Test nested child + const grandchildLogger = childLogger.child({ operation: 'query' }); + assert(grandchildLogger instanceof Logger); + assert.notStrictEqual(grandchildLogger, childLogger); +} + +// Test child logger level override +{ + const logger = createLogger({ level: 'info' }); + const childLogger = logger.child({ requestId: 'abc' }, { level: 'debug' }); + + assert.strictEqual(logger.enabled('debug'), false); + assert.strictEqual(childLogger.enabled('debug'), true); +} + +// Test JSONHandler output format +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-1.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + }); + const logger = new Logger({ handler }); + + logger.info({ msg: 'test message', userId: 123 }); + + // Flush synchronously and read the output + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + + // Parse the JSON output + const parsed = JSON.parse(output.trim()); + assert.strictEqual(parsed.level, 'info'); + assert.strictEqual(parsed.msg, 'test message'); + assert.strictEqual(parsed.userId, 123); + assert.strictEqual(typeof parsed.time, 'number'); + + // Cleanup + fs.unlinkSync(tmpfile); +} + +// Test JSONHandler with additional fields +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-2.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + fields: { hostname: 'test-host', pid: 12345 }, + }); + const logger = new Logger({ handler }); + + logger.info({ msg: 'with fields' }); + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + assert.strictEqual(parsed.hostname, 'test-host'); + assert.strictEqual(parsed.pid, 12345); + assert.strictEqual(parsed.msg, 'with fields'); + + // Cleanup + fs.unlinkSync(tmpfile); +} + +// Test child logger bindings in output +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-3.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + }); + const logger = new Logger({ handler }); + const childLogger = logger.child({ requestId: 'xyz-789' }); + + childLogger.info({ msg: 'child log', action: 'create' }); + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + assert.strictEqual(parsed.requestId, 'xyz-789'); + assert.strictEqual(parsed.action, 'create'); + assert.strictEqual(parsed.msg, 'child log'); + + // Cleanup + fs.unlinkSync(tmpfile); +} + +// Test invalid log level +{ + assert.throws(() => { + createLogger({ level: 'invalid' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + }); +} + +// Test Handler is abstract +{ + const handler = new Handler({ level: 'info' }); + assert.throws(() => { + handler.handle({}); + }, { + message: /must be implemented/, + }); +} + +// Test disabled level skips processing +{ + let handleCalled = false; + + class TestHandler extends Handler { + handle() { + handleCalled = true; + } + } + + const logger = new Logger({ + handler: new TestHandler({ level: 'warn' }), + }); + + // This should be skipped (info < warn) + logger.info({ msg: 'skipped' }); + assert.strictEqual(handleCalled, false); + + // This should be processed (error > warn) + logger.error({ msg: 'processed' }); + assert.strictEqual(handleCalled, true); +} From e149af27420cfc65c05253d3fc57d6635f498edf Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 29 Oct 2025 17:05:00 +0300 Subject: [PATCH 02/28] lib: implement flexible API and error serialization for logger --- lib/logger.js | 132 +++++++++++++++++++------ test/parallel/test-log-basic.js | 169 +++++++++++++++++++++++++++++++- 2 files changed, 267 insertions(+), 34 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index fc8a2d0ec07b38..8e7465860b3bc8 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -2,9 +2,11 @@ const { DateNow, + Error, JSONStringify, ObjectAssign, ObjectKeys, + ObjectPrototypeToString, } = primordials; const { @@ -241,24 +243,62 @@ class Logger { }); } + /** + * Check if value is an Error object + * @param {*} value + * @returns {boolean} + * @private + */ + _isError(value) { + return value !== null && + typeof value === 'object' && + (ObjectPrototypeToString(value) === '[object Error]' || + value instanceof Error); + } + /** * Internal log method * @param {string} level * @param {number} levelValue - * @param {object} obj + * @param {string|object} msgOrObj - Message string or object with msg property + * @param {object} [fields] - Optional fields to merge */ - _log(level, levelValue, obj) { - // Note: Level check is now done at method assignment time (noop) - // So this function is only called for enabled levels - - // Validate required msg field - validateObject(obj, 'obj'); - if (typeof obj.msg !== 'string') { - throw new ERR_INVALID_ARG_TYPE('obj.msg', 'string', obj.msg); - } + _log(level, levelValue, msgOrObj, fields) { + let msg; + let logFields; + + // Support two signatures: + // 1. logger.info('message', { fields }) + // 2. logger.info({ msg: 'message', other: 'fields' }) + // 3. logger.info(new Error('boom')) + + if (this._isError(msgOrObj)) { + // Handle Error as first argument + msg = msgOrObj.message; + logFields = { + err: this._serializeError(msgOrObj), + ...fields, + }; + } else if (typeof msgOrObj === 'string') { + // Support String message + msg = msgOrObj; + logFields = fields || {}; + validateObject(logFields, 'fields'); + } else { + // Support object with msg property + validateObject(msgOrObj, 'obj'); + if (typeof msgOrObj.msg !== 'string') { + throw new ERR_INVALID_ARG_TYPE('obj.msg', 'string', msgOrObj.msg); + } + const { msg: extractedMsg, ...restFields } = msgOrObj; + msg = extractedMsg; + logFields = restFields; - // Extract msg and remaining fields - const { msg, ...fields } = obj; + // Serialize Error objects in fields + if (logFields.err && this._isError(logFields.err)) { + logFields.err = this._serializeError(logFields.err); + } + } // Build log record const record = { @@ -266,58 +306,88 @@ class Logger { msg, time: DateNow(), bindings: this._bindings, - fields, + fields: logFields, }; this._handler.handle(record); } + /** + * Serialize Error object for logging + * @param {object} err - Error object to serialize + * @returns {object} Serialized error object + * @private + */ + _serializeError(err) { + const serialized = { + message: err.message, + name: err.name, + stack: err.stack, + }; + + // Add code if it exists + serialized.code ||= err.code; + + // Add any enumerable custom properties + for (const key in err) { + serialized[key] ||= err[key]; + } + + return serialized; + } + /** * Log at trace level - * @param {object} obj - Object with required msg property + * @param {string|object} msgOrObj - Message string or object with msg property + * @param {object} [fields] - Optional fields to merge */ - trace(obj) { - this._log('trace', 10, obj); + trace(msgOrObj, fields) { + this._log('trace', 10, msgOrObj, fields); } /** * Log at debug level - * @param {object} obj - Object with required msg property + * @param {string|object} msgOrObj - Message string or object with msg property + * @param {object} [fields] - Optional fields to merge */ - debug(obj) { - this._log('debug', 20, obj); + debug(msgOrObj, fields) { + this._log('debug', 20, msgOrObj, fields); } /** * Log at info level - * @param {object} obj - Object with required msg property + * @param {string|object} msgOrObj - Message string or object with msg property + * @param {object} [fields] - Optional fields to merge */ - info(obj) { - this._log('info', 30, obj); + info(msgOrObj, fields) { + this._log('info', 30, msgOrObj, fields); } /** * Log at warn level - * @param {object} obj - Object with required msg property + * @param {string|object} msgOrObj - Message string or object with msg property + * @param {object} [fields] - Optional fields to merge */ - warn(obj) { - this._log('warn', 40, obj); + warn(msgOrObj, fields) { + this._log('warn', 40, msgOrObj, fields); } /** * Log at error level - * @param {object} obj - Object with required msg property + * @param {string|object} msgOrObj - Message string or object with msg property + * @param {object} [fields] - Optional fields to merge */ - error(obj) { - this._log('error', 50, obj); + error(msgOrObj, fields) { + this._log('error', 50, msgOrObj, fields); } /** * Log at fatal level (does NOT exit the process) - * @param {object} obj - Object with required msg property + * @param {string|object} msgOrObj - Message string or object with msg property + * @param {object} [fields] - Optional fields to merge */ - fatal(obj) { - this._log('fatal', 60, obj); + fatal(msgOrObj, fields) { + this._log('fatal', 60, msgOrObj, fields); } /** diff --git a/test/parallel/test-log-basic.js b/test/parallel/test-log-basic.js index c4aa123ee58662..3ee3fb241969bf 100644 --- a/test/parallel/test-log-basic.js +++ b/test/parallel/test-log-basic.js @@ -52,6 +52,8 @@ const path = require('path'); // Test msg field is required { const logger = createLogger(); + + // Object without msg should throw assert.throws(() => { logger.info({ userId: 123 }); // Missing msg }, { @@ -63,9 +65,10 @@ const path = require('path'); }, { code: 'ERR_INVALID_ARG_TYPE', }); -} -// Test child logger context inheritance + // String without second arg should work (no assertion needed, just shouldn't throw) + logger.info('just a message'); +}// Test child logger context inheritance { const logger = createLogger({ level: 'info' }); const childLogger = logger.child({ requestId: 'abc-123' }); @@ -182,7 +185,8 @@ const path = require('path'); assert.throws(() => { handler.handle({}); }, { - message: /must be implemented/, + code: 'ERR_METHOD_NOT_IMPLEMENTED', + message: /Handler\.handle\(\) method is not implemented/, }); } @@ -208,3 +212,162 @@ const path = require('path'); logger.error({ msg: 'processed' }); assert.strictEqual(handleCalled, true); } + +// Test invalid fields argument +{ + const logger = createLogger(); + assert.throws(() => { + logger.info('message', 'not an object'); // Second arg must be object + }, { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +// Test string message signature +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-string.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + }); + const logger = new Logger({ handler }); + + logger.info('simple message'); + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + + assert.strictEqual(parsed.msg, 'simple message'); + assert.strictEqual(parsed.level, 'info'); + + fs.unlinkSync(tmpfile); +} + +// Test string message with fields +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-string-fields.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + }); + const logger = new Logger({ handler }); + + logger.info('user login', { userId: 123, ip: '127.0.0.1' }); + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + + assert.strictEqual(parsed.msg, 'user login'); + assert.strictEqual(parsed.userId, 123); + assert.strictEqual(parsed.ip, '127.0.0.1'); + + fs.unlinkSync(tmpfile); +} + +// Test Error object serialization +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-error.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + }); + const logger = new Logger({ handler }); + + const err = new Error('test error'); + err.code = 'TEST_ERROR'; + logger.error({ msg: 'operation failed', err }); + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + + // Error should be serialized with stack trace + assert.strictEqual(parsed.msg, 'operation failed'); + assert.strictEqual(typeof parsed.err, 'object'); + assert.strictEqual(parsed.err.message, 'test error'); + assert.strictEqual(parsed.err.code, 'TEST_ERROR'); + assert(parsed.err.stack); + + fs.unlinkSync(tmpfile); +} + +// Test Error as first argument +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-error-first.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + }); + const logger = new Logger({ handler }); + + const err = new Error('boom'); + logger.error(err); // Error as first arg + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + + assert.strictEqual(parsed.msg, 'boom'); // message from error + assert.strictEqual(typeof parsed.err, 'object'); + assert(parsed.err.stack); + + fs.unlinkSync(tmpfile); +} + +// Test child logger with parent fields merge +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-child-merge.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + fields: { service: 'api' }, // handler fields + }); + const logger = new Logger({ handler }); + const childLogger = logger.child({ requestId: '123' }); // child bindings + + childLogger.info('request processed', { duration: 150 }); // log fields + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + + // Merge order: handler fields < bindings < log fields + assert.strictEqual(parsed.service, 'api'); + assert.strictEqual(parsed.requestId, '123'); + assert.strictEqual(parsed.duration, 150); + assert.strictEqual(parsed.msg, 'request processed'); + + fs.unlinkSync(tmpfile); +} + +// Test field override priority +{ + const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-override.json`); + const handler = new JSONHandler({ + stream: fs.openSync(tmpfile, 'w'), + level: 'info', + fields: { env: 'dev', version: '1.0' }, + }); + const logger = new Logger({ handler }); + const childLogger = logger.child({ env: 'staging' }); + + childLogger.info('test', { env: 'production' }); + + handler.flushSync(); + handler.end(); + const output = fs.readFileSync(tmpfile, 'utf8'); + const parsed = JSON.parse(output.trim()); + + // Log fields should override everything + assert.strictEqual(parsed.env, 'production'); + assert.strictEqual(parsed.version, '1.0'); + + fs.unlinkSync(tmpfile); +} From 92f681bce0d3590458cebdf930d96591b640e5f9 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 29 Oct 2025 17:15:07 +0300 Subject: [PATCH 03/28] lib: refactor logger to use diagnostics_channel --- lib/logger.js | 170 +++++++++++++------------------ test/parallel/test-log-basic.js | 173 +++++++++++++++++++++++--------- 2 files changed, 190 insertions(+), 153 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 8e7465860b3bc8..6c6cb648e0003d 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -23,6 +23,17 @@ const { } = require('internal/validators'); const Utf8Stream = require('internal/streams/fast-utf8-stream'); +const diagnosticsChannel = require('diagnostics_channel'); + +// Create channels for each log level +const channels = { + trace: diagnosticsChannel.channel('log:trace'), + debug: diagnosticsChannel.channel('log:debug'), + info: diagnosticsChannel.channel('log:info'), + warn: diagnosticsChannel.channel('log:warn'), + error: diagnosticsChannel.channel('log:error'), + fatal: diagnosticsChannel.channel('log:fatal'), +}; // RFC5424 numerical ordering + log4j interface const LEVELS = { @@ -39,7 +50,11 @@ const LEVEL_NAMES = ObjectKeys(LEVELS); // Noop function for disabled log levels function noop() {} -class Handler { +/** + * Base consumer class for handling log records + * Consumers subscribe to diagnostics_channel events + */ +class LogConsumer { constructor(options = {}) { validateObject(options, 'options'); const { level = 'info' } = options; @@ -51,11 +66,11 @@ class Handler { } this._level = level; - this._levelValue = LEVELS[level]; // Cache numeric value + this._levelValue = LEVELS[level]; } /** - * Check if a level would be logged + * Check if a level would be consumed * @param {string} level * @returns {boolean} */ @@ -64,18 +79,39 @@ class Handler { } /** - * Handle a log record + * Attach this consumer to log channels + */ + attach() { + for (const level of LEVEL_NAMES) { + if (this.enabled(level)) { + channels[level].subscribe(this._handleLog.bind(this, level)); + } + } + } + + /** + * Internal handler called by diagnostics_channel + * @param {string} level + * @param {object} record + * @private + */ + _handleLog(level, record) { + this.handle(record); + } + + /** + * Handle a log record (must be implemented by subclass) * @param {object} record */ handle(record) { - throw new ERR_METHOD_NOT_IMPLEMENTED('Handler.handle()'); + throw new ERR_METHOD_NOT_IMPLEMENTED('LogConsumer.handle()'); } } /** - * JSON handler - outputs structured JSON logs + * JSON consumer - outputs structured JSON logs */ -class JSONHandler extends Handler { +class JSONConsumer extends LogConsumer { constructor(options = {}) { super(options); const { @@ -85,24 +121,20 @@ class JSONHandler extends Handler { validateObject(fields, 'options.fields'); - // Default to stdout this._stream = stream ? this._createStream(stream) : new Utf8Stream({ fd: 1 }); this._fields = fields; } _createStream(stream) { - // If it's already a Utf8Stream, use it directly if (stream instanceof Utf8Stream) { return stream; } - // If it's a file descriptor number if (typeof stream === 'number') { return new Utf8Stream({ fd: stream }); } - // If it's a path string if (typeof stream === 'string') { return new Utf8Stream({ dest: stream }); } @@ -113,17 +145,13 @@ class JSONHandler extends Handler { } handle(record) { - // Note: Level check already done in Logger._log() - // No need to check again here - - // Build the JSON log record const logObj = { level: record.level, time: record.time, msg: record.msg, - ...this._fields, // Additional fields (hostname, pid, etc) - ...record.bindings, // Parent context - ...record.fields, // Log-specific fields + ...this._fields, + ...record.bindings, + ...record.fields, }; const json = JSONStringify(logObj) + '\n'; @@ -146,7 +174,7 @@ class JSONHandler extends Handler { } /** - * Close the handler + * Close the consumer */ end() { this._stream.end(); @@ -160,34 +188,22 @@ class Logger { constructor(options = {}) { validateObject(options, 'options'); const { - handler = new JSONHandler(), - level, + level = 'info', bindings = {}, } = options; - if (!(handler instanceof Handler)) { - throw new ERR_INVALID_ARG_TYPE('options.handler', 'Handler', handler); + validateString(level, 'options.level'); + if (!LEVELS[level]) { + throw new ERR_INVALID_ARG_VALUE('options.level', level, + `must be one of: ${LEVEL_NAMES.join(', ')}`); } validateObject(bindings, 'options.bindings'); - this._handler = handler; + this._level = level; + this._levelValue = LEVELS[level]; this._bindings = bindings; - // If level is specified, it overrides handler's level - if (level !== undefined) { - validateString(level, 'options.level'); - if (!LEVELS[level]) { - throw new ERR_INVALID_ARG_VALUE('options.level', level, - `must be one of: ${LEVEL_NAMES.join(', ')}`); - } - this._level = level; - this._levelValue = LEVELS[level]; // Cache numeric value - } else { - this._level = handler._level; - this._levelValue = handler._levelValue; // Use handler's cached value - } - // Optimize: replace disabled log methods with noop this._setLogMethods(); } @@ -199,8 +215,6 @@ class Logger { _setLogMethods() { const levelValue = this._levelValue; - // Override instance methods for disabled levels - // This avoids the level check on every call if (levelValue > 10) this.trace = noop; if (levelValue > 20) this.debug = noop; if (levelValue > 30) this.info = noop; @@ -228,16 +242,13 @@ class Logger { validateObject(bindings, 'bindings'); validateObject(options, 'options'); - // Shallow merge parent and child bindings const mergedBindings = ObjectAssign( { __proto__: null }, this._bindings, bindings, ); - // Create new logger inheriting handler return new Logger({ - handler: this._handler, level: options.level || this._level, bindings: mergedBindings, }); @@ -264,28 +275,24 @@ class Logger { * @param {object} [fields] - Optional fields to merge */ _log(level, levelValue, msgOrObj, fields) { + if (levelValue < this._levelValue) { + return; + } + let msg; let logFields; - // Support two signatures: - // 1. logger.info('message', { fields }) - // 2. logger.info({ msg: 'message', other: 'fields' }) - // 3. logger.info(new Error('boom')) - if (this._isError(msgOrObj)) { - // Handle Error as first argument msg = msgOrObj.message; logFields = { err: this._serializeError(msgOrObj), ...fields, }; } else if (typeof msgOrObj === 'string') { - // Support String message msg = msgOrObj; logFields = fields || {}; validateObject(logFields, 'fields'); } else { - // Support object with msg property validateObject(msgOrObj, 'obj'); if (typeof msgOrObj.msg !== 'string') { throw new ERR_INVALID_ARG_TYPE('obj.msg', 'string', msgOrObj.msg); @@ -294,13 +301,11 @@ class Logger { msg = extractedMsg; logFields = restFields; - // Serialize Error objects in fields if (logFields.err && this._isError(logFields.err)) { logFields.err = this._serializeError(logFields.err); } } - // Build log record const record = { level, msg, @@ -309,7 +314,10 @@ class Logger { fields: logFields, }; - this._handler.handle(record); + const channel = channels[level]; + if (channel.hasSubscribers) { + channel.publish(record); + } } /** @@ -325,10 +333,8 @@ class Logger { stack: err.stack, }; - // Add code if it exists serialized.code ||= err.code; - // Add any enumerable custom properties for (const key in err) { serialized[key] ||= err[key]; } @@ -336,76 +342,31 @@ class Logger { return serialized; } - /** - * Log at trace level - * @param {string|object} msgOrObj - Message string or object with msg property - * @param {object} [fields] - Optional fields to merge - */ trace(msgOrObj, fields) { this._log('trace', 10, msgOrObj, fields); } - /** - * Log at debug level - * @param {string|object} msgOrObj - Message string or object with msg property - * @param {object} [fields] - Optional fields to merge - */ debug(msgOrObj, fields) { this._log('debug', 20, msgOrObj, fields); } - /** - * Log at info level - * @param {string|object} msgOrObj - Message string or object with msg property - * @param {object} [fields] - Optional fields to merge - */ info(msgOrObj, fields) { this._log('info', 30, msgOrObj, fields); } - /** - * Log at warn level - * @param {string|object} msgOrObj - Message string or object with msg property - * @param {object} [fields] - Optional fields to merge - */ warn(msgOrObj, fields) { this._log('warn', 40, msgOrObj, fields); } - /** - * Log at error level - * @param {string|object} msgOrObj - Message string or object with msg property - * @param {object} [fields] - Optional fields to merge - */ error(msgOrObj, fields) { this._log('error', 50, msgOrObj, fields); } - /** - * Log at fatal level (does NOT exit the process) - * @param {string|object} msgOrObj - Message string or object with msg property - * @param {object} [fields] - Optional fields to merge - */ fatal(msgOrObj, fields) { this._log('fatal', 60, msgOrObj, fields); } - - /** - * Flush pending writes - * @param {Function} callback - */ - flush(callback) { - this._handler.flush(callback); - } } -/** - * Create a new logger instance - * @param {object} [options] - * @param {Handler} [options.handler] - Output handler (default: JSONHandler) - * @param {string} [options.level] - Minimum log level (default: 'info') - * @returns {Logger} - */ function createLogger(options) { return new Logger(options); } @@ -413,7 +374,10 @@ function createLogger(options) { module.exports = { createLogger, Logger, - Handler, - JSONHandler, + LogConsumer, + JSONConsumer, + Handler: LogConsumer, + JSONHandler: JSONConsumer, LEVELS, + channels, }; diff --git a/test/parallel/test-log-basic.js b/test/parallel/test-log-basic.js index 3ee3fb241969bf..4127bc46203697 100644 --- a/test/parallel/test-log-basic.js +++ b/test/parallel/test-log-basic.js @@ -3,7 +3,7 @@ require('../common'); const assert = require('assert'); -const { createLogger, Logger, Handler, JSONHandler, LEVELS } = require('logger'); +const { createLogger, Logger, LogConsumer, JSONConsumer, LEVELS, channels } = require('logger'); const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -95,20 +95,21 @@ const path = require('path'); assert.strictEqual(childLogger.enabled('debug'), true); } -// Test JSONHandler output format +// Test JSONConsumer output format { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-1.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); logger.info({ msg: 'test message', userId: 123 }); // Flush synchronously and read the output - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); // Parse the JSON output @@ -122,20 +123,21 @@ const path = require('path'); fs.unlinkSync(tmpfile); } -// Test JSONHandler with additional fields +// Test JSONConsumer with additional fields { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-2.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', fields: { hostname: 'test-host', pid: 12345 }, }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); logger.info({ msg: 'with fields' }); - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); assert.strictEqual(parsed.hostname, 'test-host'); @@ -149,17 +151,18 @@ const path = require('path'); // Test child logger bindings in output { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-3.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); const childLogger = logger.child({ requestId: 'xyz-789' }); childLogger.info({ msg: 'child log', action: 'create' }); - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); assert.strictEqual(parsed.requestId, 'xyz-789'); @@ -179,30 +182,30 @@ const path = require('path'); }); } -// Test Handler is abstract +// Test LogConsumer is abstract { - const handler = new Handler({ level: 'info' }); + const consumer = new LogConsumer({ level: 'info' }); assert.throws(() => { - handler.handle({}); + consumer.handle({}); }, { code: 'ERR_METHOD_NOT_IMPLEMENTED', - message: /Handler\.handle\(\) method is not implemented/, + message: /LogConsumer\.handle\(\) method is not implemented/, }); } -// Test disabled level skips processing +// Test disabled level skips processing with diagnostics_channel { let handleCalled = false; - class TestHandler extends Handler { + class TestConsumer extends LogConsumer { handle() { handleCalled = true; } } - const logger = new Logger({ - handler: new TestHandler({ level: 'warn' }), - }); + const consumer = new TestConsumer({ level: 'warn' }); + consumer.attach(); + const logger = createLogger({ level: 'warn' }); // This should be skipped (info < warn) logger.info({ msg: 'skipped' }); @@ -226,16 +229,17 @@ const path = require('path'); // Test string message signature { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-string.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); logger.info('simple message'); - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); @@ -248,16 +252,17 @@ const path = require('path'); // Test string message with fields { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-string-fields.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); logger.info('user login', { userId: 123, ip: '127.0.0.1' }); - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); @@ -271,18 +276,19 @@ const path = require('path'); // Test Error object serialization { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-error.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); const err = new Error('test error'); err.code = 'TEST_ERROR'; logger.error({ msg: 'operation failed', err }); - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); @@ -299,17 +305,18 @@ const path = require('path'); // Test Error as first argument { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-error-first.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); const err = new Error('boom'); logger.error(err); // Error as first arg - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); @@ -323,22 +330,23 @@ const path = require('path'); // Test child logger with parent fields merge { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-child-merge.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', - fields: { service: 'api' }, // handler fields + fields: { service: 'api' }, // consumer fields }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); const childLogger = logger.child({ requestId: '123' }); // child bindings childLogger.info('request processed', { duration: 150 }); // log fields - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); - // Merge order: handler fields < bindings < log fields + // Merge order: consumer fields < bindings < log fields assert.strictEqual(parsed.service, 'api'); assert.strictEqual(parsed.requestId, '123'); assert.strictEqual(parsed.duration, 150); @@ -350,18 +358,19 @@ const path = require('path'); // Test field override priority { const tmpfile = path.join(os.tmpdir(), `test-log-${process.pid}-override.json`); - const handler = new JSONHandler({ + const consumer = new JSONConsumer({ stream: fs.openSync(tmpfile, 'w'), level: 'info', fields: { env: 'dev', version: '1.0' }, }); - const logger = new Logger({ handler }); + consumer.attach(); + const logger = createLogger({ level: 'info' }); const childLogger = logger.child({ env: 'staging' }); childLogger.info('test', { env: 'production' }); - handler.flushSync(); - handler.end(); + consumer.flushSync(); + consumer.end(); const output = fs.readFileSync(tmpfile, 'utf8'); const parsed = JSON.parse(output.trim()); @@ -371,3 +380,67 @@ const path = require('path'); fs.unlinkSync(tmpfile); } + +// Test multiple consumers (Qard's use case) +{ + const tmpfile1 = path.join(os.tmpdir(), `test-log-${process.pid}-multi1.json`); + const tmpfile2 = path.join(os.tmpdir(), `test-log-${process.pid}-multi2.json`); + + // Console consumer logs everything (debug+) + const consumer1 = new JSONConsumer({ + stream: fs.openSync(tmpfile1, 'w'), + level: 'debug', + }); + consumer1.attach(); + + // Service consumer logs only warnings+ (warn+) + const consumer2 = new JSONConsumer({ + stream: fs.openSync(tmpfile2, 'w'), + level: 'warn', + }); + consumer2.attach(); + + const logger = createLogger({ level: 'debug' }); + + logger.debug({ msg: 'debug message' }); + logger.info({ msg: 'info message' }); + logger.warn({ msg: 'warn message' }); + logger.error({ msg: 'error message' }); + + consumer1.flushSync(); + consumer1.end(); + consumer2.flushSync(); + consumer2.end(); + + const output1 = fs.readFileSync(tmpfile1, 'utf8'); + const lines1 = output1.trim().split('\n'); + + const output2 = fs.readFileSync(tmpfile2, 'utf8'); + const lines2 = output2.trim().split('\n'); + + // Consumer1 should have all 4 logs (debug+) + assert.strictEqual(lines1.length, 4); + assert.strictEqual(JSON.parse(lines1[0]).level, 'debug'); + assert.strictEqual(JSON.parse(lines1[1]).level, 'info'); + assert.strictEqual(JSON.parse(lines1[2]).level, 'warn'); + assert.strictEqual(JSON.parse(lines1[3]).level, 'error'); + + // Consumer2 should have only 2 logs (warn+) + assert.strictEqual(lines2.length, 2); + assert.strictEqual(JSON.parse(lines2[0]).level, 'warn'); + assert.strictEqual(JSON.parse(lines2[1]).level, 'error'); + + fs.unlinkSync(tmpfile1); + fs.unlinkSync(tmpfile2); +} + +// Test channels export +{ + assert.strictEqual(typeof channels, 'object'); + assert.strictEqual(typeof channels.trace, 'object'); + assert.strictEqual(typeof channels.debug, 'object'); + assert.strictEqual(typeof channels.info, 'object'); + assert.strictEqual(typeof channels.warn, 'object'); + assert.strictEqual(typeof channels.error, 'object'); + assert.strictEqual(typeof channels.fatal, 'object'); +} From 2af2358e80c986d39c28274f41780d665d258fbe Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 29 Oct 2025 17:24:56 +0300 Subject: [PATCH 04/28] doc: create md file for logger api --- doc/api/logger.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/api/logger.md diff --git a/doc/api/logger.md b/doc/api/logger.md new file mode 100644 index 00000000000000..b72cbe5159a9a4 --- /dev/null +++ b/doc/api/logger.md @@ -0,0 +1,10 @@ +# Logger + + + +> Stability: 1 - Experimental + + + +The `node:logger` module provides structured logging capabilities for Node.js +applications. From 630e66c56a5b34f2a41e55a9d99a1a149d271960 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 29 Oct 2025 22:23:36 +0300 Subject: [PATCH 05/28] update benchmark --- benchmark/log/vs-pino.js | 115 ------------------- benchmark/{log => logger}/basic-json.js | 0 benchmark/logger/vs-pino.js | 146 ++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 115 deletions(-) delete mode 100644 benchmark/log/vs-pino.js rename benchmark/{log => logger}/basic-json.js (100%) create mode 100644 benchmark/logger/vs-pino.js diff --git a/benchmark/log/vs-pino.js b/benchmark/log/vs-pino.js deleted file mode 100644 index 4ef967efa9ada4..00000000000000 --- a/benchmark/log/vs-pino.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -// Benchmark: Compare node:log vs Pino -// This benchmark compares the performance of the new node:log module -// against Pino, which is the performance target - -const common = require('../common'); -const fs = require('fs'); - -const bench = common.createBenchmark(main, { - n: [1e5], - logger: ['node-log', 'pino'], - scenario: ['simple', 'child', 'disabled', 'fields'], -}); - -function main({ n, logger, scenario }) { - const nullFd = fs.openSync('/dev/null', 'w'); - let testLogger; - let logData; - - if (logger === 'node-log') { - const { createLogger, JSONHandler } = require('node:logger'); - const handler = new JSONHandler({ stream: nullFd, level: 'info' }); - const baseLogger = createLogger({ handler }); - - switch (scenario) { - case 'simple': - testLogger = baseLogger; - logData = { msg: 'benchmark test message' }; - break; - - case 'child': - testLogger = baseLogger.child({ requestId: 'req-123', userId: 456 }); - logData = { msg: 'benchmark test message' }; - break; - - case 'disabled': { - // Debug logs when handler level is 'warn' (disabled) - const warnHandler = new JSONHandler({ stream: nullFd, level: 'warn' }); - testLogger = createLogger({ handler: warnHandler, level: 'debug' }); - logData = { msg: 'benchmark test message' }; - break; - } - - case 'fields': - testLogger = baseLogger; - logData = { - msg: 'benchmark test message', - field1: 'value1', - field2: 'value2', - field3: 'value3', - field4: 'value4', - field5: 'value5', - }; - break; - } - - bench.start(); - for (let i = 0; i < n; i++) { - testLogger.info(logData); - } - bench.end(n); - - // Don't close FD immediately, let async writes complete - - } else if (logger === 'pino') { - const pino = require('pino'); - const destination = pino.destination({ dest: nullFd, sync: false }); - - switch (scenario) { - case 'simple': - testLogger = pino({ level: 'info' }, destination); - logData = { msg: 'benchmark test message' }; - break; - - case 'child': { - const baseLogger = pino({ level: 'info' }, destination); - testLogger = baseLogger.child({ requestId: 'req-123', userId: 456 }); - logData = { msg: 'benchmark test message' }; - break; - } - - case 'disabled': - testLogger = pino({ level: 'warn' }, destination); - logData = { msg: 'benchmark test message' }; - break; - - case 'fields': - testLogger = pino({ level: 'info' }, destination); - logData = { - msg: 'benchmark test message', - field1: 'value1', - field2: 'value2', - field3: 'value3', - field4: 'value4', - field5: 'value5', - }; - break; - } - - bench.start(); - for (let i = 0; i < n; i++) { - if (scenario === 'disabled') { - // Use debug level to test disabled logging - testLogger.debug(logData); - } else { - testLogger.info(logData); - } - } - bench.end(n); - - // Don't close FD immediately for Pino either - - } -} diff --git a/benchmark/log/basic-json.js b/benchmark/logger/basic-json.js similarity index 100% rename from benchmark/log/basic-json.js rename to benchmark/logger/basic-json.js diff --git a/benchmark/logger/vs-pino.js b/benchmark/logger/vs-pino.js new file mode 100644 index 00000000000000..378c798d3480b8 --- /dev/null +++ b/benchmark/logger/vs-pino.js @@ -0,0 +1,146 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); + +const bench = common.createBenchmark(main, { + n: [1e5], + logger: ['node-logger', 'pino'], + scenario: ['simple', 'child', 'disabled', 'fields'], +}); + +function main({ n, logger, scenario }) { + const nullFd = fs.openSync('/dev/null', 'w'); + let testLogger; + let consumer; // ← Consumer'ı üstte tanımla + + if (logger === 'node-logger') { + const { createLogger, JSONConsumer } = require('logger'); + + switch (scenario) { + case 'simple': { + consumer = new JSONConsumer({ stream: nullFd, level: 'info' }); + consumer.attach(); + testLogger = createLogger({ level: 'info' }); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info('benchmark test message'); + } + bench.end(n); + break; + } + + case 'child': { + consumer = new JSONConsumer({ stream: nullFd, level: 'info' }); + consumer.attach(); + const baseLogger = createLogger({ level: 'info' }); + testLogger = baseLogger.child({ requestId: 'req-123', userId: 456 }); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info('benchmark test message'); + } + bench.end(n); + break; + } + + case 'disabled': { + consumer = new JSONConsumer({ stream: nullFd, level: 'warn' }); + consumer.attach(); + testLogger = createLogger({ level: 'warn' }); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.debug('benchmark test message'); + } + bench.end(n); + break; + } + + case 'fields': { + consumer = new JSONConsumer({ stream: nullFd, level: 'info' }); + consumer.attach(); + testLogger = createLogger({ level: 'info' }); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info('benchmark test message', { + field1: 'value1', + field2: 'value2', + field3: 'value3', + field4: 'value4', + field5: 'value5', + }); + } + bench.end(n); + break; + } + } + + if (consumer) { // ← Güvenlik kontrolü + consumer.flushSync(); + } + fs.closeSync(nullFd); + + } else if (logger === 'pino') { + const pino = require('pino'); + const destination = pino.destination({ dest: nullFd, sync: false }); + + switch (scenario) { + case 'simple': { + testLogger = pino({ level: 'info' }, destination); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info('benchmark test message'); + } + bench.end(n); + break; + } + + case 'child': { + const baseLogger = pino({ level: 'info' }, destination); + testLogger = baseLogger.child({ requestId: 'req-123', userId: 456 }); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info('benchmark test message'); + } + bench.end(n); + break; + } + + case 'disabled': { + testLogger = pino({ level: 'warn' }, destination); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.debug('benchmark test message'); + } + bench.end(n); + break; + } + + case 'fields': { + testLogger = pino({ level: 'info' }, destination); + + bench.start(); + for (let i = 0; i < n; i++) { + testLogger.info({ + msg: 'benchmark test message', + field1: 'value1', + field2: 'value2', + field3: 'value3', + field4: 'value4', + field5: 'value5', + }); + } + bench.end(n); + break; + } + } + + destination.flushSync(); + } +} \ No newline at end of file From 6b7756ba53c1cb7b2d7395ea00679d061e2fc2e6 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 29 Oct 2025 22:28:41 +0300 Subject: [PATCH 06/28] revert comments --- benchmark/logger/vs-pino.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/logger/vs-pino.js b/benchmark/logger/vs-pino.js index 378c798d3480b8..6a4756625433f4 100644 --- a/benchmark/logger/vs-pino.js +++ b/benchmark/logger/vs-pino.js @@ -12,7 +12,7 @@ const bench = common.createBenchmark(main, { function main({ n, logger, scenario }) { const nullFd = fs.openSync('/dev/null', 'w'); let testLogger; - let consumer; // ← Consumer'ı üstte tanımla + let consumer; if (logger === 'node-logger') { const { createLogger, JSONConsumer } = require('logger'); @@ -78,7 +78,7 @@ function main({ n, logger, scenario }) { } } - if (consumer) { // ← Güvenlik kontrolü + if (consumer) { consumer.flushSync(); } fs.closeSync(nullFd); From 0451872d3811970e519d99d82d7e07c9edb1163f Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 00:25:55 +0300 Subject: [PATCH 07/28] Update logger.js Co-authored-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> --- lib/logger.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index 6c6cb648e0003d..f31666bf479aa2 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -35,7 +35,10 @@ const channels = { fatal: diagnosticsChannel.channel('log:fatal'), }; -// RFC5424 numerical ordering + log4j interface +/* +* RFC5424 numerical ordering + log4j interface +* @link https://www.rfc-editor.org/rfc/rfc5424.html +*/ const LEVELS = { trace: 10, debug: 20, From 4c781ecc4cfdf0c5aa21a26335766e0c8961e60b Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 00:38:40 +0300 Subject: [PATCH 08/28] Update basic-json.js Co-authored-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> --- benchmark/logger/basic-json.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/logger/basic-json.js b/benchmark/logger/basic-json.js index b3c3f4bc50719a..ee29699bdf7837 100644 --- a/benchmark/logger/basic-json.js +++ b/benchmark/logger/basic-json.js @@ -2,7 +2,7 @@ const common = require('../common'); const { createLogger, JSONHandler } = require('node:logger'); -const fs = require('fs'); +const fs = require('node:fs'); const bench = common.createBenchmark(main, { n: [1e5], From 091f13cd478637f0af239be9d87a960fa437ca8a Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 00:38:54 +0300 Subject: [PATCH 09/28] Update vs-pino.js Co-authored-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> --- benchmark/logger/vs-pino.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/logger/vs-pino.js b/benchmark/logger/vs-pino.js index 6a4756625433f4..67781524cd6271 100644 --- a/benchmark/logger/vs-pino.js +++ b/benchmark/logger/vs-pino.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common'); -const fs = require('fs'); +const fs = require('node:fs'); const bench = common.createBenchmark(main, { n: [1e5], From 9a47a535d74dcbe4df386813aa43b075ce5d9639 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 01:01:32 +0300 Subject: [PATCH 10/28] lib: reorder logger exports to follow Node.js conventions --- lib/logger.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index f31666bf479aa2..8749626c1fbc50 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -370,17 +370,24 @@ class Logger { } } +/** + * Create a logger instance (convenience method) + * @param {object} [options] + * @param {string} [options.level] - Minimum log level (default: 'info') + * @param {object} [options.bindings] - Context fields (default: {}) + * @returns {Logger} + */ function createLogger(options) { return new Logger(options); } module.exports = { - createLogger, Logger, LogConsumer, JSONConsumer, - Handler: LogConsumer, - JSONHandler: JSONConsumer, LEVELS, channels, + createLogger, + Handler: LogConsumer, + JSONHandler: JSONConsumer, }; From 40f4f1d4b46efa0c9488fd58ccf7962cdc6b7208 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:23:00 +0300 Subject: [PATCH 11/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index 8749626c1fbc50..d4c6cc62eca67f 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -58,7 +58,7 @@ function noop() {} * Consumers subscribe to diagnostics_channel events */ class LogConsumer { - constructor(options = {}) { + constructor(options = kEmptyObject) { validateObject(options, 'options'); const { level = 'info' } = options; validateString(level, 'options.level'); From df2b7fe4d8b218f806d30a8afb21b220c702a110 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:23:13 +0300 Subject: [PATCH 12/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index d4c6cc62eca67f..500b2f2bfb7a6e 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -115,7 +115,7 @@ class LogConsumer { * JSON consumer - outputs structured JSON logs */ class JSONConsumer extends LogConsumer { - constructor(options = {}) { + constructor(options = kEmptyObject) { super(options); const { stream, From 91ae0f6a91c7f380af3c79d3abf5efd805a12b81 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:23:24 +0300 Subject: [PATCH 13/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index 500b2f2bfb7a6e..4231a9baf15a07 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -119,7 +119,7 @@ class JSONConsumer extends LogConsumer { super(options); const { stream, - fields = {}, + fields = kEmptyObject, } = options; validateObject(fields, 'options.fields'); From 41e337e30053076daa6f343d2fcb990188158928 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:23:33 +0300 Subject: [PATCH 14/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index 4231a9baf15a07..8bcdf6be032070 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -188,7 +188,7 @@ class JSONConsumer extends LogConsumer { * Logger class */ class Logger { - constructor(options = {}) { + constructor(options = kEmptyObject) { validateObject(options, 'options'); const { level = 'info', From 99163363c109511e83bd1c9d0b0e697d46044c86 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:23:43 +0300 Subject: [PATCH 15/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index 8bcdf6be032070..e05627c4a55eda 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -192,7 +192,7 @@ class Logger { validateObject(options, 'options'); const { level = 'info', - bindings = {}, + bindings = kEmptyObject, } = options; validateString(level, 'options.level'); From f4e10c9272426b09b26467a84bd6abc6017f9992 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:24:07 +0300 Subject: [PATCH 16/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index e05627c4a55eda..76bf88fd1798d1 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -293,7 +293,7 @@ class Logger { }; } else if (typeof msgOrObj === 'string') { msg = msgOrObj; - logFields = fields || {}; + logFields = fields || kEmptyObject; validateObject(logFields, 'fields'); } else { validateObject(msgOrObj, 'obj'); From 7b4e9179d0af43d5a036926cc1690d7bfa6c245b Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:24:23 +0300 Subject: [PATCH 17/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 76bf88fd1798d1..abf4e1c5bdf9f3 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -346,27 +346,27 @@ class Logger { } trace(msgOrObj, fields) { - this._log('trace', 10, msgOrObj, fields); + this._log('trace', LEVELS.trace, msgOrObj, fields); } debug(msgOrObj, fields) { - this._log('debug', 20, msgOrObj, fields); + this._log('debug', LEVELS.debug, msgOrObj, fields); } info(msgOrObj, fields) { - this._log('info', 30, msgOrObj, fields); + this._log('info', LEVELS.info, msgOrObj, fields); } warn(msgOrObj, fields) { - this._log('warn', 40, msgOrObj, fields); + this._log('warn', LEVELS.warn, msgOrObj, fields); } error(msgOrObj, fields) { - this._log('error', 50, msgOrObj, fields); + this._log('error', LEVELS.error, msgOrObj, fields); } fatal(msgOrObj, fields) { - this._log('fatal', 60, msgOrObj, fields); + this._log('fatal', LEVELS.fatal, msgOrObj, fields); } } From d9c515552e30fcd09ab6ed976e4ea3f76c111d54 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:24:41 +0300 Subject: [PATCH 18/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger.js b/lib/logger.js index abf4e1c5bdf9f3..2c39514acded5c 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -241,7 +241,7 @@ class Logger { * @param {object} [options] - Optional overrides * @returns {Logger} */ - child(bindings, options = {}) { + child(bindings, options = kEmptyObject) { validateObject(bindings, 'bindings'); validateObject(options, 'options'); From 40d8b40e70312f7ed1f015e9e148a47202ff7c90 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:24:59 +0300 Subject: [PATCH 19/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 2c39514acded5c..44ad4f64d3df35 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -218,12 +218,12 @@ class Logger { _setLogMethods() { const levelValue = this._levelValue; - if (levelValue > 10) this.trace = noop; - if (levelValue > 20) this.debug = noop; - if (levelValue > 30) this.info = noop; - if (levelValue > 40) this.warn = noop; - if (levelValue > 50) this.error = noop; - if (levelValue > 60) this.fatal = noop; + if (levelValue > LEVELS.trace) this.trace = noop; + if (levelValue > LEVELS.debug) this.debug = noop; + if (levelValue > LEVELS.info) this.info = noop; + if (levelValue > LEVELS.warn) this.warn = noop; + if (levelValue > LEVELS.error) this.error = noop; + if (levelValue > LEVELS.fatal) this.fatal = noop; } /** From 239dc56c4edebffbea89da3c31d1ff4d7edfa6e3 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:25:09 +0300 Subject: [PATCH 20/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/logger.js b/lib/logger.js index 44ad4f64d3df35..0c644b16d3f82b 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -382,6 +382,7 @@ function createLogger(options) { } module.exports = { + __proto__: null, Logger, LogConsumer, JSONConsumer, From ad2a56a2b2595883258ba35ff398985b37095cbd Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 10:25:32 +0300 Subject: [PATCH 21/28] Update lib/logger.js Co-authored-by: Chemi Atlow --- lib/logger.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 0c644b16d3f82b..56f002ae24bd96 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -297,9 +297,7 @@ class Logger { validateObject(logFields, 'fields'); } else { validateObject(msgOrObj, 'obj'); - if (typeof msgOrObj.msg !== 'string') { - throw new ERR_INVALID_ARG_TYPE('obj.msg', 'string', msgOrObj.msg); - } + validateString(msgOrObj.msg, 'obj.msg'); const { msg: extractedMsg, ...restFields } = msgOrObj; msg = extractedMsg; logFields = restFields; From 9c2e2a919c63511060b9a4bee0499d78ab9f3714 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 13:30:24 +0300 Subject: [PATCH 22/28] lib: optimize logger by checking hasSubscribers early --- benchmark/logger/vs-pino.js | 2 +- lib/logger.js | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/benchmark/logger/vs-pino.js b/benchmark/logger/vs-pino.js index 67781524cd6271..30a0d82c86ec5e 100644 --- a/benchmark/logger/vs-pino.js +++ b/benchmark/logger/vs-pino.js @@ -143,4 +143,4 @@ function main({ n, logger, scenario }) { destination.flushSync(); } -} \ No newline at end of file +} diff --git a/lib/logger.js b/lib/logger.js index 56f002ae24bd96..be678bd92fec50 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -24,6 +24,7 @@ const { const Utf8Stream = require('internal/streams/fast-utf8-stream'); const diagnosticsChannel = require('diagnostics_channel'); +const { kEmptyObject } = require('internal/util'); // Create channels for each log level const channels = { @@ -282,6 +283,20 @@ class Logger { return; } + if (typeof msgOrObj === 'string') { + if (fields !== undefined) { + validateObject(fields, 'fields'); + } + } else if (!this._isError(msgOrObj)) { + validateObject(msgOrObj, 'obj'); + validateString(msgOrObj.msg, 'obj.msg'); + } + + const channel = channels[level]; + if (!channel.hasSubscribers) { + return; + } + let msg; let logFields; @@ -294,10 +309,7 @@ class Logger { } else if (typeof msgOrObj === 'string') { msg = msgOrObj; logFields = fields || kEmptyObject; - validateObject(logFields, 'fields'); } else { - validateObject(msgOrObj, 'obj'); - validateString(msgOrObj.msg, 'obj.msg'); const { msg: extractedMsg, ...restFields } = msgOrObj; msg = extractedMsg; logFields = restFields; @@ -315,10 +327,7 @@ class Logger { fields: logFields, }; - const channel = channels[level]; - if (channel.hasSubscribers) { - channel.publish(record); - } + channel.publish(record); } /** From 11a9b97237bf67626bf46c131e14f3db42cefd74 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 13:43:16 +0300 Subject: [PATCH 23/28] lib: handle cause case --- lib/logger.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index be678bd92fec50..e50aa729b91efd 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -7,6 +7,7 @@ const { ObjectAssign, ObjectKeys, ObjectPrototypeToString, + SafeSet, } = primordials; const { @@ -331,22 +332,38 @@ class Logger { } /** - * Serialize Error object for logging + * Serialize Error object for logging (with recursive cause handling) * @param {object} err - Error object to serialize + * @param {Set} [seen] - Set to track circular references * @returns {object} Serialized error object * @private */ - _serializeError(err) { + _serializeError(err, seen = new SafeSet()) { + if (seen.has(err)) { + return '[Circular]'; + } + seen.add(err); + const serialized = { message: err.message, name: err.name, stack: err.stack, }; - serialized.code ||= err.code; + if (err.code !== undefined) { + serialized.code = err.code; + } + + if (err.cause !== undefined) { + serialized.cause = this._isError(err.cause) ? + this._serializeError(err.cause, seen) : + err.cause; + } for (const key in err) { - serialized[key] ||= err[key]; + if (serialized[key] === undefined) { + serialized[key] = err[key]; + } } return serialized; From b216a271c99cb4c805c9ece1587c65350dc45869 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 22:08:10 +0300 Subject: [PATCH 24/28] lib: use validateOneOf --- lib/logger.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index e50aa729b91efd..f5b2ccddabcad4 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -21,6 +21,7 @@ const { const { validateObject, validateString, + validateOneOf, } = require('internal/validators'); const Utf8Stream = require('internal/streams/fast-utf8-stream'); @@ -65,10 +66,7 @@ class LogConsumer { const { level = 'info' } = options; validateString(level, 'options.level'); - if (!LEVELS[level]) { - throw new ERR_INVALID_ARG_VALUE('options.level', level, - `must be one of: ${LEVEL_NAMES.join(', ')}`); - } + validateOneOf(level, 'options.level', LEVEL_NAMES); this._level = level; this._levelValue = LEVELS[level]; From 7f45ee45331386f98f9b295fb66676c33f01e72b Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 22:14:15 +0300 Subject: [PATCH 25/28] lib: use ErrorIsError --- lib/logger.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index f5b2ccddabcad4..460988b59066f2 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -2,11 +2,10 @@ const { DateNow, - Error, + ErrorIsError, JSONStringify, ObjectAssign, ObjectKeys, - ObjectPrototypeToString, SafeSet, } = primordials; @@ -264,10 +263,7 @@ class Logger { * @private */ _isError(value) { - return value !== null && - typeof value === 'object' && - (ObjectPrototypeToString(value) === '[object Error]' || - value instanceof Error); + return ErrorIsError(value); } /** From f2aa86d83f04b127f17e80ded0834ad4dcb9a7c0 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Thu, 30 Oct 2025 22:38:20 +0300 Subject: [PATCH 26/28] lib: add logger module with diagnostics_channel --- lib/logger.js | 47 ++++++++++++++++++++-------- test/parallel/test-log-basic.js | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 460988b59066f2..0eaee9e134b22f 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -2,10 +2,11 @@ const { DateNow, - ErrorIsError, + Error, JSONStringify, ObjectAssign, ObjectKeys, + ObjectPrototypeToString, SafeSet, } = primordials; @@ -129,21 +130,32 @@ class JSONConsumer extends LogConsumer { } _createStream(stream) { + // Fast path: already a Utf8Stream if (stream instanceof Utf8Stream) { return stream; } + // Number: file descriptor if (typeof stream === 'number') { return new Utf8Stream({ fd: stream }); } + // String: file path if (typeof stream === 'string') { return new Utf8Stream({ dest: stream }); } - throw new ERR_INVALID_ARG_TYPE('options.stream', - ['number', 'string', 'Utf8Stream'], - stream); + // Object: custom stream with write method + if (typeof stream === 'object' && stream !== null && + typeof stream.write === 'function') { + return stream; + } + + throw new ERR_INVALID_ARG_TYPE( + 'options.stream', + ['number', 'string', 'Utf8Stream', 'object with write method'], + stream, + ); } handle(record) { @@ -189,15 +201,15 @@ class JSONConsumer extends LogConsumer { class Logger { constructor(options = kEmptyObject) { validateObject(options, 'options'); - const { - level = 'info', - bindings = kEmptyObject, - } = options; + const { level = 'info', bindings = kEmptyObject } = options; validateString(level, 'options.level'); if (!LEVELS[level]) { - throw new ERR_INVALID_ARG_VALUE('options.level', level, - `must be one of: ${LEVEL_NAMES.join(', ')}`); + throw new ERR_INVALID_ARG_VALUE( + 'options.level', + level, + `must be one of: ${LEVEL_NAMES.join(', ')}`, + ); } validateObject(bindings, 'options.bindings'); @@ -206,7 +218,6 @@ class Logger { this._levelValue = LEVELS[level]; this._bindings = bindings; - // Optimize: replace disabled log methods with noop this._setLogMethods(); } @@ -263,7 +274,13 @@ class Logger { * @private */ _isError(value) { - return ErrorIsError(value); + // TODO(@mertcanaltin): Use ErrorIsError from primordials when available + // For now, use manual check until ErrorIsError is added to primordials + + return value !== null && + typeof value === 'object' && + (ObjectPrototypeToString(value) === '[object Error]' || + value instanceof Error); } /** @@ -303,7 +320,7 @@ class Logger { }; } else if (typeof msgOrObj === 'string') { msg = msgOrObj; - logFields = fields || kEmptyObject; + logFields = fields ?? kEmptyObject; } else { const { msg: extractedMsg, ...restFields } = msgOrObj; msg = extractedMsg; @@ -312,6 +329,10 @@ class Logger { if (logFields.err && this._isError(logFields.err)) { logFields.err = this._serializeError(logFields.err); } + + if (logFields.error && this._isError(logFields.error)) { + logFields.error = this._serializeError(logFields.error); + } } const record = { diff --git a/test/parallel/test-log-basic.js b/test/parallel/test-log-basic.js index 4127bc46203697..86e7306c726403 100644 --- a/test/parallel/test-log-basic.js +++ b/test/parallel/test-log-basic.js @@ -444,3 +444,57 @@ const path = require('path'); assert.strictEqual(typeof channels.error, 'object'); assert.strictEqual(typeof channels.fatal, 'object'); } + +// Test: Support both 'err' and 'error' fields +{ + const logs = []; + const consumer = new JSONConsumer({ + stream: { + write(data) { logs.push(JSON.parse(data)); }, + flush() {}, + flushSync() {}, + end() {}, + }, + level: 'info', + }); + consumer.attach(); + + const logger = new Logger({ level: 'info' }); + + const err = new Error('Error 1'); + const error = new Error('Error 2'); + + logger.error({ msg: 'Multiple errors', err, error }); + + assert.strictEqual(logs.length, 1); + assert.strictEqual(logs[0].err.message, 'Error 1'); + assert.strictEqual(logs[0].error.message, 'Error 2'); + assert.ok(logs[0].err.stack); + assert.ok(logs[0].error.stack); +} + +// Test: 'error' field serialization +{ + const logs = []; + const consumer = new JSONConsumer({ + stream: { + write(data) { logs.push(JSON.parse(data)); }, + flush() {}, + flushSync() {}, + end() {}, + }, + level: 'info', + }); + consumer.attach(); + + const logger = new Logger({ level: 'info' }); + const error = new Error('Test error'); + error.code = 'TEST_CODE'; + + logger.error({ msg: 'Operation failed', error }); + + assert.strictEqual(logs.length, 1); + assert.strictEqual(logs[0].error.message, 'Test error'); + assert.strictEqual(logs[0].error.code, 'TEST_CODE'); + assert.ok(logs[0].error.stack); +} From f1abaadc3d0d2e7d46a8383aade548a2bd54708f Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Fri, 31 Oct 2025 06:36:31 +0300 Subject: [PATCH 27/28] git commit -m "make internal properties and methods truly private --- lib/logger.js | 97 +++++++++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 0eaee9e134b22f..03f86382e16860 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -8,6 +8,7 @@ const { ObjectKeys, ObjectPrototypeToString, SafeSet, + Symbol, } = primordials; const { @@ -53,6 +54,18 @@ const LEVELS = { const LEVEL_NAMES = ObjectKeys(LEVELS); +const kLevel = Symbol('level'); +const kLevelValue = Symbol('levelValue'); +const kBindings = Symbol('bindings'); +const kFields = Symbol('fields'); +const kStream = Symbol('stream'); +const kHandleLog = Symbol('handleLog'); +const kCreateStream = Symbol('createStream'); +const kSetLogMethods = Symbol('setLogMethods'); +const kIsError = Symbol('isError'); +const kLog = Symbol('log'); +const kSerializeError = Symbol('serializeError'); + // Noop function for disabled log levels function noop() {} @@ -68,8 +81,8 @@ class LogConsumer { validateOneOf(level, 'options.level', LEVEL_NAMES); - this._level = level; - this._levelValue = LEVELS[level]; + this[kLevel] = level; + this[kLevelValue] = LEVELS[level]; } /** @@ -78,7 +91,7 @@ class LogConsumer { * @returns {boolean} */ enabled(level) { - return LEVELS[level] >= this._levelValue; + return LEVELS[level] >= this[kLevelValue]; } /** @@ -87,7 +100,7 @@ class LogConsumer { attach() { for (const level of LEVEL_NAMES) { if (this.enabled(level)) { - channels[level].subscribe(this._handleLog.bind(this, level)); + channels[level].subscribe(this[kHandleLog].bind(this, level)); } } } @@ -98,7 +111,7 @@ class LogConsumer { * @param {object} record * @private */ - _handleLog(level, record) { + [kHandleLog](level, record) { this.handle(record); } @@ -124,12 +137,12 @@ class JSONConsumer extends LogConsumer { validateObject(fields, 'options.fields'); - this._stream = stream ? this._createStream(stream) : + this[kStream] = stream ? this[kCreateStream](stream) : new Utf8Stream({ fd: 1 }); - this._fields = fields; + this[kFields] = fields; } - _createStream(stream) { + [kCreateStream](stream) { // Fast path: already a Utf8Stream if (stream instanceof Utf8Stream) { return stream; @@ -163,13 +176,13 @@ class JSONConsumer extends LogConsumer { level: record.level, time: record.time, msg: record.msg, - ...this._fields, + ...this[kFields], ...record.bindings, ...record.fields, }; const json = JSONStringify(logObj) + '\n'; - this._stream.write(json); + this[kStream].write(json); } /** @@ -177,21 +190,21 @@ class JSONConsumer extends LogConsumer { * @param {Function} callback */ flush(callback) { - this._stream.flush(callback); + this[kStream].flush(callback); } /** * Flush pending writes synchronously */ flushSync() { - this._stream.flushSync(); + this[kStream].flushSync(); } /** * Close the consumer */ end() { - this._stream.end(); + this[kStream].end(); } } @@ -214,19 +227,19 @@ class Logger { validateObject(bindings, 'options.bindings'); - this._level = level; - this._levelValue = LEVELS[level]; - this._bindings = bindings; + this[kLevel] = level; + this[kLevelValue] = LEVELS[level]; + this[kBindings] = bindings; - this._setLogMethods(); + this[kSetLogMethods](); } /** * Replace disabled log methods with noop for performance * @private */ - _setLogMethods() { - const levelValue = this._levelValue; + [kSetLogMethods]() { + const levelValue = this[kLevelValue]; if (levelValue > LEVELS.trace) this.trace = noop; if (levelValue > LEVELS.debug) this.debug = noop; @@ -242,7 +255,7 @@ class Logger { * @returns {boolean} */ enabled(level) { - return LEVELS[level] >= this._levelValue; + return LEVELS[level] >= this[kLevelValue]; } /** @@ -257,12 +270,12 @@ class Logger { const mergedBindings = ObjectAssign( { __proto__: null }, - this._bindings, + this[kBindings], bindings, ); return new Logger({ - level: options.level || this._level, + level: options.level || this[kLevel], bindings: mergedBindings, }); } @@ -273,7 +286,7 @@ class Logger { * @returns {boolean} * @private */ - _isError(value) { + [kIsError](value) { // TODO(@mertcanaltin): Use ErrorIsError from primordials when available // For now, use manual check until ErrorIsError is added to primordials @@ -290,8 +303,8 @@ class Logger { * @param {string|object} msgOrObj - Message string or object with msg property * @param {object} [fields] - Optional fields to merge */ - _log(level, levelValue, msgOrObj, fields) { - if (levelValue < this._levelValue) { + [kLog](level, levelValue, msgOrObj, fields) { + if (levelValue < this[kLevelValue]) { return; } @@ -299,7 +312,7 @@ class Logger { if (fields !== undefined) { validateObject(fields, 'fields'); } - } else if (!this._isError(msgOrObj)) { + } else if (!this[kIsError](msgOrObj)) { validateObject(msgOrObj, 'obj'); validateString(msgOrObj.msg, 'obj.msg'); } @@ -312,10 +325,10 @@ class Logger { let msg; let logFields; - if (this._isError(msgOrObj)) { + if (this[kIsError](msgOrObj)) { msg = msgOrObj.message; logFields = { - err: this._serializeError(msgOrObj), + err: this[kSerializeError](msgOrObj), ...fields, }; } else if (typeof msgOrObj === 'string') { @@ -326,12 +339,12 @@ class Logger { msg = extractedMsg; logFields = restFields; - if (logFields.err && this._isError(logFields.err)) { - logFields.err = this._serializeError(logFields.err); + if (logFields.err && this[kIsError](logFields.err)) { + logFields.err = this[kSerializeError](logFields.err); } - if (logFields.error && this._isError(logFields.error)) { - logFields.error = this._serializeError(logFields.error); + if (logFields.error && this[kIsError](logFields.error)) { + logFields.error = this[kSerializeError](logFields.error); } } @@ -339,7 +352,7 @@ class Logger { level, msg, time: DateNow(), - bindings: this._bindings, + bindings: this[kBindings], fields: logFields, }; @@ -353,7 +366,7 @@ class Logger { * @returns {object} Serialized error object * @private */ - _serializeError(err, seen = new SafeSet()) { + [kSerializeError](err, seen = new SafeSet()) { if (seen.has(err)) { return '[Circular]'; } @@ -370,8 +383,8 @@ class Logger { } if (err.cause !== undefined) { - serialized.cause = this._isError(err.cause) ? - this._serializeError(err.cause, seen) : + serialized.cause = this[kIsError](err.cause) ? + this[kSerializeError](err.cause, seen) : err.cause; } @@ -385,27 +398,27 @@ class Logger { } trace(msgOrObj, fields) { - this._log('trace', LEVELS.trace, msgOrObj, fields); + this[kLog]('trace', LEVELS.trace, msgOrObj, fields); } debug(msgOrObj, fields) { - this._log('debug', LEVELS.debug, msgOrObj, fields); + this[kLog]('debug', LEVELS.debug, msgOrObj, fields); } info(msgOrObj, fields) { - this._log('info', LEVELS.info, msgOrObj, fields); + this[kLog]('info', LEVELS.info, msgOrObj, fields); } warn(msgOrObj, fields) { - this._log('warn', LEVELS.warn, msgOrObj, fields); + this[kLog]('warn', LEVELS.warn, msgOrObj, fields); } error(msgOrObj, fields) { - this._log('error', LEVELS.error, msgOrObj, fields); + this[kLog]('error', LEVELS.error, msgOrObj, fields); } fatal(msgOrObj, fields) { - this._log('fatal', LEVELS.fatal, msgOrObj, fields); + this[kLog]('fatal', LEVELS.fatal, msgOrObj, fields); } } From 5815b292b6d16afa872a1beaef1b4a632b9bf8e6 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Fri, 31 Oct 2025 06:36:31 +0300 Subject: [PATCH 28/28] make internal properties and methods truly private --- lib/logger.js | 97 +++++++++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 0eaee9e134b22f..03f86382e16860 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -8,6 +8,7 @@ const { ObjectKeys, ObjectPrototypeToString, SafeSet, + Symbol, } = primordials; const { @@ -53,6 +54,18 @@ const LEVELS = { const LEVEL_NAMES = ObjectKeys(LEVELS); +const kLevel = Symbol('level'); +const kLevelValue = Symbol('levelValue'); +const kBindings = Symbol('bindings'); +const kFields = Symbol('fields'); +const kStream = Symbol('stream'); +const kHandleLog = Symbol('handleLog'); +const kCreateStream = Symbol('createStream'); +const kSetLogMethods = Symbol('setLogMethods'); +const kIsError = Symbol('isError'); +const kLog = Symbol('log'); +const kSerializeError = Symbol('serializeError'); + // Noop function for disabled log levels function noop() {} @@ -68,8 +81,8 @@ class LogConsumer { validateOneOf(level, 'options.level', LEVEL_NAMES); - this._level = level; - this._levelValue = LEVELS[level]; + this[kLevel] = level; + this[kLevelValue] = LEVELS[level]; } /** @@ -78,7 +91,7 @@ class LogConsumer { * @returns {boolean} */ enabled(level) { - return LEVELS[level] >= this._levelValue; + return LEVELS[level] >= this[kLevelValue]; } /** @@ -87,7 +100,7 @@ class LogConsumer { attach() { for (const level of LEVEL_NAMES) { if (this.enabled(level)) { - channels[level].subscribe(this._handleLog.bind(this, level)); + channels[level].subscribe(this[kHandleLog].bind(this, level)); } } } @@ -98,7 +111,7 @@ class LogConsumer { * @param {object} record * @private */ - _handleLog(level, record) { + [kHandleLog](level, record) { this.handle(record); } @@ -124,12 +137,12 @@ class JSONConsumer extends LogConsumer { validateObject(fields, 'options.fields'); - this._stream = stream ? this._createStream(stream) : + this[kStream] = stream ? this[kCreateStream](stream) : new Utf8Stream({ fd: 1 }); - this._fields = fields; + this[kFields] = fields; } - _createStream(stream) { + [kCreateStream](stream) { // Fast path: already a Utf8Stream if (stream instanceof Utf8Stream) { return stream; @@ -163,13 +176,13 @@ class JSONConsumer extends LogConsumer { level: record.level, time: record.time, msg: record.msg, - ...this._fields, + ...this[kFields], ...record.bindings, ...record.fields, }; const json = JSONStringify(logObj) + '\n'; - this._stream.write(json); + this[kStream].write(json); } /** @@ -177,21 +190,21 @@ class JSONConsumer extends LogConsumer { * @param {Function} callback */ flush(callback) { - this._stream.flush(callback); + this[kStream].flush(callback); } /** * Flush pending writes synchronously */ flushSync() { - this._stream.flushSync(); + this[kStream].flushSync(); } /** * Close the consumer */ end() { - this._stream.end(); + this[kStream].end(); } } @@ -214,19 +227,19 @@ class Logger { validateObject(bindings, 'options.bindings'); - this._level = level; - this._levelValue = LEVELS[level]; - this._bindings = bindings; + this[kLevel] = level; + this[kLevelValue] = LEVELS[level]; + this[kBindings] = bindings; - this._setLogMethods(); + this[kSetLogMethods](); } /** * Replace disabled log methods with noop for performance * @private */ - _setLogMethods() { - const levelValue = this._levelValue; + [kSetLogMethods]() { + const levelValue = this[kLevelValue]; if (levelValue > LEVELS.trace) this.trace = noop; if (levelValue > LEVELS.debug) this.debug = noop; @@ -242,7 +255,7 @@ class Logger { * @returns {boolean} */ enabled(level) { - return LEVELS[level] >= this._levelValue; + return LEVELS[level] >= this[kLevelValue]; } /** @@ -257,12 +270,12 @@ class Logger { const mergedBindings = ObjectAssign( { __proto__: null }, - this._bindings, + this[kBindings], bindings, ); return new Logger({ - level: options.level || this._level, + level: options.level || this[kLevel], bindings: mergedBindings, }); } @@ -273,7 +286,7 @@ class Logger { * @returns {boolean} * @private */ - _isError(value) { + [kIsError](value) { // TODO(@mertcanaltin): Use ErrorIsError from primordials when available // For now, use manual check until ErrorIsError is added to primordials @@ -290,8 +303,8 @@ class Logger { * @param {string|object} msgOrObj - Message string or object with msg property * @param {object} [fields] - Optional fields to merge */ - _log(level, levelValue, msgOrObj, fields) { - if (levelValue < this._levelValue) { + [kLog](level, levelValue, msgOrObj, fields) { + if (levelValue < this[kLevelValue]) { return; } @@ -299,7 +312,7 @@ class Logger { if (fields !== undefined) { validateObject(fields, 'fields'); } - } else if (!this._isError(msgOrObj)) { + } else if (!this[kIsError](msgOrObj)) { validateObject(msgOrObj, 'obj'); validateString(msgOrObj.msg, 'obj.msg'); } @@ -312,10 +325,10 @@ class Logger { let msg; let logFields; - if (this._isError(msgOrObj)) { + if (this[kIsError](msgOrObj)) { msg = msgOrObj.message; logFields = { - err: this._serializeError(msgOrObj), + err: this[kSerializeError](msgOrObj), ...fields, }; } else if (typeof msgOrObj === 'string') { @@ -326,12 +339,12 @@ class Logger { msg = extractedMsg; logFields = restFields; - if (logFields.err && this._isError(logFields.err)) { - logFields.err = this._serializeError(logFields.err); + if (logFields.err && this[kIsError](logFields.err)) { + logFields.err = this[kSerializeError](logFields.err); } - if (logFields.error && this._isError(logFields.error)) { - logFields.error = this._serializeError(logFields.error); + if (logFields.error && this[kIsError](logFields.error)) { + logFields.error = this[kSerializeError](logFields.error); } } @@ -339,7 +352,7 @@ class Logger { level, msg, time: DateNow(), - bindings: this._bindings, + bindings: this[kBindings], fields: logFields, }; @@ -353,7 +366,7 @@ class Logger { * @returns {object} Serialized error object * @private */ - _serializeError(err, seen = new SafeSet()) { + [kSerializeError](err, seen = new SafeSet()) { if (seen.has(err)) { return '[Circular]'; } @@ -370,8 +383,8 @@ class Logger { } if (err.cause !== undefined) { - serialized.cause = this._isError(err.cause) ? - this._serializeError(err.cause, seen) : + serialized.cause = this[kIsError](err.cause) ? + this[kSerializeError](err.cause, seen) : err.cause; } @@ -385,27 +398,27 @@ class Logger { } trace(msgOrObj, fields) { - this._log('trace', LEVELS.trace, msgOrObj, fields); + this[kLog]('trace', LEVELS.trace, msgOrObj, fields); } debug(msgOrObj, fields) { - this._log('debug', LEVELS.debug, msgOrObj, fields); + this[kLog]('debug', LEVELS.debug, msgOrObj, fields); } info(msgOrObj, fields) { - this._log('info', LEVELS.info, msgOrObj, fields); + this[kLog]('info', LEVELS.info, msgOrObj, fields); } warn(msgOrObj, fields) { - this._log('warn', LEVELS.warn, msgOrObj, fields); + this[kLog]('warn', LEVELS.warn, msgOrObj, fields); } error(msgOrObj, fields) { - this._log('error', LEVELS.error, msgOrObj, fields); + this[kLog]('error', LEVELS.error, msgOrObj, fields); } fatal(msgOrObj, fields) { - this._log('fatal', LEVELS.fatal, msgOrObj, fields); + this[kLog]('fatal', LEVELS.fatal, msgOrObj, fields); } }