Go OpenTelemetry-Tracing für End-to-End-API-Sichtbarkeit
OpenTelemetry-Tracing in Go verständlich erklärt: praktische Schritte, um Traces, Metriken und Logs über HTTP-Anfragen, Hintergrundjobs und Drittanbieter-Aufrufe zu korrelieren.

Was End-to-End-Tracing für eine Go-API bedeutet
Ein Trace ist die Zeitachse einer einzelnen Anfrage, während sie durch Ihr System läuft. Er beginnt, wenn ein API-Aufruf eintrifft, und endet, wenn Sie die Antwort senden.
Innerhalb eines Traces gibt es Spans. Ein Span ist ein einzelner zeitlicher Schritt, zum Beispiel „Request parsen“, „SQL ausführen“ oder „Zahlungsanbieter aufrufen“. Spans können auch nützliche Details enthalten, wie einen HTTP-Statuscode, eine sichere Benutzerkennung oder wie viele Zeilen eine Abfrage zurückgegeben hat.
„End-to-end“ bedeutet, dass der Trace nicht beim ersten Handler aufhört. Er folgt der Anfrage durch jene Bereiche, in denen Probleme sich üblicherweise verbergen: Middleware, Datenbankabfragen, Cache-Aufrufe, Hintergrund-Jobs, Drittanbieter-APIs (Zahlungen, E-Mail, Karten) und andere interne Dienste.
Tracing ist besonders wertvoll, wenn Probleme intermittierend sind. Wenn eine von 200 Anfragen langsam ist, sehen Logs für schnelle und langsame Fälle oft gleich aus. Ein Trace macht den Unterschied deutlich: Eine Anfrage hat 800 ms auf einen externen Aufruf gewartet, wurde zweimal neu versucht und hat dann einen Folgejob gestartet.
Logs sind auch schwer über Dienste hinweg zu verbinden. Sie haben vielleicht eine Log-Zeile in der API, eine andere im Worker und nichts dazwischen. Mit Tracing teilen sich diese Ereignisse dieselbe Trace-ID, sodass Sie die Kette ohne Raten nachvollziehen können.
Traces, Metriken und Logs: wie sie zusammenpassen
Traces, Metriken und Logs beantworten unterschiedliche Fragen.
Traces zeigen, was für eine einzelne Echt-Anfrage passiert ist. Sie sagen Ihnen, wo die Zeit im Handler, bei Datenbankaufrufen, Cache-Lookups und Drittanbieter-Anfragen verbracht wurde.
Metriken zeigen den Trend. Sie sind das beste Werkzeug für Alerts, weil sie stabil und billig zu aggregieren sind: Latenz-Perzentile, Anfrage-Rate, Fehlerrate, Queue-Tiefe und Sättigung.
Logs sind das „Warum“ in Klartext: Validierungsfehler, unerwartete Eingaben, Randfälle und Entscheidungen, die Ihr Code getroffen hat.
Der eigentliche Gewinn ist die Korrelation. Wenn dieselbe Trace-ID in Spans und strukturierten Logs auftaucht, können Sie von einer Fehler-Logzeile direkt zum genauen Trace springen und sofort sehen, welche Abhängigkeit langsamer wurde oder welcher Schritt fehlgeschlagen ist.
Ein einfaches mental model
Nutzen Sie jedes Signal für das, wofür es am besten geeignet ist:
- Metriken sagen Ihnen, dass etwas nicht stimmt.
- Traces zeigen, wo die Zeit bei einer einzelnen Anfrage hinging.
- Logs erklären, was Ihr Code entschieden hat und warum.
Beispiel: Ihr POST /checkout Endpoint beginnt zu timen out. Metriken zeigen steigende p95-Latenzen. Ein Trace zeigt, dass die meiste Zeit in einem Aufruf zum Zahlungsanbieter verbracht wurde. Ein korrelierter Log-Eintrag innerhalb dieses Spans zeigt Wiederholungen wegen eines 502, was Sie auf Backoff-Einstellungen oder einen Vorfall beim Upstream-Dienst hinweist.
Bevor Sie Code hinzufügen: Namensgebung, Sampling und was zu verfolgen ist
Ein wenig Planung im Vorfeld macht Traces später durchsuchbar. Ohne diese Planung sammeln Sie zwar Daten, aber grundlegende Fragen werden schwer: „War das Stage oder Prod?“ „Welcher Dienst hat das Problem gestartet?“
Beginnen Sie mit konsistenter Identität. Wählen Sie für jede Go-API einen klaren service.name (zum Beispiel checkout-api) und ein einziges Umfeldfeld wie deployment.environment=dev|staging|prod. Halten Sie diese stabil. Wenn Namen mitten in der Woche wechseln, sehen Diagramme und Suchen aus wie verschiedene Systeme.
Entscheiden Sie dann das Sampling. Jede Anfrage zu trace'n ist in der Entwicklung gut, aber in Produktion oft zu teuer. Ein üblicher Ansatz ist, einen kleinen Prozentsatz normalen Traffics zu sampeln und Traces für Fehler und langsame Anfragen beizubehalten. Wenn Sie bereits wissen, dass bestimmte Endpunkte hohes Volumen haben (Health-Checks, Polling), trace'n Sie diese seltener oder gar nicht.
Schließlich einigen Sie sich darauf, welche Attribute Sie auf Spans taggen und welche Sie niemals sammeln. Halten Sie eine kurze Allowlist von Attributen, die helfen, Ereignisse zwischen Diensten zu verbinden, und formulieren Sie einfache Datenschutzregeln.
Gute Tags enthalten üblicherweise stabile IDs und grobe Anfrageninformationen (Routen-Template, Methode, Statuscode). Vermeiden Sie komplett sensible Payloads: Passwörter, Zahlungsdaten, vollständige E-Mails, Auth-Tokens und rohe Request-Bodies. Wenn Sie Benutzerbezogene Werte einfügen müssen, hashen oder redigieren Sie diese, bevor Sie sie hinzufügen.
Schritt für Schritt: OpenTelemetry-Tracing zu einer Go HTTP-API hinzufügen
Sie richten einen Tracer Provider einmal beim Start ein. Dieser entscheidet, wohin Spans gehen und welche Resource-Attribute jedem Span angehängt werden.
1) OpenTelemetry initialisieren
Stellen Sie sicher, dass Sie service.name setzen. Ohne diesen werden Traces verschiedener Dienste vermischt und Charts schwer lesbar.
// main.go (startup)
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())
res, _ := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceName("checkout-api"),
),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
Das ist die Grundlage für Go OpenTelemetry Tracing. Als Nächstes brauchen Sie pro eingehender Anfrage einen Span.
2) HTTP-Middleware hinzufügen und wichtige Felder erfassen
Verwenden Sie HTTP-Middleware, die automatisch einen Span startet und Statuscode sowie Dauer aufzeichnet. Setzen Sie den Span-Namen mit dem Routen-Template (z. B. /users/:id), nicht mit der rohen URL, sonst bekommen Sie tausende eindeutige Pfade.
Streben Sie eine saubere Basislinie an: ein Server-Span pro Anfrage, routenbasierte Span-Namen, HTTP-Status erfasst, Handler-Fehler als Span-Errors markiert und Dauer im Trace-Viewer sichtbar.
3) Fehler deutlich machen
Wenn etwas schiefgeht, geben Sie einen Fehler zurück und markieren Sie den aktuellen Span als fehlgeschlagen. Das lässt den Trace schon hervorstechen, bevor Sie in die Logs schauen.
In Handlers können Sie z. B. folgendes tun:
span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
4) Trace-IDs lokal verifizieren
Starten Sie die API und rufen Sie einen Endpunkt auf. Loggen Sie die Trace-ID aus dem Request-Context einmal, um zu bestätigen, dass sie sich pro Anfrage ändert. Wenn sie immer leer ist, benutzt Ihre Middleware nicht denselben Context, den Ihr Handler erhält.
Kontext durch DB- und Drittanbieter-Aufrufe mitnehmen
End-to-end-Visibility bricht zusammen, sobald Sie context.Context verlieren. Der eingehende Request-Context sollte der Faden sein, den Sie zu jedem DB-Aufruf, HTTP-Aufruf und Helfer weitergeben. Wenn Sie ihn mit context.Background() ersetzen oder vergessen weiterzugeben, verwandelt sich Ihr Trace in getrennte, nicht zusammenhängende Arbeiten.
Für ausgehende HTTP-Aufrufe verwenden Sie einen instrumentierten Transport, sodass jedes Do(req) zu einem Child-Span unter der aktuellen Anfrage wird. Leiten Sie W3C-Trace-Header bei ausgehenden Anfragen weiter, damit Downstream-Dienste ihre Spans an denselben Trace anhängen können.
Datenbankaufrufe brauchen dieselbe Behandlung. Verwenden Sie einen instrumentierten Treiber oder umrahmen Sie Aufrufe mit Spans um QueryContext und ExecContext. Zeichnen Sie nur sichere Details auf. Sie wollen langsame Abfragen finden, ohne Daten zu leaken.
Nützliche, risikoarme Attribute sind ein Operationsname (z. B. SELECT user_by_id), Tabellen- oder Modelname, Zeilenanzahl (nur zählen), Dauer, Retry-Anzahl und ein grober Fehlertyp (timeout, canceled, constraint).
Timeouts sind Teil der Geschichte, nicht nur Fehler. Setzen Sie sie mit context.WithTimeout für DB- und Drittanbieter-Aufrufe und lassen Sie Abbrüche nach oben durchreichen. Wenn ein Aufruf abgebrochen wird, markieren Sie den Span als Fehler und fügen einen kurzen Grund wie deadline_exceeded hinzu.
Hintergrund-Jobs und Queues trace'n
Hintergrundarbeit ist oft der Ort, an dem Traces aufhören. Eine HTTP-Anfrage endet, dann nimmt ein Worker später eine Nachricht auf einer anderen Maschine ohne gemeinsamen Context. Wenn Sie nichts tun, bekommen Sie zwei Geschichten: den API-Trace und einen Job-Trace, der aussieht, als käme er aus dem Nichts.
Die Lösung ist einfach: Wenn Sie einen Job enqueuen, erfassen Sie den aktuellen Trace-Context und speichern ihn in den Job-Metadaten (Payload, Headers oder Attributes, abhängig von Ihrer Queue). Wenn der Worker startet, extrahieren Sie diesen Context und starten einen neuen Span als Child des ursprünglichen Requests.
Context sicher propagieren
Kopieren Sie nur den Trace-Context, nicht Benutzerdaten.
- Injizieren Sie nur Trace-Identifiers und Sampling-Flags (W3C traceparent-Style).
- Bewahren Sie ihn getrennt von Business-Feldern auf (z. B. ein dediziertes "otel"- oder "trace"-Feld).
- Behandeln Sie ihn als untrusted input beim Zurücklesen (Format validieren, fehlende Daten handhaben).
- Vermeiden Sie Tokens, E-Mails oder Request-Bodies in Job-Metadaten.
Spans, die Sie hinzufügen sollten (ohne Traces zur Geräuschquelle zu machen)
Lesbare Traces haben üblicherweise einige aussagekräftige Spans, nicht dutzende winziger. Erstellen Sie Spans um Grenzen und "Wartepunkte". Ein guter Anfang ist ein enqueue-Span im API-Handler und ein job.run-Span im Worker.
Fügen Sie eine kleine Menge Kontext hinzu: Versuchsnummer, Queue-Name, Job-Typ und Payload-Größe (nicht den Payload-Inhalt). Wenn Retries passieren, zeichnen Sie diese als separate Spans oder Events, damit Sie Backoff-Verzögerungen sehen können.
Geplante Tasks brauchen auch einen Parent. Wenn es keinen eingehenden Request gibt, erstellen Sie für jeden Lauf einen neuen Root-Span und taggen Sie ihn mit einem Schedule-Namen.
Logs mit Traces korrelieren (und Logs sicher halten)
Traces sagen Ihnen, wo die Zeit war. Logs sagen Ihnen, was passiert ist und warum. Der einfachste Weg, sie zu verbinden, ist, trace_id und span_id als strukturierte Felder zu jeder Logzeile hinzuzufügen.
In Go holen Sie den aktiven Span aus context.Context und erweitern Ihren Logger einmal pro Anfrage (oder Job). Dann verweist jede Logzeile auf einen bestimmten Trace.
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
logger := baseLogger.With(
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
)
logger.Info("charge_started", "order_id", orderID)
Das reicht, um von einem Log-Eintrag direkt zum exakten Span zu springen, der gerade lief, als das Log geschrieben wurde. Es macht auch fehlenden Context offensichtlich: trace_id ist dann leer.
Logs nützlich halten, ohne PII zu leaken
Logs leben oft länger und reisen weiter als Traces, also seien Sie strenger. Bevorzugen Sie stabile Kennungen und Ergebnisse: user_id, order_id, payment_provider, status und error_code. Wenn Sie Benutzereingaben loggen müssen, redigieren Sie sie zuerst und begrenzen Sie die Länge.
Fehler leicht gruppierbar machen
Verwenden Sie konsistente Event-Namen und Fehlertypen, damit Sie sie zählen und durchsuchen können. Wenn die Formulierung bei jedem Mal anders ist, sieht derselbe Fehler wie viele verschiedene aus.
Metriken hinzufügen, die tatsächlich beim Finden von Problemen helfen
Metriken sind Ihr Frühwarnsystem. In einer Umgebung, die bereits Go OpenTelemetry Tracing nutzt, sollten Metriken beantworten: wie oft, wie schlimm und seit wann.
Beginnen Sie mit einer kleinen Menge, die für fast jede API funktioniert: Anfrage-Anzahl, Fehler-Anzahl (nach Status-Klasse), Latenz-Perzentile (p50, p95, p99), laufende Anfragen und Abhängigkeitslatenz für Ihre DB und wichtige Drittanbieter-Aufrufe.
Um Metriken mit Traces in Einklang zu halten, verwenden Sie dieselben Routen-Templates und Namen. Wenn Ihre Spans /users/{id} benutzen, sollten Ihre Metriken das auch tun. Dann können Sie, wenn ein Diagramm „p95 für /checkout stieg“, direkt in Traces filtern, die zu dieser Route gehören.
Achten Sie auf Labels (Attribute). Ein schlechtes Label kann Kosten explodieren lassen und Dashboards unbrauchbar machen. Routen-Template, Methode, Statusklasse und Service-Name sind normalerweise sicher. User-IDs, E-Mails, vollständige URLs und rohe Fehlermeldungen sind es meistens nicht.
Fügen Sie ein paar benutzerdefinierte Metriken für geschäftskritische Ereignisse hinzu (z. B. checkout started/completed, payment failures nach Ergebniscode-Gruppe, Hintergrund-Job Erfolg vs Retry). Halten Sie die Menge klein und entfernen Sie, was Sie nie verwenden.
Telemetrie exportieren und sicher einführen
Export ist der Punkt, an dem OpenTelemetry real wird. Ihr Service muss Spans, Metriken und Logs an einen verlässlichen Ort senden, ohne Anfragen zu verlangsamen.
Für die lokale Entwicklung halten Sie es einfach. Ein Console-Exporter (oder OTLP an einen lokalen Collector) lässt Sie Traces schnell sehen und Span-Namen sowie Attribute validieren. In Produktion bevorzugen Sie OTLP an einen Agent oder OpenTelemetry Collector in der Nähe des Dienstes. Das gibt Ihnen einen zentralen Ort für Retries, Routing und Filtering.
Batching ist wichtig. Senden Sie Telemetrie in Batches in kurzen Intervallen mit engen Timeouts, damit ein hängendes Netzwerk Ihre App nicht blockiert. Telemetrie sollte nicht auf dem kritischen Pfad liegen. Wenn der Exporter nicht mithalten kann, sollte er Daten verwerfen, statt Speicher aufzubauen.
Sampling hält die Kosten vorhersehbar. Beginnen Sie mit head-based Sampling (z. B. 1–10 % der Anfragen) und fügen Sie einfache Regeln hinzu: Fehler immer sampeln und langsame Anfragen über einer Schwelle immer sampeln. Wenn Sie hochvolumige Hintergrund-Jobs haben, sampeln Sie diese niedriger.
Rollen Sie schrittweise aus: Dev mit 100 % Sampling, Staging mit realistischem Traffic und niedrigerem Sampling, dann Produktion mit konservativem Sampling und Alerts bei Exporter-Fehlern.
Häufige Fehler, die End-to-End-Visibility zunichtemachen
End-to-End-Visibility scheitert meist aus einfachen Gründen: Die Daten existieren, aber sie verbinden sich nicht.
Die Probleme, die verteiltes Tracing in Go kaputtmachen, sind meist diese:
- Context zwischen Schichten verlieren. Ein Handler erstellt einen Span, aber ein DB-Aufruf, HTTP-Client oder Goroutine verwendet
context.Background()statt des Request-Context. - Fehler zurückgeben, ohne Spans zu markieren. Wenn Sie den Fehler nicht aufzeichnen und den Span-Status setzen, sehen Traces „grün“ aus, auch wenn Nutzer 500er sehen.
- Alles instrumentieren. Wenn jeder Helfer zu einem Span wird, werden Traces zur Geräuschquelle und kosten mehr.
- High-Cardinality-Attribute hinzufügen. Volle URLs mit IDs, E-Mails, rohe SQL-Werte, Request-Bodies oder rohe Fehlerstrings können Millionen eindeutiger Werte erzeugen.
- Performance nach Durchschnittswerten beurteilen. Vorfälle zeigen sich in Perzentilen (p95/p99) und Fehlerrate, nicht in der mittleren Latenz.
Ein schneller Sanity-Check ist, eine echte Anfrage zu nehmen und ihr über Grenzen zu folgen. Wenn Sie nicht sehen können, wie eine Trace-ID durch den eingehenden Request, die DB-Abfrage, den Drittanbieter-Aufruf und den asynchronen Worker fließt, haben Sie noch keine End-to-End-Visibility.
Eine praktische "Done"-Checkliste
Sie sind fast fertig, wenn Sie von einer Nutzer-Meldung zur exakten Anfrage gehen und diese dann über jeden Hop verfolgen können.
- Wählen Sie eine API-Logzeile und finden Sie den exakten Trace über
trace_id. Bestätigen Sie, dass tiefere Logs derselben Anfrage (DB, HTTP-Client, Worker) denselben Trace-Context tragen. - Öffnen Sie den Trace und prüfen Sie die Verschachtelung: ein HTTP-Server-Span oben, mit Child-Spans für DB-Aufrufe und Drittanbieter-APIs. Eine flache Liste bedeutet oft, dass Context verloren ging.
- Triggern Sie einen Background-Job aus einer API-Anfrage (z. B. E-Mail-Receipt senden) und bestätigen Sie, dass der Worker-Span zurück zur Anfrage verbindet.
- Prüfen Sie die Metriken für die Basics: Anfrage-Anzahl, Fehlerrate und Latenz-Perzentile. Bestätigen Sie, dass Sie nach Route oder Operation filtern können.
- Scannen Sie Attribute und Logs auf Sicherheit: keine Passwörter, Tokens, vollständigen Kreditkartennummern oder rohe personenbezogene Daten.
Ein einfacher Realitätstest ist, einen langsamen Checkout zu simulieren, bei dem der Zahlungsanbieter verzögert. Sie sollten einen Trace sehen mit einem klar beschrifteten externen Aufruf-Span sowie einen Metrik-Ausreißer in der p95-Latenz für die Checkout-Route.
Wenn Sie Go-Backends generieren (zum Beispiel mit AppMaster), hilft es, diese Checkliste Teil Ihrer Release-Routine zu machen, damit neue Endpunkte und Worker beim Wachsen der App tracebar bleiben. AppMaster (appmaster.io) erzeugt echte Go-Services, sodass Sie ein einheitliches OpenTelemetry-Setup standardisieren und über Dienste und Hintergrund-Jobs hinweg verwenden können.
Beispiel: Debugging eines langsamen Checkouts über Dienste hinweg
Eine Kundenmeldung lautet: „Checkout hängt manchmal.“ Sie können es nicht zuverlässig reproduzieren — genau dann zahlt sich Go OpenTelemetry Tracing aus.
Beginnen Sie mit Metriken, um die Form des Problems zu verstehen. Schauen Sie sich Anfrage-Rate, Fehlerrate und p95 oder p99 Latenz für den Checkout-Endpunkt an. Wenn die Verlangsamung in kurzen Bursts und nur bei einer Teilmenge der Anfragen auftritt, deutet das meist auf eine Abhängigkeit, Queueing oder Retry-Verhalten hin, nicht auf CPU.
Öffnen Sie dann einen langsamen Trace aus dem gleichen Zeitfenster. Ein Trace reicht oft. Ein gesunder Checkout könnte 300–600 ms end-to-end dauern. Ein schlechter dagegen 8–12 Sekunden, wobei die meiste Zeit in einem einzigen Span steckt.
Ein typisches Muster: Der API-Handler ist schnell, die DB-Arbeit ist größtenteils in Ordnung, dann zeigt ein Zahlungsanbieter-Span Wiederholungen mit Backoff, und ein nachgelagerter Aufruf wartet hinter einer Sperre oder Queue. Die Antwort kann trotzdem 200 zurückgeben, sodass Alerts, die nur auf Fehler basieren, nie auslösen.
Korrelierte Logs sagen Ihnen dann den genauen Pfad in Klartext: „retrying Stripe charge: timeout“, gefolgt von „db tx aborted: serialization failure“, gefolgt von „retry checkout flow“. Das ist ein klares Signal dafür, dass sich ein paar kleine Probleme zu einer schlechten Nutzererfahrung summieren.
Sobald Sie den Engpass gefunden haben, sorgt Konsistenz dafür, dass alles über die Zeit lesbar bleibt. Standardisieren Sie Span-Namen, Attribute (sicherer Benutzer-ID-Hash, Bestell-ID, Abhängigkeitsname) und Sampling-Regeln über Dienste hinweg, damit alle Traces gleich gelesen werden.


