Tracing OpenTelemetry in Go per visibilità end-to-end
Spiegazione pratica del tracciamento OpenTelemetry in Go: passaggi concreti per correlare trace, metriche e log attraverso richieste HTTP, job in background e chiamate a terzi.

Cosa significa il tracciamento end-to-end per un'API Go
Una trace è la linea temporale di una singola richiesta mentre attraversa il sistema. Inizia quando arriva una chiamata API e termina quando invii la risposta.
All'interno di una trace ci sono gli span. Uno span è un singolo passo temporizzato, come “parsing della richiesta”, “esecuzione SQL” o “chiamata al provider di pagamenti”. Gli span possono anche contenere dettagli utili, come un codice di stato HTTP, un identificatore utente non sensibile o quante righe ha restituito una query.
“End-to-end” significa che la trace non si ferma al primo handler. Segue la richiesta attraverso i punti in cui i problemi si nascondono di solito: middleware, query al database, chiamate alla cache, job in background, API di terze parti (pagamenti, email, mappe) e altri servizi interni.
Il tracciamento è più prezioso quando i problemi sono intermittenti. Se una richiesta su 200 è lenta, i log spesso sembrano identici nei casi veloci e lenti. Una trace rende la differenza evidente: una richiesta ha impiegato 800 ms in attesa di una chiamata esterna, ha ritentato due volte e poi ha avviato un job di follow-up.
I log sono inoltre difficili da collegare tra servizi. Potresti avere una riga di log nell'API, un'altra in un worker e nulla nel mezzo. Con il tracciamento, quegli eventi condividono lo stesso trace ID, così puoi seguire la catena senza indovinare.
Trace, metriche e log: come si integrano
Trace, metriche e log rispondono a domande diverse.
Le trace mostrano cosa è successo per una singola richiesta reale. Dicono dove è passato il tempo nel tuo handler, nelle chiamate al database, nelle letture dalla cache e nelle richieste a servizi esterni.
Le metriche mostrano la tendenza. Sono lo strumento migliore per gli alert perché sono stabili e facili da aggregare: percentili di latenza, tasso di richieste, tasso di errori, profondità delle code e saturazione.
I log sono il “perché” in testo semplice: errori di validazione, input imprevisti, casi limite e decisioni prese dal tuo codice.
Il vero vantaggio è la correlazione. Quando lo stesso trace ID compare in span e log strutturati, puoi passare da un log di errore alla trace esatta e vedere immediatamente quale dipendenza ha rallentato o quale passaggio è fallito.
Un modello mentale semplice
Usa ogni segnale per quello che sa fare meglio:
- Le metriche ti dicono che qualcosa non va.
- Le trace mostrano dove è passato il tempo per una richiesta.
- I log spiegano cosa ha deciso il codice e perché.
Esempio: il tuo endpoint POST /checkout comincia a scadere. Le metriche mostrano un picco della p95 di latenza. Una trace mostra che la maggior parte del tempo è dentro una chiamata al provider di pagamenti. Un log correlato all'interno di quello span mostra ritenti a causa di un 502, il che indirizza verso impostazioni di backoff o un incidente a monte.
Prima di aggiungere codice: naming, sampling e cosa tracciare
Un po' di pianificazione iniziale rende le trace ricercabili in seguito. Senza, raccoglierai comunque dati, ma le domande di base diventano difficili: “Questo era staging o prod?” “Quale servizio ha iniziato il problema?”
Inizia con un'identità coerente. Scegli un service.name chiaro per ogni API Go (per esempio, checkout-api) e un unico campo di ambiente come deployment.environment=dev|staging|prod. Mantienili stabili. Se i nomi cambiano a metà settimana, grafici e ricerche sembreranno sistemi diversi.
Poi decidi il sampling. Tracciare tutte le richieste è ottimo in sviluppo, ma spesso troppo costoso in produzione. Un approccio comune è campionare una piccola percentuale del traffico normale e mantenere le trace per errori e richieste lente. Se sai già che certi endpoint hanno alto volume (health check, polling), tracciali meno o per nulla.
Infine, concorda cosa taggare sugli span e cosa non raccogliere mai. Mantieni una allowlist corta di attributi che aiutano a collegare gli eventi tra i servizi e scrivi regole semplici sulla privacy.
I tag utili includono solitamente ID stabili e informazioni grossolane sulla richiesta (template della route, metodo, codice di stato). Evita payload sensibili del tutto: password, dati di pagamento, email complete, token di autenticazione e corpi delle richieste non filtrati. Se devi includere valori legati all'utente, hashali o redattali prima di aggiungerli.
Passo dopo passo: aggiungere OpenTelemetry a un'API HTTP Go
Imposterai un tracer provider all'avvio dell'app. Questo decide dove vanno gli span e quali attributi di risorsa sono allegati a ogni span.
1) Inizializzare OpenTelemetry
Assicurati di impostare service.name. Senza, le trace di servizi diversi possono mescolarsi e i grafici diventano difficili da leggere.
// 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)
Questa è la base per il tracciamento OpenTelemetry in Go. Dopo, ti serve uno span per ogni richiesta in ingresso.
2) Aggiungere middleware HTTP e catturare i campi chiave
Usa un middleware HTTP che avvii automaticamente uno span e registri codice di stato e durata. Imposta il nome dello span usando il template della route (come /users/:id), non l'URL grezzo, altrimenti avrai migliaia di percorsi unici.
Punta a una baseline pulita: uno span server per richiesta, nomi degli span basati sulla route, codice di stato HTTP catturato, fallimenti dell'handler riflessi come errori nello span e durata visibile nel visualizzatore di trace.
3) Rendere i fallimenti evidenti
Quando qualcosa va storto, restituisci un errore e marca lo span corrente come fallito. Questo fa risaltare la trace anche prima di guardare i log.
Negli handler puoi fare:
span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
4) Verificare gli ID delle trace in locale
Esegui l'API e colpisci un endpoint. Logga l'ID della trace dal contesto della richiesta una volta per confermare che cambia per richiesta. Se è sempre vuoto, il tuo middleware non sta usando lo stesso context che riceve l'handler.
Portare il contesto in DB e nelle chiamate a terze parti
La visibilità end-to-end si rompe nel momento in cui perdi context.Context. Il contesto della richiesta in ingresso dovrebbe essere il filo che passi a ogni chiamata al DB, richiesta HTTP e helper. Se lo sostituisci con context.Background() o ti dimentichi di passarne uno, la tua trace si trasforma in lavori separati e non correlati.
Per le chiamate HTTP in uscita, usa un transport strumentato in modo che ogni Do(req) diventi uno span figlio della richiesta corrente. Inoltra le intestazioni W3C di trace sulle richieste in uscita così i servizi downstream possono aggiungere i loro span alla stessa trace.
Anche le chiamate al database devono ricevere lo stesso trattamento. Usa un driver strumentato o avvolgi le chiamate con span intorno a QueryContext e ExecContext. Registra solo dettagli sicuri. Vuoi individuare query lente senza esporre dati.
Attributi utili e a basso rischio includono un nome dell'operazione (per esempio, SELECT user_by_id), nome della tabella o del modello, conteggio righe (solo il numero), durata, numero di ritenti e un tipo di errore generico (timeout, canceled, constraint).
I timeout fanno parte della storia, non solo dei fallimenti. Impostali con context.WithTimeout per DB e chiamate a terze parti e lascia che le cancellazioni risalgano. Quando una chiamata viene cancellata, marca lo span come errore e aggiungi una breve ragione come deadline_exceeded.
Tracciare job in background e code
Il lavoro in background è dove le trace spesso si fermano. Una richiesta HTTP finisce e poi un worker prende un messaggio più tardi su un'altra macchina senza contesto condiviso. Se non fai nulla, ottieni due storie: la trace dell'API e una trace del job che sembra iniziare dal nulla.
La soluzione è semplice: quando accodi un job, cattura il contesto della trace corrente e salvalo nei metadata del job (payload, header o attributi, a seconda della tua coda). Quando il worker parte, estrai quel contesto e avvia un nuovo span come figlio della richiesta originale.
Propagare il contesto in modo sicuro
Copia solo il contesto di trace, non i dati utente.
- Injecta solo gli identificatori di trace e i flag di sampling (stile W3C traceparent).
- Tienilo separato dai campi business (per esempio, un campo dedicato "otel" o "trace").
- Trattalo come input non attendibile quando lo leggi (valida il formato, gestisci i dati mancanti).
- Evita di mettere token, email o corpi di richiesta nei metadata del job.
Span da aggiungere (senza trasformare le trace in rumore)
Le trace leggibili hanno solitamente pochi span significativi, non dozzine di piccoli. Crea span intorno ai confini e ai punti di attesa. Un buon punto di partenza è uno span enqueue nell'handler API e uno job.run nel worker.
Aggiungi una piccola quantità di contesto: numero di tentativi, nome della coda, tipo di job e dimensione del payload (non il contenuto). Se avvengono ritenti, registrali come span o eventi separati così puoi vedere i ritardi di backoff.
I task schedulati hanno bisogno di un parent anche loro. Se non c'è una richiesta in ingresso, crea una nuova root span per ogni esecuzione e taggala con il nome dello schedule.
Correlare log e trace (e mantenere i log sicuri)
Le trace ti dicono dove è passato il tempo. I log ti dicono cosa è successo e perché. Il modo più semplice per collegarli è aggiungere trace_id e span_id a ogni voce di log come campi strutturati.
In Go, prendi lo span attivo dal context.Context e arricchisci il logger una volta per richiesta (o job). Così ogni riga di log punta a una trace specifica.
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)
Questo è sufficiente per saltare da una voce di log allo span esatto che stava girando in quel momento. Rende anche ovvia la mancanza di contesto: trace_id sarà vuoto.
Tenere i log utili senza perdere dati sensibili
I log spesso vivono più a lungo e viaggiano più lontano delle trace, quindi sii più rigoroso. Preferisci identificatori stabili e risultati: user_id, order_id, payment_provider, status e error_code. Se devi loggare input utente, redattali prima e limita la lunghezza.
Rendere gli errori facili da raggruppare
Usa nomi di evento e tipi di errore coerenti così puoi contarli e cercarli. Se la formulazione cambia ogni volta, lo stesso problema sembrerà molti diversi.
Aggiungere metriche che aiutino davvero a trovare i problemi
Le metriche sono il tuo sistema di allerta precoce. In un setup che già usa OpenTelemetry in Go, le metriche dovrebbero rispondere: quanto spesso, quanto grave e da quando.
Inizia con un piccolo set che funziona per quasi tutte le API: conteggio richieste, conteggio errori (per classe di stato), percentili di latenza (p50, p95, p99), richieste in corso e latenza delle dipendenze per DB e chiamate esterne chiave.
Per mantenere le metriche allineate alle trace, usa gli stessi template di route e nomi. Se i tuoi span usano /users/{id}, anche le metriche dovrebbero farlo. Così quando un grafico mostra “p95 per /checkout è salito”, puoi passare direttamente alle trace filtrate per quella route.
Fai attenzione alle label (attributi). Una label sbagliata può far esplodere i costi e rendere le dashboard inutili. Template della route, metodo, classe di stato e nome del servizio sono generalmente sicuri. User ID, email, URL completi e messaggi di errore grezzi di solito non lo sono.
Aggiungi alcune metriche custom per eventi business-critical (per esempio, checkout iniziato/completato, errori di pagamento per gruppo di codice, job in background successo vs retry). Mantieni il set piccolo e rimuovi ciò che non usi mai.
Esportare la telemetria e fare rollout in sicurezza
L'esportazione è dove OpenTelemetry diventa concreto. Il servizio deve inviare span, metriche e log da qualche parte di affidabile senza rallentare le richieste.
Per lo sviluppo locale, tienilo semplice. Un exporter su console (o OTLP verso un collector locale) ti permette di vedere le trace rapidamente e convalidare nomi e attributi degli span. In produzione, preferisci OTLP verso un agente o OpenTelemetry Collector vicino al servizio. Ti dà un posto unico per gestire ritenti, routing e filtraggio.
Il batching conta. Invia la telemetria a batch con intervalli brevi, con timeout stretti così una rete bloccata non rallenti l'app. La telemetria non dovrebbe essere sul percorso critico. Se l'exporter non riesce a stare al passo, dovrebbe scartare dati piuttosto che accumulare memoria.
Il sampling mantiene i costi prevedibili. Inizia con sampling head-based (per esempio, 1-10% delle richieste), poi aggiungi regole semplici: campiona sempre gli errori e le richieste lente oltre una soglia. Se hai job in background ad alto volume, campionali a tassi inferiori.
Fai rollout a piccoli passi: dev con 100% di sampling, staging con traffico realistico e sampling più basso, poi produzione con sampling conservativo e alert su fallimenti dell'exporter.
Errori comuni che rovinano la visibilità end-to-end
La visibilità end-to-end fallisce più spesso per ragioni semplici: i dati esistono, ma non si collegano.
I problemi che rompono il tracing distribuito in Go sono solitamente questi:
- Perdere il context tra livelli. Un handler crea uno span, ma una chiamata DB, un client HTTP o una goroutine usa
context.Background()invece del context della richiesta. - Restituire errori senza marcare gli span. Se non registri l'errore e non imposti lo status dello span, le trace sembrano “verdi” anche quando gli utenti vedono 500.
- Strumentare tutto. Se ogni helper diventa uno span, le trace diventano rumore e costano di più.
- Aggiungere attributi ad alta cardinalità. URL completi con ID, email, valori SQL grezzi, corpi di richiesta o stringhe di errore grezze possono creare milioni di valori unici.
- Giudicare le prestazioni dalle medie. Gli incidenti emergono nei percentili (p95/p99) e nel tasso di errore, non nella latenza media.
Un controllo rapido è scegliere una richiesta reale e seguirla attraverso i confini. Se non riesci a vedere un trace ID che scorre dalla richiesta in ingresso, alla query DB, alla chiamata a terzi e al worker asincrono, non hai ancora visibilità end-to-end.
Una checklist pratica di "fatto"
Sei vicino quando puoi passare da un report utente alla richiesta esatta e poi seguirla attraverso ogni hop.
- Scegli una riga di log API e trova la trace esatta tramite
trace_id. Conferma che log più profondi della stessa richiesta (DB, client HTTP, worker) portino lo stesso contesto di trace. - Apri la trace e verifica l'annidamento: uno span server HTTP in cima, con span figli per DB e API esterne. Una lista piatta spesso significa che il contesto è stato perso.
- Avvia un job in background da una richiesta API (per esempio, invio ricevuta via email) e conferma che lo span del worker si colleghi alla richiesta.
- Controlla le metriche per le basi: conteggio richieste, tasso di errori e percentili di latenza. Conferma che puoi filtrare per route o operazione.
- Scansiona attributi e log per la sicurezza: niente password, token, numeri di carta completi o dati personali grezzi.
Un semplice test di realtà è simulare un checkout lento dove il provider di pagamenti è in ritardo. Dovresti vedere una singola trace con uno span esterno chiaramente etichettato, più un picco di metriche nella p95 per la route di checkout.
Se generi backend Go (per esempio, con AppMaster), è utile rendere questa checklist parte della routine di rilascio così i nuovi endpoint e worker restano tracciabili con la crescita dell'app. AppMaster (appmaster.io) genera servizi Go reali, quindi puoi standardizzare una configurazione OpenTelemetry e portarla across servizi e job in background.
Esempio: debug di un checkout lento attraverso i servizi
Un cliente scrive: “Il checkout a volte si blocca.” Non riesci a riprodurlo sempre, ed è proprio in questi casi che il tracciamento OpenTelemetry in Go è utile.
Inizia con le metriche per capire la forma del problema. Guarda tasso di richieste, tasso di errori e p95 o p99 di latenza per l'endpoint di checkout. Se il rallentamento avviene a brevi scoppi e solo per una fetta di richieste, di solito punta a una dipendenza, code o comportamento di retry piuttosto che alla CPU.
Poi apri una trace lenta nello stesso intervallo di tempo. Una trace è spesso sufficiente. Un checkout sano può durare 300–600 ms end-to-end. Uno cattivo potrebbe durare 8–12 secondi, con la maggior parte del tempo in uno span singolo.
Un pattern comune è questo: l'handler API è veloce, il lavoro DB va abbastanza bene, poi uno span del provider di pagamenti mostra ritenti con backoff e una chiamata a valle aspetta dietro un lock o una coda. La risposta potrebbe comunque tornare 200, quindi gli alert basati solo sugli errori non si attivano.
I log correlati ti dicono poi il percorso esatto in linguaggio semplice: “retrying Stripe charge: timeout”, seguito da “db tx aborted: serialization failure”, seguito da “retry checkout flow”. È un segnale chiaro che hai alcuni piccoli problemi che si combinano in una pessima esperienza utente.
Una volta trovato il collo di bottiglia, la coerenza mantiene le cose leggibili nel tempo. Standardizza i nomi degli span, gli attributi (hash ID utente non sensibile, order ID, nome della dipendenza) e le regole di sampling tra i servizi così tutti leggono le trace nello stesso modo.


