feat: implement diagnostic channels for observability
Actual Diff is here: https://github.com/logaretm/node-redis/pull/1/changes
This is currently waiting on #3110 to get merged and then it will show the true size of changes
Re-implements otel metrics tracing using diagnostic and tracing channels, while setting up more channels for tracing.
The key shift: core code now describes what is happening, not what to compute.
Before, every instrumented code path had to know about OTel, creating closures, capturing timestamps, building attribute maps, recording histograms. The code was telling the system how to measure itself. And every metric class needed a noop counterpart for when its metric group was disabled.
Now core code says "a command started," "a connection closed," "an error occurred." Subscribers like OTel, Sentry, a debug logger, anything can independently decide what to compute from those events.
The domain knowledge in #3110 (which metrics to emit, which semantic conventions to follow, how to classify errors) is preserved. It just moved from being inline in the hot path to being a subscriber that opts in when needed.
Noops are gone. When a metric group is disabled, no subscription is created. No subscribers means hasSubscribers is false, which means trace() and publish() skip the channel entirely, zero cost by design, not by maintaining parallel noop implementations. Both noop-metrics.ts and noop-meter.ts are deleted — when OTel is not initialized, no OTelMetrics instance is created, no instruments are registered, no subscriptions exist.
Two functions for all observability:
trace()— wraps async operations viaTracingChannel(commands, batches, connections, pool waits)publish()— emits point events viadc.channel()(errors, cache hits/misses, pubsub, maintenance, connection lifecycle)
Both use factory callbacks + hasSubscribers checks for zero overhead when no APM subscribes. This is true for metrics, if the user never calls the opentelemetry plugin then no channel subscriptions occur, and no object allocation happens compared to what we had before.
Channels
TracingChannel (async lifecycle: start/end/asyncStart/asyncEnd/error)
This sets up tracing with spans in the future, but still can be used by metrics or logs.
| Channel | Wraps |
|---|---|
node-redis:command | Individual command execution |
node-redis:batch | MULTI/PIPELINE as a whole |
node-redis:connect | Client connection |
node-redis:connection:wait | Pool wait for available client |
Single instance events
This is what metrics usually use, not suitable for traces but can also be used by logs.
| Channel | Emits |
|---|---|
node-redis:connection:ready | Connection established (includes create time) |
node-redis:connection:closed | Connection closed (includes reason) |
node-redis:connection:relaxed-timeout | Maintenance timeout relaxation |
node-redis:connection:handoff | Connection handed off to new node |
node-redis:error | Client/cluster errors |
node-redis:maintenance | Enterprise maintenance notifications |
node-redis:pubsub | PubSub messages in/out |
node-redis:cache:request | Client-side cache hit/miss |
node-redis:cache:eviction | Cache eviction |
node-redis:command:reply | Command reply (for stream lag, pubsub out) |
What this enables
Any APM can subscribe without monkey-patching or depending on OTel:
import dc from 'node:diagnostics_channel';
import { CHANNELS } from 'redis';
// Create spans
dc.tracingChannel(CHANNELS.TRACE_COMMAND).subscribe({
start(ctx) { /* start span */ },
asyncEnd(ctx) { /* end span */ },
error(ctx) { /* record error, end span */ },
});
// Track errors
dc.channel(CHANNELS.ERROR).subscribe((ctx) => { /* record error metric */ });
Key Changes
- Zero
OTelMetricsimports in core code - Zero noop files — both
noop-metrics.tsandnoop-meter.tsdeleted - Existing production code: +603 / -1,152 = -549 lines
- New channel infrastructure (
tracing.ts): +280 lines - Net production code: -269 lines
- All channel names exported via
CHANNELSconst map - All event types exported for consumer typing
Setting Up OTEL Tracing
Almost Everything Is Already Done
- Async context propagation: tracePromise (used by trace()) already manages AsyncLocalStorage context. The OTel context manager piggybacks on this. No context.with(trace.setSpan(...)) needed, spans created in start are automatically the active span for downstream operations.
- Sanitized args:
ctx.argsis already sanitized bysanitizeArgs(), sodb.query.textis just a simple join (e.g., SET mykey ?). - Error classification:
getErrorInfo()already extracts errorType, category, and statusCode for span attributes.
Here is an example of tracing for command calls:
import { trace, context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
import dc from "node:diagnostics_channel";
import { CHANNELS } from "redis";
const tracer = trace.getTracer("node-redis");
const commandTC = dc.tracingChannel(CHANNELS.TRACE_COMMAND);
commandTC.subscribe({
start(ctx) {
const span = tracer.startSpan(ctx.command, {
kind: SpanKind.CLIENT,
attributes: {
"db.system.name": "redis",
"db.operation.name": ctx.command,
"db.query.text": ctx.args.join(" "),
"db.namespace": String(ctx.database),
"server.address": ctx.serverAddress,
"server.port": ctx.serverPort,
},
});
// Set span on OTel context — AsyncLocalStorage propagates it
// to asyncEnd/error automatically via TracingChannel
trace.setSpan(context.active(), span);
},
asyncEnd() {
trace.getSpan(context.active())?.end();
},
error(ctx) {
const span = trace.getSpan(context.active());
if (span) {
span.recordException(ctx.error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: ctx.error?.message,
});
span.end();
}
},
});
Note that the exact implementation can differ since we may need to use bindStore on the AsyncLocalStorage instance inside OTEL itself, but should be straight forward.
[!NOTE]
Medium Risk
Touches core command execution, connection, pooling, and cache paths to emit diagnostics events, so regressions could impact performance or error handling despite the largely additive nature. Also raises the minimum supported Node.js version to >=18.19.0, which can be a breaking runtime requirement for some consumers.
Overview
Adds first-class observability via Node.js diagnostics_channel/TracingChannel: core client paths now emit tracing events for commands/batches/connect and point events for connections, errors, maintenance, pubsub, client-side cache, command replies, and pool waits (with argument sanitization).
Refactors OpenTelemetry metrics to be implemented as diagnostics_channel subscribers (removing inline metric recording/noop implementations), updates tests accordingly, and exports CHANNELS plus payload types from @redis/client with new documentation. Updates package engines.node across packages to require >= 18.19.0.
Written by Cursor Bugbot for commit dc7da510733d278acdf3a1959297be3c8b24e6be. This will update automatically on new commits. Configure here.