← Recipes

阅读中文版本

Add OpenTelemetry Tracing to a Node.js Service

Initialize the OpenTelemetry SDK before the app loads, instrument HTTP and database calls, export traces, and shut down cleanly.

~40 minobservabilitytracingnodejs

Task

Produce end-to-end traces with W3C context propagation and useful span attributes without blocking requests or leaking data.

When to use this

A request is slow, but logs show only one service and metrics show only aggregate latency. The team needs to see which downstream call consumed the time.

Prerequisites

  • A Node.js service with HTTP and database calls
  • An OpenTelemetry-compatible backend or a local collector
  • Permission to install runtime dependencies

Steps

1. Initialize the SDK before importing the application, with a service name and OTLP exporter.

// tracing.ts, imported before the app
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const sdk = new NodeSDK({
  serviceName: 'orders-api',
  traceExporter: new OTLPTraceExporter({ url: process.env.OTLP_URL }),
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

Expected output: The service exports spans with the configured service name.

2. Propagate the incoming trace context and add meaningful attributes without secrets or unbounded user data.

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

const tracer = trace.getTracer('orders');
await tracer.startActiveSpan('reserve-inventory', async (span) => {
  try {
    span.setAttribute('inventory.sku', sku);
    return await inventory.reserve(sku);
  } catch (error) {
    span.setStatus({ code: SpanStatusCode.ERROR });
    throw error;
  } finally {
    span.end();
  }
});

Expected output: The trace shows a child span with the SKU and an error status when reservation fails.

3. Flush and shut down the SDK on termination, and verify a trace across two services.

OTEL_SERVICE_NAME=orders-api node --import ./tracing.js server.js

Expected output: A request produces one trace with parent-child spans across services.

Troubleshooting

Problem

No traces appear

Solution

The SDK is initialized after the app imports, the exporter URL is wrong, or the process exits before flushing. Initialize first and shut down explicitly.

Problem

Context is lost between services

Solution

The HTTP client must inject W3C traceparent headers; auto-instrumentation covers common clients, custom transports need manual propagation.

Limitations

  • Tracing every request is expensive; sample and preserve errors and slow traces.
  • Span attributes can leak personal data; follow the same redaction rules as logs.

Initialize before the app

Tracing must start before the instrumented libraries are imported. In Node.js, import the tracing module first with --import, or require it at the very top of the entry point. If the SDK starts after the HTTP client is loaded, some spans and context propagation will be missing.

node --import ./tracing.js server.js

Propagate and attribute

Incoming W3C traceparent headers create the parent context. Outbound HTTP and database calls should continue the same trace. Add attributes that help diagnosis, such as route, dependency, and status, but never authorization headers or raw user input. Sample high-volume traffic and keep error and slow traces.

Flush on shutdown

The exporter buffers spans. On SIGTERM, stop accepting traffic, finish in-flight work, and call sdk.shutdown() before exit. Without a flush, the last traces before a crash or deploy are lost, often the most interesting ones.

Verify the trace

Expected outcome: one request produces a single trace with gateway, service, and database spans, and the child spans share the incoming trace id. Confirm by sending a request with a known traceparent, querying the backend for that trace id, and checking that the outbound call appears as a child. A trace that stops at the first service means context propagation or SDK initialization is wrong.