Distributed Tracing: Comprendere il Flusso delle Richieste
Il distributed tracing e la tecnica che permette di seguire il percorso completo di una richiesta attraverso un sistema distribuito, dal punto di ingresso fino alla risposta finale. In un'architettura a microservizi dove una singola richiesta utente può attraversare 10, 20 o più servizi, il tracing distribuito e l'unico modo per ottenere una visione end-to-end del flusso e identificare colli di bottiglia, errori e dipendenze nascoste.
Ogni richiesta genera una trace (traccia), composta da una serie di span collegati tra loro in una struttura ad albero. Ogni span rappresenta un'unita di lavoro: una chiamata HTTP, una query al database, un messaggio pubblicato su una coda. Insieme, gli span raccontano la storia completa di cosa e successo durante l'elaborazione della richiesta.
Cosa Imparerai in Questo Articolo
- La struttura di Trace e Span in OpenTelemetry
- Il protocollo W3C Trace Context per la propagazione
- Le relazioni parent-child e i link tra span
- SpanKind: CLIENT, SERVER, PRODUCER, CONSUMER, INTERNAL
- Attributi, eventi e status degli span
- Pattern di visualizzazione: waterfall e service map
Anatomia di una Trace
Una trace e un grafo aciclico diretto (DAG) di span che rappresenta il percorso
di una richiesta. Ogni trace ha un identificatore univoco (trace_id) di 128 bit
che viene propagato attraverso tutti i servizi coinvolti. Lo span iniziale si chiama
root span e rappresenta l'intera operazione dall'inizio alla fine.
Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
[Root Span] API Gateway - POST /api/orders (250ms)
|
+-- [Child Span] Order Service - validate-order (15ms)
|
+-- [Child Span] Order Service - check-inventory (45ms)
| |
| +-- [Child Span] Inventory Service - GET /stock (40ms)
| |
| +-- [Child Span] Database - SELECT stock (8ms)
|
+-- [Child Span] Order Service - process-payment (120ms)
| |
| +-- [Child Span] Payment Service - charge (115ms)
| |
| +-- [Child Span] External API - Stripe charge (100ms)
|
+-- [Child Span] Order Service - send-confirmation (30ms)
|
+-- [Child Span] Notification Service - send-email (25ms)
Struttura di uno Span
Uno span rappresenta una singola operazione all'interno di una trace. Contiene tutte le informazioni necessarie per comprendere cosa e successo, quanto tempo ha impiegato e se si sono verificati errori. Ogni span ha una struttura ben definita:
from opentelemetry import trace
from opentelemetry.trace import StatusCode, SpanKind
tracer = trace.get_tracer("order-service", "1.0.0")
def process_payment(order_id, amount, currency):
# Creare uno span con tutti i componenti
with tracer.start_as_current_span(
name="process-payment",
kind=SpanKind.CLIENT, # Tipo di span
attributes={ # Attributi iniziali
"order.id": order_id,
"payment.amount": amount,
"payment.currency": currency,
"payment.provider": "stripe"
}
) as span:
try:
# Aggiungere un evento (timestamp automatico)
span.add_event("payment.validation.started", {
"validation.rules": "amount,currency,card"
})
validate_payment(amount, currency)
span.add_event("payment.validation.completed")
# Chiamata al provider di pagamento
result = stripe_client.charge(amount, currency)
# Aggiungere attributi dopo l'esecuzione
span.set_attribute("payment.transaction_id", result.tx_id)
span.set_attribute("payment.status", "success")
# Impostare lo status
span.set_status(StatusCode.OK)
return result
except PaymentDeclinedException as e:
# Registrare l'eccezione come evento
span.record_exception(e)
span.set_status(StatusCode.ERROR, "Payment declined")
span.set_attribute("payment.decline_reason", str(e))
raise
except TimeoutError as e:
span.record_exception(e)
span.set_status(StatusCode.ERROR, "Payment gateway timeout")
span.add_event("payment.retry.scheduled", {
"retry.attempt": 1,
"retry.delay_ms": 1000
})
raise
Componenti di uno Span
| Componente | Descrizione | Esempio |
|---|---|---|
| trace_id | ID univoco della trace (128 bit) | 4bf92f3577b34da6a3ce929d0e0e4736 |
| span_id | ID univoco dello span (64 bit) | 00f067aa0ba902b7 |
| parent_span_id | ID dello span genitore (vuoto per root) | a1b2c3d4e5f6a7b8 |
| name | Nome descrittivo dell'operazione | process-payment |
| kind | Tipo di span (CLIENT, SERVER, ecc.) | SpanKind.CLIENT |
| start_time | Timestamp di inizio | 2026-02-17T10:30:00.000Z |
| end_time | Timestamp di fine | 2026-02-17T10:30:00.120Z |
| attributes | Coppie chiave-valore descrittive | payment.amount=99.99 |
| events | Log puntuali con timestamp | payment.validation.completed |
| status | OK, ERROR o UNSET | StatusCode.OK |
W3C Trace Context: Lo Standard di Propagazione
Il W3C Trace Context e lo standard che definisce come il contesto di una trace viene propagato attraverso i confini dei servizi. Specifica due header HTTP che devono essere inclusi in ogni richiesta tra servizi:
# Header W3C Trace Context
# traceparent: contiene trace_id, parent_span_id, trace_flags
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
| | | |
v v v v
version trace_id (32 hex) parent_span_id (16 hex) flags (sampled=01)
# tracestate: stato specifico del vendor (opzionale)
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7
Quando un servizio riceve una richiesta con l'header traceparent, estrae il
trace_id e il parent_span_id, crea un nuovo span figlio con lo stesso
trace_id e propaga il contesto aggiornato alle chiamate successive. Questo meccanismo
garantisce che tutti gli span di una richiesta condividano lo stesso trace_id.
SpanKind: Classificare le Operazioni
Il SpanKind classifica il ruolo di uno span nel flusso della richiesta. Questa informazione e fondamentale per i backend che devono ricostruire la topologia del sistema e calcolare le latenze tra servizi.
from opentelemetry.trace import SpanKind
# SERVER: il servizio riceve una richiesta da un client remoto
with tracer.start_as_current_span("handle-request", kind=SpanKind.SERVER):
# Gestire la richiesta HTTP in arrivo
pass
# CLIENT: il servizio invia una richiesta a un servizio remoto
with tracer.start_as_current_span("call-payment-api", kind=SpanKind.CLIENT):
# Chiamata HTTP verso payment-service
pass
# PRODUCER: il servizio invia un messaggio a un broker
with tracer.start_as_current_span("publish-order-event", kind=SpanKind.PRODUCER):
# Pubblicazione su Kafka topic
pass
# CONSUMER: il servizio riceve un messaggio da un broker
with tracer.start_as_current_span("process-order-event", kind=SpanKind.CONSUMER):
# Consumo da Kafka topic
pass
# INTERNAL: operazione interna al servizio (default)
with tracer.start_as_current_span("validate-order", kind=SpanKind.INTERNAL):
# Logica interna di validazione
pass
Regole per la Scelta del SpanKind
- SERVER: usa quando il tuo servizio e il destinatario di una richiesta sincrona (HTTP, gRPC)
- CLIENT: usa quando il tuo servizio invia una richiesta sincrona a un altro servizio
- PRODUCER: usa quando il tuo servizio produce un messaggio asincrono (Kafka, RabbitMQ, SQS)
- CONSUMER: usa quando il tuo servizio consuma un messaggio asincrono
- INTERNAL: usa per operazioni interne che non attraversano confini di rete
Span Links: Collegare Tracce Diverse
Oltre alle relazioni parent-child, OTel supporta i Span Links per collegare span appartenenti a tracce diverse. I link sono utili in scenari asincroni dove un'operazione e causata da un'altra ma non ne e figlia diretta. Ad esempio, un messaggio in una coda può essere linkato allo span che lo ha prodotto, anche se la trace del consumer e diversa.
from opentelemetry import trace, context
# Scenario: batch processing di messaggi da una coda
# Ogni messaggio ha il suo trace context originale
def process_batch(messages):
# Creare link a tutti i messaggi del batch
links = []
for msg in messages:
# Estrarre il trace context dal messaggio
msg_context = extract_context(msg.headers)
span_context = trace.get_current_span(msg_context).get_span_context()
links.append(trace.Link(
context=span_context,
attributes={"messaging.message.id": msg.id}
))
# Creare uno span con link a tutti i messaggi originali
with tracer.start_as_current_span(
name="process-message-batch",
kind=SpanKind.CONSUMER,
links=links,
attributes={
"messaging.batch.message_count": len(messages)
}
) as span:
for msg in messages:
process_single_message(msg)
Visualizzazione delle Tracce
Le tracce distribuite vengono tipicamente visualizzate in due modi complementari:
Waterfall View (Gantt Chart)
La vista waterfall mostra gli span in ordine temporale, con indentazione per le relazioni parent-child. Permette di identificare immediatamente dove il tempo viene speso e quali operazioni sono sequenziali vs parallele. E la vista più usata per il debugging di singole richieste lente.
Service Map (Topology)
La service map mostra le dipendenze tra servizi come un grafo diretto. Ogni nodo rappresenta un servizio, ogni arco una comunicazione. Le metriche aggregate (latenza, error rate, throughput) vengono sovrapposte agli archi. E la vista ideale per comprendere l'architettura complessiva e identificare servizi critici o punti di failure.
Best Practice per il Distributed Tracing
Nominare gli span con operazioni, non URL: usa create-order, non
POST /api/v1/orders. Gli URL ad alta cardinalita (con ID) creano problemi di aggregazione.
Aggiungere attributi business-relevant: order.id, user.tier,
payment.method trasformano le tracce da strumenti tecnici a strumenti di analisi di business.
Registrare le eccezioni con record_exception: cattura automaticamente tipo, messaggio
e stacktrace dell'errore come evento dello span.
Usare SpanKind correttamente: permette ai backend di calcolare latenze di rete
(differenza tra span CLIENT e corrispondente span SERVER).
Conclusioni e Prossimi Passi
Il distributed tracing e lo strumento fondamentale per comprendere il comportamento dei sistemi distribuiti. La combinazione di trace context propagation (W3C), span strutturati con attributi semantici e visualizzazioni waterfall/service map fornisce la visibilità necessaria per diagnosticare problemi complessi che attraversano i confini dei servizi.
La scelta corretta di SpanKind, l'uso di attributi significativi e la registrazione di eventi ed eccezioni trasformano le tracce da semplici timeline a strumenti di analisi potenti che raccontano la storia completa di ogni richiesta.
Nel prossimo articolo esploreremo l'auto-instrumentation, la tecnica che permette di ottenere tracce distribuite senza modificare il codice applicativo, usando agent e librerie di instrumentazione automatica per Java, Python e Node.js.







