OpenTelemetry für PHP und Node: Eine Instrumentierungs-Baseline ohne Vendor-Lock-in

#OpenTelemetry PHP
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Die OpenTelemetry-Unterstützung für PHP ist 2026 so weit gereift, dass es keinen guten Grund mehr gibt, zuerst zu einem proprietären SDK zu greifen. Wenn du heute einen PHP- oder Node.js-Dienst instrumentierst, ist der Start mit einem anbieterspezifischen Agent eine Wahl, für die du wahrscheinlich später zahlst - in Migrationsaufwand, in überraschenden Preisen oder in der Reibung des Umstiegs, wenn dein Monitoring-Anbieter übernommen wird oder die Preise erhöht.

Dieser Beitrag geht eine praktische Instrumentierungs-Baseline für PHP und Node.js mit OpenTelemetry durch. Am Ende hast du Traces, Metriken und strukturierte Logs, die zu einem Collector fließen - und du wirst das Backend austauschen können, ohne Anwendungscode anzufassen.


Warum OpenTelemetry und nicht ein Anbieter-SDK?

Die ehrliche Antwort ist: OpenTelemetry erzwingt eine gute Instrumentierungshygiene.

Anbieter-SDKs neigen dazu, passive Instrumentierung zu fördern - installiere den Agent, lass ihn alles auto-instrumentieren und hoffe, dass die Traces Sinn ergeben. OpenTelemetry funktioniert bei der Auto-Instrumentierung genauso, aber weil das Datenmodell offen und breit dokumentiert ist, denkst du am Ende über Span-Attribute, Propagation-Header und Sampling-Strategien nach, statt Observability als Blackbox zu behandeln.

Der andere Grund ist Kostenkontrolle. Sobald deine Signale durch den OpenTelemetry Collector laufen, kannst du aggressiv sampeln, bevor du exportierst. Head-based Sampling auf Collector-Ebene kann deine Ingestion-Rechnung um 60-80% senken, ohne eine Zeile Anwendungscode zu ändern. Proprietäre Agents geben dir hier im Allgemeinen weniger Kontrolle.


Was die Baseline abdeckt

Ein minimal tragfähiges Observability-Setup für einen produktiven PHP- oder Node.js-Dienst braucht drei Dinge:

  • Verteilte Traces - Request-Flüsse über Dienstgrenzen hinweg, einschließlich Datenbankabfragen, externer HTTP-Aufrufe und Queue-Operationen
  • Runtime-Metriken - Speicher, CPU, Event-Loop-Lag (Node), Garbage-Collection-Häufigkeit (PHP/Node)
  • Strukturierte Logs mit Trace-Kontext - Log-Zeilen, die mit der Trace-ID korreliert sind, sodass du von einem Span zu den relevanten Log-Zeilen springen kannst

Dieser Beitrag konzentriert sich auf Traces und Metriken. Log-Korrelation ist ein Einzeiler, sobald der Trace-Kontext korrekt propagiert wird.


PHP-Instrumentierung mit opentelemetry-php

Das PHP-SDK ist für Traces stabil und für Metriken Beta, Stand Anfang 2026. Für die meisten Produktionsanwendungsfälle reicht das.

Das SDK installieren

composer require open-telemetry/sdk open-telemetry/exporter-otlp

Für Auto-Instrumentierung von Symfony, PSR-7-HTTP-Clients, PDO und Redis füge die relevanten Contrib-Pakete hinzu:

composer require open-telemetry/opentelemetry-auto-symfony \
  open-telemetry/opentelemetry-auto-pdo \
  open-telemetry/opentelemetry-auto-redis

Auto-Instrumentierung klinkt sich in Symfonys Kernel-Events, die PDO-Statement-Ausführung und Redis-Befehle ein, ohne dass in deinem Code manuelle Span-Erzeugung nötig ist.

Das SDK über Umgebungsvariablen konfigurieren

OpenTelemetry respektiert die Standard-Spezifikation für Umgebungsvariablen, sodass du es außerhalb deines Anwendungscodes konfigurierst:

OTEL_SERVICE_NAME=my-php-app
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1

OTEL_TRACES_SAMPLER_ARG=0.1 zu setzen sampelt 10% der Root-Spans, die keinen Parent haben. Passe es an dein Traffic-Volumen und das Kostenmodell deines Backends an.

Kontext-Propagation verifizieren

Der häufigste Fehler bei der PHP-Instrumentierung ist gebrochener Trace-Kontext über asynchrone Grenzen hinweg - Jobs, die an eine Queue geschickt werden, oder Events, die in einem separaten Prozess behandelt werden. Wenn ein Hintergrund-Job einen neuen Root-Span startet, statt den Trace vom auslösenden HTTP-Request fortzusetzen, verlierst du das Gesamtbild.

Um korrekt zu propagieren, serialisiere den aktuellen Kontext beim Dispatchen:

$propagator = Globals::propagator();
$carrier = [];
$propagator->inject($carrier);
// Store $carrier alongside the job payload

Und extrahiere ihn am Beginn des Job-Handlers:

$propagator = Globals::propagator();
$context = $propagator->extract($carrier);
$span = $tracer->spanBuilder('process-job')
    ->setParent($context)
    ->startSpan();

Das ist der Teil, den Anbieter-Auto-Instrumentierung am häufigsten falsch macht. Explizite Propagation ist die zusätzlichen Zeilen wert.


Node.js-Instrumentierung mit @opentelemetry/sdk-node

Das Node.js-SDK ist das reifste im OpenTelemetry-Ökosystem. Auto-Instrumentierung deckt Express, Fastify, Koa, HTTP, gRPC, Prisma, Sequelize, Redis und die meisten Datenbanken ab, die du wahrscheinlich verwendest.

Installation

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

Bootstrap-Datei

Erstelle eine tracing.js (oder tracing.ts), die das SDK vor allen anderen Imports initialisiert:

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';

const sdk = new NodeSDK({
  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: 30000,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

Starte deine Anwendung mit dieser Datei zuerst geladen:

node --require ./tracing.js index.js

Oder mit --import für ESM:

node --import ./tracing.js index.js

Event-Loop-Metriken

Die Standard-Auto-Instrumentierung enthält Node.js-Runtime-Metriken - Event-Loop-Lag, aktive Handles, Garbage-Collection-Dauer. Das sind die Metriken, die dir sagen, ob dein Dienst unter Last gesund ist, bevor Fehler auftauchen.

Wenn du ein Framework wie Fastify verwendest, füge eine Prüfung auf Prozessebene hinzu, die alarmiert, wenn der Event-Loop-Lag 100 ms überschreitet. Diese Schwelle fängt die meisten blockierenden Operationen ab, bevor Nutzer sie bemerken.


Der OpenTelemetry Collector: Deine Sampling- und Routing-Ebene

Sowohl die PHP- als auch die Node-Dienste oben exportieren zu einem OpenTelemetry Collector. Das ist nicht optional, wenn du Anbieterunabhängigkeit willst.

Eine minimale Collector-Konfiguration, die OTLP akzeptiert, Tail-Sampling anwendet und an Grafana Tempo (oder ein beliebiges OTLP-kompatibles Backend) weiterleitet:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100
    policies:
      - name: errors-policy
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-traces-policy
        type: latency
        latency: { threshold_ms: 1000 }
      - name: probabilistic-policy
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }
  batch:

exporters:
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [tail_sampling, batch]
      exporters: [otlp]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp]

Die Tail-Sampling-Policy hier behält 100% der Fehler-Traces und langsamen Traces (über 1 Sekunde) und sampelt 5% von allem anderen. Für einen Dienst, der 1.000 Requests pro Minute verarbeitet, bedeutet das, dass du grob 100 Traces pro Minute plus alle Fehler speicherst - mehr als genug fürs Debugging.


Häufige Fallstricke

Mit allen Signalen bei vollem Volumen starten. Schalte Sampling ab dem ersten Tag ein. Ein Trace-Backend, das ungesampelten Produktions-Traffic aufnimmt, wird innerhalb eines Monats mehr kosten als deine gesamte Infrastruktur.

OTEL_SERVICE_NAME nicht setzen. Ohne Dienstnamen landen alle deine Traces in einem Default-Bucket und die Korrelation über Dienste hinweg wird unmöglich.

Span-Attribute auf eigenen Spans überspringen. Auto-Instrumentierung gibt dir HTTP-Methode, URL, Statuscode und Datenbankabfrage. Aber Kontext auf Geschäftsebene - Nutzer-ID, Mandanten-ID, Bestell-ID - erfordert manuelles Setzen von Attributen. Füge diese auf den Spans hinzu, die für deinen Debugging-Workflow wichtig sind, nicht überall.

Das SDK-Shutdown vergessen. Registriere in Node.js einen process.on('SIGTERM')-Handler, der sdk.shutdown() aufruft. Ohne dies geht der letzte Batch Telemetrie vor einem Pod-Neustart verloren.


Ein Backend wählen

Sobald du zum Collector exportierst, ist das Backend eine Konfigurationsänderung. Gängige Optionen:

  • Grafana Tempo + Prometheus - Open Source, läuft auf deiner Infrastruktur, keine Preise pro Platz
  • Jaeger - einfacheres Setup als Tempo, gut für kleinere Teams
  • Honeycomb, Grafana Cloud, Datadog - Managed-Optionen, die OTLP direkt akzeptieren

Weil deine Daten durch den Collector fließen, dauert der Wechsel von Jaeger zu Tempo oder von Tempo zu einem Managed-Dienst etwa 10 Minuten Konfigurationsänderungen und einen Collector-Neustart. Keine Code-Änderungen nötig. Das ist das gesamte Wertversprechen.


Observability mit Code-Quality-Arbeit verbinden

Observability und Code-Quality-Consulting sind stärker verbunden, als Teams oft erkennen. Ein gut instrumentierter Dienst macht Performance-Regressionen sichtbar, bevor sie die Produktion erreichen, verwandelt vage "die App ist langsam"-Beschwerden in konkrete Spans und gibt dir die Daten, um Refactoring-Arbeit zu rechtfertigen. Wenn deine Codebasis derzeit kein Tracing hat und du herausfinden willst, wo in einem Request die Zeit verbraucht wird, ist das auch ein Zeichen, dass die Architektur von einer breiteren Überprüfung profitieren könnte.

Wenn du einen Dienst baust oder modernisierst und Instrumentierung von Anfang an eingebaut haben willst, statt sie später anzuschrauben, melde dich unter hello@wolf-tech.io oder besuche wolf-tech.io. Eine instrumentierte Codebasis ist eine messbare Codebasis - und das verändert, wie zuversichtlich du ausliefern kannst.


Kurzreferenz

PHPNode.js
Kernpaketopen-telemetry/sdk@opentelemetry/sdk-node
Auto-Instrumentierungopentelemetry-auto-symfony, opentelemetry-auto-pdo@opentelemetry/auto-instrumentations-node
Trace-Exporteropen-telemetry/exporter-otlp@opentelemetry/exporter-trace-otlp-http
Metrics-Exporteropen-telemetry/exporter-otlp@opentelemetry/exporter-metrics-otlp-http
Trace-SDK-ReifeStabilStabil
Metrics-SDK-ReifeBetaStabil
Kontext-PropagationManuell für Async/QueueAutomatisch für HTTP, manuell für Queues

FAQ

Ist OpenTelemetry PHP produktionsreif? Das Traces-SDK ist stabil. Das Metrics-SDK ist Beta, wird aber seit Ende 2025 von größeren Teams in der Produktion ohne größere Probleme eingesetzt. Wenn du Stabilitätsgarantien für Metriken brauchst, starte nur mit Traces und füge Metriken hinzu, sobald du die Integration verifiziert hast.

Funktioniert OpenTelemetry mit Symfony? Ja. Das Paket opentelemetry-auto-symfony klinkt sich in Kernel-Request/Response-Events, Controller-Auflösung und Konsolenbefehle ein. Du erhältst HTTP-Traces ohne manuelle Code-Änderungen.

Kann ich OpenTelemetry neben einem bestehenden APM-Agent verwenden? Im Prinzip ja, aber in der Praxis kollidieren sie oft bei den Instrumentierungs-Hooks. Der sauberere Weg ist, vollständig zu migrieren, statt beide gleichzeitig zu betreiben.

Wie viel Overhead fügt OpenTelemetry hinzu? Mit aktiviertem Sampling und dem für asynchrones Batching konfigurierten OTLP-Exporter liegt der Overhead typischerweise unter 1 ms pro Request für PHP und unter 0,5 ms für Node.js. Ungesampelt und mit synchronem Export ist es eine andere Geschichte - nutze immer Batching.