Zum Inhalt springen
DevOps

Observability mit OpenTelemetry: Traces, Metrics, Logs vereinheitlichen

11 min Lesezeit
Observability OpenTelemetry Monitoring

Drei Teams, drei Tools, drei Wahrheiten. Das Ops-Team schaut auf Grafana-Dashboards mit Prometheus-Metriken. Die Entwickler durchsuchen Elasticsearch nach Logs. Und wenn ein Request durch fuenf Services wandert und irgendwo haengen bleibt, oeffnet jemand Jaeger und hofft, dass die Trace-IDs irgendwie korrelieren. Das ist der Status quo in den meisten Organisationen: Observability als Flickenteppich aus isolierten Signalen, proprietaeren Agents und vendor-spezifischen SDKs.

OpenTelemetry loest dieses Problem nicht, indem es ein weiteres Tool auf den Stapel wirft. Es loest es, indem es die Instrumentierungsschicht standardisiert — unabhängig davon, welches Backend die Daten am Ende speichert und visualisiert.

Was OpenTelemetry ist (und was nicht)

OpenTelemetry (OTel) ist ein CNCF-Projekt, das APIs, SDKs und Tools für die Erzeugung, Sammlung und den Export von Telemetriedaten bereitstellt. Es ist das zweitaktivste CNCF-Projekt nach Kubernetes — kein Nischenprojekt, sondern Industriestandard.

Wichtig ist, was OTel nicht ist: kein Backend, kein Dashboard, keine Speicherloesung. OTel kuemmert sich ausschließlich um die Instrumentierung und den Transport. Wo die Daten landen — Jaeger, Grafana Tempo, Datadog, Honeycomb — ist eine separate Entscheidung. Genau das ist die Stärke: Die Instrumentierung wird einmal geschrieben und ueberlebt jeden Backend-Wechsel.

Die drei Signale: Traces, Metrics, Logs

Observability basiert auf drei komplementaeren Signaltypen. Jeder beantwortet eine andere Frage.

Traces zeigen den Weg eines Requests durch das System. Ein Trace besteht aus Spans — einzelne Arbeitseinheiten mit Start, Ende, Attributen und Status. Spans sind hierarchisch: ein Root-Span repraesentiert den gesamten Request, Child-Spans die einzelnen Schritte (Datenbankabfrage, HTTP-Call, Queue-Verarbeitung). Traces beantworten: Wo war der Request langsam, welcher Service hat den Fehler verursacht?

Metrics sind numerische Messwerte über Zeit: Request-Rate, Latenz-Percentile, Error-Rate, CPU-Auslastung. Sie sind billig zu speichern und eignen sich für Alerting und Dashboards. Metrics beantworten: Wie viel und wie schnell?

Logs sind einzelne Ereignisse mit Kontext: eine Fehlermeldung, ein Audit-Eintrag, ein Debug-Output. Sie sind unstrukturiert oder semi-strukturiert und beantworten: Was genau ist passiert?

Die eigentliche Kraft entsteht durch die Korrelation. Ein Alert auf einer Metrik (Error-Rate über 5%) fuehrt zum relevanten Trace, der zeigt welcher Service fehlschlägt. Der Trace verweist auf die zugehoerigen Logs mit der konkreten Fehlermeldung. OpenTelemetry macht diese Korrelation möglich, indem alle drei Signale denselben Kontext teilen: Trace-ID und Span-ID.

Architektur: SDK, Exporters, Collector

Die OTel-Architektur besteht aus drei Schichten:

SDK: Wird in die Anwendung eingebunden. Erzeugt Spans, zaehlt Metriken, strukturiert Logs. Das SDK bietet Auto-Instrumentierung für gaengige Frameworks und Libraries sowie eine API für manuelle Instrumentierung.

Exporters: Serialisieren die Telemetriedaten und senden sie an ein Ziel. OTLP (OpenTelemetry Protocol) ist der Standard, aber es gibt Exporter für Jaeger, Prometheus, Zipkin und proprietaere Backends.

Collector: Ein optionaler, aber dringend empfohlener Proxy-Dienst, der Telemetriedaten empfaengt, verarbeitet und weiterleitet. Der Collector entkoppelt die Anwendung vom Backend und ermöglicht Batching, Retry, Sampling und Routing.

Praktische Instrumentierung: Node.js mit Auto-Instrumentation

Die schnellste Art, OpenTelemetry in eine bestehende Node.js-Anwendung zu integrieren, ist die Auto-Instrumentierung. Sie patcht bekannte Libraries (Express, http, pg, redis) automatisch und erzeugt Spans ohne Code-Änderungen.

Dependencies installieren bash
npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http

Die Instrumentierung muss vor dem Anwendungscode geladen werden. Eine separate Datei, die per --require eingebunden wird:

tracing.ts -- OTel SDK Konfiguration typescript
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]: 'order-service',
    [ATTR_SERVICE_VERSION]: '1.4.2',
  }),
  traceExporter: new OTLPTraceExporter({
    url: 'http://otel-collector:4318/v1/traces',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'http://otel-collector:4318/v1/metrics',
    }),
    exportIntervalMillis: 15000,
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-fs': { enabled: false },
    }),
  ],
});

sdk.start();

process.on('SIGTERM', () => {
  sdk.shutdown().then(() => process.exit(0));
});

Start der Anwendung mit node --require ./tracing.js dist/server.js. Ab diesem Moment erzeugt jeder eingehende HTTP-Request automatisch einen Trace mit Spans für Express-Routing, ausgehende HTTP-Calls und Datenbankabfragen. Ohne eine einzige Zeile im Anwendungscode.

Custom Spans: Business-relevantes Tracing

Auto-Instrumentierung deckt die Infrastruktur ab — HTTP, Datenbank, Queue. Aber die wirklich wertvollen Informationen sind oft business-spezifisch: Wie lange dauert die Preisberechnung? Welche Zahlungsmethode wurde gewaehlt? Warum wurde ein Rabatt abgelehnt?

Dafuer braucht es manuelle Instrumentierung mit Custom Spans und Attributen:

Custom Spans für Business-Logik typescript
import { trace, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('order-service', '1.4.2');

async function processOrder(order: Order): Promise<OrderResult> {
  return tracer.startActiveSpan('order.process', async (span) => {
    try {
      span.setAttribute('order.id', order.id);
      span.setAttribute('order.item_count', order.items.length);
      span.setAttribute('order.currency', order.currency);

      // Preisberechnung als eigener Span
      const total = await tracer.startActiveSpan(
        'order.calculate_total',
        async (calcSpan) => {
          const result = await calculateTotal(order.items);
          calcSpan.setAttribute('order.total_cents', result);
          calcSpan.setAttribute('order.discount_applied', result.discountApplied);
          calcSpan.end();
          return result;
        }
      );

      // Zahlungsverarbeitung als eigener Span
      const payment = await tracer.startActiveSpan(
        'order.process_payment',
        async (paySpan) => {
          paySpan.setAttribute('payment.method', order.paymentMethod);
          const result = await chargePayment(order, total);
          paySpan.setAttribute('payment.status', result.status);
          paySpan.end();
          return result;
        }
      );

      span.setAttribute('order.status', 'completed');
      span.end();
      return { orderId: order.id, status: 'completed' };
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      span.end();
      throw error;
    }
  });
}

Das Ergebnis: Im Trace-Backend sieht man nicht nur “POST /api/orders dauerte 340ms”, sondern dass die Preisberechnung 12ms brauchte, die Zahlungsverarbeitung 280ms — und dass die Kreditkarten-API der Flaschenhals war. Mit den Custom-Attributen laesst sich nach konkreten Bestellungen, Zahlungsmethoden oder Fehlerstatus filtern.

Der OpenTelemetry Collector

Den Collector direkt aus der Anwendung heraus zu umgehen und Telemetriedaten ans Backend zu senden, funktioniert technisch. Ist aber eine schlechte Idee. Der Collector ist der zentrale Baustein für eine robuste Observability-Pipeline.

Die Architektur folgt einem einfachen Muster: Receivers nehmen Daten entgegen, Processors transformieren sie, Exporters leiten sie weiter. Alles wird in Pipelines zusammengesteckt.

otel-collector-config.yaml yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024

  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128

  attributes:
    actions:
      - key: environment
        value: production
        action: upsert

  tail_sampling:
    decision_wait: 10s
    policies:
      - name: error-traces
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-traces
        type: latency
        latency: { threshold_ms: 2000 }
      - name: sample-rest
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }

exporters:
  otlphttp/tempo:
    endpoint: http://tempo:4318

  prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write

  loki:
    endpoint: http://loki:3100/loki/api/v1/push

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch, attributes]
      exporters: [otlphttp/tempo]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [loki]

Die Tail-Sampling-Konfiguration ist entscheidend: Alle Fehler und alle langsamen Requests (über 2 Sekunden) werden behalten. Vom Rest werden nur 10% gespeichert. Das reduziert das Datenvolumen um 90%, ohne dass die relevanten Traces verloren gehen. Head-Sampling in der Anwendung selbst würde diese Entscheidung zu früh treffen — ein Request, der erst beim fünften Service-Hop fehlschlägt, wäre möglicherweise schon vorher verworfen worden.

Context Propagation: Der unsichtbare Klebstoff

Damit ein Trace über Service-Grenzen hinweg zusammenhaengend bleibt, muss der Trace-Kontext (Trace-ID, Span-ID, Flags) von Service zu Service weitergereicht werden. OTel nutzt dafür den W3C Trace Context Standard — ein HTTP-Header namens traceparent.

Bei Auto-Instrumentierung passiert das automatisch: Ausgehende HTTP-Requests erhalten den Header, eingehende Requests lesen ihn aus. Aber bei asynchroner Kommunikation über Message Queues (RabbitMQ, Kafka) muss der Kontext manuell in die Message-Metadaten geschrieben werden.

Context Propagation über Message Queues typescript
import { context, propagation, trace } from '@opentelemetry/api';

// Producer: Kontext in Message-Header injizieren
function publishEvent(queue: string, payload: object): void {
  const carrier: Record<string, string> = {};
  propagation.inject(context.active(), carrier);

  channel.publish(queue, {
    body: JSON.stringify(payload),
    headers: carrier, // enthaelt traceparent + tracestate
  });
}

// Consumer: Kontext aus Message-Header extrahieren
function handleMessage(message: ConsumeMessage): void {
  const parentContext = propagation.extract(
    context.active(),
    message.properties.headers
  );

  context.with(parentContext, () => {
    const span = trace.getTracer('order-service')
      .startSpan('process_message', undefined, parentContext);

    try {
      processPayload(JSON.parse(message.content.toString()));
      span.end();
    } catch (error) {
      span.recordException(error);
      span.end();
      throw error;
    }
  });
}

Ohne korrekte Context Propagation zerfaellt ein verteilter Trace in isolierte Fragmente. Jeder Service erzeugt einen eigenen Trace statt eines zusammenhaengenden Bildes. Das ist der häufigste Grund, warum Teams OpenTelemetry einrichten und trotzdem keine Ende-zu-Ende-Sichtbarkeit haben.

Backends anbinden: Jaeger, Tempo, Prometheus

Der Collector macht die Backend-Wahl zur reinen Konfigurationsfrage. Gaengige Kombinationen:

Traces: Grafana Tempo (kosteneffizient, object-storage-basiert) oder Jaeger (bewahrt, gut für den Einstieg). Beide verstehen OTLP nativ.

Metrics: Prometheus bleibt der De-facto-Standard. Der Collector kann Metriken per Remote Write pushen oder als Prometheus-Endpoint exponieren, den Prometheus scrapt.

Logs: Grafana Loki für kosteneffizientes Log-Management, Elasticsearch für Volltextsuche. Der OTel Collector kann in beide exportieren.

Der Grafana-Stack (Tempo, Prometheus, Loki) hat einen entscheidenden Vorteil: Grafana kann Trace-IDs in Logs als Links zu Tempo darstellen und Exemplars in Metriken direkt mit Traces verbinden. Die drei Signale werden im Dashboard korrelierbar.

Haeufige Fehler und Anti-Patterns

Alles tracen, nichts samplen: Ohne Sampling erzeugt ein Service mit 1.000 Requests pro Sekunde Gigabytes an Trace-Daten pro Stunde. Tail-Sampling im Collector ist Pflicht für jede Produktionsumgebung.

Fehlende Resource-Attribute: Ohne service.name, service.version und deployment.environment sind Traces im Backend nicht zuordenbar. Diese drei Attribute sind das absolute Minimum.

Synchrone Exporter in der Anwendung: Wer Telemetriedaten synchron exportiert, macht die Anwendungsperformance vom Backend abhängig. Immer den Collector als Puffer dazwischenschalten. Wenn das Backend langsam ist oder ausfaellt, puffert der Collector — die Anwendung merkt nichts.

Sensitive Daten in Spans: Span-Attribute wie db.statement können SQL-Queries mit Nutzerdaten enthalten. HTTP-Header-Attribute können Auth-Tokens leaken. Der Collector kann mit Attribute-Processors gezielt Felder redacten oder entfernen — aber nur, wenn man daran denkt, das zu konfigurieren.

Instrumentierung ohne Konvention: OTel definiert Semantic Conventions — standardisierte Attributnamen wie http.request.method, db.system, messaging.system. Eigene Namen wie myapp.db_type statt db.system brechen die Kompatibilitaet mit Dashboards und Alerting-Rules, die auf den Standard aufbauen.

Fazit

OpenTelemetry ist keine weitere Option im Observability-Werkzeugkasten — es ist die Konvergenz. Die Fragmentierung aus proprietaeren Agents, inkompatiblen Datenformaten und vendor-spezifischen SDKs hat ein Ablaufdatum. Wer heute instrumentiert, sollte das vendor-neutral tun. Die Investition in OTel ist eine Investition in Portabilitaet, Standardisierung und die Freiheit, Backends nach Leistung und Kosten zu waehlen statt nach Lock-in.

Der Einstieg ist niedrigschwellig: Auto-Instrumentierung liefert sofort Ergebnisse, der Collector ist in fuenf Minuten konfiguriert, und Custom Spans können schrittweise dort ergänzt werden, wo sie den größten Erkenntnisgewinn bringen. Die drei Signale unter einem Dach — das ist keine Vision mehr, sondern produktionsreifer Standard.