Skip to content

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 ​

bash
npm install @opentelemetry/sdk-node \
            @opentelemetry/auto-instrumentations-node \
            @opentelemetry/exporter-trace-otlp-http

The 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 ​

typescript
// 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 ​

typescript
// 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 ​

typescript
// 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.

typescript
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) ← RedisX

Express Instrumentation ​

Adds route information to HTTP spans.

typescript
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';

instrumentations: [
  new ExpressInstrumentation(),
]

Span attributes:

json
{
  "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.

typescript
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 ​

http
GET /api/users/123 HTTP/1.1
traceparent: 00-abc123-def456-01
tracestate: redisx=789

Format: version-trace_id-parent_id-flags

Async Context ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
span.setAttributes({
  'db.system': 'redis',
  'db.operation': 'GET',
  'db.statement': 'GET user:123',
  'db.redis.database_index': 0,
});

HTTP Operations ​

typescript
span.setAttributes({
  'http.method': 'GET',
  'http.url': 'https://api.example.com/users/123',
  'http.status_code': 200,
  'http.response_content_length': 1234,
});

Custom Operations ​

typescript
span.setAttributes({
  'service.name': 'user-service',
  'service.version': '1.2.3',
  'deployment.environment': 'production',
});

Sampling ​

Built-in Samplers ​

typescript
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 ​

typescript
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) ​

typescript
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) ​

typescript
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.

typescript
import { detectResources, envDetector, hostDetector, processDetector } from '@opentelemetry/resources';

const resource = await detectResources({
  detectors: [envDetector, hostDetector, processDetector],
});

const sdk = new NodeSDK({ resource });

Detected attributes:

json
{
  "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

typescript
// main.ts
import './tracing';  // ← First!
import { NestFactory } from '@nestjs/core';

2. Use BatchSpanProcessor in production

typescript
const processor = new BatchSpanProcessor(exporter);

3. Follow semantic conventions

typescript
// ✅ Good
span.setAttribute('db.system', 'redis');

// ❌ Bad
span.setAttribute('database', 'redis');

4. Handle errors in spans

typescript
try {
  await operation();
  span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
  span.setStatus({ code: SpanStatusCode.ERROR });
  span.recordException(error);
  throw error;
}

5. Always end spans

typescript
const span = tracer.startSpan('operation');
try {
  await doWork();
} finally {
  span.end();  // ← Always!
}

Next Steps ​

Released under the MIT License.