Skip to content

lib: added logger api in node core#60468

Open
mertcanaltin wants to merge 21 commits intonodejs:mainfrom
mertcanaltin:mert/create-logger-api/node-core
Open

lib: added logger api in node core#60468
mertcanaltin wants to merge 21 commits intonodejs:mainfrom
mertcanaltin:mert/create-logger-api/node-core

Conversation

@mertcanaltin
Copy link
Copy Markdown
Member

@mertcanaltin mertcanaltin commented Oct 28, 2025

Description

Adds an experimental node:logger module that provides a structured, high-performance logging API for Node.js. Available behind the --experimental-logger flag.

Refs: #49296

Architecture

  • Uses diagnostics_channel as the dispatch mechanism, loggers publish log records to level-specific channels, consumers subscribe to receive them
  • Pre-serializes bindings at construction time to avoid per-log overhead
  • Disabled log levels become noop functions, eliminating runtime level checks
  • JSON output uses the existing internal Utf8Stream (SonicBoom port) for high-throughput writes

API Surface

  • Logger, Producer class with trace, debug, info, warn, error, fatal methods
  • LogConsumer, Base consumer class for custom log handling
  • JSONConsumer, Built-in consumer that outputs structured JSON via Utf8Stream
  • child(bindings, options), Child loggers with inherited context and serializers
  • logger.<level>.enabled, Getter for conditional logging (typo-safe)
  • stdSerializers, Built-in serializers for err, req, res
  • serialize symbol, Symbol.for('nodejs.logger.serialize'), custom object serialization hook (stacked with field serializers)

Log levels follow RFC 5424 numerical ordering internally (trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60). The API accepts string level names. diagnostics_channel channel names (log:trace, log:debug, log:info, log:warn, log:error, log:fatal) are used internally and can be subscribed to directly via diagnostics_channel if needed.

Performance

Benchmarked against Pino (output to /dev/null, n=100000):

Scenario node:logger pino Difference
simple 6.06M ops/s 3.48M ops/s +74%
child 5.76M ops/s 4.41M ops/s +31%
disabled 174M ops/s 146M ops/s +19%
fields 2.13M ops/s 1.36M ops/s +56%

New files

  • lib/logger.js, Main module (Logger, LogConsumer, JSONConsumer)
  • lib/internal/logger/serializers.js, Standard err/req/res serializers
  • doc/api/logger.md, Full API documentation
  • benchmark/logger/basic-json.js, Benchmarks
  • test/parallel/test-log-basic.js, Core logger tests
  • test/parallel/test-logger-serializers.js, Serializer tests

Modified files

  • src/node_options.h / src/node_options.cc, --experimental-logger flag
  • lib/internal/process/pre_execution.js, setupLogger() to allow module loading when flag is set
  • lib/internal/bootstrap/realm.js, Module registration
  • doc/api/cli.md, Flag documentation
  • doc/api/index.md / doc/node.1, Index updates
  • test/parallel/test-code-cache.js / test-process-get-builtin.mjs / test-require-resolve.js, Test adjustments for new module

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Oct 28, 2025
@mertcanaltin mertcanaltin changed the title lib: added logger package in node core lib: added logger api in node core Oct 28, 2025
@mertcanaltin mertcanaltin changed the title lib: added logger api in node core [WIP] lib: added logger api in node core Oct 28, 2025
Comment thread lib/logger.js Outdated
@mertcanaltin mertcanaltin requested a review from Qard October 29, 2025 14:16
@mertcanaltin
Copy link
Copy Markdown
Member Author

mertcanaltin commented Oct 29, 2025

thats my first performance results:

I think need improvement for child logger area, but ı'm so happy because other results it looks nice.
cc @mcollina @Qard @trentm @jsumners

node:logger vs pino

➜ node git:(mert/create-logger-api/node-core) ✗ ./node benchmark/logger/vs-pino.js n=100000
logger/vs-pino.js scenario="simple" logger="node-logger" n=100000: 5,240,540.823813018
logger/vs-pino.js scenario="child" logger="node-logger" n=100000: 2,635,847.7027229806
logger/vs-pino.js scenario="disabled" logger="node-logger" n=100000: 159,436,487.67795104
logger/vs-pino.js scenario="fields" logger="node-logger" n=100000: 3,619,336.304205216
logger/vs-pino.js scenario="simple" logger="pino" n=100000: 3,398,489.9761368227
logger/vs-pino.js scenario="child" logger="pino" n=100000: 4,489,799.803418606
logger/vs-pino.js scenario="disabled" logger="pino" n=100000: 119,772,384.56038144
logger/vs-pino.js scenario="fields" logger="pino" n=100000: 1,257,930.8609750536

Comment thread lib/logger.js Outdated
Comment thread benchmark/logger/basic-json.js Outdated
Comment thread benchmark/logger/vs-pino.js Outdated
Comment thread lib/logger.js Outdated
@mertcanaltin
Copy link
Copy Markdown
Member Author

mertcanaltin commented Oct 29, 2025

I now learn this feat in Pino

pinojs/pino#2281

I will try add in node:logger

Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js
@mcollina
Copy link
Copy Markdown
Member

This will require support for serializers

@mertcanaltin
Copy link
Copy Markdown
Member Author

mertcanaltin commented Oct 30, 2025

This will require support for serializers

I wonder, should we name them like Pino does (built-in serializers), or go with something like standardSerializers ?

Comment thread test/parallel/test-log-basic.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
Comment thread lib/logger.js Outdated
@mcollina
Copy link
Copy Markdown
Member

mcollina commented Nov 8, 2025

This will require support for serializers

I wonder, should we name them like Pino does (built-in serializers), or go with something like standardSerializers ?

Follow pino and we’ll change it

@mertcanaltin
Copy link
Copy Markdown
Member Author

mertcanaltin commented Nov 10, 2025

This will require support for serializers

I wonder, should we name them like Pino does (built-in serializers), or go with something like standardSerializers ?

Follow pino and we’ll change it

I tryed serializer implement for logger, and some bench result repaired

and fields and simple are experiencing a decline; I will try to resolve these

fields: 3.62M → 2.16M (-40%)
simple: 5.24M → 4.87M (-7%)

previously, the results for the child logger were quite slow at around 70%, but the new results have dropped to 18% and have actually improved significantly.

I continue to try new methods.

➜  node git:(mert/create-logger-api/node-core) ✗ ./node benchmark/logger/vs-pino.js n=100000
logger/vs-pino.js scenario="simple" logger="node-logger" n=100000: 4,868,164.032787085
logger/vs-pino.js scenario="child" logger="node-logger" n=100000: 3,894,327.425314102
logger/vs-pino.js scenario="disabled" logger="node-logger" n=100000: 160,503,080.85663706
logger/vs-pino.js scenario="fields" logger="node-logger" n=100000: 2,157,462.3927336666
logger/vs-pino.js scenario="simple" logger="pino" n=100000: 3,424,706.4418693925
logger/vs-pino.js scenario="child" logger="pino" n=100000: 4,753,595.477010947
logger/vs-pino.js scenario="disabled" logger="pino" n=100000: 122,100,122.10012211
logger/vs-pino.js scenario="fields" logger="pino" n=100000: 1,411,215.99189962
➜  node git:(mert/create-logger-api/node-core) ✗ 

@mertcanaltin
Copy link
Copy Markdown
Member Author

mertcanaltin commented Nov 10, 2025

This will require support for serializers

I wonder, should we name them like Pino does (built-in serializers), or go with something like standardSerializers ?

Follow pino and we’ll change it

I tryed serializer implement for logger, and some bench result repaired

and fields and simple are experiencing a decline; I will try to resolve these

fields: 3.62M → 2.16M (-40%) simple: 5.24M → 4.87M (-7%)

previously, the results for the child logger were quite slow at around 70%, but the new results have dropped to 18% and have actually improved significantly.

I continue to try new methods.

➜  node git:(mert/create-logger-api/node-core) ✗ ./node benchmark/logger/vs-pino.js n=100000
logger/vs-pino.js scenario="simple" logger="node-logger" n=100000: 4,868,164.032787085
logger/vs-pino.js scenario="child" logger="node-logger" n=100000: 3,894,327.425314102
logger/vs-pino.js scenario="disabled" logger="node-logger" n=100000: 160,503,080.85663706
logger/vs-pino.js scenario="fields" logger="node-logger" n=100000: 2,157,462.3927336666
logger/vs-pino.js scenario="simple" logger="pino" n=100000: 3,424,706.4418693925
logger/vs-pino.js scenario="child" logger="pino" n=100000: 4,753,595.477010947
logger/vs-pino.js scenario="disabled" logger="pino" n=100000: 122,100,122.10012211
logger/vs-pino.js scenario="fields" logger="pino" n=100000: 1,411,215.99189962
➜  node git:(mert/create-logger-api/node-core) ✗ 

I inspected pino and I learn some patterns, than I applied this commit and new results! 1ededc7

I used this patterns

removed the cost of serializing bindings in each log for the child logger,
skipped unnecessary serializer checks,
used direct string concatenation instead of object merging,
sped up type checking

simple: 6.06M vs 3.48M ops/s (+74% faster)
child: 5.76M vs 4.41M ops/s (+31% faster)
disabled: 174M vs 146M ops/s (+19% faster)
fields: 2.13M vs 1.36M ops/s (+56% faster)

➜  node git:(mert/create-logger-api/node-core) ✗ ./node benchmark/logger/vs-pino.js n=100000
logger/vs-pino.js scenario="simple" logger="node-logger" n=100000: 6,062,182.962986493
logger/vs-pino.js scenario="child" logger="node-logger" n=100000: 5,758,903.394222795
logger/vs-pino.js scenario="disabled" logger="node-logger" n=100000: 174,026,539.04720467
logger/vs-pino.js scenario="fields" logger="node-logger" n=100000: 2,126,059.37552321
logger/vs-pino.js scenario="simple" logger="pino" n=100000: 3,477,918.037575009
logger/vs-pino.js scenario="child" logger="pino" n=100000: 4,407,389.658686015
logger/vs-pino.js scenario="disabled" logger="pino" n=100000: 145,551,509.22359914
logger/vs-pino.js scenario="fields" logger="pino" n=100000: 1,363,125.197883181
➜  node git:(mert/create-logger-api/node-core) ✗ 

@mertcanaltin
Copy link
Copy Markdown
Member Author

mertcanaltin commented Nov 20, 2025

hello @mcollina do you any comments or suggestions for this end commits, I'm curious 🙏 .

@mertcanaltin mertcanaltin marked this pull request as ready for review November 29, 2025 14:16
@codecov
Copy link
Copy Markdown

codecov bot commented Nov 29, 2025

Codecov Report

❌ Patch coverage is 95.93023% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.64%. Comparing base (adeb14a) to head (f2d9074).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
lib/logger.js 95.84% 23 Missing and 1 partial ⚠️
lib/internal/logger/serializers.js 95.83% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #60468      +/-   ##
==========================================
- Coverage   89.68%   89.64%   -0.05%     
==========================================
  Files         706      708       +2     
  Lines      218222   219823    +1601     
  Branches    41768    42092     +324     
==========================================
+ Hits       195717   197062    +1345     
- Misses      14423    14641     +218     
- Partials     8082     8120      +38     
Files with missing lines Coverage Δ
lib/internal/bootstrap/realm.js 96.22% <100.00%> (+<0.01%) ⬆️
lib/internal/process/pre_execution.js 98.15% <100.00%> (-0.23%) ⬇️
src/node_options.cc 76.63% <100.00%> (+0.02%) ⬆️
src/node_options.h 98.00% <100.00%> (+0.01%) ⬆️
lib/internal/logger/serializers.js 95.83% <95.83%> (ø)
lib/logger.js 95.84% <95.84%> (ø)

... and 42 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mertcanaltin mertcanaltin requested a review from a team as a code owner November 30, 2025 17:53
@mertcanaltin mertcanaltin force-pushed the mert/create-logger-api/node-core branch 2 times, most recently from f5b0108 to 429359b Compare November 30, 2025 18:01
Comment thread lib/logger.js Outdated
Comment thread lib/internal/logger/serializers.js Outdated
@mertcanaltin mertcanaltin force-pushed the mert/create-logger-api/node-core branch from 2d9d338 to de802ea Compare April 15, 2026 05:24
@mertcanaltin mertcanaltin force-pushed the mert/create-logger-api/node-core branch 2 times, most recently from a9d7b73 to b31a049 Compare April 15, 2026 05:27
@mertcanaltin
Copy link
Copy Markdown
Member Author

mertcanaltin commented Apr 15, 2026

Hello everyone, I pushed a small follow-up, moved the hasSubscribers check before input validation so disabled consumers skip parsing (the original intent from @atlowChemi / @Qard).

Plus a few doc bits:
Error exception note in the JSON section, log channel names under LogConsumer and in
diagnostics_channel.md, and a Stability 1.1 marker on logger.child()

@RafaelGSS this round also covers your remaining points (channel names + Stability 1.1 on child), mind taking another look when you get a chance?

Thanks again for all the reviews.

@mertcanaltin mertcanaltin force-pushed the mert/create-logger-api/node-core branch from b31a049 to 3bf29a1 Compare April 15, 2026 05:40
@mertcanaltin mertcanaltin force-pushed the mert/create-logger-api/node-core branch from 3bf29a1 to 0f9833a Compare April 15, 2026 05:41
Comment thread lib/internal/logger/serializers.js
Comment thread lib/internal/logger/serializers.js
Comment thread lib/internal/logger/serializers.js
Comment thread lib/logger.js
mertcanaltin and others added 4 commits April 15, 2026 07:50
Co-authored-by: Chemi Atlow <[email protected]>
Co-authored-by: Chemi Atlow <[email protected]>
Co-authored-by: Chemi Atlow <[email protected]>
@mertcanaltin
Copy link
Copy Markdown
Member Author

@mertcanaltin
Copy link
Copy Markdown
Member Author

Hello again I try this two test in my linux machine and get a passed

mert@lima-default:/Users/mert/Desktop/enjoy/linux-node/node$ out/Release/node test/parallel/test-snapshot-reproducible.js
mert@lima-default:/Users/mert/Desktop/enjoy/linux-node/node$ 

Comment thread lib/logger.js Outdated
// Add consumer fields
const consumerFields = this.#fields;
for (const key of ObjectKeys(consumerFields)) {
json += `,${JSONStringify(key)}:${JSONStringify(consumerFields[key])}`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a if check to ensure consumerFIelds[key] is not undefined, otherwise will produce invalid JSON.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And same question as the other, what we do when fields has keys conflicting with other fields?

Copy link
Copy Markdown
Member Author

@mertcanaltin mertcanaltin Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, skipping undefined in handle() too.

Copy link
Copy Markdown
Member Author

@mertcanaltin mertcanaltin Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And same question as the other, what we do when fields has keys conflicting with other fields?

By design, the last used value takes precedence (can you check the field override priority test?)
The level time msg fields are reserved.

Comment thread lib/logger.js
Comment on lines +298 to +299
const serialized = this.#serializeValue(bindings[key], key);
result += `,${JSONStringify(key)}:${JSONStringify(serialized)}`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this, check for undefined to avoid invalid JSON.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping undefined in #serializeBindings too.

Comment thread lib/logger.js
Comment on lines +96 to +107
/**
* Attach this consumer to log channels
*/
attach() {
for (const level of LEVEL_NAMES) {
if (this[level].enabled) {
const handler = this.handle.bind(this);
this.#handlers[level] = handler;
channels[level].subscribe(handler);
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add flag to ensure we can't attach twice

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added #attached flag, attach are idempotent now.

Comment thread doc/api/logger.md Outdated
Comment on lines +521 to +522
* `stream` {number|string|Object} Output destination. Can be a file
descriptor (number), file path (string), or a writable stream object.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would not be better to create an interface for object and explicitly declare the methods allowed? Eg: JSONConsumer calls flush/flushSync so I can't use it with createWriteStream

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now requires write, flush, flushSync, end, docs list them.

Comment thread lib/logger.js
};

// Apply serializers to additional fields
if (fields) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing validate fields as object when is defined.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoisted validateObject(fields, 'fields') so it runs for all branches.

Comment thread lib/logger.js Outdated
let logFields;

if (isNativeError(msgOrObj)) {
msg = msgOrObj.message;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is undefined, it will likely produce invalid json

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ?? '' fallback for subclasses that leave message undefined.

Comment thread lib/logger.js
logFields = fields ? this.#applySerializers(fields) : kEmptyObject;
} else {
const { msg: extractedMsg, ...restFields } = msgOrObj;
msg = extractedMsg;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this one, can produce invalid json

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already guarded by validateString(msgOrObj.msg) above.

Comment thread lib/logger.js
Comment on lines +194 to +197
const fields = record.fields;
for (const key in fields) {
json += `,${JSONStringify(key)}:${JSONStringify(fields[key])}`;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not serialize the entire fields and just remove first/last bracket?

What happens when fields has keys that conflicts with other fields?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bulk stringify: follow-up with a bench.

ast-wins by design, level, time, msg reserved.

@mertcanaltin mertcanaltin requested review from H4ad and atlowChemi April 19, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. tsc-agenda Issues and PRs to discuss during the meetings of the TSC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.