diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index e63f23829f90..b745dad594db 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1636,6 +1636,89 @@ diagnosticsChannel.subscribe('crypto.fips.indicator', (message) => { }); ``` +#### Filesystem + +> Stability: 1 - Experimental + +These channels are emitted for file system operations performed through +`node:fs` and `node:fs/promises`. Each operation has its own +[`TracingChannel`][] family named `fs.`, where `` is a +stable operation name such as `open`, `read`, `write`, `stat`, `readdir`, or +`realpath`. Subscribers can use [`diagnostics_channel.tracingChannel()`][] to +subscribe to all events of a given operation at once: + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channel = diagnostics_channel.tracingChannel('fs.open'); +channel.subscribe({ + start: (event) => console.log('start', event), + end: (event) => console.log('end', event), + error: (event) => console.log('error', event), +}); +``` + +The events are published from the internal file system implementation, so they +are observed for every public `fs` operation regardless of whether the +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: + +* `api` {string} The API that performed the operation: `'sync'`, `'callback'`, + or `'promise'`. +* `path` {string|undefined} The path argument for path-based operations, or + the source path for operations with a destination. +* `dest` {string|undefined} The destination argument for operations that + accept one, such as `rename`, `link`, `symlink`, or `copyFile`. +* `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` +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. + +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. + +##### Event: `'tracing:fs.:start'` + +Emitted synchronously when an operation begins, before the operation is +submitted. For synchronous operations this is followed by `end` (or `error`); +for asynchronous operations it is followed by `end` and then `asyncStart`/ +`asyncEnd` (or `error`). + +##### Event: `'tracing:fs.:end'` + +* `result` {any} The result of the operation. + +Emitted when the operation completes. For synchronous operations this carries +the operation `result`; for asynchronous operations it is emitted when the +operation is submitted and carries no `result` (the `result` is delivered on +the `asyncEnd` event). + +##### Event: `'tracing:fs.:asyncStart'` + +Emitted when the asynchronous work for an operation begins (when the +completion callback is invoked). + +##### Event: `'tracing:fs.:asyncEnd'` + +* `result` {any} The result of the operation. + +Emitted when the asynchronous work for an operation completes, carrying the +operation `result`. + +##### Event: `'tracing:fs.:error'` + +* `error` {Error} The error that caused the operation to fail. + +Emitted when an operation fails. + #### HTTP > Stability: 1 - Experimental diff --git a/src/env_properties.h b/src/env_properties.h index 3eb1db96940b..6f4c89682d0a 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -87,6 +87,7 @@ V(allow_bare_named_params_string, "allowBareNamedParameters") \ V(allow_unknown_named_params_string, "allowUnknownNamedParameters") \ V(alpn_callback_string, "ALPNCallback") \ + V(api_string, "api") \ V(args_string, "args") \ V(arguments_string, "arguments") \ V(async_ids_stack_string, "async_ids_stack") \ diff --git a/src/node_file-inl.h b/src/node_file-inl.h index ebd97c1c8c52..84abc5c1b3f6 100644 --- a/src/node_file-inl.h +++ b/src/node_file-inl.h @@ -214,6 +214,7 @@ FSReqPromise::FSReqPromise(BindingData* binding_data, template void FSReqPromise::Reject(v8::Local reject) { finished_ = true; + PublishFSOpCompletionEvent(this, FSOperationChannel::kError, "error", reject); v8::HandleScope scope(env()->isolate()); InternalCallbackScope callback_scope(this); v8::Local value; @@ -232,6 +233,8 @@ void FSReqPromise::Reject(v8::Local reject) { template void FSReqPromise::Resolve(v8::Local value) { finished_ = true; + PublishFSOpCompletionEvent(this, FSOperationChannel::kAsyncEnd, "result", + value); v8::HandleScope scope(env()->isolate()); InternalCallbackScope callback_scope(this); v8::Local val; @@ -311,6 +314,7 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo& args, result = FSReqPromise::New(binding_data, use_bigint); } + result->set_is_promise(true); } } if (result != nullptr) { @@ -328,6 +332,21 @@ FSReqBase* AsyncDestCall(Environment* env, FSReqBase* req_wrap, Func fn, Args... fn_args) { CHECK_NOT_NULL(req_wrap); req_wrap->Init(syscall, dest, len, enc); + BindingData* binding = req_wrap->binding_data(); + const char* api = req_wrap->is_promise() ? "promise" : "callback"; + FSOperationChannels* channels = nullptr; + // See SyncCallAndThrowIf: instrumentation is unsafe with a pending + // exception. + if (binding != nullptr && !env->isolate()->HasPendingException()) { + channels = &GetFSOperationChannels(binding, env, syscall); + req_wrap->set_op_channels(channels); + if (FSOperationChannelHasSubscribers(*channels, + FSOperationChannel::kStart)) { + PublishFSOperationEvent(env, *channels, FSOperationChannel::kStart, + api, nullptr, req_wrap->data(), -1, nullptr, + v8::Local()); + } + } int err = req_wrap->Dispatch(fn, fn_args..., after); if (err < 0) { uv_fs_t* uv_req = req_wrap->req(); @@ -335,6 +354,18 @@ FSReqBase* AsyncDestCall(Environment* env, FSReqBase* req_wrap, uv_req->path = nullptr; after(uv_req); // after may delete req_wrap if there is an error req_wrap = nullptr; + } else if (channels != nullptr && + AnyFSOperationChannelHasSubscribers(*channels)) { + const char* path = req_wrap->req()->path; + int fd = -1; + if (OperationUsesFd(req_wrap->req()->fs_type)) fd = req_wrap->req()->file; + req_wrap->set_fd(fd); + // The path is captured for the completion events; it requires a copy + // since the uv request is cleaned up before they fire. + req_wrap->set_op_path(path == nullptr ? std::string() : path); + PublishFSOperationEvent(env, *channels, FSOperationChannel::kEnd, api, + path, req_wrap->data(), fd, nullptr, + v8::Local()); } return req_wrap; } @@ -389,7 +420,54 @@ int SyncCallAndThrowIf(Predicate should_throw, Func fn, Args... args) { env->PrintSyncTrace(); + BindingData* binding = Realm::GetBindingData(env->context()); + FSOperationChannels* channels = nullptr; + // The instrumentation creates V8 objects and may run subscribers, neither + // of which is safe with a pending exception (a multi-step operation keeps + // going after a failed step to clean up, e.g. write + close). + if (binding != nullptr && !env->isolate()->HasPendingException()) { + channels = &GetFSOperationChannels(binding, env, req_wrap->syscall_p); + if (FSOperationChannelHasSubscribers(*channels, + FSOperationChannel::kStart)) { + PublishFSOperationEvent(env, *channels, FSOperationChannel::kStart, + "sync", req_wrap->path_p, req_wrap->dest_p, -1, + nullptr, v8::Local()); + } + } int result = fn(nullptr, &(req_wrap->req), args..., nullptr); + if (channels != nullptr) { + if (should_throw(result)) { + // The error object is only built when someone is listening; the throw + // path below creates its own copy. + if (FSOperationChannelHasSubscribers(*channels, + FSOperationChannel::kError)) { + int fd = -1; + if (OperationUsesFd(req_wrap->req.fs_type)) fd = req_wrap->req.file; + v8::Local error = UVException(env->isolate(), + result, + req_wrap->syscall_p, + nullptr, + req_wrap->path_p, + req_wrap->dest_p); + PublishFSOperationEvent(env, *channels, FSOperationChannel::kError, + "sync", req_wrap->path_p, req_wrap->dest_p, + fd, "error", error); + } + } else if (FSOperationChannelHasSubscribers(*channels, + FSOperationChannel::kEnd)) { + int fd = -1; + if (OperationUsesFd(req_wrap->req.fs_type)) fd = req_wrap->req.file; + PublishFSOperationEvent(env, + *channels, + FSOperationChannel::kEnd, + "sync", + req_wrap->path_p, + req_wrap->dest_p, + fd, + "result", + v8::Integer::New(env->isolate(), result)); + } + } if (should_throw(result)) { env->ThrowUVException(result, req_wrap->syscall_p, diff --git a/src/node_file.cc b/src/node_file.cc index de199dfea1a1..3e524fcfd337 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -93,6 +93,125 @@ using v8::Uint8Array; using v8::Undefined; using v8::Value; +// Event names for the built-in per-operation fs tracing channel families, +// one per FSOperationChannel in node_file.h. Each operation gets its own +// channel family named `tracing:fs.:`. +const char* const kFSOperationEventNames[kNumFSOperationChannels] = { + "start", + "end", + "asyncStart", + "asyncEnd", + "error", +}; + +FSOperationChannels& GetFSOperationChannels(BindingData* binding, + Environment* env, + const char* operation) { + auto& names = binding->fs_op_channel_names_; + for (size_t i = 0; i < names.size(); i++) { + if (names[i] == operation) { + return *binding->fs_op_channel_sets_[i]; + } + } + auto set = std::make_unique(); + for (size_t i = 0; i < kNumFSOperationChannels; i++) { + std::string name = std::string("tracing:fs.") + operation + ":" + + kFSOperationEventNames[i]; + (*set)[i] = diagnostics_channel::Channel::Get(env, name); + } + names.push_back(operation); + binding->fs_op_channel_sets_.push_back(std::move(set)); + return *binding->fs_op_channel_sets_.back(); +} + +void PublishFSOperationEvent(Environment* env, + FSOperationChannels& channels, + FSOperationChannel channel, + const char* api, + const char* path, + const char* dest, + int fd, + const char* value_key, + Local value) { + const size_t index = static_cast(channel); + CHECK_LT(index, kNumFSOperationChannels); + diagnostics_channel::Channel* ch = channels[index].get(); + if (ch == nullptr || !ch->HasSubscribers()) { + return; + } + + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + Local context = env->context(); + Local obj = Object::New(isolate); + obj->Set(context, + env->api_string(), + ToV8Value(context, api, isolate).ToLocalChecked()) + .Check(); + if (path != nullptr && path[0] != '\0') { + obj->Set(context, + env->path_string(), + ToV8Value(context, path, isolate).ToLocalChecked()) + .Check(); + } + if (dest != nullptr && dest[0] != '\0') { + obj->Set(context, + env->dest_string(), + ToV8Value(context, dest, isolate).ToLocalChecked()) + .Check(); + } + if (fd != -1) { + obj->Set(context, env->fd_string(), Integer::New(isolate, fd)).Check(); + } + if (value_key != nullptr && !value.IsEmpty()) { + obj->Set(context, OneByteString(isolate, value_key), value).Check(); + } + ch->Publish(env, obj); +} + +void PublishFSOpCompletionEvent(FSReqBase* req_wrap, + FSOperationChannel channel, + const char* value_key, + Local value) { + FSOperationChannels* channels = req_wrap->op_channels(); + if (channels == nullptr || + !FSOperationChannelHasSubscribers(*channels, channel)) { + return; + } + const char* api = req_wrap->is_promise() ? "promise" : "callback"; + PublishFSOperationEvent(req_wrap->env(), + *channels, + channel, + api, + req_wrap->op_path().c_str(), + req_wrap->data(), + req_wrap->fd(), + value_key, + value); +} + +// Returns true if the libuv fs request type operates on an existing file +// descriptor (as opposed to taking a path). These are the request types whose +// `file` field holds the input descriptor. +bool OperationUsesFd(uv_fs_type fs_type) { + switch (fs_type) { + case UV_FS_CLOSE: + case UV_FS_READ: + case UV_FS_WRITE: + case UV_FS_FSTAT: + case UV_FS_FTRUNCATE: + case UV_FS_FDATASYNC: + case UV_FS_FSYNC: + case UV_FS_FUTIME: + case UV_FS_FCHMOD: + case UV_FS_FCHOWN: + case UV_FS_SENDFILE: + return true; + default: + return false; + } +} + #ifndef S_ISDIR #define S_ISDIR(mode) (((mode)&S_IFMT) == S_IFDIR) #endif @@ -227,6 +346,7 @@ FSReqBase::~FSReqBase() = default; void FSReqBase::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("continuation_data", continuation_data_); + tracker->TrackField("op_path", op_path_); } // The FileHandle object wraps a file descriptor and will close it on garbage @@ -734,6 +854,7 @@ int FileHandle::DoShutdown(ShutdownWrap* req_wrap) { } void FSReqCallback::Reject(Local reject) { + PublishFSOpCompletionEvent(this, FSOperationChannel::kError, "error", reject); MakeCallback(env()->oncomplete_string(), 1, &reject); } @@ -746,6 +867,8 @@ void FSReqCallback::ResolveStatFs(const uv_statfs_t* stat) { } void FSReqCallback::Resolve(Local value) { + PublishFSOpCompletionEvent(this, FSOperationChannel::kAsyncEnd, "result", + value); Local argv[2]{Null(env()->isolate()), value}; MakeCallback(env()->oncomplete_string(), value->IsUndefined() ? 1 : arraysize(argv), @@ -774,6 +897,10 @@ FSReqAfterScope::FSReqAfterScope(FSReqBase* wrap, uv_fs_t* req) handle_scope_(wrap->env()->isolate()), context_scope_(wrap->env()->context()) { CHECK_EQ(wrap_->req(), req); + // The async work for the operation has completed; the continuation window + // begins here. + PublishFSOpCompletionEvent(wrap, FSOperationChannel::kAsyncStart, nullptr, + Local()); } FSReqAfterScope::~FSReqAfterScope() { diff --git a/src/node_file.h b/src/node_file.h index fab01a4c17b8..571e8fa8fdfb 100644 --- a/src/node_file.h +++ b/src/node_file.h @@ -3,8 +3,12 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include +#include #include +#include #include "aliased_buffer.h" +#include "node_diagnostics_channel.h" #include "node_messaging.h" #include "node_snapshotable.h" #include "stream_base.h" @@ -56,6 +60,86 @@ enum class FsStatFsOffset { constexpr size_t kFsStatFsBufferLength = static_cast(FsStatFsOffset::kFsStatFsFieldsNumber); +// Built-in diagnostics channels for filesystem operations. Each operation has +// its own channel family named `tracing:fs.:`, following the +// tracing channel convention so that subscribers can use +// `diagnostics_channel.tracingChannel('fs.open')` to subscribe to all events +// of one operation, or subscribe to individual channels by name. +enum class FSOperationChannel { + kStart, + kEnd, + kAsyncStart, + kAsyncEnd, + kError, + kChannelCount, +}; + +static constexpr size_t kNumFSOperationChannels = + static_cast(FSOperationChannel::kChannelCount); + +class FSReqBase; +class BindingData; + +// The event channels of one operation's channel family, all created when the +// operation is first seen. +using FSOperationChannels = + std::array, + kNumFSOperationChannels>; + +// Returns the channel set for `operation`, fetching it once per call site so +// that publishing several events for one operation costs a single lookup. +// `operation` must be a string literal: the cache is keyed on its identity. +// The returned reference stays valid for the lifetime of the BindingData. +FSOperationChannels& GetFSOperationChannels(BindingData* binding, + Environment* env, + const char* operation); + +// Returns true if the given event channel has subscribers. Check before +// calling PublishFSOperationEvent so the no-subscriber fast path stays free +// of out-of-line calls and payload preparation. +inline bool FSOperationChannelHasSubscribers(FSOperationChannels& channels, + FSOperationChannel channel) { + diagnostics_channel::Channel* ch = + channels[static_cast(channel)].get(); + return ch != nullptr && ch->HasSubscribers(); +} + +// Returns true if any of the operation's event channels has subscribers. +inline bool AnyFSOperationChannelHasSubscribers(FSOperationChannels& channels) { + for (size_t i = 0; i < kNumFSOperationChannels; i++) { + diagnostics_channel::Channel* ch = channels[i].get(); + if (ch != nullptr && ch->HasSubscribers()) return true; + } + return false; +} + +// Publishes an event on one of the operation's channels. Payload fields are +// set only when applicable: `path`/`dest` (which may be null or empty) for +// path-based operations, `fd` for operations that operate on an existing file +// descriptor, and `result`/`error` on the matching end/error channels, +// following the TracingChannel conventions. +void PublishFSOperationEvent(Environment* env, + FSOperationChannels& channels, + FSOperationChannel channel, + const char* api, + const char* path, + const char* dest, + int fd, + const char* value_key, + v8::Local value); + +// Publishes a completion event (asyncStart/asyncEnd/error) for an in-flight +// async fs operation, deriving the event context from the request wrap. +void PublishFSOpCompletionEvent(FSReqBase* req_wrap, + FSOperationChannel channel, + const char* value_key, + v8::Local value); + +// Returns true if the libuv fs request type operates on an existing file +// descriptor (as opposed to taking a path), i.e. its `file` field holds the +// input descriptor. +bool OperationUsesFd(uv_fs_type fs_type); + class BindingData : public SnapshotableObject { public: struct InternalFieldInfo : public node::InternalFieldInfoBase { @@ -81,6 +165,17 @@ class BindingData : public SnapshotableObject { AliasedFloat64Array statfs_field_array; AliasedBigInt64Array statfs_field_bigint_array; + // Lazily created built-in per-operation fs tracing channel sets, recreated + // after snapshot deserialization (fresh BindingData). The names vector is + // scanned by pointer identity of the operation name string literal — the + // number of distinct operations is small and hot operations sit near the + // front, so the scan beats hashing. Distinct literals with equal contents + // would only create duplicate entries resolving to the same channels. The + // sets are heap-allocated so references handed out by + // GetFSOperationChannels stay stable. + std::vector fs_op_channel_names_; + std::vector> fs_op_channel_sets_; + std::vector> file_handle_read_wrap_freelist; SERIALIZABLE_OBJECT_METHODS() @@ -164,6 +259,25 @@ class FSReqBase : public ReqWrap { bool is_plain_open() const { return is_plain_open_; } bool with_file_types() const { return with_file_types_; } + // Whether this request was created for the promise-based fs API. + bool is_promise() const { return is_promise_; } + void set_is_promise(bool value) { is_promise_ = value; } + // The operation's tracing channel set, captured at dispatch time so + // completion events publish without further lookups. Null for requests + // dispatched outside the instrumented call paths, which therefore publish + // no events. Points into the BindingData-owned cache, which outlives the + // request. + FSOperationChannels* op_channels() const { return op_channels_; } + void set_op_channels(FSOperationChannels* channels) { + op_channels_ = channels; + } + // Path and file descriptor captured at dispatch time, used to publish + // fs operation tracing events even after the uv request is cleaned up. + const std::string& op_path() const { return op_path_; } + void set_op_path(std::string value) { op_path_ = std::move(value); } + int fd() const { return fd_; } + void set_fd(int value) { fd_ = value; } + void set_is_plain_open(bool value) { is_plain_open_ = value; } void set_with_file_types(bool value) { with_file_types_ = value; } @@ -192,6 +306,10 @@ class FSReqBase : public ReqWrap { bool use_bigint_ = false; bool is_plain_open_ = false; bool with_file_types_ = false; + bool is_promise_ = false; + FSOperationChannels* op_channels_ = nullptr; + std::string op_path_; + int fd_ = -1; const char* syscall_ = nullptr; BaseObjectPtr binding_data_; diff --git a/test/parallel/test-diagnostics-channel-fs.js b/test/parallel/test-diagnostics-channel-fs.js new file mode 100644 index 000000000000..3535cc15d05e --- /dev/null +++ b/test/parallel/test-diagnostics-channel-fs.js @@ -0,0 +1,138 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const dc = require('node:diagnostics_channel'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const tmpdir = require('node:os').tmpdir(); +const { join } = require('node:path'); + +const events = []; +const eventTypes = ['start', 'end', 'asyncStart', 'asyncEnd', 'error']; +const operations = ['open', 'read', 'stat', 'rename']; +for (const operation of operations) { + for (const type of eventTypes) { + dc.channel(`tracing:fs.${operation}:${type}`).subscribe((event) => { + events.push({ operation, type, event }); + }); + } +} + +const target = join(tmpdir, `node-test-diagnostics-channel-fs-${process.pid}-${Date.now()}`); +const source = join(tmpdir, `node-test-diagnostics-channel-fs-src-${process.pid}-${Date.now()}`); + +function byOperation(operation) { + return events.filter((e) => e.operation === operation); +} + +// Sync operations publish start/end (or error) with api: 'sync'. +fs.writeFileSync(target, 'hello'); + +const openSync = byOperation('open'); +assert.ok(openSync.length >= 2, 'expected open start/end for writeFileSync'); +const openStart = openSync[0]; +const openEnd = openSync[openSync.length - 1]; +assert.strictEqual(openStart.type, 'start'); +assert.strictEqual(openStart.event.api, 'sync'); +assert.strictEqual(openStart.event.path, target); +assert.strictEqual(openStart.event.operation, undefined); +assert.strictEqual(openEnd.type, 'end'); +assert.strictEqual(openEnd.event.api, 'sync'); +assert.strictEqual(openEnd.event.path, target); +assert.strictEqual(typeof openEnd.event.result, 'number'); // the returned fd + +function testCallbackRead(done) { + // Callback operations publish start, end, asyncStart, asyncEnd with api. + // fs.read is used directly because fs.readFile takes a one-shot fast path + // that batches open/fstat/read/close in a single background job and is not + // covered by these channels. + fs.open(target, 'r', common.mustSucceed((fd) => { + const buffer = Buffer.alloc(16); + fs.read(fd, buffer, 0, buffer.length, 0, common.mustSucceed((bytesRead) => { + assert.strictEqual(buffer.toString('utf8', 0, bytesRead), 'hello'); + + const readEvents = byOperation('read'); + assert.ok(readEvents.length >= 4, + 'expected read start/end/asyncStart/asyncEnd'); + const readStart = readEvents[0]; + const readEnd = readEvents[1]; + const readAsyncStart = readEvents[2]; + const readAsyncEnd = readEvents[readEvents.length - 1]; + + assert.strictEqual(readStart.type, 'start'); + assert.strictEqual(readStart.event.api, 'callback'); + assert.strictEqual(readStart.event.fd, undefined); + assert.strictEqual(readEnd.type, 'end'); + assert.strictEqual(readEnd.event.api, 'callback'); + assert.strictEqual(readEnd.event.fd, fd); + assert.strictEqual(readEnd.event.result, undefined); + assert.strictEqual(readAsyncStart.type, 'asyncStart'); + assert.strictEqual(readAsyncStart.event.api, 'callback'); + assert.strictEqual(readAsyncStart.event.fd, fd); + assert.strictEqual(readAsyncEnd.type, 'asyncEnd'); + assert.strictEqual(readAsyncEnd.event.api, 'callback'); + assert.strictEqual(readAsyncEnd.event.fd, fd); + assert.strictEqual(readAsyncEnd.event.result, bytesRead); + + fs.closeSync(fd); + done(); + })); + })); +} + +function testPromiseStat(done) { + // Promise operations publish the same events with api: 'promise'. + fsp.stat(target).then(common.mustCall((stats) => { + assert.strictEqual(typeof stats.size, 'number'); + + const statEvents = byOperation('stat'); + assert.ok(statEvents.length >= 4, + 'expected stat start/end/asyncStart/asyncEnd'); + const statAsyncEnd = statEvents[statEvents.length - 1]; + assert.strictEqual(statAsyncEnd.type, 'asyncEnd'); + assert.strictEqual(statAsyncEnd.event.api, 'promise'); + assert.strictEqual(statAsyncEnd.event.path, target); + assert.ok('result' in statAsyncEnd.event); + done(); + })); +} + +function testRenameDest(done) { + // Destination operations carry the `dest` field. + fs.rename(target, source, common.mustSucceed(() => { + const renameEvents = byOperation('rename'); + assert.ok(renameEvents.length >= 4, + 'expected rename start/end/asyncStart/asyncEnd'); + const renameAsyncEnd = renameEvents[renameEvents.length - 1]; + assert.strictEqual(renameAsyncEnd.type, 'asyncEnd'); + assert.strictEqual(renameAsyncEnd.event.path, target); + assert.strictEqual(renameAsyncEnd.event.dest, source); + done(); + })); +} + +function testErrorEvent(done) { + // Failed operations publish an `error` event on the operation's own + // channel family, carrying the error object. + fs.open('/nonexistent-node-diagnostics-channel-fs', 'r', common.mustCall((err) => { + assert.ok(err); + const errorEvents = byOperation('open').filter((e) => e.type === 'error'); + const error = errorEvents[errorEvents.length - 1]; + assert.ok(error, 'expected an open error event'); + assert.strictEqual(error.event.api, 'callback'); + assert.strictEqual(error.event.path, '/nonexistent-node-diagnostics-channel-fs'); + assert.strictEqual(error.event.error.code, 'ENOENT'); + done(); + })); +} + +testCallbackRead(common.mustCall(() => { + testPromiseStat(common.mustCall(() => { + testRenameDest(common.mustCall(() => { + testErrorEvent(common.mustCall(() => { + fs.rm(source, { force: true }, common.mustSucceed()); + })); + })); + })); +}));