typescript 97 lines · 3 tabs

OpenTelemetry tracing for Node HTTP

Shared by codesnips Jan 2026
3 tabs
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import {
  SEMRESATTRS_SERVICE_NAME,
  SEMRESATTRS_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

const exporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
});

const sdk = new NodeSDK({
  resource: new Resource({
    [SEMRESATTRS_SERVICE_NAME]: 'checkout-api',
    [SEMRESATTRS_SERVICE_VERSION]: process.env.APP_VERSION ?? 'dev',
  }),
  spanProcessors: [new BatchSpanProcessor(exporter)],
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-fs': { enabled: false },
    }),
  ],
});

sdk.start();

process.on('SIGTERM', () => {
  sdk.shutdown().finally(() => process.exit(0));
});
3 files · typescript Explain with highlit

This snippet shows how a Node HTTP service is wired for distributed tracing with OpenTelemetry, from SDK bootstrap to per-request spans and downstream context propagation. The central idea is that a trace is a tree of Spans stitched together by a shared SpanContext that travels between processes as W3C traceparent headers, so the same request can be followed across service boundaries.

In tracing.ts the SDK is initialized before the app loads. A Resource tags every span with service.name and service.version, which is what backends use to group traces by service. The OTLPTraceExporter ships spans over OTLP/HTTP to a collector, and BatchSpanProcessor buffers and flushes them in batches so exporting never blocks the request path. getNodeAutoInstrumentations patches the built-in http module and common libraries automatically, meaning most spans appear without touching handler code. Registering start() at module load — and it must be imported first via -r ./tracing or an early import — ensures the monkey-patching happens before those modules are required.

The withSpan helper in withSpan.ts wraps manual work in a Span. It calls tracer.startActiveSpan, which both creates the span and makes it the active span in the current async context, so any child spans and auto-instrumented calls nest correctly. The try/catch/finally shape is the important part: errors are recorded with recordException and the status is set to ERROR, while span.end() in finally guarantees the span is always closed even on throw. Leaking unended spans is a common pitfall that leads to missing or infinitely-open traces.

In server.ts an incoming request handler uses withSpan to create a checkout span, sets domain attributes like order.id, and then makes an outbound fetch. Because the fetch runs inside the active span's context, the auto-instrumentation injects the current traceparent into the outgoing headers, propagating the trace to the payment service. Attributes should stay low-cardinality where they become metrics, and heavy payloads are best avoided as attribute values. Together these files give end-to-end visibility with minimal handler-level noise.


Related snips

Share this code

Here's the card — post it anywhere.

OpenTelemetry tracing for Node HTTP — share card
Link copied