Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,30 +328,31 @@ Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master

### OpenTelemetry

#### OpenTelemetry Metrics Instrumentation

```typescript
import { createClient, OpenTelemetry } from "redis";

OpenTelemetry.init({
metrics: {
enabled: true
}
enabled: true,
},
tracing: {
enabled: true,
},
});

const client = createClient()
const client = createClient();

await client.connect();
// ... use the client as usual
```

**Important:** Initializing `OpenTelemetry` only enables node-redis metrics instrumentation and requires both `@opentelemetry/api` and an OpenTelemetry SDK configured in your application.
**Important:** Initializing `OpenTelemetry` only enables node-redis instrumentation and requires both `@opentelemetry/api` and an OpenTelemetry SDK configured in your application.

**Important:** Initialize `OpenTelemetry` before creating Redis clients.
For SDK/provider/exporter setup, verification, and advanced configuration, see:

- [OpenTelemetry Metrics docs](./docs/otel-metrics.md)
- [OpenTelemetry Metrics example](./examples/otel-metrics.js)
- [OpenTelemetry Metrics docs](./docs/otel-metrics.md) · [example](./examples/otel-metrics.js)
- [OpenTelemetry Tracing docs](./docs/otel-tracing.md) · [example](./examples/otel-tracing.js)

### Diagnostics Channel

Expand Down
218 changes: 218 additions & 0 deletions docs/otel-tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# OpenTelemetry Tracing

## Get started

### Step 1. Install node-redis dependencies

```bash
npm install redis @opentelemetry/api
```

`@opentelemetry/api` is required at runtime for `OpenTelemetry.init(...)`.

### Step 2. Install OpenTelemetry SDK packages

```bash
npm install @opentelemetry/sdk-trace-base @opentelemetry/context-async-hooks
```

Alternative (Node SDK):

```bash
npm install @opentelemetry/sdk-node
```

If you export to OTLP or another backend, install the matching OpenTelemetry exporter package (e.g. `@opentelemetry/exporter-trace-otlp-http`).

For more information, see the [OpenTelemetry Tracing documentation](https://opentelemetry.io/docs/instrumentation/js/exporters/#traces).

### Step 3. Register OpenTelemetry

Option A: Use `@opentelemetry/sdk-trace-base` directly

```typescript
import { trace, context } from "@opentelemetry/api";
import {
BasicTracerProvider,
SimpleSpanProcessor,
ConsoleSpanExporter,
} from "@opentelemetry/sdk-trace-base";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";

const provider = new BasicTracerProvider({
spanProcessors: [
new SimpleSpanProcessor(new ConsoleSpanExporter()),
],
});

context.setGlobalContextManager(new AsyncLocalStorageContextManager());
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The AsyncLocalStorageContextManager snippet is missing .enable(). Without enabling, OpenTelemetry context propagation can be disabled, so Redis spans may not become children of active application spans as described later in this doc.

Suggested change
context.setGlobalContextManager(new AsyncLocalStorageContextManager());
context.setGlobalContextManager(
new AsyncLocalStorageContextManager().enable()
);

Copilot uses AI. Check for mistakes.
trace.setGlobalTracerProvider(provider);
```

Option B: Use `@opentelemetry/sdk-node`

```typescript
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";

const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
});

await sdk.start();
```

### Step 4. Initialize node-redis instrumentation before creating clients

```typescript
import { createClient, OpenTelemetry } from "redis";

OpenTelemetry.init({
tracing: {
enabled: true,
},
});

const client = createClient();
await client.connect();
```

## Examples

### Minimal Example

```typescript
import { OpenTelemetry } from "redis";

OpenTelemetry.init({
tracing: {
enabled: true,
},
});
```

### Full Example

```typescript
import { OpenTelemetry } from "redis";

OpenTelemetry.init({
tracing: {
enabled: true,
tracerProvider: customTracerProvider,
includeCommands: ["GET", "SET", "HSET"],
excludeCommands: ["PING"],
enableConnectionSpans: true,
},
});
```

### Combined with Metrics

```typescript
import { OpenTelemetry } from "redis";

OpenTelemetry.init({
metrics: {
enabled: true,
},
tracing: {
enabled: true,
enableConnectionSpans: true,
},
});
```

## Configuration

### TracingConfig

| Property | Default | Description |
| -------- | ------- | ----------- |
| enabled | **false** | Enables span creation. |
| tracerProvider | | Uses this provider instead of the global provider from @opentelemetry/api. |
| includeCommands | **[]** | Case-insensitive allow-list for command spans. |
| excludeCommands | **[]** | Case-insensitive deny-list for command spans. If both include and exclude match, exclude wins. |
| enableConnectionSpans | **false** | Creates spans for `connect()` calls. |

## Span types

### Command spans

Created for each Redis command (`GET`, `SET`, `HSET`, etc.).

| Attribute | Value |
| --------- | ----- |
| `db.system.name` | `redis` |
| `db.operation.name` | The command name (e.g. `SET`) |
| `db.namespace` | The selected database index |
| `db.query.text` | The sanitized command and arguments |
| `server.address` | Redis server host |
| `server.port` | Redis server port |
| `redis.client.library` | `node-redis:<version>` |

### Batch spans (MULTI / PIPELINE)

Created for `MULTI`/`EXEC` transactions and pipelines. Individual commands within the batch are traced as child spans.

| Attribute | Value |
| --------- | ----- |
| `db.system.name` | `redis` |
| `db.operation.name` | `MULTI` or `PIPELINE` |
| `db.namespace` | The selected database index |
| `db.operation.batch.size` | Number of commands in the batch |
| `server.address` | Redis server host |
| `server.port` | Redis server port |
| `redis.client.library` | `node-redis:<version>` |

### Connection spans

Created for `connect()` calls when `enableConnectionSpans` is `true`.

| Attribute | Value |
| --------- | ----- |
| `db.system.name` | `redis` |
| `server.address` | Redis server host |
| `server.port` | Redis server port |
| `redis.client.library` | `node-redis:<version>` |

## Error handling

When a command fails, the span records the exception and sets the following:

| Attribute | Value |
| --------- | ----- |
| `error.type` | The error class name |
| `db.response.status_code` | The Redis error prefix (e.g. `WRONGTYPE`) if available |

The span status is set to `ERROR` with the error message.

## Context propagation

Spans created by node-redis automatically participate in the active OpenTelemetry context. If you create an application-level span and execute Redis commands within it, the Redis spans appear as children:

```typescript
const tracer = trace.getTracer("my-app");

await tracer.startActiveSpan("handleRequest", async (span) => {
// These Redis spans are children of "handleRequest"
await client.get("session:abc");
await client.set("last-seen", Date.now().toString());
span.end();
});
```

This also works with nested batch operations — individual command spans appear as children of their `MULTI` or `PIPELINE` parent span.

Context propagation requires `@opentelemetry/context-async-hooks` (or the Node SDK) to be registered.

## Notes

- `OpenTelemetry` is a singleton and a second `init` call throws.
- If `@opentelemetry/api` is not installed, `init` throws.
- Command arguments in `db.query.text` are sanitized — values for write commands are replaced with `?` to avoid leaking sensitive data.
- Tracing is built on top of the [Diagnostics Channel](./diagnostics-channel.md) `TracingChannel` infrastructure. Requires Node.js >= 18.19.0.

## Runnable example

See ../examples/otel-tracing.js.
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This folder contains example scripts showing how to use Node Redis in different
| `lua-multi-incr.js` | Define a custom lua script that allows you to perform INCRBY on multiple keys. |
| `managing-json.js` | Store, retrieve and manipulate JSON data atomically with [RedisJSON](https://redisjson.io/). |
| `otel-metrics.js` | Enable OpenTelemetry metrics for node-redis, generate command and resiliency signals, and export them via OpenTelemetry SDK metrics. |
| `otel-tracing.js` | Enable OpenTelemetry tracing for node-redis, demonstrating command spans, MULTI transactions, nested app spans, and error recording. |
| `pubsub-publisher.js` | Adds multiple messages on 2 different channels messages to Redis. |
| `pubsub-subscriber.js` | Reads messages from channels using `PSUBSCRIBE` command. |
| `search-hashes.js` | Uses [RediSearch](https://redisearch.io) to index and search data in hashes. |
Expand Down
73 changes: 73 additions & 0 deletions examples/otel-tracing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// OpenTelemetry tracing example for node-redis.
// Demonstrates span creation for commands, MULTI transactions, and nested app spans.

import { createClient, OpenTelemetry } from 'redis';
import { trace, context } from '@opentelemetry/api';
import {
BasicTracerProvider,
SimpleSpanProcessor,
ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';

// Export spans to console for easy local verification.
const provider = new BasicTracerProvider({
spanProcessors: [
new SimpleSpanProcessor(new ConsoleSpanExporter()),
],
});

context.setGlobalContextManager(new AsyncLocalStorageContextManager());
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

AsyncLocalStorageContextManager needs to be enabled (e.g. new AsyncLocalStorageContextManager().enable()) before setting it as the global context manager; otherwise context propagation (and therefore parent/child span relationships) may not work as documented.

Suggested change
context.setGlobalContextManager(new AsyncLocalStorageContextManager());
const contextManager = new AsyncLocalStorageContextManager().enable();
context.setGlobalContextManager(contextManager);

Copilot uses AI. Check for mistakes.
trace.setGlobalTracerProvider(provider);

// Enable OpenTelemetry before creating clients.
OpenTelemetry.init({
tracing: {
enabled: true,
enableConnectionSpans: true,
},
});

const client = createClient();

client.on('error', (err) => {
console.error('Redis client error:', err);
});

const tracer = trace.getTracer('example-app');

try {
await client.connect();

// Normal command traffic — each creates a span.
await client.set('otel:example:key', 'hello');
await client.get('otel:example:key');

// MULTI transaction — creates a parent batch span with child command spans.
await client.multi()
.set('otel:example:a', '1')
.set('otel:example:b', '2')
.get('otel:example:a')
.exec();

// Application span wrapping Redis commands — demonstrates parent-child propagation.
await tracer.startActiveSpan('myApp.processRequest', async (span) => {
await client.get('otel:example:key');
await client.set('otel:example:visited', 'true');
span.end();
});

// Generate a handled error to demonstrate error span recording.
await client.hSet('otel:example:hash', 'field', 'value');
try {
await client.incr('otel:example:hash');
} catch (err) {
console.log('Expected command error:', err);
}

// Force export so output is visible immediately.
await provider.forceFlush();
} finally {
client.destroy();
await provider.shutdown();
}
Loading
Loading