Introduction
In microservices, a single request spans multiple services. Distributed tracing tracks requests across service boundaries, showing latency at each hop and identifying slow services.
Key Concepts
- Trace: End-to-end request journey
- Span: Single operation within a trace
- Context Propagation: Passing trace ID between services
- OpenTelemetry: Standard for observability
Real World Context
Distributed tracing answers:
- Which service is slow?
- Where did the error occur?
- What's the request flow?
- What are the dependencies?
Deep Dive
OpenTelemetry Setup
The @opentelemetry/exporter-jaeger package is deprecated. Use the OTLP exporter instead — Jaeger natively supports OTLP ingestion:
bashnpm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-proto
typescript// tracing.ts (load before app) import { NodeSDK } from '@opentelemetry/sdk-node'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; const sdk = new NodeSDK({ serviceName: 'nestjs-app', traceExporter: new OTLPTraceExporter({ url: 'http://jaeger:4318/v1/traces', }), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); process.on('SIGTERM', () => { sdk.shutdown().finally(() => process.exit(0)); });
typescript// main.ts import './tracing'; // Import first! import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();
Custom Spans
typescriptimport { trace, context, SpanStatusCode } from '@opentelemetry/api'; @Injectable() export class OrdersService { private tracer = trace.getTracer('orders-service'); async createOrder(dto: CreateOrderDto): Promise<Order> { return this.tracer.startActiveSpan('createOrder', async (span) => { try { span.setAttribute('order.customer_id', dto.customerId); span.setAttribute('order.item_count', dto.items.length); // Nested span for database operation const order = await this.tracer.startActiveSpan('db.insert', async (dbSpan) => { const result = await this.ordersRepo.save(dto); dbSpan.end(); return result; }); // Nested span for external call await this.tracer.startActiveSpan('payment.process', async (paymentSpan) => { await this.paymentService.charge(order); paymentSpan.end(); }); span.setStatus({ code: SpanStatusCode.OK }); return order; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); span.recordException(error); throw error; } finally { span.end(); } }); } }
Context Propagation in HTTP Calls
typescriptimport { context, propagation } from '@opentelemetry/api'; @Injectable() export class ExternalApiService { constructor(private httpService: HttpService) {} async callExternalService(data: any): Promise<any> { // Extract current context const headers: Record<string, string> = {}; propagation.inject(context.active(), headers); // Pass trace context to downstream service return firstValueFrom( this.httpService.post('https://external-api.com/endpoint', data, { headers, }), ); } }
Correlation ID Middleware
typescriptimport { trace, context } from '@opentelemetry/api'; @Injectable() export class CorrelationMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const span = trace.getActiveSpan(); const traceId = span?.spanContext().traceId || uuid(); req['correlationId'] = traceId; res.setHeader('X-Correlation-ID', traceId); next(); } }
Trace-Aware Logging
typescriptimport { trace } from '@opentelemetry/api'; @Injectable() export class LoggerService { log(message: string, context?: Record<string, any>) { const span = trace.getActiveSpan(); const traceContext = span?.spanContext(); console.log(JSON.stringify({ timestamp: new Date().toISOString(), message, traceId: traceContext?.traceId, spanId: traceContext?.spanId, ...context, })); } }
Common Pitfalls
- Missing context propagation: Traces break without header propagation.
- Too many spans: Keep spans meaningful, not for every function.
- No sampling: 100% tracing in production is expensive.
Best Practices
- Use OpenTelemetry for vendor-neutral tracing
- Propagate context in all inter-service calls
- Add meaningful attributes to spans
- Sample traces in production (1-10%)
- Correlate logs with trace IDs
Summary
Distributed tracing tracks requests across services. Use OpenTelemetry with OTLP exporters for vendor-neutral instrumentation, and Jaeger or Zipkin for visualization. Propagate context in HTTP headers and correlate logs with trace IDs for complete observability.
Code Examples
typescript
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
const sdk = new NodeSDK({
serviceName: 'nestjs-app',
traceExporter: new OTLPTraceExporter({
url: 'http://jaeger:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();