1. OTel Konzepte — Traces, Spans, Metrics, Logs
OpenTelemetry (kurz: OTel) ist das CNCF-Projekt, das Observability-Daten standardisiert. Statt proprietärer SDKs für Datadog, New Relic oder Dynatrace schreibst du einmal OTel — und wählst danach frei dein Backend. Das ist der Paradigmenwechsel, den 2026 endgültig alle größeren Node.js-Stacks vollzogen haben.
TRACE Das OTel-Datenmodell auf einen Blick
OTel kennt drei Signaltypen — jeder hat einen eigenen Exportpfad, aber alle teilen denselben Kontext (TraceID, SpanID):
- Traces — Eine Folge von Spans, die eine einzige Request-Reise durch alle Services beschreiben. Die TraceID verbindet alles.
- Spans — Einzelne Operationen innerhalb eines Traces (z.B. "HTTP GET /users", "DB SELECT"). Jeder Span hat Start, Ende, Attribute und Status.
- Metrics — Aggregierte Messwerte über Zeit: Counter (monoton steigend), Histogram (Verteilung), Gauge (aktueller Wert).
- Logs — Strukturierte Ereignisse, die optional an einen aktiven Span gebunden werden (Korrelation via TraceID).
KONZEPT Span-Hierarchie und TraceID
Jeder Span kennt seinen Parent-Span. So entsteht ein Baum: Root-Span (z.B. der HTTP-Handler) → Kind-Spans (DB-Query, externes API-Call, Cache-Lookup). Die TraceID ist für alle Spans gleich — sie macht das Distributed Tracing möglich, auch über mehrere Services hinweg.
// Trace-Struktur (konzeptuell)
Trace abc123
└─ Span: POST /checkout (root, 450ms)
├─ Span: validateCart (12ms)
├─ Span: DB INSERT order (38ms)
└─ Span: HTTP → payment-svc (395ms)
├─ Span: Stripe charge (380ms)
└─ Span: DB INSERT tx (10ms)
Claude Code Tipp: Frage Claude Code nach dem OTel-Datenmodell mit claude "Erkläre den Unterschied zwischen SpanContext und SpanLink in OpenTelemetry" — du bekommst sofort praxisnahe TypeScript-Beispiele.
2. Node.js SDK Setup — @opentelemetry/sdk-node und Auto-Instrumentation
Das offizielle Node.js SDK bündelt alles: SDK-Core, Auto-Instrumentation und OTLP-Exporter. Claude Code generiert das vollständige Setup — inklusive Registrierung aller gängigen Auto-Instrumentierungen für HTTP, Express, gRPC, Prisma und mehr.
SETUP Pakete installieren
# Core SDK + Auto-Instrumentierung + OTLP-Exporter
npm install \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions
SETUP instrumentation.ts — Einmal initialisieren, überall verfügbar
Diese Datei muss vor allem anderen Code geladen werden — per --require Flag oder als erster Import in server.ts.
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 {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
const resource = Resource.default().merge(
new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'checkout-service',
[SEMRESATTRS_SERVICE_VERSION]: process.env.APP_VERSION ?? '0.0.0',
'deployment.environment': process.env.NODE_ENV ?? 'development',
})
);
const traceExporter = new OTLPTraceExporter({
url:
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
'http://localhost:4318/v1/traces',
});
const metricExporter = new OTLPMetricExporter({
url:
process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ??
'http://localhost:4318/v1/metrics',
});
const sdk = new NodeSDK({
resource,
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 15_000, // alle 15 Sekunden exportieren
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false }, // fs ist zu chatty
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) =>
req.url?.startsWith('/health') ?? false,
},
}),
],
});
sdk.start();
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('OTel SDK sauber beendet'))
.catch((err) => console.error('Shutdown-Fehler:', err))
.finally(() => process.exit(0));
});
SETUP package.json — Instrumentation vor dem Server laden
{
"scripts": {
"start": "node --require ./dist/instrumentation.js dist/server.js",
"dev": "ts-node --require ./src/instrumentation.ts src/server.ts"
}
}
3. Custom Spans und Attributes
Auto-Instrumentation erfasst HTTP, DB und gRPC automatisch. Für Business-Logik brauchst du Custom Spans — z.B. "Warenkorb validieren", "Preis berechnen", "Fraud-Check". Claude Code erstellt diese Spans typsicher mit vollständigen Attributen.
TRACE tracer.startActiveSpan() — Der empfohlene Weg
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
const tracer = trace.getTracer('checkout-service', '1.0.0');
async function processCheckout(
cartId: string,
userId: string
): Promise<Order> {
return tracer.startActiveSpan(
'checkout.process',
{
kind: SpanKind.SERVER,
attributes: {
'checkout.cart_id': cartId,
'checkout.user_id': userId,
'checkout.source': 'web',
},
},
async (span) => {
try {
const cart = await loadCart(cartId);
span.setAttribute('checkout.item_count', cart.items.length);
span.setAttribute('checkout.total_eur', cart.totalEur);
// Fraud-Check als Kind-Span
const isSafe = await tracer.startActiveSpan(
'checkout.fraud_check',
async (s) => {
const result = await fraudService.check(userId, cart);
s.setAttribute('fraud.score', result.score);
s.setAttribute('fraud.decision', result.decision);
s.end();
return result.decision === 'allow';
}
);
if (!isSafe) {
span.setStatus({ code: SpanStatusCode.ERROR, message: 'Fraud detected' });
span.setAttribute('checkout.blocked', true);
span.end();
throw new Error('Transaction blocked by fraud detection');
}
const order = await createOrder(cart, userId);
span.setAttribute('checkout.order_id', order.id);
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end(); // IMMER im finally-Block!
}
}
);
}
TRACE Span-Events — Zeitstempel ohne eigenen Span
Für wichtige Zwischenschritte innerhalb eines Spans eignen sich Events — sie haben einen Zeitstempel und Attribute, aber keinen eigenen Scope:
span.addEvent('cache.miss', {
'cache.key': cacheKey,
'cache.ttl_remaining': 0,
});
// Später im Code:
span.addEvent('db.query.start', { 'db.statement': query });
const rows = await db.query(query);
span.addEvent('db.query.end', { 'db.rows_returned': rows.length });
Achtung: Span-Attribute niemals mit PII (E-Mail, Passwort, Kreditkartennummer) befüllen. OTel-Backends sind oft langzeit-persistent — im Zweifel hashen oder weglassen.
4. Metrics — Counter, Histogram, Gauge, MeterProvider
Metrics sind aggregierte Zeitreihen — ideal für Dashboards, Alerts und SLO-Tracking. OTel kennt drei Instruments: Counter (immer steigend), Histogram (Verteilung von Werten) und Gauge (aktueller Zustand). Claude Code wählt automatisch das richtige Instrument für deinen Use-Case.
METRIC Counter, Histogram, Gauge — alle drei in einem Service
import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('checkout-service', '1.0.0');
// Counter: monoton steigend — für Requests, Errors, Events
const checkoutCounter = meter.createCounter('checkout.requests.total', {
description: 'Anzahl Checkout-Anfragen',
unit: '{requests}',
});
// Histogram: Verteilung — für Latenz, Größen, Beträge
const latencyHistogram = meter.createHistogram('checkout.duration.ms', {
description: 'Checkout-Dauer in Millisekunden',
unit: 'ms',
advice: {
explicitBucketBoundaries: [10, 50, 100, 250, 500, 1000, 2500, 5000],
},
});
// Gauge: aktueller Wert — für Queue-Größe, aktive Sessions
const activeSessionsGauge = meter.createObservableGauge(
'checkout.sessions.active',
{ description: 'Aktuell aktive Checkout-Sessions' }
);
activeSessionsGauge.addCallback((result) => {
result.observe(sessionStore.size());
});
// Im Request-Handler:
async function handleCheckout(req: Request, res: Response) {
const start = Date.now();
const labels = {
payment_method: req.body.paymentMethod,
region: req.body.region,
};
try {
const order = await processCheckout(req.body.cartId, req.user.id);
checkoutCounter.add(1, { ...labels, status: 'success' });
res.json({ orderId: order.id });
} catch (err) {
checkoutCounter.add(1, { ...labels, status: 'error' });
res.status(500).json({ error: 'Checkout fehlgeschlagen' });
} finally {
latencyHistogram.record(Date.now() - start, labels);
}
}
METRIC Prometheus Exporter — für Grafana-Dashboards
Neben OTLP unterstützt OTel auch Prometheus nativ — ideal wenn Grafana bereits im Stack ist:
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { MeterProvider } from '@opentelemetry/sdk-metrics';
const prometheusExporter = new PrometheusExporter({
port: 9464, // GET /metrics für Prometheus-Scrape
preventServerStart: false,
});
const meterProvider = new MeterProvider({
readers: [prometheusExporter],
resource,
});
// In prometheus.yml:
// scrape_configs:
// - job_name: checkout-service
// static_configs:
// - targets: ['checkout-service:9464']
Claude Code Tipp: Bitte Claude Code um ein vollständiges Grafana-Dashboard JSON für deine OTel-Metrics — es generiert direkt importierbare Dashboard-Definitionen mit den richtigen PromQL-Queries.
5. Distributed Tracing — Context Propagation, W3C Trace Context, B3 Headers
Distributed Tracing wird erst dann wertvoll, wenn Spans über Servicegrenzen hinweg verbunden sind. OTel löst das mit Context Propagation: Der Trace-Kontext wird als HTTP-Header mitgeschickt — entweder im W3C-Standard (traceparent) oder im älteren B3-Format (Zipkin-Kompatibilität).
DIST W3C Trace Context — der moderne Standard
// W3C traceparent Header-Format:
// traceparent: 00-{traceId}-{spanId}-{flags}
// Beispiel:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^
version trace-id (128-bit) parent-span-id sampled
DIST Propagator konfigurieren — W3C + B3 parallel
import {
CompositePropagator,
W3CTraceContextPropagator,
W3CBaggagePropagator,
} from '@opentelemetry/core';
import { B3Propagator, B3InjectEncoding } from '@opentelemetry/propagator-b3';
// In NodeSDK-Konfiguration:
const sdk = new NodeSDK({
// ...andere Optionen...
textMapPropagator: new CompositePropagator({
propagators: [
new W3CTraceContextPropagator(), // modern, default
new W3CBaggagePropagator(), // Baggage für Business-Daten
new B3Propagator({ // Legacy Zipkin/Istio Kompatibilität
injectEncoding: B3InjectEncoding.MULTI_HEADER,
}),
],
}),
});
DIST Manuelle Context-Injection für HTTP-Clients
Auto-Instrumentation übernimmt das für fetch, axios und http. Für eigene HTTP-Clients oder Message-Queue-Nachrichten muss der Kontext manuell eingefügt werden:
import { context, propagation } from '@opentelemetry/api';
async function callPaymentService(
payload: PaymentPayload
): Promise<PaymentResult> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// Aktuellen Kontext in Headers injizieren
propagation.inject(context.active(), headers);
// Headers enthalten jetzt: traceparent, tracestate, baggage
const response = await fetch('http://payment-service/charge', {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
return response.json();
}
// Context aus eingehenden Requests extrahieren (für eigene HTTP-Server):
function extractContextFromRequest(req: IncomingMessage) {
const ctx = propagation.extract(context.active(), req.headers);
return ctx;
}
DIST Baggage — Business-Kontext über Servicegrenzen
Baggage ist ein Key-Value-Store im Trace-Kontext — für Werte die alle Services kennen müssen (Tenant-ID, Feature-Flags, A/B-Test-Gruppe):
import { propagation, context } from '@opentelemetry/api';
// Baggage setzen (z.B. im Auth-Middleware):
const bag = propagation.createBaggage({
'tenant.id': { value: tenantId },
'feature.checkout_v2': { value: 'true' },
'ab.group': { value: user.abGroup },
});
const ctx = propagation.setBaggage(context.active(), bag);
// Alle Kind-Spans + alle aufgerufenen Services haben Zugriff:
context.with(ctx, async () => {
const currentBag = propagation.getBaggage(context.active());
const tenantId = currentBag?.getEntry('tenant.id')?.value;
// tenantId ist überall verfügbar, auch 3 Service-Hops tiefer
});
6. Backends — Jaeger, Grafana Tempo, Honeycomb, Datadog
Das Beste an OTel: Du schreibst den Code einmal — und wechselst das Backend ohne eine Zeile Instrumentierungs-Code zu ändern. Nur der Exporter-Endpoint ändert sich. Claude Code generiert die passenden Konfigurationen für alle gängigen Backends.
BACKEND Jaeger — kostenlos, selbst gehostet, ideal für Entwicklung
# Jaeger All-in-One mit Docker (OTLP-Port 4317/4318)
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
# OTel Exporter auf Jaeger zeigen:
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
# UI: http://localhost:16686
BACKEND Grafana Tempo + Loki + Prometheus — der vollständige Stack
# docker-compose.yml (Ausschnitt)
services:
tempo:
image: grafana/tempo:latest
command: -config.file=/etc/tempo.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "3200:3200" # Tempo API
prometheus:
image: prom/prometheus:latest
ports: ["9090:9090"]
grafana:
image: grafana/grafana:latest
ports: ["3000:3000"]
environment:
- GF_FEATURE_TOGGLES_ENABLE=traceqlEditor
# Datasources: Tempo (Traces) + Prometheus (Metrics) + Loki (Logs)
# Grafana verknüpft automatisch Traces <-> Logs via TraceID
BACKEND Honeycomb — managed, developer-friendly, mächtiges Querying
// Honeycomb akzeptiert nativ OTLP — nur API-Key als Header:
const traceExporter = new OTLPTraceExporter({
url: 'https://api.honeycomb.io/v1/traces',
headers: {
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY!,
'x-honeycomb-dataset': 'checkout-service',
},
});
// Metrics zu Honeycomb:
const metricExporter = new OTLPMetricExporter({
url: 'https://api.honeycomb.io/v1/metrics',
headers: {
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY!,
'x-honeycomb-dataset': 'checkout-service-metrics',
},
});
BACKEND Datadog — Enterprise, OTLP via Datadog Agent
# datadog-agent.yaml (OTLP Receiver aktivieren)
otlp_config:
receiver:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
// App zeigt auf lokalen Datadog Agent (kein direkter DD-Endpoint nötig):
// OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://datadog-agent:4318/v1/traces
// OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://datadog-agent:4318/v1/metrics
// DD_API_KEY wird nur vom Agent gebraucht, nicht von der App
BACKEND Backend-Vergleich auf einen Blick
| Backend |
Hosting |
OTLP-native |
Best for |
| Jaeger |
Self-hosted |
Ja |
Entwicklung, kostenfrei |
| Grafana Tempo |
Self- oder Cloud |
Ja |
Open-Source Full-Stack |
| Honeycomb |
Managed Cloud |
Ja |
Developer Experience |
| Datadog |
Managed Cloud |
Via Agent |
Enterprise, Full-APM |
Empfehlung 2026: Starte lokal mit Jaeger (5 Minuten Setup). Für Produktion: Grafana Cloud (Tempo + Loki + Prometheus) ist für kleine Teams oft kosteneffizienter als Datadog — OTel macht den Wechsel jederzeit möglich.
7. Claude Code als OTel-Copilot — Praktischer Workflow
Claude Code versteht das OTel-API vollständig und kennt alle aktuellen SDK-Versionen (Stand 2026). Du beschreibst deine Business-Logik — Claude Code ergänzt die Instrumentierung ohne deine Kernfunktionen zu verändern.
WORKFLOW Typische Claude Code Prompts für OTel
# Bestehende Funktion instrumentieren:
claude "Füge OpenTelemetry Custom Spans zu dieser Funktion hinzu.
Erfasse: Eingangsparameter als Attribute, Fehler mit recordException,
Status OK/ERROR. Nutze tracer.startActiveSpan()."
# Metrics für Express-Route:
claude "Erstelle OTel Metrics für diese Express-Route:
Counter für Requests (mit status/method Labels),
Histogram für Response-Zeit, Gauge für aktive Connections."
# Komplettes OTel-Setup generieren:
claude "Erstelle instrumentation.ts für einen Node.js Service.
Backend: Grafana Tempo (OTLP HTTP). Instrumentiere: HTTP, Express,
Prisma ORM. Exportiere Metrics via Prometheus auf Port 9464."
WORKFLOW Sampling-Strategie für Produktions-Traffic
In der Produktion willst du nicht jeden Span exportieren — das kostet Geld und Performance. OTel bietet mehrere Sampling-Strategien:
import {
ParentBasedSampler,
TraceIdRatioBasedSampler,
AlwaysOnSampler,
} from '@opentelemetry/sdk-trace-base';
const sampler = new ParentBasedSampler({
// Root-Spans: 10% samplen
root: new TraceIdRatioBasedSampler(0.1),
// Kind-Spans: Entscheidung des Parents respektieren
remoteParentSampled: new AlwaysOnSampler(),
remoteParentNotSampled: {
shouldSample: () => ({ decision: SamplingDecision.NOT_RECORD }),
},
});
const sdk = new NodeSDK({
sampler,
// ...rest of config
});
// Tipp: Errors IMMER samplen, auch wenn Rate-Sampler sagt Nein
// → Custom Sampler der bei span.setStatus(ERROR) immer aufzeichnet
TRACE Logs mit Traces korrelieren
OTel-Logs ohne Trace-Korrelation sind schwer nutzbar. Füge TraceID + SpanID in jeden Log-Eintrag ein — dann kannst du von Grafana Loki direkt in Tempo springen:
import { trace, context } from '@opentelemetry/api';
import winston from 'winston';
const otelFormat = winston.format((info) => {
const span = trace.getActiveSpan();
if (span) {
const spanContext = span.spanContext();
info.trace_id = spanContext.traceId;
info.span_id = spanContext.spanId;
info.trace_flags = spanContext.traceFlags.toString(16);
}
return info;
});
const logger = winston.createLogger({
format: winston.format.combine(
otelFormat(),
winston.format.json()
),
transports: [new winston.transports.Console()],
});
// Log-Output enthält jetzt:
// {"level":"info","message":"Order created","trace_id":"4bf92f35...","span_id":"00f067aa..."}
Observability-Modul im Kurs
Im Claude Code Mastery Kurs: vollständiges Observability-Stack — OpenTelemetry, Sentry, Grafana und Prometheus für produktionsreife Node.js-Anwendungen. Schritt für Schritt, mit echtem Produktions-Code.
14 Tage kostenlos testen →