Context Propagation: Correlare Log, Tracce e Metriche
La context propagation e il meccanismo che permette di collegare tutti i segnali di telemetria generati durante l'elaborazione di una richiesta, indipendentemente da quanti servizi attraversa. Senza propagazione del contesto, tracce, log e metriche restano segnali isolati; con essa, diventano una narrativa coerente che racconta la storia completa di ogni richiesta.
In questo articolo analizzeremo i meccanismi di propagazione del contesto in OpenTelemetry, dal trace context ai baggage, dalla correlazione log-trace ai pattern per garantire che nessun segnale vada perso attraverso i confini dei servizi.
Cosa Imparerai in Questo Articolo
- Come funziona la propagazione del trace context tra servizi
- W3C Baggage: trasportare metadata personalizzati tra servizi
- Correlazione log-trace: collegare log strutturati alle tracce
- Pattern per la correlazione cross-service completa
- Gestire la propagazione in scenari asincroni (code, eventi)
- Troubleshooting dei problemi di propagazione
Trace Context Propagation
La propagazione del trace context e il meccanismo fondamentale che collega gli span di servizi diversi nella stessa traccia. Quando un servizio chiama un altro servizio, il trace context (trace_id, span_id, trace_flags) viene serializzato in header della richiesta e deserializzato dal servizio ricevente per creare uno span figlio.
OpenTelemetry usa i Propagators per gestire la serializzazione e deserializzazione del contesto. Il propagator di default e il W3C TraceContext, ma OTel supporta anche B3 (Zipkin), Jaeger e formati custom.
from opentelemetry import trace, context
from opentelemetry.propagate import inject, extract
from opentelemetry.propagators.textmap import DefaultTextMapPropagator
import requests
tracer = trace.get_tracer("order-service")
# --- LATO CLIENT: iniettare il contesto nella richiesta ---
def call_payment_service(order):
with tracer.start_as_current_span("call-payment-service") as span:
span.set_attribute("order.id", order.id)
# Creare un dizionario per gli header
headers = {}
# Iniettare il trace context negli header
inject(headers)
# Ora headers contiene:
# { "traceparent": "00-4bf92f35...-00f067aa...-01",
# "tracestate": "" }
# Inviare la richiesta con gli header di contesto
response = requests.post(
"http://payment-service/api/charge",
json={"order_id": order.id, "amount": order.total},
headers=headers
)
return response.json()
# --- LATO SERVER: estrarre il contesto dalla richiesta ---
from flask import Flask, request as flask_request
app = Flask(__name__)
@app.route("/api/charge", methods=["POST"])
def handle_charge():
# Estrarre il trace context dagli header della richiesta
ctx = extract(flask_request.headers)
# Creare uno span figlio nel contesto estratto
with tracer.start_as_current_span(
"process-charge",
context=ctx,
kind=trace.SpanKind.SERVER,
attributes={
"payment.amount": flask_request.json["amount"]
}
) as span:
result = process_payment(flask_request.json)
span.set_attribute("payment.status", result.status)
return result.to_json()
W3C Baggage: Metadata tra Servizi
Il W3C Baggage e un meccanismo per trasportare coppie chiave-valore personalizzate attraverso i confini dei servizi, insieme al trace context. A differenza degli attributi degli span (che sono locali), il baggage viene propagato a tutti i servizi downstream.
Il baggage e utile per trasportare informazioni di contesto che non appartengono allo span ma che servono a più servizi: customer tier, A/B test variant, request priority, region di origine.
from opentelemetry import baggage, context
from opentelemetry.propagate import inject
# --- Impostare il baggage nel servizio di origine ---
def handle_api_request(request):
# Impostare valori di baggage
ctx = baggage.set_baggage("customer.tier", "premium")
ctx = baggage.set_baggage("ab.test.variant", "checkout-v2", context=ctx)
ctx = baggage.set_baggage("request.priority", "high", context=ctx)
# Attaccare il contesto
token = context.attach(ctx)
try:
with tracer.start_as_current_span("handle-request") as span:
# Il baggage verrà propagato automaticamente
# a tutti i servizi downstream
headers = {}
inject(headers)
# headers ora contiene anche:
# "baggage": "customer.tier=premium,ab.test.variant=checkout-v2,request.priority=high"
call_downstream_service(headers)
finally:
context.detach(token)
# --- Leggere il baggage in un servizio downstream ---
def handle_downstream_request(request):
ctx = extract(request.headers)
token = context.attach(ctx)
try:
# Leggere i valori di baggage
customer_tier = baggage.get_baggage("customer.tier")
ab_variant = baggage.get_baggage("ab.test.variant")
priority = baggage.get_baggage("request.priority")
with tracer.start_as_current_span("downstream-operation") as span:
# Usare il baggage come attributi dello span
span.set_attribute("customer.tier", customer_tier or "standard")
span.set_attribute("ab.test.variant", ab_variant or "control")
# Differenziare il comportamento in base al baggage
if priority == "high":
process_with_priority(request)
else:
process_normal(request)
finally:
context.detach(token)
Baggage: Quando Usarlo e Quando Evitarlo
| Scenario | Raccomandazione | Motivo |
|---|---|---|
| Customer tier per routing | Si, usa baggage | Serve a più servizi per decisioni di routing |
| A/B test variant | Si, usa baggage | Ogni servizio deve sapere quale variante servire |
| Dati sensibili (PII, token) | No, mai baggage | Il baggage transita in chiaro negli header HTTP |
| Dati grandi (payload, JSON) | No, evita | Il baggage aumenta la dimensione di ogni richiesta |
| Request ID / correlation ID | Usa trace_id invece | Il trace_id e già propagato dal trace context |
Correlazione Log-Trace
La correlazione log-trace e il pattern che collega i log generati durante
l'elaborazione di una richiesta alla traccia corrispondente. Si ottiene iniettando
trace_id e span_id in ogni riga di log, permettendo di filtrare
tutti i log associati a una specifica traccia.
import logging
import json
from opentelemetry import trace
class TraceContextFilter(logging.Filter):
"""Filtro che aggiunge trace_id e span_id a ogni record di log"""
def filter(self, record):
span = trace.get_current_span()
if span and span.is_recording():
ctx = span.get_span_context()
record.trace_id = format(ctx.trace_id, "032x")
record.span_id = format(ctx.span_id, "016x")
record.trace_flags = format(ctx.trace_flags, "02x")
record.service_name = "order-service"
else:
record.trace_id = "0" * 32
record.span_id = "0" * 16
record.trace_flags = "00"
record.service_name = "order-service"
return True
# Configurare il logger con il filtro
logger = logging.getLogger("order-service")
logger.addFilter(TraceContextFilter())
# Formatter JSON che include trace context
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(json.dumps({
"timestamp": "%(asctime)s",
"level": "%(levelname)s",
"message": "%(message)s",
"service": "%(service_name)s",
"trace_id": "%(trace_id)s",
"span_id": "%(span_id)s",
"trace_flags": "%(trace_flags)s",
"logger": "%(name)s"
})))
logger.addHandler(handler)
# Uso: i log avranno automaticamente trace_id e span_id
def process_order(order):
with tracer.start_as_current_span("process-order") as span:
logger.info(f"Processing order {order.id}")
# Output: {"trace_id": "4bf92f35...", "span_id": "00f067aa...",
# "message": "Processing order ORD-123", ...}
validate_order(order)
logger.info(f"Order {order.id} validated successfully")
process_payment(order)
logger.info(f"Payment processed for order {order.id}")
Propagazione in Scenari Asincroni
La propagazione del contesto in scenari asincroni (code di messaggi, eventi, task schedulati) richiede attenzione speciale. A differenza delle chiamate HTTP sincrone dove gli header trasportano il contesto, nei sistemi asincroni il contesto deve essere serializzato nel payload del messaggio o nei suoi metadata.
from opentelemetry import trace, context
from opentelemetry.propagate import inject, extract
import json
tracer = trace.get_tracer("order-service")
# --- PRODUCER: iniettare contesto nel messaggio Kafka ---
def publish_order_event(order, kafka_producer):
with tracer.start_as_current_span(
"publish-order-created",
kind=trace.SpanKind.PRODUCER,
attributes={
"messaging.system": "kafka",
"messaging.destination.name": "order-events",
"messaging.operation.type": "publish",
"order.id": order.id
}
) as span:
# Iniettare il trace context negli header del messaggio
headers = {}
inject(headers)
# Convertire gli header in formato Kafka
kafka_headers = [(k, v.encode()) for k, v in headers.items()]
kafka_producer.produce(
topic="order-events",
key=order.id.encode(),
value=json.dumps(order.to_dict()).encode(),
headers=kafka_headers
)
# --- CONSUMER: estrarre contesto dal messaggio Kafka ---
def consume_order_events(kafka_consumer):
for message in kafka_consumer:
# Convertire header Kafka in dizionario
headers = {k: v.decode() for k, v in message.headers()}
# Estrarre il trace context
ctx = extract(headers)
# Creare uno span nel contesto estratto
with tracer.start_as_current_span(
"process-order-event",
context=ctx,
kind=trace.SpanKind.CONSUMER,
attributes={
"messaging.system": "kafka",
"messaging.destination.name": "order-events",
"messaging.operation.type": "process"
}
) as span:
order_data = json.loads(message.value())
span.set_attribute("order.id", order_data["id"])
process_order_event(order_data)
Checklist per la Propagazione del Contesto
- HTTP sincrono: il context viene propagato automaticamente dall'auto-instrumentation tramite header HTTP
- gRPC: propagazione automatica tramite metadata gRPC (auto-instrumentation)
- Message queue: iniettare/estrarre manualmente il contesto negli header del messaggio
- Thread pool: catturare e riattaccare il contesto nel nuovo thread con
attach/detach - Async/await: verificare che il framework di async mantenga il contesto (Python asyncio lo supporta nativamente)
- Scheduled tasks: creare un nuovo root span o linkare alla traccia originale
Troubleshooting della Propagazione
I problemi di propagazione sono tra i più comuni nell'adozione dell'observability. Quando le tracce appaiono "spezzate" (span isolati senza parent), tipicamente c'è un problema di propagazione del contesto. Ecco le cause più frequenti e le soluzioni.
Problemi Comuni di Propagazione
Tracce spezzate: gli span appaiono come tracce separate anziche collegate. Causa:
un proxy o load balancer intermedio rimuove gli header traceparent. Soluzione: configurare
il proxy per passare gli header di tracing.
Contesto perso nei thread: gli span creati in thread secondari non hanno parent.
Causa: il contesto non viene propagato attraverso i confini dei thread. Soluzione: usare
context.get_current() e attach() nel nuovo thread.
Propagator non configurato: i servizi usano formati diversi (W3C vs B3).
Soluzione: configurare il CompositePropagator con tutti i formati necessari.
Baggage non propagato: i valori di baggage non arrivano ai servizi downstream.
Causa: il BaggagePropagator non e registrato. Soluzione: aggiungere il
W3CBaggagePropagator alla configurazione dei propagatori.
Conclusioni e Prossimi Passi
La context propagation e il collante che tiene insieme l'intero sistema di observability. Senza di essa, tracce, log e metriche restano segnali isolati. Con una propagazione corretta, diventano una narrativa unificata che permette di seguire ogni richiesta dal punto di ingresso fino alla risposta, attraverso tutti i servizi coinvolti.
I tre meccanismi chiave sono: il W3C TraceContext per la propagazione del trace_id, il W3C Baggage per il trasporto di metadata personalizzati, e la log-trace correlation per collegare i log alle tracce corrispondenti.
Nel prossimo articolo esploreremo l'eBPF instrumentation, una tecnica rivoluzionaria che permette di ottenere observability a livello kernel senza modificare il codice applicativo e senza agenti, usando programmi eBPF per intercettare le chiamate di sistema.







