7. Context propagation

Por: Artiko
jaegercontext-propagationw3cb3baggage

7. Context propagation

Si el servicio A genera un trace y llama a B sin propagar el contexto, B inicia un trace nuevo. Resultado: en lugar de ver una traza distribuida con la operación completa, ves dos trazas separadas que no sabés que están relacionadas.

Context propagation es el mecanismo por el cual trace_id y span_id viajan entre servicios para que todos contribuyan al mismo trace.


Qué se propaga

Tres cosas viajan en cada salto entre servicios:

  1. Trace context: trace_id, span_id (el del span actual, que será el padre del siguiente), flags.
  2. Tracestate: información vendor-específica.
  3. Baggage: pares clave-valor que la aplicación quiere propagar.

El transporte estándar son headers HTTP (o el equivalente en otros protocolos).


W3C Trace Context (el estándar moderno)

Recomendación oficial del W3C. Lo soportan todos los SDKs modernos.

Header traceparent

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Estructura:

ParteBitsDescripción
008Versión (siempre 00 por ahora)
4bf92f3577...128trace_id (32 hex chars)
00f067aa0b...64parent_span_id (16 hex chars)
018Flags (bit 0 = sampled)

Header tracestate

Opcional. Cada vendor pone su info:

tracestate: jaeger=01a23b4567,datadog=89c10d

Pensado para que múltiples sistemas de tracing convivan sin pisarse.


B3 (legacy, Zipkin / Brave)

Inventado por Twitter para Brave/Zipkin. Sigue muy presente en proyectos viejos:

B3 Multi-Header

X-B3-TraceId: 80f198ee56343ba864fe8b2a57d3eff7
X-B3-SpanId: e457b5a2e4d86bd1
X-B3-ParentSpanId: 05e3ac9a4f6e3b90
X-B3-Sampled: 1

B3 Single (más nuevo, recomendado)

b3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90

Jaeger Headers (legacy)

El propio formato de Jaeger v1, hoy en deprecación:

uber-trace-id: 4bf92f3577b34da6a3ce929d0e0e4736:00f067aa0ba902b7:0:01

Si tu sistema todavía lo usa, configurá los SDKs para que lo emitan y lo acepten mientras migrás a W3C.


Configurar el propagator

OpenTelemetry permite múltiples propagators activos: lee los headers de cualquiera y emite los configurados.

Variable de entorno

# El default ya es tracecontext + baggage (W3C)
export OTEL_PROPAGATORS=tracecontext,baggage

# Convivencia con sistemas Zipkin/B3
export OTEL_PROPAGATORS=tracecontext,baggage,b3multi

# Migración desde Jaeger legacy
export OTEL_PROPAGATORS=tracecontext,baggage,jaeger

En código (Node.js)

const { CompositePropagator, W3CTraceContextPropagator, W3CBaggagePropagator } = require('@opentelemetry/core');
const { B3Propagator } = require('@opentelemetry/propagator-b3');
const { propagation } = require('@opentelemetry/api');

propagation.setGlobalPropagator(
  new CompositePropagator({
    propagators: [
      new W3CTraceContextPropagator(),
      new W3CBaggagePropagator(),
      new B3Propagator(),
    ],
  }),
);

Cómo viaja en HTTP

sequenceDiagram
    participant A as Servicio A
    participant N as Network
    participant B as Servicio B
    Note over A: span S1 activo<br/>trace_id=abc, span_id=111
    A->>A: HTTP client crea request
    A->>A: propagator.inject(headers, ctx)
    A->>N: GET /api/x<br/>traceparent: 00-abc-111-01
    N->>B: GET /api/x
    B->>B: propagator.extract(headers) → ctx
    B->>B: span S2 con<br/>trace_id=abc, parent=111, span_id=222
    B->>B: hace su trabajo
    B-->>A: 200

Las auto-instrumentaciones de HTTP lo hacen automáticamente: en clients inyectan headers, en servers extraen el contexto antes de invocar tu handler.


Cómo viaja en gRPC

gRPC usa metadata en lugar de headers HTTP. Los propagators trabajan igual:

ctx, _ := propagator.Extract(ctx, propagation.HeaderCarrier(metadata))

Las auto-instrumentaciones de gRPC oficiales (otelgrpc) ya hacen el inject/extract automáticamente.


Cómo viaja en Kafka

Producer:

flowchart LR
    P[Producer\nspan kind=PRODUCER] -->|"inject context\nen headers del mensaje"| K[(Kafka)]
    K -->|"mensaje con headers"| C[Consumer\nspan kind=CONSUMER\nlinks→producer]

Los headers del mensaje Kafka contienen el traceparent. El consumer los extrae.

Detalle clave: en mensajería el consumer no es hijo del producer en sentido temporal. Conviene usar links en lugar de parent-child:

ctx = propagator.extract(carrier=msg.headers)
parent_span_context = trace.get_span(ctx).get_span_context()

# Crear span del consumer linkeado al producer, NO como hijo
with tracer.start_as_current_span(
    "process_message",
    kind=SpanKind.CONSUMER,
    links=[Link(parent_span_context)],
) as span:
    procesar(msg)

Baggage

Headers separados del trace context:

baggage: user.tier=premium,region=us-east-1

Lectura:

const { propagation } = require('@opentelemetry/api');

// Get baggage del contexto activo
const baggage = propagation.getActiveBaggage();
const tier = baggage.getEntry('user.tier')?.value;

// Setear en el contexto activo
const newCtx = propagation.setBaggage(
  context.active(),
  propagation.createBaggage({
    'user.tier': { value: 'premium' },
  }),
);

Reglas de oro:


Cuándo se rompe la propagación

Síntoma típico: en Jaeger ves trazas separadas que tendrían que estar juntas. Causas comunes:

1. Servicio sin instrumentación

Un middleware o proxy en medio (NGINX, un servicio legacy) que no propaga headers. Solución: configurarlo para que pase los headers traceparent, b3, etc.

2. Código que crea HTTP clients sin auto-instrumentación

Por ejemplo, código que usa node:http puro pero las auto-instrumentaciones no detectaron el require correctamente. Importás el módulo antes que el SDK.

3. Async fuera de contexto

setTimeout(() => {
  // FUERA del contexto del span — no se propaga
  fetch('https://api.x');
}, 100);

OpenTelemetry usa AsyncLocalStorage (Node) o contextvars (Python). Si tu código rompe esa cadena (callbacks viejos, librerías de Promise no estándar) perdés el contexto.

Solución: re-anclar el contexto manualmente:

const ctx = context.active();
setTimeout(() => {
  context.with(ctx, () => {
    fetch('https://api.x');
  });
}, 100);

4. Workers / threads / procesos hijos

Cada worker tiene su contexto propio. Hay que pasar traceparent al worker manualmente.

5. Propagators distintos en cada servicio

Servicio A emite W3C, servicio B solo entiende B3. Resultado: B no encuentra contexto, inicia trace nuevo.

Solución: configurá todos los servicios con los mismos propagators, o al menos con un superconjunto compatible.


Diagnóstico rápido

Si dos servicios no aparecen en la misma traza:

  1. Hacé una request de prueba.
  2. Mirá los headers de salida de A: ¿está el traceparent?
  3. Mirá los logs de B: ¿el span del request tiene el mismo trace_id?
  4. ¿El propagator de B incluye el formato que A está mandando?
# Ver headers de salida con curl como cliente
curl -v http://servicio-a/endpoint 2>&1 | grep -i traceparent

Recap

flowchart LR
    A[Span en\nservicio A] -->|"propagator.inject\n(traceparent header)"| Net[Network]
    Net -->|"propagator.extract\n(traceparent header)"| B[Span en\nservicio B]
    Bag[Baggage] -.acompaña.-> Net

Con context propagation funcionando, ya tenés trazas distribuidas reales. Pero a alta escala vas a empezar a sentir el costo de almacenar todo. En el próximo capítulo vemos sampling: cómo decidir qué trazas guardar y cuáles descartar.