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.
Domande frequenti
Qual è il meccanismo che collega tutti i segnali di telemetria generati durante l'elaborazione di una richiesta?
Il meccanismo si chiama context propagation e permette di collegare tutti i segnali di telemetria indipendentemente da quanti servizi attraversa.
Qual è il ruolo del trace context nella propagazione del contesto?
La propagazione del trace context è il meccanismo fondamentale che collega gli span di servizi diversi nella stessa traccia.
Come viene trasportato il baggage, ovvero le coppie chiave-valore personalizzate, attraverso i confini dei servizi?
Il W3C Baggage è un meccanismo che trasporta le coppie chiave-valore personalizzate insieme al trace context attraverso i confini dei servizi.
Qual è il problema più comune nell'adozione dell'observability e come si risolve?
Il problema più comune è quello delle tracce spezzate, ovvero gli span appaiono come tracce separate anziché collegate. La soluzione consiste nel configurare il proxy per passare gli header di tracing.

Commenti
Caricamento commenti...
Accedi per commentare