OpenTelemetry in der Produktion: Traces, Context und was wirklich zählt
Produktions-OpenTelemetry-Patterns. Context Propagation über Queues und Worker, LLM-Calls tracen, Sampling-Strategien für KI-Workloads, datenschutzkonforme Spans und die Baggage API.
Warum OpenTelemetry gewonnen hat
Vor drei Jahren war die Observability-Landschaft fragmentiert. Jaeger für Tracing, Prometheus für Metriken, Fluentd für Logs, jedes mit eigenem SDK, eigenem Protokoll, eigenem Vendor Lock-in. OpenTelemetry hat sie in einen einzigen Standard vereint: ein SDK, ein Protokoll (OTLP), ein Collector, der an jedes Backend routed.
Wir haben OpenTelemetry in unseren Produktionssystemen eingeführt. Dieser Artikel behandelt die Patterns, die in der Produktion wirklich zählen, kein Setup-Tutorial. Für die übergeordnete Observability-Strategie (wann Alerts auslösen, was loggen, wie Metriken strukturieren) siehe unseren KI-Observability-Guide. Dieser Artikel geht tiefer in OpenTelemetry-spezifische Implementierung.
Context Propagation: Der schwierigste Teil
Ein einzelner User-Request kann 5 Services, 3 Message Queues, 2 Worker-Prozesse und einen LLM-Aufruf durchlaufen. Context Propagation stellt sicher, dass der Trace den Request über alle Stationen hinweg verfolgt.
HTTP Propagation (Einfach)
OpenTelemetry instrumentiert HTTP-Clients und -Server automatisch. Der Trace Context wird über traceparent- und tracestate-Header propagiert. Das funktioniert direkt out of the box.
// Auto-instrumentiert: kein Code nötig für HTTP Propagation
// Das SDK fügt den traceparent Header zu ausgehenden Requests hinzu
// Der empfangende Service extrahiert ihn und setzt den Trace fort
Queue Propagation (Schwierig)
Message Queues unterbrechen die automatische Propagation. Wenn du eine Nachricht einreihst, muss der Trace Context in die Message-Header serialisiert werden. Wenn ein Worker sie dequeued, muss der Context extrahiert und der Trace fortgesetzt werden.
// Producer: Trace Context in Message-Header injizieren
import { context, propagation } from '@opentelemetry/api';
async function enqueueMessage(queue: string, payload: any) {
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
await messageQueue.send(queue, {
body: payload,
headers: carrier, // Enthält traceparent, tracestate
});
}
// Consumer: Trace Context aus Message-Header extrahieren
async function processMessage(message: QueueMessage) {
const parentContext = propagation.extract(context.active(), message.headers);
await context.with(parentContext, async () => {
const span = tracer.startSpan('process_message', {
attributes: {
'messaging.system': 'rabbitmq',
'messaging.operation': 'process',
'messaging.destination': message.queue,
},
});
try {
await handleMessage(message.body);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
});
}
Dieses Pattern funktioniert für RabbitMQ, Kafka, BullMQ, SQS und Symfony Messenger. Die Message-Header tragen den Trace Context. Der Consumer extrahiert ihn und erstellt Child Spans unter dem originalen Trace.
Worker-Prozess-Propagation
Für Vendures BullMQ Worker, Pimcores Symfony Messenger Worker und ähnliche Background-Job-Systeme ist das Pattern identisch: Context in den Job-Payload serialisieren, auf der Worker-Seite extrahieren.
// BullMQ: Trace Context zu Job-Daten hinzufügen
async function addJob(queue: Queue, data: any) {
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
await queue.add('process', {
...data,
_traceContext: carrier,
});
}
// BullMQ: Trace Context im Worker extrahieren
worker.on('process', async (job) => {
const parentContext = propagation.extract(context.active(), job.data._traceContext || {});
await context.with(parentContext, async () => {
const span = tracer.startSpan(`job:${job.name}`);
try {
await processJob(job.data);
} finally {
span.end();
}
});
});
LLM-Calls tracen
LLM-Aufrufe sind die teuersten Operationen in KI-Systemen. Sie mit den richtigen Attributen zu tracen ermöglicht Cost Tracking, Latenz-Analyse und Quality Monitoring.
async function tracedLlmCall(prompt: string, options: LlmOptions): Promise<string> {
const span = tracer.startSpan('llm.generate', {
attributes: {
'llm.provider': options.provider, // "openai", "anthropic"
'llm.model': options.model, // "gpt-4o", "claude-sonnet-4-20250514"
'llm.temperature': options.temperature,
'llm.max_tokens': options.maxTokens,
'llm.prompt_tokens': estimateTokens(prompt), // Schätzung vor dem Aufruf
},
});
try {
const response = await llmClient.generate(prompt, options);
span.setAttributes({
'llm.response_tokens': response.usage.completionTokens,
'llm.total_tokens': response.usage.totalTokens,
'llm.finish_reason': response.finishReason,
'llm.cost_usd': calculateCost(response.usage, options.model),
});
span.setStatus({ code: SpanStatusCode.OK });
return response.text;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.setAttribute('llm.error_type', error.constructor.name);
throw error;
} finally {
span.end();
}
}
Schreibe KEINE Prompt-Texte in Span-Attribute. Prompts enthalten personenbezogene Daten (PII). Span-Attribute werden an dein Observability-Backend gesendet (Jaeger, Grafana Tempo, Datadog). Logge stattdessen den Prompt-Hash oder den Token Count. Für die vollständige PII-sichere Logging-Architektur siehe unseren KI-Datenleck-Prävention-Guide.
LLM Span Attributes Convention
| Attribut | Typ | Beispiel |
|---|---|---|
llm.provider | string | "openai" |
llm.model | string | "gpt-4o" |
llm.temperature | float | 0.7 |
llm.prompt_tokens | int | 1250 |
llm.response_tokens | int | 340 |
llm.total_tokens | int | 1590 |
llm.finish_reason | string | "stop" |
llm.cost_usd | float | 0.023 |
llm.error_type | string | "RateLimitError" |
llm.cache_hit | boolean | false |
Sampling-Strategien
In hochvolumigen KI-Systemen ist das Tracen jedes Requests zu teuer. Sampling reduziert das Volumen und bewahrt gleichzeitig die Sichtbarkeit auf wichtige Traces.
Head-Based Sampling
Entscheide am Anfang des Trace, ob er gesampled wird. Einfach, aber verlustbehaftet.
// 10% aller Traces samplen
const sampler = new TraceIdRatioBased(0.1);
// Fehler immer samplen (Override der Rate für Error Traces)
const compositeSampler = new ParentBasedSampler({
root: new TraceIdRatioBased(0.1),
// Errors werden immer via Span Processor gesampled
});
Tail-Based Sampling (Empfohlen für KI)
Entscheide nach Abschluss des Trace, ob er behalten wird. Behält alle interessanten Traces (Fehler, langsame Antworten, hohe Kosten) und verwirft Routine-Traces.
// OpenTelemetry Collector: Tail-Based Sampling Konfiguration
processors:
tail_sampling:
decision_wait: 10s
policies:
# Alle Fehler behalten
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
# Langsame Traces behalten (> 5 Sekunden)
- name: slow
type: latency
latency: { threshold_ms: 5000 }
# Teure LLM-Aufrufe behalten (> $0.10)
- name: expensive_llm
type: string_attribute
string_attribute:
key: llm.cost_usd
values: [] # Custom: Filterung in der Pipeline
enabled_regex_matching: true
# 5% von allem anderen samplen
- name: baseline
type: probabilistic
probabilistic: { sampling_percentage: 5 }
Tail-Based Sampling erfordert den OpenTelemetry Collector. Der Collector puffert vollständige Traces, evaluiert Policies und leitet nur gesamplete Traces ans Backend weiter. Das fügt Latenz hinzu (die decision_wait-Periode), reduziert aber die Speicherkosten dramatisch, während alle interessanten Daten erhalten bleiben.
Datenschutzkonforme Spans
Span-Attribute, Span-Namen und Span-Events werden alle an dein Observability-Backend gesendet. Wenn eines davon personenbezogene Daten enthält, wird deine Tracing-Infrastruktur zu einer Datenschutz-Haftung.
// SCHLECHT: PII in Span-Attributen
span.setAttribute('user.email', 'sara.mustermann@beispiel.de');
span.setAttribute('user.name', 'Sara Mustermann');
span.setAttribute('request.body', JSON.stringify(requestBody)); // Enthält PII
// GUT: Nur Token-IDs und aggregierte Daten
span.setAttribute('user.id', 'usr_abc123'); // Opaque ID, kein PII
span.setAttribute('entities.detected', 3);
span.setAttribute('entities.types', ['person', 'email', 'phone']);
span.setAttribute('policy.applied', 'german-support');
Regeln für datenschutzkonformes Tracing:
- User-IDs: nur opaque Identifier (keine E-Mails, keine Namen)
- Request Bodies: niemals Rohdaten einschließen. Logge Entity Counts und Typen.
- LLM-Prompts: niemals einschließen. Logge Token Counts und Prompt-Hash.
- Fehlermeldungen: sanitize vor dem Anhängen an Spans. Entferne alle Benutzerdaten.
Die Baggage API
OpenTelemetry Baggage transportiert Key-Value-Paare über Service-Grenzen hinweg. Anders als Span-Attribute (die beim Span bleiben) propagiert Baggage automatisch an alle Downstream-Services.
import { propagation, context, baggage } from '@opentelemetry/api';
// Baggage am API Gateway setzen
const bag = propagation.createBaggage({
'tenant.id': { value: 'tenant_acme' },
'request.priority': { value: 'high' },
'feature.flags': { value: 'new-checkout,beta-search' },
});
const ctx = propagation.setBaggage(context.active(), bag);
// Downstream-Services können Baggage lesen
const tenantId = propagation.getBaggage(context.active())?.getEntry('tenant.id')?.value;
Nützlich für:
- Tenant-ID-Propagation (jeder Downstream-Service kennt den Tenant)
- Feature Flags (Experiment-Zuweisungen über Services hinweg propagieren)
- Priority Routing (hochpriorisierte Requests bekommen andere Queue-Behandlung)
- Debug Marker (bestimmte Requests für ausführliches Logging markieren)
Baggage reist mit dem Trace Context in HTTP-Headern und Message-Metadaten. Jeder Service, der den Trace Context extrahiert, bekommt auch die Baggage.
Collector-Architektur
Der OpenTelemetry Collector ist die zentrale Routing-Schicht zwischen deinen Anwendungen und deinen Observability-Backends.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service A │ │ Service B │ │ Worker C │
│ (OTLP gRPC) │ │ (OTLP HTTP) │ │ (OTLP gRPC) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ OTel Collector │
│ │
│ Receivers: OTLP (gRPC + HTTP) │
│ Processors: batch, tail_sampling, attributes │
│ Exporters: Tempo, Prometheus, Loki │
└─────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Grafana │ │ Prometheus │ │ Grafana │
│ Tempo │ │ │ │ Loki │
│ (Traces) │ │ (Metriken) │ │ (Logs) │
└──────────────┘ └──────────────┘ └──────────────┘
Der Collector handhabt Batching (reduziert Netzwerkaufrufe), Sampling (reduziert Speicher), Attribut-Verarbeitung (fügt Attribute hinzu/entfernt sie) und Routing (verschiedene Signale an verschiedene Backends). Deploye ihn als Sidecar oder als zentralen Service, abhängig von deiner Infrastruktur.
Für Cloud-Deployment-Patterns einschließlich Observability-Infrastruktur beschreibt diese Seite unseren Ansatz.
Häufige Stolperfallen
-
Keine Context Propagation über Queues. HTTP Propagation ist automatisch. Queue Propagation nicht. Wenn du Context nicht in Message-Header injizierst/extrahierst, brechen Traces an jeder Queue-Grenze.
-
PII in Span-Attributen. Dein Tracing-Backend indiziert alles. Wenn Spans E-Mails, Namen oder Request Bodies enthalten, ist dein Grafana Tempo Cluster ein PII-Speicher.
-
Jeden Request in der Produktion tracen. Bei 1000 RPS generiert volles Tracing Terabytes an Daten. Verwende Tail-Based Sampling, um Fehler, langsame Traces und teure Operationen zu behalten.
-
Keine LLM-spezifischen Attribute. Ohne Token Counts, Kosten, Model-ID und Finish Reason auf LLM-Spans kannst du KI-Kosten nicht tracken oder Qualitätsprobleme diagnostizieren.
-
Head-Based Sampling verwirft Fehler. Wenn du 10% der Traces samplest und ein Fehler in den 90% passiert, die du verwirfst, siehst du ihn nie. Verwende Tail-Based Sampling oder Always-Sample-Errors-Policies.
-
Baggage für große Payloads. Baggage reist mit jedem Request. Große Werte erhöhen die Header-Größe bei jedem HTTP-Aufruf. Halte Baggage-Werte klein (IDs, Flags, Prioritäten).
Kernerkenntnisse
-
Context Propagation über Queues ist der schwierigste Teil. HTTP ist automatisch. Queues erfordern manuelles Inject/Extract des Trace Context in Message-Headern. Hier scheitern die meisten Distributed-Tracing-Implementierungen.
-
Trace LLM-Aufrufe mit Kosten- und Token-Attributen. Model, Provider, Token Counts, Kosten, Finish Reason. Diese Attribute ermöglichen KI-Kosten-Dashboards und Quality Monitoring.
-
Tail-Based Sampling für KI-Workloads. Behalte alle Fehler, langsamen Traces und teuren Operationen. Verwirf Routine-Traces. Reduziert den Speicher um 90%+ und behält alle interessanten Daten.
-
Kein PII in Spans. Opaque User-IDs, Entity Counts, Token Types. Niemals Rohdaten, E-Mails, Namen oder Request Bodies.
-
Baggage propagiert Tenant Context. Setze Tenant-ID, Feature Flags und Priorität am Edge. Jeder Downstream-Service liest es aus der Baggage, ohne explizite Parameterübergabe.
Wir implementieren OpenTelemetry in unseren KI-Services, Custom-Software-Projekten und unserer Cloud-Infrastruktur. Wenn du Observability für ein verteiltes System aufbaust, sprich mit unserem Team oder fordere ein Angebot an.
Behandelte Themen
Verwandte Guides
Observability, die um 3 Uhr nachts hilft: Logs, Traces und was wirklich zählt
Produktions-Observability jenseits von Dashboards. Strukturiertes Logging, Correlation IDs, PII-freie Logs, Alert-Fatigue-Prävention, Kostenmanagement und das Observability-Reifegradmodell.
Guide lesenUnternehmenshandbuch zu Agentischen KI-Systemen
Technischer Leitfaden zu agentischen KI-Systemen in Unternehmen. Erfahre mehr ueber Architektur, Faehigkeiten und Anwendungen autonomer KI-Agenten.
Guide lesenAgentic Commerce: Wie du KI-Agenten sicher einkaufen lässt
Wie du gesteuerten, KI-initiierten Handel designst. Policy Engines, HITL-Freigabe-Gates, HMAC-Quittungen, Idempotenz, Tenant-Scoping und das vollständige Agentic Checkout Protocol.
Guide lesenBereit, produktionsreife KI-Systeme zu bauen?
Unser Team ist spezialisiert auf produktionsreife KI-Systeme. Lass uns besprechen, wie wir deinem Unternehmen helfen können.
Gespräch starten