OpenTelemetry
Integrate NestJS RedisX tracing with OpenTelemetry ecosystem.
What is OpenTelemetry?
OpenTelemetry (OTel) is an open-source observability framework for:
- Traces — Request flow through distributed systems
- Metrics — Performance measurements
- Logs — Event records
Installation
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-httpThe plugin adapts to your SDK automatically
With the default provider: 'auto', TracingPlugin first checks whether a global OpenTelemetry tracer provider is already registered. If it is (like the NodeSDK setups on this page), the plugin becomes a pure @opentelemetry/api consumer: your SDK owns the pipeline, the plugin's exporter/sampling options are unused, and all RedisX spans flow through your SDK's exporter and sampler on the same traces. The plugin only sets up an own NodeTracerProvider when NO global provider exists — it never overrides yours. Initialize your SDK before the application loads (the standard node -r ./tracing.js / top-of-main import pattern) so it is registered by the time the plugin initializes; see Provider Modes.
One caveat with getNodeAutoInstrumentations(): it enables @opentelemetry/instrumentation-ioredis, which would duplicate the plugin's native redis.<COMMAND> spans. The plugin detects the active instrumentation and pauses its own hook automatically — or disable one side explicitly, see Trace Redis Commands.
Basic Setup
1. Create Tracing Configuration
// src/tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'user-service',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-express': { enabled: true },
}),
],
});
sdk.start();
// Graceful shutdown
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.error('Error terminating tracing', error))
.finally(() => process.exit(0));
});2. Initialize Before Application
// src/main.ts
import './tracing'; // ← MUST be first import!
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();3. Configure RedisX Tracing Plugin
// app.module.ts
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { TracingPlugin } from '@nestjs-redisx/tracing';
@Module({
imports: [
RedisModule.forRoot({
clients: {
host: 'localhost',
port: 6379,
},
plugins: [
new TracingPlugin({
serviceName: 'user-service',
exporter: {
type: 'otlp',
endpoint: 'http://localhost:4318',
},
}),
],
}),
],
})
export class AppModule {}Auto-Instrumentation
OpenTelemetry automatically instruments common libraries.
HTTP Instrumentation
Traces incoming HTTP requests.
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
instrumentations: [
new HttpInstrumentation({
ignoreIncomingPaths: ['/health', '/metrics'],
}),
]Result:
Trace ID: abc123
├── HTTP GET /api/users/123 (50ms)
│ ├── redis.GET user:123 (1ms) ← RedisX
│ ├── database.query (35ms)
│ └── redis.SET user:123 (2ms) ← RedisXExpress Instrumentation
Adds route information to HTTP spans.
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
instrumentations: [
new ExpressInstrumentation(),
]Span attributes:
{
"http.method": "GET",
"http.route": "/api/users/:id",
"http.target": "/api/users/123",
"http.status_code": 200
}Database Instrumentation
Traces database queries alongside Redis operations.
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
instrumentations: [
new PgInstrumentation(),
]Result:
GET /api/users/123
├── redis.GET user:123 (miss)
├── pg.query SELECT * FROM users WHERE id = $1 (30ms)
└── redis.SET user:123 (2ms)Context Propagation
OpenTelemetry automatically propagates context across:
- HTTP requests (W3C Trace Context headers)
- Async operations (AsyncLocalStorage)
- Database queries
- Redis commands
HTTP Headers
GET /api/users/123 HTTP/1.1
traceparent: 00-abc123-def456-01
tracestate: redisx=789Format: version-trace_id-parent_id-flags
Async Context
import { context, trace } from '@opentelemetry/api';
@Get('/users/:id')
async getUser(@Param('id') id: string) {
const span = trace.getActiveSpan(); // Current HTTP span
// Redis span inherits context automatically
const user = await this.cache.get(`user:${id}`);
return user;
}Manual Instrumentation
Create custom spans alongside auto-instrumentation.
Using TracingService
import { Injectable, Inject } from '@nestjs/common';
import { TRACING_SERVICE, ITracingService } from '@nestjs-redisx/tracing';
@Injectable()
export class UserProcessingService {
constructor(
@Inject(TRACING_SERVICE) private readonly tracing: ITracingService,
) {}
async processUser(userId: string): Promise<void> {
await this.tracing.withSpan('process.user', async () => {
this.tracing.setAttribute('user.id', userId);
// Business logic here
await this.validateUser(userId);
await this.enrichUserData(userId);
this.tracing.addEvent('user.processed');
});
}
private async validateUser(_userId: string): Promise<void> {
// validation logic
}
private async enrichUserData(_userId: string): Promise<void> {
// enrichment logic
}
}Using OpenTelemetry API Directly
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('user-service');
async function processOrder(orderId: string): Promise<void> {
const span = tracer.startSpan('process.order');
try {
span.setAttribute('order.id', orderId);
await validateOrder(orderId);
await chargePayment(orderId);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.recordException(error);
throw error;
} finally {
span.end();
}
}Semantic Conventions
Follow OpenTelemetry semantic conventions for consistency.
Database Operations
span.setAttributes({
'db.system': 'redis',
'db.operation': 'GET',
'db.statement': 'GET user:123',
'db.redis.database_index': 0,
});HTTP Operations
span.setAttributes({
'http.method': 'GET',
'http.url': 'https://api.example.com/users/123',
'http.status_code': 200,
'http.response_content_length': 1234,
});Custom Operations
span.setAttributes({
'service.name': 'user-service',
'service.version': '1.2.3',
'deployment.environment': 'production',
});Sampling
Built-in Samplers
import { TraceIdRatioBasedSampler, AlwaysOnSampler, AlwaysOffSampler } from '@opentelemetry/sdk-trace-base';
// 10% sampling
const sdk = new NodeSDK({
sampler: new TraceIdRatioBasedSampler(0.1),
});
// Always sample
const sdk = new NodeSDK({
sampler: new AlwaysOnSampler(),
});
// Never sample
const sdk = new NodeSDK({
sampler: new AlwaysOffSampler(),
});Custom Sampler
import { Sampler, SamplingDecision } from '@opentelemetry/sdk-trace-base';
class CustomSampler implements Sampler {
shouldSample(context, traceId, spanName, spanKind, attributes) {
// Always sample errors
if (attributes['http.status_code'] >= 400) {
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
// Sample 1% of success requests
if (Math.random() < 0.01) {
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
return { decision: SamplingDecision.NOT_RECORD };
}
}
const sdk = new NodeSDK({
sampler: new CustomSampler(),
});Span Processors
Batch Processor (Production)
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
const exporter = new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
});
const processor = new BatchSpanProcessor(exporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
});Benefits:
- Batches spans for efficiency
- Reduces network overhead
- Better performance
Simple Processor (Development)
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
const processor = new SimpleSpanProcessor(new ConsoleSpanExporter());Benefits:
- Immediate export
- Real-time debugging
- No batching delay
Resource Detection
Automatically detect deployment environment.
import { detectResources, envDetector, hostDetector, processDetector } from '@opentelemetry/resources';
const resource = await detectResources({
detectors: [envDetector, hostDetector, processDetector],
});
const sdk = new NodeSDK({ resource });Detected attributes:
{
"host.name": "ip-10-0-1-42",
"host.arch": "x64",
"process.pid": 1234,
"process.executable.name": "node",
"process.runtime.name": "nodejs",
"process.runtime.version": "20.10.0"
}Best Practices
1. Initialize tracing before app
// main.ts
import './tracing'; // ← First!
import { NestFactory } from '@nestjs/core';2. Use BatchSpanProcessor in production
const processor = new BatchSpanProcessor(exporter);3. Follow semantic conventions
// ✅ Good
span.setAttribute('db.system', 'redis');
// ❌ Bad
span.setAttribute('database', 'redis');4. Handle errors in spans
try {
await operation();
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(error);
throw error;
}5. Always end spans
const span = tracer.startSpan('operation');
try {
await doWork();
} finally {
span.end(); // ← Always!
}