OpenTelemetry-Instrumentierung für Node.js
Praxisnahe Anleitung zur Instrumentierung von Node.js mit OpenTelemetry: Traces, Metriken, Logs, Auto-Instrumentierung, Spans und Exporter.

OpenTelemetry ist der offene Standard zum Erfassen von Traces, Metriken und Logs aus Anwendungen. Er ersetzt herstellerspezifische SDKs durch eine einzige Instrumentierungsschicht, die in jedes Backend exportieren kann — Jaeger, Grafana, Datadog oder New Relic. Statt die Instrumentierung bei jedem Wechsel des Observability-Anbieters herauszureißen, änderst du einfach die Konfiguration des Exporters.
Für Node.js bietet OpenTelemetry eine Auto-Instrumentierung, die HTTP-Requests, Datenbankabfragen und Framework-Operationen ohne Codeänderungen erfasst. Benutzerdefinierte Spans fügen anwendungsspezifischen Kontext hinzu: welcher Nutzer den Request ausgelöst hat, welcher Feature-Flag-Pfad durchlaufen wurde, wie lange die Geschäftslogik getrennt von I/O gebraucht hat.
Auto-Instrumentierung einrichten
Die Auto-Instrumentierung patcht gängige Bibliotheken (Express, Fastify, pg, Redis, HTTP), um automatisch Spans zu erzeugen. Du initialisierst sie, bevor du den Code deiner Anwendung importierst.
// tracing.ts — load this BEFORE your application
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
getNodeAutoInstrumentations,
} from "@opentelemetry/auto-instrumentations-node";
import {
OTLPTraceExporter,
} from "@opentelemetry/exporter-trace-otlp-http";
import {
OTLPMetricExporter,
} from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { Resource } from "@opentelemetry/resources";
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: "payment-service",
[ATTR_SERVICE_VERSION]: "2.4.1",
environment: process.env.NODE_ENV ?? "development",
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + "/v1/traces",
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + "/v1/metrics",
}),
exportIntervalMillis: 15_000,
}),
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-fs": { enabled: false },
"@opentelemetry/instrumentation-http": {
ignoreIncomingPaths: ["/health", "/ready"],
},
}),
],
});
sdk.start();
process.on("SIGTERM", async () => {
await sdk.shutdown();
process.exit(0);
});// package.json — start with tracing loaded first
{
"scripts": {
"start": "node --require ./dist/tracing.js ./dist/index.js",
"dev": "tsx --require ./src/tracing.ts ./src/index.ts"
}
}Benutzerdefinierte Spans hinzufügen
Die Auto-Instrumentierung erfasst Infrastrukturoperationen, kann aber keine Geschäftslogik abbilden. Benutzerdefinierte Spans fügen Kontext hinzu, etwa "Zahlung für Nutzer X wird verarbeitet" oder "Rabattregeln werden angewendet".
import { trace, SpanStatusCode, context } from "@opentelemetry/api";
const tracer = trace.getTracer("payment-service", "2.4.1");
interface PaymentRequest {
userId: string;
amount: number;
currency: string;
method: "card" | "bank_transfer" | "wallet";
}
async function processPayment(request: PaymentRequest): Promise<string> {
return tracer.startActiveSpan("processPayment", async (span) => {
try {
// Add attributes for filtering and grouping in your backend
span.setAttributes({
"payment.user_id": request.userId,
"payment.amount": request.amount,
"payment.currency": request.currency,
"payment.method": request.method,
});
// Nested span for validation
const validated = await tracer.startActiveSpan(
"validatePayment",
async (validationSpan) => {
const result = await validatePaymentDetails(request);
validationSpan.setAttribute("payment.valid", result.valid);
validationSpan.end();
return result;
}
);
if (!validated.valid) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: validated.reason,
});
throw new Error(validated.reason);
}
// Nested span for provider call
const chargeId = await tracer.startActiveSpan(
"chargeProvider",
async (providerSpan) => {
providerSpan.setAttribute("payment.provider", "stripe");
const id = await chargePaymentProvider(request);
providerSpan.setAttribute("payment.charge_id", id);
providerSpan.end();
return id;
}
);
span.setStatus({ code: SpanStatusCode.OK });
return chargeId;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: (error as Error).message,
});
throw error;
} finally {
span.end();
}
});
}Benutzerdefinierte Metriken
Traces zeigen einzelne Requests. Metriken zeigen aggregiertes Verhalten — Request-Raten, Fehlerraten, Latenzen und Geschäftskennzahlen wie den Umsatz pro Minute.
import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("payment-service", "2.4.1");
// Counter: monotonically increasing value
const paymentCounter = meter.createCounter("payments.total", {
description: "Total number of payment attempts",
unit: "1",
});
// Histogram: distribution of values (latencies, sizes)
const paymentDuration = meter.createHistogram("payments.duration", {
description: "Payment processing duration",
unit: "ms",
});
// Up/Down Counter: value that increases and decreases
const activePayments = meter.createUpDownCounter(
"payments.active",
{
description: "Currently processing payments",
unit: "1",
}
);
// Usage in request handler
async function handlePayment(request: PaymentRequest): Promise<void> {
const startTime = Date.now();
activePayments.add(1, { method: request.method });
try {
await processPayment(request);
paymentCounter.add(1, {
method: request.method,
status: "success",
});
} catch (error) {
paymentCounter.add(1, {
method: request.method,
status: "failure",
error_type: (error as Error).name,
});
} finally {
const duration = Date.now() - startTime;
paymentDuration.record(duration, { method: request.method });
activePayments.add(-1, { method: request.method });
}
}// ❌ High-cardinality attributes cause metric explosion
paymentCounter.add(1, {
user_id: request.userId, // Millions of unique values
request_id: request.id, // Unique per request
timestamp: Date.now().toString(), // Unique per call
});
// Result: millions of time series, backend crashes or bill explodes
// ✅ Low-cardinality attributes for metrics
paymentCounter.add(1, {
method: request.method, // 3-4 values: card, bank, wallet
status: "success", // 2 values: success, failure
currency: request.currency, // ~10 values: USD, EUR, GBP...
});
// Result: ~80 time series (4 × 2 × 10), manageable and usefulKontextpropagierung zwischen Services
Wenn Service A Service B aufruft, muss der Trace-Kontext weitergegeben werden, damit die Spans beider Services im selben Trace erscheinen. OpenTelemetry übernimmt das bei HTTP-Aufrufen automatisch über W3C-Trace-Context-Header.
// Service A: makes an outgoing HTTP call
// OpenTelemetry auto-instrumentation automatically injects
// traceparent and tracestate headers:
//
// traceparent: 00-<trace-id>-<span-id>-01
// tracestate: <vendor-specific data>
//
// You don't need to do anything — the HTTP instrumentation handles it.
import express from "express";
const app = express();
app.post("/api/orders", async (req, res) => {
// This span is automatically created by Express instrumentation
// The HTTP call below automatically propagates trace context
const paymentResult = await fetch(
"http://payment-service:3001/api/charge",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: req.body.total }),
}
);
// Both the Express span and the fetch span (and the
// payment-service spans) appear in the same trace
res.json({ orderId: "ord_123", paymentStatus: "charged" });
});# Docker Compose for local development with OpenTelemetry
version: "3.8"
services:
payment-service:
build: ./payment-service
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_SERVICE_NAME: payment-service
order-service:
build: ./order-service
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_SERVICE_NAME: order-service
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-config.yaml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317" # gRPC receiver
- "4318:4318" # HTTP receiver
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UIDie wichtigsten Erkenntnisse
- Initialisiere OpenTelemetry, bevor du den Anwendungscode importierst — die Auto-Instrumentierung patcht Bibliotheken zum Importzeitpunkt; lädst du sie erst nach deiner App, gehen die Instrumentierungs-Hooks verloren.
- Die Auto-Instrumentierung deckt die Infrastruktur ab, benutzerdefinierte Spans die Geschäftslogik — HTTP-Requests und Datenbankabfragen werden automatisch erfasst; ergänze benutzerdefinierte Spans für domänenspezifische Vorgänge.
- Verwende Attribute mit niedriger Kardinalität für Metriken — Nutzer-IDs, Request-IDs und Zeitstempel erzeugen Millionen von Zeitreihen; beschränke dich auf Dimensionen wie Methode, Status und Kategorie.
- Die Kontextpropagierung läuft bei HTTP automatisch ab — W3C-Trace-Context-Header werden von der HTTP-Instrumentierung injiziert und ausgelesen; eine manuelle Header-Verwaltung ist nicht nötig.
- Zeichne Exceptions auf und setze bei Fehlern den Span-Status —
span.recordException()erfasst den Stacktrace;span.setStatus(ERROR)markiert den Span in Trace-Visualisierungen rot.


