feat: Implement `TracingChannel` support
This PR implements tracing channels for command and connect operations. This allows APM libraries (OTEL, Datadog, Sentry) to instrument redis commands without monkey-patching.
It also enables users to setup their own custom instrumentations and analytics.
This is a port of redis/node-redis#3195 to ioredis, based on the proposal in redis/node-redis#2590.
Tracing Channels
All channels in this PR use the Node.js TracingChannel API, which provides start, end, asyncStart, asyncEnd, and error sub-channels automatically.
| TracingChannel | Tracks | Context fields |
|---|---|---|
ioredis:command | Individual Redis command execution from queue to reply (standalone, MULTI, and pipeline) | command, args, database, serverAddress, serverPort, and optionally batchMode, batchSize for MULTI/pipeline commands |
ioredis:connect | Client connection lifecycle (initial connections and reconnections) | serverAddress, serverPort, connectionEpoch |
Context Properties
The payload sent in the trace events closely follows what OTEL currently extracts as attributes in their instrumentations:
| Field | Source | OTEL attribute it enables |
|---|---|---|
command | command name | db.operation.name |
args | full args array | db.query.text (APM serializes/sanitizes) |
database | selected DB | db.namespace |
serverAddress | socket options host (or path for IPC) | server.address |
serverPort | socket options port (undefined for IPC) | server.port |
For batch commands we include these properties:
| Field | Source | OTEL attribute it enables |
|---|---|---|
batchMode | 'PIPELINE' or 'MULTI' | db.operation.name prefix |
batchSize | number of user commands (excludes MULTI/EXEC protocol wrappers) | db.operation.batch.size |
For connect events:
| Field | Source | Description |
|---|---|---|
connectionEpoch | internal reconnection counter | 0 for initial connect, 1+ for reconnections. APMs can use this to distinguish initial connections, detect flapping, or correlate traces across reconnection cycles. |
Backward Compatibility
Zero-cost when no subscribers are registered. Silently skipped on Node.js versions where TracingChannel is unavailable (e.g. Node 18).
const dc = ('getBuiltinModule' in process)
? process.getBuiltinModule('node:diagnostics_channel')
: require('node:diagnostics_channel');
Usage Examples
Consumers will be doing something like this:
const dc = require('node:diagnostics_channel');
// Trace individual Redis commands
dc.tracingChannel('ioredis:command').subscribe({
start(ctx) {
console.log(`→ ${ctx.command}`, ctx.args, `db=${ctx.database}`);
// APM: create span here
// ctx.span = tracer.startSpan(`redis ${ctx.command}`, { attributes: {
// 'db.system': 'redis',
// 'db.operation.name': ctx.command,
// 'db.namespace': String(ctx.database),
// 'server.address': ctx.serverAddress,
// 'server.port': ctx.serverPort,
// }});
},
asyncEnd(ctx) {
console.log(`✓ ${ctx.command} done`);
// ctx.span?.end();
},
error(ctx) {
console.error(`✗ ${ctx.command} failed:`, ctx.error?.message);
// ctx.span?.setStatus({ code: SpanStatusCode.ERROR });
},
});
// Trace connection
dc.tracingChannel('ioredis:connect').subscribe({
start(ctx) {
console.log(`Connecting to ${ctx.serverAddress}:${ctx.serverPort} (epoch: ${ctx.connectionEpoch})`);
},
asyncEnd() {
console.log('Connected');
},
error(ctx) {
console.error('Connection failed:', ctx.error?.message);
},
});
// Works with all ioredis operations — no monkey-patching needed
const redis = new Redis();
await redis.set('foo', 'bar'); // ioredis:command { command: 'SET', args: ['foo', 'bar'] }
await redis.get('foo'); // ioredis:command { command: 'GET', args: ['foo'] }
// Pipeline/MULTI commands include batch metadata
const pipeline = redis.pipeline();
pipeline.set('a', '1');
pipeline.set('b', '2');
await pipeline.exec(); // ioredis:command { command: 'SET', batchMode: 'PIPELINE', batchSize: 2 }
Future Improvements
- Offline queue tracing channel: A dedicated
ioredis:command:queuechannel could emit events when commands enter the offline queue, allowing APMs to measure queue wait time separately from command execution time and track how many commands hit the offline queue (a signal of connection instability). Note that the existing OTEL monkey-patch instrumentation (@opentelemetry/instrumentation-ioredis) has the same gap — it doesn't distinguish queued vs. written commands either. - Sentinel-aware server address:
_getServerAddress()currently uses configuredhost/portoptions. For sentinel setups, the trace context could reflect the actual resolved endpoint rather than the configured defaults.
[!NOTE]
Medium Risk
Touches core Redis.connect() and sendCommand() paths to wrap operations in tracing helpers; while intended to be zero-overhead when unused, mistakes could affect command execution flow or error handling. Adds argument sanitization rules to avoid leaking secrets, but incorrect redaction logic could still expose or over-redact data.
Overview
Adds Node.js diagnostics_channel/TracingChannel instrumentation for ioredis operations, emitting telemetry for individual commands (ioredis:command), MULTI batches (ioredis:batch), and connection attempts (ioredis:connect) when subscribers are present (silently no-op otherwise).
Redis.connect() is now traced via traceConnect, and sendCommand() wraps the written command promise with traceCommand exactly once per command (using a new Command.isTraced flag) to avoid duplicate events during offline-queue flushes or command resends after reconnect.
Introduces lib/tracing.ts with safe-by-default argument sanitization (redacting sensitive values via sanitizeArgs) and exported context types, updates docs with subscription examples, and adds comprehensive functional tests validating tracing behavior, deduplication, error tracing, and lack of unhandled rejections.
Written by Cursor Bugbot for commit 79d3220c1406b0eb78688be2eff06c0e19593223. This will update automatically on new commits. Configure here.