Salta al contenuto principale

08 - Context Propagation - Correlating Logs, Traces, and Metrics

Context Propagation: Correlare Log, Tracce e Metriche La context propagation e il meccanismo che permette di collegare tutti i segnali di telemetria...

Cosa ne pensi di questo articolo?

08 - Context Propagation - Correlating Logs, Traces, and Metrics
ObservabilityFoto:Keysi EstradaPexels

In Breve

Context Propagation: Correlare Log, Tracce e Metriche La context propagation e il meccanismo che permette di collegare tutti i segnali di telemetria...

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.

Fonti e approfondimenti

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti