mdashikjs/blog
All posts
Distributed Tracing with OpenTelemetry in Node.js
DevOps

Distributed Tracing with OpenTelemetry in Node.js

DevOps5 min

Distributed Tracing with OpenTelemetry in Node.js

Logs tell you what happened. Metrics tell you how much. Traces tell you where the time went. OpenTelemetry is the standard that ties all three together across service boundaries.

OpenTelemetryTracingObservabilityNode.js
Share:

What a trace looks like

A trace is a tree of spans. Each span represents a unit of work:

[HTTP GET /api/orders/123]  total: 145ms
  ├── [auth middleware]       12ms
  ├── [db: SELECT order]      45ms
  ├── [redis: GET cache]       2ms
  └── [HTTP: payment-service] 85ms
       ├── [db: SELECT payment] 30ms
       └── [stripe API call]    52ms

Without tracing, you know the request took 145ms. With tracing, you know the payment service's Stripe call is the bottleneck.

Setting up auto-instrumentation

OpenTelemetry's Node.js SDK can instrument Express, pg, ioredis, and HTTP calls automatically:

// tracing.ts — import this BEFORE anything else
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 { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: 'order-service',
  }),
  traceExporter: new OTLPTraceExporter({
    url: 'http://otel-collector:4318/v1/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

That is it. Every HTTP request, database query, and Redis command is traced automatically. No code changes in your routes or services.

Custom spans for business logic

Auto-instrumentation covers infrastructure. For business logic, create custom spans:

import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('order-service');

async function processOrder(orderId: string) {
  return tracer.startActiveSpan('processOrder', async (span) => {
    span.setAttribute('order.id', orderId);

    const order = await getOrder(orderId);
    span.setAttribute('order.total', order.total);

    const result = await chargePayment(order);
    span.setAttribute('payment.status', result.status);

    span.end();
    return result;
  });
}

Context propagation

The magic of distributed tracing is that the trace ID propagates across service boundaries. When service A calls service B, the trace context is injected into HTTP headers automatically:

GET /api/payments/pay_123
traceparent: 00-abc123-def456-01

Service B picks up the context and continues the trace. No manual header passing required — the auto-instrumentation handles it.

The collector pattern

Do not export traces directly from your app to Jaeger or Datadog. Use an OpenTelemetry Collector as a buffer:

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1000

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger]

The collector batches, retries, and routes telemetry. If Jaeger is down for 30 seconds, your app does not notice — the collector buffers.

Sampling in production

Tracing every request in production generates too much data. Use tail-based sampling to keep only interesting traces:

  • All error traces
  • All traces slower than P95
  • 10 percent of normal traces

This keeps your storage costs manageable while ensuring you never miss a problematic request.

What tracing is not

Tracing is not a replacement for logs or metrics. It answers "where did the time go?" not "what happened?" or "how much?" Use all three — and OpenTelemetry is the framework that unifies them.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.