Skip to content

fs: add per-operation fs diagnostics channels - #65370

Open
mcollina wants to merge 3 commits into
nodejs:mainfrom
mcollina:fs-diagnostics-channel
Open

mcollina wants to merge 3 commits into
nodejs:mainfrom
mcollina:fs-diagnostics-channel

Conversation

@mcollina

@mcollina mcollina commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

Adds built-in node:diagnostics_channel channels for file system operations performed through node:fs and node:fs/promises, addressing #65330.

Each operation gets its own TracingChannel family named fs.<operation> (e.g. fs.open, fs.read, fs.stat), with channels tracing:fs.<operation>:start, :end, :asyncStart, :asyncEnd, and :error. The event payload carries the API (sync/callback/promise), path/dest/fd fields when applicable, plus result/error following TracingChannel conventions. Subscribers can use diagnostics_channel.tracingChannel('fs.open') to subscribe to all events of one operation at once, or subscribe to individual channels by name.

Events are published from the internal shared file system layer rather than the JS wrappers, so captured function references still emit events. Adds documentation and a new test/parallel/test-diagnostics-channel-fs.js.


AI generated, humanly reviewed.

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 18, 2026
@mcollina

Copy link
Copy Markdown
Member Author

@nodejs/diagnostics wdyt? Should we do this or should we have one event for each operation?

Comment thread src/node_file.cc Outdated
if (!path.empty()) {
obj->Set(context,
env->path_string(),
String::NewFromUtf8(isolate,

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.

ToV8Value?

@jasnell

jasnell commented Aug 18, 2026

Copy link
Copy Markdown
Member

SGTM

@timfish

timfish commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Does this impact perf. much when the channels are not subscribed to?
What's the impact like when there is a subscriber that does nothing?

Depending on the impact with a subscriber it might be worth having a channel per operation so that we can be selective over which ops we want to impact?

@logaretm

Copy link
Copy Markdown
Contributor

I think having granularity via multiple channels is better because it allows APMs to choose what to subscribe to without adding overhead to NO-OP events.

@mcollina
mcollina force-pushed the fs-diagnostics-channel branch from 657db86 to 508ffc0 Compare August 28, 2026 18:17
@mcollina mcollina changed the title fs: add built-in fs.operation diagnostics channels fs: add per-operation fs diagnostics channels Aug 28, 2026
Add built-in node:diagnostics_channel channels for file system
operations performed through node:fs and node:fs/promises. Each
operation gets its own TracingChannel family named fs.<operation>,
with channels tracing:fs.<operation>:start, :end, :asyncStart,
:asyncEnd, and :error. The event payload carries the API
(sync/callback/promise), path/dest/fd fields when applicable, plus
result/error following TracingChannel conventions.

Events are published from the internal shared file system layer rather
than the JS wrappers, so captured function references still emit events.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina force-pushed the fs-diagnostics-channel branch from 508ffc0 to 6e1aa61 Compare September 5, 2026 14:17
@mcollina
mcollina marked this pull request as ready for review September 17, 2026 08:09
Define the inline channel helpers in node_file-inl.h so that including
node_file.h alone builds under -Werror=undefined-inline. Hold the cached
channels weakly, as the permission code does, and skip the
instrumentation while building a snapshot: linking a native channel
creates JS channel objects that cannot be serialized.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
The libuv request's file field is private and a union on Windows, so
reading it does not compile there. Every descriptor-based libuv fs
operation takes the descriptor as its first argument, so pick it up
from the call arguments instead.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.38636% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.26%. Comparing base (2d22505) to head (7954bfc).
⚠️ Report is 239 commits behind head on main.

Files with missing lines Patch % Lines
src/node_file-inl.h 70.58% 13 Missing and 12 partials ⚠️
src/node_file.cc 92.59% 1 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65370      +/-   ##
==========================================
+ Coverage   90.19%   90.26%   +0.06%     
==========================================
  Files         770      789      +19     
  Lines      264410   271799    +7389     
  Branches    50243    51879    +1636     
==========================================
+ Hits       238479   245333    +6854     
+ Misses      16926    16919       -7     
- Partials     9005     9547     +542     
Files with missing lines Coverage Δ
src/node_file.h 81.96% <100.00%> (+3.53%) ⬆️
src/node_file.cc 75.64% <92.59%> (+0.76%) ⬆️
src/node_file-inl.h 81.59% <70.58%> (-3.47%) ⬇️

... and 202 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.

* `fd` {number|undefined} The file descriptor for operations that operate on
an existing file descriptor, such as `read`, `write`, `fsync`, or `close`.

Large read/write buffers are not copied into the event payload. The `start`

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 include them? Not copy, but pass the reference.

Large read/write buffers are not copied into the event payload. The `start`
and `asyncStart` events carry no `result` or `error`; the `end` and `asyncEnd`
events carry the `result` of the operation, and the `error` event carries the
`error`, following the [TracingChannel Channels][] conventions.

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.

The convention includes result fields in asyncStart https://nodejs.org/docs/latest/api/diagnostics_channel.html#asyncstartevent

In fact this is necessary for many kinds of instrumentation.

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.

This one is the only blocker, the rest can be handled in future PRs if desired.

`error`, following the [TracingChannel Channels][] conventions.

Operations performed through streams (`fs.createReadStream` and
`fs.createWriteStream`), most `FileHandle` methods, and the `fs.readFile`

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 FileHandle?

The streams should be doable with diagnostics channels, but maybe just not TracingChannels.

function reference was captured before subscribing or whether the operation
uses the callback, promise, or synchronous API.

Each event carries an object with the following common 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.

Other args may be necessary for o11y, for example the args to chmod/chown other than the path.

@logaretm logaretm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few comments on top of what @bengl brought up, so doing the channels from C++ as it is now does a few things:

  1. Skips the single context object correlation, I think this is a major blocker. Otherwise APMs cannot use this to create spans and will mostly rely on external maps to guess the correlation.
  2. It also means the context propagation isn't there, because it publishes without runStores, so bindStore() on these channels does nothing. Technically there isn't any nested operations here so it may not matter. So this is more of a nit.
  3. The error event issue that I brought below, the error event is not a terminal event, there must be always an end or asyncEnd after it.

I think switching to the JS API here would solve all of these issues and make sure the publishing semantics are consistent/correct.

Comment thread src/node_file.cc
Isolate* isolate = env->isolate();
HandleScope scope(isolate);
Local<Context> context = env->context();
Local<Object> obj = Object::New(isolate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Each event gets its own payload object here. Subscribers usually store per-operation state (like a span) on the context object in start and read it back in asyncEnd or error.

With separate objects there's no way to corrolate those events, especially for concurrent calls on the same path.

Could one object be created per operation and reused for every event? If not then it might be better to use the JS API here for those channels.

Comment on lines +1682 to +1686
Operations performed through streams (`fs.createReadStream` and
`fs.createWriteStream`), most `FileHandle` methods, and the `fs.readFile`
fast path (which batches open/stat/read/close into a single background job)
are not covered by these channel families, and may not emit the full set of
events.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This says the readFile fast path isn't covered, but the paragraph above says every public fs operation is.

Also, writeFile and fs.promises.readFile skip the channels, and so do readFileSync with an encoding and existsSync. Those are the fs calls we instrument the most.

I think the PR should cover them too, so all the public fs APIs emit.

Comment thread src/node_file-inl.h
req_wrap->dest_p);
PublishFSOperationEvent(env,
*channels,
FSOperationChannel::kError,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On failure this emits error and then stops, with no end after it. traceSync always emits end after error, for APMs end is important because that's when they end the span or flush any telemetry.

Comment thread src/node_file.cc
}

void FSReqCallback::Reject(Local<Value> reject) {
PublishFSOpCompletionEvent(this, FSOperationChannel::kError, "error", reject);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same for callback/promise calls: error is emitted but asyncEnd never follows it, while traceCallback/tracePromise always emit asyncEnd after error.

@Qard Qard left a comment

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.

As has already been stated, this needs a shared context object between tracing channel events, and we'd also need a proper runStores for this to be complete TracingChannel behaviour.

However, it's also worth pointing out it'd likely actually be faster to just do all this in JS as also already suggested as the entire data and subscription model is already in JS. Putting it in native code adds an extra native barrier cross for not really any reason. We should generally avoid native-side publishing except for cases where there is no viable hook point from JS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants