10 nov 2025·6 min leestijd

Go OpenTelemetry tracing voor end-to-end API-zichtbaarheid

Go OpenTelemetry tracing uitgelegd met praktische stappen om traces, metrics en logs te correleren over HTTP-requests, achtergrondjobs en externe calls.

Go OpenTelemetry tracing voor end-to-end API-zichtbaarheid

Wat end-to-end tracing betekent voor een Go API

Een trace is de tijdlijn van één verzoek terwijl het door je systeem beweegt. Het begint wanneer een API-aanvraag binnenkomt en eindigt wanneer je de response terugstuurt.

In een trace zitten spans. Een span is één getimede stap, zoals “parse request”, “run SQL” of “call payment provider.” Spans kunnen ook nuttige details bevatten, zoals een HTTP-statuscode, een veilige gebruikersidentifier of hoeveel rijen een query teruggaf.

"End-to-end" betekent dat de trace niet stopt bij je eerste handler. Hij volgt het verzoek door plekken waar problemen meestal zitten: middleware, database-queries, cache-aanroepen, achtergrondjobs, externe API's (betalingen, e-mail, kaarten) en andere interne services.

Tracing is het meest waardevol wanneer problemen intermittend zijn. Als één van de 200 verzoeken traag is, lijken logs voor snelle en trage gevallen vaak identiek. Een trace maakt het verschil duidelijk: één verzoek wachtte 800 ms op een externe call, probeerde twee keer opnieuw en startte daarna een follow-up job.

Logs zijn ook lastig te koppelen tussen services. Je hebt misschien één logregel in de API, een andere in een worker en niets ertussen. Met tracing delen die gebeurtenissen dezelfde trace-id, zodat je de keten kunt volgen zonder te raden.

Traces, metrics en logs: hoe ze samenhangen

Traces, metrics en logs beantwoorden verschillende vragen.

Traces laten zien wat er gebeurde voor één echt verzoek. Ze vertellen waar de tijd werd besteed over je handler, database-aanroepen, cache-lookups en externe verzoeken.

Metrics tonen de trend. Ze zijn het beste gereedschap voor alerts omdat ze stabiel en goedkoop te aggregeren zijn: latency-percentielen, request-rate, error-rate, queue-diepte en saturatie.

Logs zijn de "waarom" in platte tekst: validatiefouten, onverwachte inputs, randgevallen en beslissingen die je code nam.

De echte winst is correlatie. Wanneer dezelfde trace-id terugkomt in spans en gestructureerde logs, kun je van een error-log direct springen naar de exacte trace en onmiddellijk zien welke dependency vertraagde of welke stap faalde.

Een simpel mentaal model

Gebruik elk signaal waarvoor het het beste is:

  • Metrics vertellen je dat er iets mis is.
  • Traces tonen waar de tijd naartoe ging voor één verzoek.
  • Logs leggen uit welke beslissing je code nam en waarom.

Voorbeeld: je POST /checkout endpoint begint timeouts te krijgen. Metrics tonen een piek in p95-latentie. Een trace laat zien dat het merendeel van de tijd in een payment-provider-aanroep zit. Een gecorreleerde logregel binnen die span toont retries vanwege een 502, wat je wijst naar backoff-instellingen of een upstream-incident.

Voordat je code toevoegt: naamgeving, sampling en wat je moet vastleggen

Een beetje planning van tevoren maakt traces later doorzoekbaar. Zonder dat verzamel je nog steeds data, maar basale vragen worden lastig: “Was dit staging of prod?” “Welke service startte het probleem?”

Begin met consistente identiteit. Kies een duidelijke service.name voor elke Go API (bijvoorbeeld checkout-api) en één environment-veld zoals deployment.environment=dev|staging|prod. Houd deze stabiel. Als namen halverwege de week veranderen, lijken grafieken en zoekopdrachten op verschillende systemen.

Bepaal daarna sampling. Alle verzoeken tracen is fijn in development, maar vaak te duur in productie. Een gangbare aanpak is een klein percentage van normaal verkeer te sample-en en altijd traces te bewaren voor errors en trage requests. Als je al weet dat bepaalde endpoints veel verkeer hebben (health checks, polling), traceer die dan minder of helemaal niet.

Tot slot, spreek af welke tags je op spans zet en wat je nooit verzamelt. Houd een korte allowlist van attributen die helpen gebeurtenissen tussen services te verbinden, en schrijf eenvoudige privacyregels.

Goede tags bevatten meestal stabiele ID's en ruwe request-info (route-template, methode, statuscode). Vermijd gevoelige payloads volledig: wachtwoorden, betalingsgegevens, volledige e-mails, auth-tokens en ruwe request-bodies. Als je gebruikersgerelateerde waarden moet opnemen, hash of redacteer ze voordat je ze toevoegt.

Stap-voor-stap: voeg OpenTelemetry tracing toe aan een Go HTTP API

Je zet één tracer provider op tijdens startup. Die bepaalt waar spans heen gaan en welke resource-attributen aan elke span worden gekoppeld.

1) Initialiseert OpenTelemetry

Zorg dat je service.name instelt. Zonder die instelling raken traces van verschillende services door elkaar en worden grafieken lastig leesbaar.

// 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)

Dat is de basis voor Go OpenTelemetry tracing. Vervolgens heb je per inkomend verzoek een span nodig.

2) Voeg HTTP-middleware toe en leg sleutelvelden vast

Gebruik HTTP-middleware die automatisch een span start en statuscode en duur vastlegt. Zet de span-naam met het route-template (zoals /users/:id), niet met de ruwe URL, anders krijg je duizenden unieke paden.

Streef naar een schone baseline: één server-span per request, route-gebaseerde span-namen, vastleggen van HTTP-status, handler-fouten zichtbaar maken als span-errors, en duur zichtbaar in je trace-viewer.

3) Maak fouten zichtbaar

Wanneer er iets misgaat, return een error en markeer de huidige span als failed. Dat laat de trace opvallen nog voordat je logs bekijkt.

In handlers kun je bijvoorbeeld:

span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())

4) Verifieer trace IDs lokaal

Draai de API en raak een endpoint aan. Log de trace-id uit de request-context één keer om te bevestigen dat deze per request verandert. Als hij altijd leeg is, gebruikt je middleware niet dezelfde context als je handler ontvangt.

Draag context mee door DB- en externe calls

Build traceable Go APIs
Bouw een Go-backend die je van begin tot eind kunt instrumenteren naarmate je API groeit.
Probeer AppMaster

End-to-end zichtbaarheid valt uit elkaar zodra je context.Context loslaat. De inkomende request-context moet de draad zijn die je doorgeeft aan elke DB-call, HTTP-call en helper. Als je die vervangt door context.Background() of vergeet door te geven, verandert je trace in afzonderlijke, niet-gerelateerde stukken werk.

Voor uitgaande HTTP calls, gebruik een geïnstrumenteerde transport zodat elke Do(req) een child-span wordt onder het huidige verzoek. Forward W3C trace-headers op outbound requests zodat downstream services hun spans aan dezelfde trace kunnen hangen.

Database-aanroepen hebben dezelfde behandeling nodig. Gebruik een geïnstrumenteerde driver of wikkel calls met spans rond QueryContext en ExecContext. Leg alleen veilige details vast. Je wilt trage queries vinden zonder data te lekken.

Nuttige, laag-risico attributen omvatten een operation-naam (bijvoorbeeld SELECT user_by_id), tabel- of modelnaam, rij-aantal (alleen tellen), duur, retry-aantal en een grove fouttype (timeout, canceled, constraint).

Timeouts horen bij het verhaal, niet alleen failures. Stel ze in met context.WithTimeout voor DB- en externe calls en laat cancellations omhoog bubbelen. Wanneer een call wordt geannuleerd, markeer de span als error en voeg een korte reden toe zoals deadline_exceeded.

Tracing van achtergrondjobs en queues

Own the source code
Genereer productieklare code die je kunt exporteren en instrumenteren zoals jouw team wil.
Probeer AppMaster

Achtergrondwerk is waar traces vaak stoppen. Een HTTP-request eindigt, daarna pakt een worker later een bericht op op een andere machine zonder gedeelde context. Als je niets doet, krijg je twee verhalen: de API-trace en een job-trace die lijkt te zijn begonnen uit het niets.

De oplossing is eenvoudig: wanneer je een job in de queue zet, vang je de huidige trace-context en sla je die op in job-metadata (payload, headers of attributes, afhankelijk van je queue). Wanneer de worker start, extraheer je die context en start je een nieuwe span als child van het originele verzoek.

Propagateer context veilig

Kopieer alleen trace-context, geen gebruikersdata.

  • Injecteer alleen trace-identifiers en sampling-flags (W3C traceparent-stijl).
  • Houd het gescheiden van business-velden (bijvoorbeeld een dedicated "otel" of "trace" veld).
  • Behandel het als onbetrouwbare input bij het uitlezen (valideer formaat, ga om met ontbrekende data).
  • Vermijd het plaatsen van tokens, e-mails of request-bodies in job-metadata.

Spans om toe te voegen (zonder traces ruis te maken)

Leesbare traces hebben meestal een paar betekenisvolle spans, niet tientallen kleine. Maak spans rond grenzen en "wait points." Een goed begin is een enqueue span in de API-handler en een job.run span in de worker.

Voeg een kleine hoeveelheid context toe: poging-nummer, queue-naam, job-type en payload-grootte (niet de inhoud). Als er retries plaatsvinden, registreer die als aparte spans of events zodat je backoff-achterstanden kunt zien.

Geplande taken hebben ook een parent nodig. Als er geen inkomend verzoek is, creëer dan voor elke run een nieuwe root-span en tag die met een schedule-naam.

Koppel logs aan traces (en houd logs veilig)

Traces vertellen je waar de tijd naartoe ging. Logs vertellen je wat er gebeurde en waarom. De eenvoudigste manier om ze te verbinden is trace_id en span_id als gestructureerde velden toe te voegen aan elke logregel.

In Go pak je de actieve span uit context.Context en enrich je je logger één keer per request (of job). Dan wijst elke logregel naar een specifieke 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)

Dat is genoeg om van een log-entry naar de exacte span te springen die op dat moment draaide. Het maakt ook ontbrekende context duidelijk: trace_id zal leeg zijn.

Hou logs nuttig zonder PII te lekken

Logs leven vaak langer en reizen verder dan traces, dus wees strenger. Geef de voorkeur aan stabiele identifiers en uitkomsten: user_id, order_id, payment_provider, status en error_code. Als je gebruikersinput moet loggen, redacteer die eerst en beperk lengtes.

Maak errors eenvoudig te groeperen

Gebruik consistente event-namen en fouttypes zodat je ze kunt tellen en doorzoeken. Als de bewoording elke keer verandert, lijkt hetzelfde probleem op veel verschillende issues.

Voeg metrics toe die je echt helpen issues vinden

Design DB-first backends
Modelleer data in PostgreSQL visueel en voeg tracing toe rond de queries die ertoe doen.
Maak app

Metrics zijn je vroege waarschuwingssysteem. In een setup die al Go OpenTelemetry tracing gebruikt, zouden metrics moeten beantwoorden: hoe vaak, hoe erg en sinds wanneer.

Begin met een kleine set die voor vrijwel elke API werkt: request count, error count (per statusklasse), latency-percentielen (p50, p95, p99), in-flight requests en dependency-latency voor je DB en belangrijke externe calls.

Om metrics op één lijn te houden met traces, gebruik dezelfde route-templates en namen. Als je spans /users/{id} gebruiken, zouden je metrics dat ook moeten doen. Dan kun je bij een chart die “p95 voor /checkout” toont direct naar traces filteren voor die route.

Wees voorzichtig met labels (attributes). Eén slechte label kan kosten exploderen en dashboards nutteloos maken. Route-template, methode, status-klasse en service-naam zijn doorgaans veilig. User IDs, e-mails, volledige URLs en ruwe foutmeldingen meestal niet.

Voeg een paar custom metrics toe voor business-critische gebeurtenissen (bijvoorbeeld checkout gestart/voltooid, betaalfouten per result-code groep, achtergrondjob succes vs retry). Houd de set klein en verwijder wat je nooit gebruikt.

Telemetry exporteren en veilig uitrollen

Exporteren is waar OpenTelemetry echt werkt. Je service moet spans, metrics en logs ergens betrouwbaar naartoe sturen zonder requests te vertragen.

Voor lokale ontwikkeling, houd het simpel. Een console-exporter (of OTLP naar een lokale collector) laat je snel traces zien en validate span-namen en attributen. In productie heeft de voorkeur OTLP naar een agent of OpenTelemetry Collector dicht bij de service. Dat geeft je één plek om retries, routing en filtering te beheren.

Batching is belangrijk. Stuur telemetry in batches met een korte interval en strakke timeouts zodat een vastgelopen netwerk je app niet blokkeert. Telemetry hoort niet op het kritieke pad te zitten. Als de exporter niet kan bijhouden, moet hij data droppen in plaats van geheugen op te bouwen.

Sampling houdt kosten voorspelbaar. Begin met head-based sampling (bijvoorbeeld 1–10% van de requests), en voeg daarna eenvoudige regels toe: sample altijd errors en sample altijd trage requests boven een drempel. Als je hoge-volume achtergrondjobs hebt, sample die op lagere rates.

Rol uit in kleine stappen: dev met 100% sampling, staging met realistisch verkeer en lagere sampling, en daarna productie met conservatieve sampling en alerts op exporter-failures.

Veelgemaakte fouten die end-to-end zichtbaarheid kapotmaken

Create an internal tool
Bouw interne tools en admin panels met API's die je vanaf dag één kunt observeren.
Aan de slag

End-to-end zichtbaarheid faalt meestal om eenvoudige redenen: de data bestaat, maar het connect niet.

Problemen die distributed tracing in Go breken zijn meestal:

  • Context tussen lagen loslaten. Een handler maakt een span, maar een DB-call, HTTP-client of goroutine gebruikt context.Background() in plaats van de request-context.
  • Fouten teruggeven zonder spans te markeren. Als je de error niet opneemt en de span-status niet zet, lijken traces "green" terwijl gebruikers 500s zien.
  • Alles instrumenteren. Als elke helper een span wordt, veranderen traces in ruis en worden ze duurder.
  • High-cardinality attributen toevoegen. Volledige URLs met ID's, e-mails, ruwe SQL-waarden, request-bodies of ruwe foutstrings kunnen miljoenen unieke waarden creëren.
  • Performance beoordelen op gemiddelden. Incidenten tonen zich in percentielen (p95/p99) en error-rate, niet in mean latency.

Een snelle sanity-check is één echt verzoek pakken en het volgen over grenzen heen. Als je één trace-id niet door de inbound request, de DB-query, de externe call en de async worker kunt zien stromen, heb je nog geen end-to-end zichtbaarheid.

Een praktische "done" checklist

Connect frontend to traced APIs
Lever web- en mobiele apps met backends die je kunt debuggen met trace IDs in logs.
Probeer het nu

Je bent er bijna als je van een gebruikersrapport naar het exacte verzoek kunt gaan en het vervolgens over elke hop kunt volgen.

  • Pak één API-logregel en vind de exacte trace via trace_id. Bevestig dat diepere logs van hetzelfde verzoek (DB, HTTP-client, worker) dezelfde trace-context dragen.
  • Open de trace en verifieer nesting: een HTTP-server-span bovenaan, met child-spans voor DB-calls en externe API's. Een platte lijst betekent vaak dat context verloren is gegaan.
  • Trigger een achtergrondjob vanuit een API-request (bijv. een e-mailbewijs) en bevestig dat de worker-span teruglinkt naar het verzoek.
  • Check metrics voor de basics: request count, error-rate en latency-percentielen. Bevestig dat je kunt filteren op route of operatie.
  • Scan attributen en logs op veiligheid: geen wachtwoorden, tokens, volledige creditcardnummers of ruwe persoonlijke data.

Een eenvoudige realiteitstest is het simuleren van een trage checkout waarbij de payment-provider vertraagd is. Je zou één trace moeten zien met een duidelijk gelabelde externe call-span, plus een metric-piek in p95-latentie voor de checkout-route.

Als je Go-backends genereert (bijvoorbeeld met AppMaster), helpt het om deze checklist onderdeel van je release-routine te maken zodat nieuwe endpoints en workers traceerbaar blijven naarmate de app groeit. AppMaster (appmaster.io) genereert echte Go-services, waardoor je één OpenTelemetry-setup kunt standaardiseren en kunt doorslepen over services en achtergrondjobs.

Voorbeeld: debuggen van een trage checkout over services heen

Een klant meldt: “Checkout hangt soms.” Je kunt het niet reproduceren op commando, en dat is precies wanneer Go OpenTelemetry tracing van pas komt.

Begin met metrics om de vorm van het probleem te begrijpen. Kijk naar request-rate, error-rate en p95 of p99-latentie voor het checkout-endpoint. Als de vertraging in korte bursts voorkomt en alleen voor een deel van de requests, wijst dat meestal op een dependency, queuing of retry-gedrag in plaats van CPU.

Open vervolgens een trage trace uit hetzelfde tijdvenster. Eén trace is vaak genoeg. Een gezonde checkout is misschien 300–600 ms end-to-end. Een slechte kan 8–12 seconden zijn, met het grootste deel van de tijd in één enkele span.

Een veelvoorkomend patroon ziet er zo uit: de API-handler is snel, het DB-werk is grotendeels oké, daarna toont een payment-provider-span retries met backoff en wacht een downstream call achter een lock of queue. De response kan alsnog 200 teruggeven, dus alerts die alleen op errors gebaseerd zijn, vuren nooit.

Gecorreleerde logs vertellen dan precies het pad in platte taal: “retrying Stripe charge: timeout”, gevolgd door “db tx aborted: serialization failure”, gevolgd door “retry checkout flow”. Dat is een duidelijk signaal dat een paar kleine issues samen een slechte gebruikerservaring veroorzaken.

Als je de bottleneck gevonden hebt, is consistentie wat dingen leesbaar houdt over tijd. Standaardiseer span-namen, attributen (veilige user-id-hash, order-id, dependency-naam) en sampling-regels over services heen zodat iedereen traces op dezelfde manier leest.

Gemakkelijk te starten
Maak iets geweldigs

Experimenteer met AppMaster met gratis abonnement.
Als je er klaar voor bent, kun je het juiste abonnement kiezen.

Aan de slag