Skip to content
Volver al blog
Tutoriales

Cabecera traceparent: guía completa de W3C Trace Context

La cabecera traceparent campo a campo: qué significa cada segmento hexadecimal, qué la invalida y por qué se rompen las trazas. Decodificador online gratis.

13 min de lectura

Cabecera traceparent: guía completa de W3C Trace Context

La cabecera traceparent es el estándar que hace funcionar las trazas distribuidas sobre HTTP: una sola línea de ASCII que lleva la identidad de una petición a través de todos los servicios que toca. En la versión actual mide exactamente 55 caracteres y tiene cuatro campos separados por guiones:

00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│  │                                │                │
│  │                                │                └─ trace-flags (2 hex, 1 byte)
│  │                                └─ parent-id     (16 hex, 8 bytes)
│  └─ trace-id                                       (32 hex, 16 bytes)
└─ version                                           (2 hex, 1 byte)

Dos de esos campos se comportan de forma distinta a medida que la petición avanza. El trace-id se mantiene idéntico en cada salto: es el nombre de la petición, desde el proxy de borde hasta la última consulta a base de datos. El parent-id cambia en cada salto, porque nombra al span que te llamó, no a la petición. Confundir ambos explica buena parte de los tickets del tipo “mis trazas se ven mal”.

Esa es la anatomía. Lo difícil es lo que la tabla de campos no puede contarte: qué invalida una cabecera, qué hace un receptor conforme cuando le llega una así, y en qué puntos la cabecera desaparece sin ruido entre dos servicios que dicen soportar trazas. Si tienes una cabecera real delante, pégala en el decodificador traceparent gratis y sigue la lectura: separa los campos, expande el byte de flags bit por bit y nombra la regla que incumple una cabecera rota.

La cabecera traceparent de un vistazo

La cabecera traceparent es una sola cabecera HTTP que lleva una misma traza de un servicio al siguiente. Son cuatro campos hexadecimales separados por guiones, version, trace-id, parent-id y trace-flags, y en la versión actual el total mide exactamente 55 caracteres. El trace-id nombra la petición entera; el parent-id nombra el span que te llamó.

CampoDígitos hexBytesQué identifica¿Cambia por salto?
version21Qué formato sigue el resto. Hoy siempre 00No
trace-id3216La petición completa, de punta a puntaNo
parent-id168El span que llama (el ID de span de quien te invocó)
trace-flags21Un campo de 8 bits; el bit 0 es sampledRara vez

Suma tres guiones a esos 52 dígitos hexadecimales y obtienes 55 caracteres. Vale la pena memorizar ese número, porque una cabecera de versión 00 con cualquier otra longitud es inválida, y la longitud es lo más rápido de verificar a simple vista.

Todo en la cabecera es hexadecimal en minúsculas. No “hexadecimal, sin distinguir mayúsculas”: minúsculas. La gramática de la recomendación W3C Trace Context admite 0-9 y a-f y nada más, y por eso un trace ID en mayúsculas con un valor perfectamente correcto termina descartado río abajo.

Campo por campo

Cada campo tiene su propio ancho, sus propios valores inválidos y su propia forma de salir mal.

version — por qué no es simplemente “siempre 00”

Hoy el byte de versión es 00, y seguirá siendo 00 por un buen rato. Pero ff está explícitamente prohibido: la especificación lo reserva como valor inválido, así que una cabecera que empieza con ff llega muerta sin importar lo que venga después.

La regla interesante es la que habla de versiones que nunca has visto. Un parser que hace if (version !== '00') reject() está mal, y mal de una forma cara. La especificación pide que el receptor intente el parseo cuando la versión es mayor y la cabecera mide al menos lo mismo que el formato conocido: lee los campos que reconoces, tolera datos extra al final y sigue adelante. Rechazar significa que tu servicio se convierte en la frontera donde la traza se corta y empieza otra nueva, en el instante en que alguien río arriba actualice.

// Mal: convierte a tu servicio en el lugar donde mueren las trazas
if (version !== '00') throw new Error('bad traceparent');

// Bien: parsea el prefijo que sí entiendes
if (version !== '00' && header.length >= 55) {
  // lee version, trace-id, parent-id, trace-flags; ignora el resto
}

trace-id — 16 bytes, la identidad de toda la petición

Treinta y dos dígitos hexadecimales en minúsculas, constantes durante toda la vida de la traza. Sea cual sea el servicio que lo generó al inicio, cada salto lo copia hacia adelante sin tocarlo. Cuando buscas una traza en tu backend de observabilidad, esta es la cadena que pegas.

Dos reglas gobiernan su valor. Debe tener 32 dígitos hexadecimales y no puede ser todo ceros. 00000000000000000000000000000000 no es “una traza que aún no tiene datos”: la especificación lo nombra como valor inválido y exige que el receptor ignore la cabecera completa. En la práctica, un trace ID de puros ceros significa un SDK que nunca se inicializó, o un middleware insertando un marcador de posición porque no tenía contexto real que reenviar.

Un trace-id son 128 bits, el mismo ancho que un UUID, y no es un UUID. No hay bits de versión, ni bits de variante, ni guiones, ni estructura de ningún tipo: dieciséis bytes opacos. No puedes extraerle una v4, y un UUID al que le quitas los guiones tampoco se convierte automáticamente en un trace-id válido, porque los nibbles de versión y variante hacen que su aleatoriedad no sea uniforme. Si quieres ver qué reserva realmente un UUID dentro de esos 128 bits, qué codifica de verdad un UUID recorre la distribución, y el generador de UUID muestra los bits de versión y variante en su sitio.

parent-id — 8 bytes, el span que te llamó

Dieciséis dígitos hexadecimales, reescritos en cada salto. El nombre genera más confusión de la que el campo merece: la especificación del W3C lo llama parent-id, OpenTelemetry llama a esos mismos 8 bytes un span ID, y son lo mismo visto desde dos ángulos. Desde el punto de vista de tu servicio es el padre; desde el punto de vista de quien llama, es el ID del span que acaba de crear para la petición saliente.

Así que cuando el servicio A llama al servicio B, A pone su propio ID de span en la ranura del parent-id. B crea entonces un span hijo, y cuando B llama a C pone ahí el ID de span de B. El trace-id no se toca en ningún momento. Ese es el algoritmo de propagación completo.

Los parent-id de puros ceros también son inválidos, por la misma razón que los trace-id: 0000000000000000 significa que quien llamó no aportó un span real, y la cabecera debe descartarse en lugar de honrarse a medias.

trace-flags — parece un booleano, en realidad son ocho bits

Casi toda cabecera que veas termina en 01, así que es natural leer el campo como un sí/no. Es un byte, y los bits están asignados así:

  • bit 0, máscara 0x01sampled
  • bit 1, máscara 0x02random-trace-id, agregado en Trace Context Level 2
  • bits 2–7 — reservados; ignóralos al recibir, límpialos en las peticiones salientes

Esto es lo que decodifican las combinaciones:

HexBinariosampledrandom-trace-id¿Se cumple flags === 0x01?
0000000000falsefalsefalse
0100000001truefalsetrue
0200000010falsetruefalse
0300000011truetruefalse ← el bug

Lee la última fila otra vez. Una traza con flags 03 está muestreada. Cualquier código que compare el byte completo contra 01 la reporta como no muestreada, en silencio, y solo para el subconjunto de tráfico donde el flag de Level 2 resulta estar activo, que es la peor forma posible de fallar: parece un problema de tasa de muestreo y no un bug de parseo.

const flags = parseInt(traceFlags, 16);

// Mal: trata un campo de bits como si fuera una enumeración
const sampled = traceFlags === '01';

// Bien
const sampled       = (flags & 0x01) !== 0;
const randomTraceId = (flags & 0x02) !== 0;

¿Qué afirma exactamente random-trace-id? Que al menos los 7 bytes más a la derecha del trace-id se generaron con aleatoriedad uniforme. Suena académico hasta que piensas en el muestreo consistente: si un sistema río abajo quiere conservar el 1 % de las trazas y necesita que cada servicio decida por su cuenta cuál es ese 1 %, puede tomar esos bytes módulo algo en vez de hashear el ID primero. El flag es la promesa de río arriba de que hacerlo es seguro.

Qué invalida una cabecera traceparent

Este es el conjunto completo de rechazos para una cabecera de versión 00:

SíntomaReglaResultado
00-4BF92F35...-01La gramática solo admite hexadecimal en minúsculasInválida: el valor es correcto, pero la cabecera se rechaza
ff-...La versión ff está prohibida por la especificaciónInválida
trace-id es 00000000000000000000000000000000Un trace-id de puros ceros es un valor inválido con nombre propioInválida
parent-id es 0000000000000000Un parent-id de puros ceros es un valor inválido con nombre propioInválida
trace-id no tiene 32 dígitos hexAncho fijoInválida
parent-id no tiene 16 dígitos hexAncho fijoInválida
trace-flags no tiene 2 dígitos hexAncho fijoInválida
La cabecera no mide exactamente 55 caracteres, versión 00Los datos extra al final solo son legales bajo una versión futuraInválida
Cualquier carácter fuera de 0-9a-f y los guionesNo es hexadecimalInválida

La consecuencia práctica de cualquiera de esas filas es siempre la misma:

Un receptor conforme no repara una cabecera traceparent inválida ni la reenvía. Descarta la cabecera y arranca una traza completamente nueva con un trace-id recién generado.

Lo que significa que el síntoma en tu pantalla no es una traza rota. Son dos trazas cortas y desconectadas: una que termina de golpe en el servicio que emitió la cabecera defectuosa, y otra que parece nacer de la nada en el servicio que la recibió. Nada queda marcado como error en ningún lado. Ambas trazas se ven sanas por separado. Hay quien pierde tardes enteras buscando el eslabón perdido entre las dos cuando la respuesta es que un middleware pasó una cadena hexadecimal a mayúsculas, o que una cabecera armada a mano salió de 54 caracteres.

La longitud y las mayúsculas son los dos modos de fallo que no se detectan mirando fijo. Pega la cabecera en el decodificador y te nombra la regla exacta que se incumplió, en lugar de hacerte contar dígitos.

tracestate: la cabecera compañera que todos entienden mal

traceparent lleva la identidad estándar. La cabecera tracestate lleva lo que cada proveedor quiera agregar junto a ella, como miembros key=value separados por comas:

tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

Una implementación que no reconoce una clave debe reenviarla sin tocarla. Ese es todo el objetivo de diseño: los proveedores pueden llevar estado propietario a cuestas de una traza estándar sin que cada salto tenga que entenderlo.

La gramática, eso sí, tiene dientes, y tres de sus reglas explican síntomas reales de producción.

32 list-members es un techo duro. Esto no es una recomendación, es la gramática: list = list-member 0*31( OWS "," OWS list-member ). Un tracestate con 33 miembros no es un tracestate con una entrada de más, es una cabecera inválida, y los receptores están en su derecho de descartarla entera. Eso explica un síntoma que de otro modo parece brujería: datos de proveedor presentes en el borde, presentes dos saltos adentro y completamente ausentes en el salto cinco. Cada salto iba agregando su propio miembro, la lista cruzó los 32, y de ahí en adelante la cabecera completa se descartó en vez de recortarse.

Los valores miden de 1 a 256 caracteres y nunca pueden estar vacíos. La producción del valor termina con un carácter no blanco obligatorio, así que vendor= no es “una clave sin valor”: es un error de sintaxis. Solo admite ASCII imprimible, y nunca una coma ni un signo igual dentro del valor.

La gramática de las claves cambió entre Level 1 y Level 2. Level 1 definía las claves mediante una producción tenant@vendor, donde @ era un separador estructural. Level 2 la reemplazó por una clase de caracteres plana: una clave empieza con una letra minúscula o un dígito y continúa con a-z, 0-9, _, -, *, / y @. Bajo Level 2, @ es un carácter común y corriente, las claves pueden empezar con un dígito, y a@b@c es una clave perfectamente legal que la producción de Level 1 rechazaría. Si tienes un proxy validando contra Level 1 y un servicio emitiendo claves de Level 2, un lado acepta lo que el otro rechaza, y la cabecera se pierde en exactamente un salto.

Quedan dos reglas más que conviene conocer. Las claves duplicadas son inválidas sin más. Y cuando modificas el parent-id del traceparent, tienes que mover tu propia entrada de tracestate al frente de la lista: la lista está ordenada de más reciente a más antigua. Saltarse ese paso deja estado de proveedor obsoleto en un lugar donde quien lo lea lo tomará por actual.

Por último, la regla amable: los miembros vacíos son legales. Cuando un middlebox elimina una entrada, a menudo deja la coma atrás y produce rojo=1,,congo=2. La especificación lo permite de forma explícita, así que un parser debería descartar el miembro vacío y continuar en lugar de declarar la cabecera malformada. La vista de tracestate del decodificador lista cada miembro con validación individual y un conteo en vivo contra el límite de 32 miembros, que suele ser más rápido que contar comas.

Cómo viajan las cabeceras de trazas distribuidas: una petición, cuatro saltos

Sigue una petición a través de un proxy de borde, un servicio de API y dos servicios río abajo:

Cliente
  │  (sin traceparent — el borde es la raíz)

Proxy de borde      genera trace-id 4bf9…4736, span 00f0…02b7
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Servicio API        lo lee, crea el span a1b2c3d4e5f60718
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-a1b2c3d4e5f60718-01

Servicio Pedidos    lo lee, crea el span 9f8e7d6c5b4a3928
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-9f8e7d6c5b4a3928-01

Servicio Inventario

Cada salto hace las mismas tres cosas: leer la cabecera entrante, reemplazar el parent-id con su propio ID de span para cada llamada saliente, y reenviar el trace-id y los flags sin cambios. Cuando no llega ninguna cabecera (el cliente del diagrama), el servicio receptor es la raíz: genera un trace-id y toma la decisión de muestreo para todo lo que venga después.

Puedes inyectar una cabecera a mano para probar una cadena de punta a punta:

curl -sS -o /dev/null -w '%{http_code}\n' \
  -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
  -H 'tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE' \
  https://example.com/api

Reproduce contra staging una cabecera capturada en producción y verás aparecer el mismo trace-id en tu backend. El generador de comandos cURL arma los flags por ti si vas a agregar autenticación o un cuerpo, y la chuleta de curl cubre las opciones de cabeceras y de modo detallado que vas a querer mientras depuras.

Para ver qué recibió realmente un servicio y no lo que crees que enviaste, levanta un servidor de eco desechable y apunta un salto hacia él:

python3 - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer

class Echo(BaseHTTPRequestHandler):
    def do_GET(self):
        for name, value in self.headers.items():
            print(f"{name}: {value}")
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"ok\n")

HTTPServer(("127.0.0.1", 8080), Echo).serve_forever()
PY

Después haz curl -H 'traceparent: …' http://127.0.0.1:8080/ y lee lo que salió del otro lado. Buena parte de las investigaciones de “el proxy se está comiendo mi cabecera” terminan aquí.

trace-flags sampled: una decisión de río arriba, no un acuse de recibo

Un bit sampled de 1 en trace-flags significa que el servicio río arriba decidió registrar esta traza. No promete que los datos hayan llegado a tu backend.

El muestreo head-based toma esa decisión en la raíz, antes de que haya pasado nada, y la propaga hacia abajo: barato, consistente entre servicios y ciego, porque no puede saber que la petición estaba a punto de fallar. El muestreo tail-based guarda los spans en un búfer hasta que la traza termina y recién entonces decide, así que puede conservar cada traza que contenga un error, a cambio de mantener spans en memoria y de necesitar que los spans de todos los servicios aterricen en el mismo colector.

Con muestreo tail-based, una traza puede llegar marcada 01 en cada salto y aun así descartarse al final. Los límites de tasa y las cuotas de exportación también pueden descartarla. Así que 01 en el borde y ninguna traza en la interfaz no es necesariamente un bug de propagación; revisa las métricas de descarte del propio colector antes de ir a mirar cabeceras.

El caso contrario importa más en el día a día. Si los flags entrantes son 00, quien llamó ejecutó su muestreador y eligió no registrar. Nada en tu servicio está mal configurado, y auditar tu propio muestreador es tiempo perdido: la pregunta es qué servicio río arriba está decidiendo no muestrear.

Convertir entre formatos de propagación

W3C Trace Context ganó, pero muchos sistemas todavía hablan algo más viejo, y los gateways traducen entre ellos. Este es el mismo ejemplo de traceparent escrito en cuatro formatos:

FormatoCabecera(s)Valor para nuestro ejemplo
W3Ctraceparent00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
B3 singleb34bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1
B3 multiX-B3-TraceId, X-B3-SpanId, X-B3-Sampled4bf92f3577b34da6a3ce929d0e0e4736, 00f067aa0ba902b7, 1
Datadogx-datadog-trace-id, x-datadog-parent-id, etiqueta _dd.p.tid11803532876627986230, 67667974448284343, 4bf92f3577b34da6
AWS X-RayX-Amzn-Trace-IdRoot=1-4bf92f35-77b34da6a3ce929d0e0e4736;Parent=00f067aa0ba902b7;Sampled=1

Datadog: la división de 64 bits altos y bajos

Los identificadores de Datadog son anteriores a los trace ID de 128 bits, y esa capa de compatibilidad es donde se tuercen la mayoría de las conversiones. x-datadog-trace-id lleva los 64 bits bajos como cadena decimal. Los 64 bits altos viajan aparte, en hexadecimal, dentro de la etiqueta _dd.p.tid, que a su vez viaja en la cabecera x-datadog-tags.

const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
const [, traceId, parentId] = traceparent.split('-');

const datadogTraceId  = BigInt('0x' + traceId.slice(16)).toString(10);
const higher64Hex     = traceId.slice(0, 16);
const datadogParentId = BigInt('0x' + parentId).toString(10);

console.log('x-datadog-trace-id:',  datadogTraceId);  // 11803532876627986230
console.log('x-datadog-tags:',      higher64Hex);     // _dd.p.tid=4bf92f3577b34da6
console.log('x-datadog-parent-id:', datadogParentId); // 67667974448284343

El error clásico es convertir los 128 bits completos en un solo número decimal:

BigInt('0x' + traceId).toString(10);
// 100985939111033328018442752961257817910 — no coincide con nada en la interfaz

Ese valor no es aritmética equivocada. Es la representación decimal correcta de la cantidad equivocada, y por eso sobrevive a la revisión de código y luego, calladamente, no coincide con ninguna traza.

La segunda trampa es la precisión numérica. Un identificador de 64 bits supera Number.MAX_SAFE_INTEGER, que vale 9007199254740991, así que cualquier ruta de código que permita que un trace ID se convierta en un number de JavaScript corrompe sus dígitos bajos. Mantén los trace ID como cadenas y recurre a BigInt solo cuando tengas que hacer aritmética; un ID que llega sin comillas en un JSON ya viene dañado cuando lo ves.

AWS X-Ray: la marca de tiempo que no está ahí

Un trace ID de X-Ray se ve como 1-{8 hex}-{24 hex}, y los 8 dígitos hexadecimales iniciales son la hora de creación en segundos desde epoch. Convertir desde W3C es mecánico:

const traceId = '4bf92f3577b34da6a3ce929d0e0e4736';
const epochHex     = traceId.slice(0, 8);              // 4bf92f35
const epochSeconds = parseInt(epochHex, 16);           // 1274621749
const xrayId       = `1-${epochHex}-${traceId.slice(8)}`;
// 1-4bf92f35-77b34da6a3ce929d0e0e4736
// X-Amzn-Trace-Id: Root=1-4bf92f35-77b34da6a3ce929d0e0e4736;Parent=00f067aa0ba902b7;Sampled=1

new Date(epochSeconds * 1000).toISOString();           // 2010-05-23T13:35:49.000Z

Mira esa fecha. La cabecera de ejemplo de la especificación decodifica a mayo de 2010, lo cual es un disparate evidente, y ese es precisamente el punto. Un trace-id de W3C no contiene ninguna marca de tiempo. Dieciséis bytes aleatorios producen con toda naturalidad un epoch de aspecto plausible cuando lees los primeros cuatro como si fueran uno, y el número no significa nada a menos que el identificador venga genuinamente de X-Ray. Decodificar una hora a partir de un trace-id cualquiera es leer un número aleatorio y creerle.

Cuando el ID sí vino de X-Ray, la conversión es útil: mete esos ocho dígitos hexadecimales en el convertidor de timestamp Unix para obtener una fecha legible, y la guía del epoch cubre las trampas de segundos contra milisegundos y de zonas horarias que vienen después.

B3: el linaje de Zipkin

B3 viene de Zipkin y es el formato que te encuentras en mallas de servicios antiguas. La forma de cabecera única es traceId-spanId-sampled, donde el campo sampled es 1 o 0 en lugar de un byte hexadecimal, así que el bit random-trace-id de Level 2 no tiene dónde ir y se pierde en la traducción. La forma multicabecera reparte los mismos valores entre X-B3-TraceId, X-B3-SpanId y X-B3-Sampled.

El detalle histórico es el ancho. Los trace ID de B3 pueden ser de 64 bits, es decir 16 dígitos hexadecimales en vez de 32. Convertir un ID B3 de 64 bits a W3C significa rellenar con ceros a la izquierda hasta llegar a 32 dígitos, y convertir de vuelta significa decidir si truncar. Rellenar a la izquierda es seguro; truncar no lo es, porque dos trazas que solo se diferencian en sus bytes altos colapsan en una.

Dónde se pierde traceparent en producción

Todo lo anterior asume que la cabecera llega. Muchas veces no llega. Estos son los cuatro lugares donde se pierde.

El navegador la descarta en llamadas cross-origin

Síntoma: existen trazas del frontend, existen trazas del backend, y nada las enlaza. O la petición cross-origin falla directamente con un error de CORS.

Causa: traceparent es una cabecera personalizada, así que agregarla hace que la petición deje de ser simple y dispara un preflight OPTIONS. Si la respuesta del preflight del servidor no lista la cabecera en Access-Control-Allow-Headers, el navegador bloquea la petición real. Por separado, la instrumentación de navegador de OpenTelemetry se niega a inyectar cabeceras de traza en peticiones cross-origin a menos que le digas qué orígenes están permitidos.

Solución: en el servidor, responde Access-Control-Allow-Headers: traceparent, tracestate para el preflight. En el SDK del navegador, configura propagateTraceHeaderCorsUrls con un patrón que coincida con los orígenes de tu API. Hacen falta las dos cosas; cualquiera de ellas por separado te deja con el mismo síntoma. Si un preflight regresa con un estado inesperado, vale la pena contrastarlo con la chuleta de códigos de estado HTTP antes de dar por hecho que el problema es la cabecera.

Proxies, WAF y balanceadores de carga eliminan cabeceras desconocidas

Síntoma: la cabecera está presente cuando haces curl directo al servicio y desaparece cuando la misma petición pasa por el gateway.

Causa: reenvío basado en listas de permitidos. Muchísimas configuraciones de proxy, reglas de WAF y balanceadores de carga administrados solo reenvían las cabeceras que reconocen, y traceparent no está en la lista por defecto. Algunas mallas además reescriben la cabecera, generando su propio trace-id y descartando el tuyo.

Solución: haz bisección con el servidor de eco de más arriba: colócalo detrás de cada salto por turnos y observa qué capa descarta la cabecera. Después permite explícitamente traceparent y tracestate en las reglas de reenvío de esa capa. Si el proxy es nginx, ten en cuenta que el bloque que atiende una ruta decide qué cabeceras pasa, y el bloque que atiende una ruta no siempre es el que crees; las reglas de prioridad de location en nginx explican por qué una configuración de cabeceras puede parecer completamente ignorada.

Las colas de mensajes no tienen cabeceras HTTP

Síntoma: la traza termina en el momento en que una petición se convierte en un trabajo en segundo plano.

Causa: no hay ninguna petición HTTP cruzando esa frontera, así que no hay dónde propagar la cabecera. Kafka tiene record headers, SQS tiene atributos de mensaje, y la instrumentación HTTP no llena ninguno de los dos por ti.

Solución: inyecta el contexto en el mensaje del lado del productor y extráelo del lado del consumidor. Todos los SDK de OpenTelemetry exponen inject y extract justo para esto, y el formato en el cable es la misma cadena del W3C; lo único que cambia es el portador, que pasa de un mapa de cabeceras HTTP a metadatos del mensaje. La documentación de propagadores de OpenTelemetry cubre la interfaz del portador en cada lenguaje.

Mayúsculas, y qué convierte a minúsculas HTTP/2 en realidad

Síntoma: confusión en la revisión de código sobre si Traceparent es aceptable.

Causa: dos reglas distintas se funden en una. Los nombres de cabecera de HTTP/1.1 no distinguen mayúsculas, y HTTP/2 exige que se codifiquen en minúsculas en el cable. Eso es sobre el nombre. De forma independiente, el hexadecimal del valor de la cabecera debe ir en minúsculas, porque así lo dice la gramática del W3C, y ninguna versión del protocolo va a arreglártelo.

Solución: envía el nombre como traceparent y nunca pases el valor a mayúsculas. Un gateway que normaliza nombres de cabecera no va a normalizar tus dígitos hexadecimales, y un trace-id en mayúsculas atraviesa sin problema todas las capas de transporte antes de que lo rechace la aplicación que finalmente lo parsea.

¿Deberías confiar en un traceparent entrante?

Un traceparent que llega desde la internet pública es entrada controlada por el usuario: una cadena que eligió un cliente anónimo, y que la mayoría de los servicios acepta sin pensarlo dos veces.

De ahí se derivan tres riesgos concretos. Primero, el empalme de trazas: un atacante que envía un trace-id observado en otro lado logra que su petición quede cosida a una traza existente, lo que contamina el grafo y puede exponer tiempos internos a quien pueda leer esa traza. Segundo, el quemado de cuota: fijar 01 a fuego fuerza el muestreo en cada petición, y una avalancha moderada se convierte en una factura de ingesta muy grande o, peor, expulsa las trazas que sí necesitabas. Tercero, la correlación entre inquilinos: reutilizar un mismo trace-id en peticiones de distintos inquilinos enlaza registros que tu herramienta luego trata como una sola operación lógica.

La postura pragmática es aceptar en el borde pero no confiar. Valida la gramática y rechaza las cabeceras malformadas en vez de pasarlas hacia adentro. Para tráfico no autenticado, vuelve a ejecutar tu propia decisión de muestreo en lugar de honrar el flag entrante, para que ningún cliente externo pueda dejar tu muestreador fijo en “registrar siempre”. Para tráfico autenticado, honrar la decisión de quien llama suele estar bien, porque sabes quién es.

Y trata el trace-id como público. No es un secreto y nunca lo fue: aparece en los logs, en las páginas de error, en las cabeceras de respuesta y en las capturas de pantalla pegadas en tickets de soporte. Nunca codifiques dentro de uno un ID de usuario, un nombre de inquilino ni nada con significado, y nunca lo uses como llave de autorización. Es un identificador de correlación, y eso es todo lo que debería ser.

Preguntas frecuentes

¿Cuál es la diferencia entre traceparent y tracestate?

traceparent lleva la identidad estandarizada (trace-id, parent-id y los flags de muestreo) y toda implementación debe entenderla. tracestate lleva estado específico de cada proveedor que las implementaciones ajenas reenvían sin tocar. Las dos están ligadas: cuando el traceparent es inválido, la especificación exige ignorar también el tracestate.

¿Por qué mi traza empieza de cero a mitad de la cadena de llamadas?

Una traza vuelve a empezar a mitad de la cadena casi siempre porque un salto recibió una cabecera que no pasó la gramática, la descartó y generó un trace-id nuevo. El hexadecimal en mayúsculas, un trace-id de puros ceros y una cabecera que no mide exactamente 55 caracteres provocan esto. Si la cabecera está bien formada, los siguientes sospechosos son un proxy que la elimina y un preflight cross-origin que falla.

¿Necesito configurar CORS para enviar traceparent desde un navegador?

Sí, configurar CORS es obligatorio. traceparent es una cabecera personalizada, así que hace que la petición deje de ser simple y dispara un preflight; el servidor debe listar traceparent en Access-Control-Allow-Headers. La instrumentación de navegador de OpenTelemetry además necesita propagateTraceHeaderCorsUrls configurado, porque por defecto no inyecta cabeceras de traza cross-origin.

¿Cómo propago el contexto de traza a través de Kafka o SQS?

Escribe el valor de traceparent en un record header de Kafka o en un atributo de mensaje de SQS del lado del productor, y léelo de vuelta del lado del consumidor para restaurar el contexto. Los SDK de OpenTelemetry exponen inject y extract para esto en todos los lenguajes. El formato no cambia; lo único distinto es el portador respecto a un mapa de cabeceras HTTP.

¿Es seguro exponer un trace ID en logs o respuestas?

Sí, un trace ID se puede exponer sin problema. Es un identificador aleatorio sin identidad embebida y sin poder de autorización. Eso sí, correlaciona registros entre sistemas, así que nunca codifiques dentro de uno un ID de usuario o un nombre de inquilino, y nunca lo aceptes como prueba de nada. Trátalo como una clave de correlación pública y no hay problema en registrarlo o devolverlo en una respuesta.

¿Quién genera la cabecera traceparent?

La cabecera traceparent la genera el primer servicio que atiende una petición que llega sin ella. Suele ser un proxy de borde, un API gateway o un SDK de navegador, y ese servicio queda como raíz de la traza: genera el trace-id, crea el primer span y toma la decisión de muestreo. Cada salto posterior solo reescribe el parent-id.

¿Es obligatoria la cabecera traceparent?

No. La cabecera traceparent es opcional a nivel de protocolo, y una petición sin ella es perfectamente válida: el servicio receptor pasa a ser la raíz de una traza nueva. Obligatoria lo es solo en el sentido práctico, porque sin ella el trabajo que cruza la frontera entre dos servicios no se puede correlacionar en una sola traza.

¿traceparent agrega una sobrecarga medible?

No de forma significativa. Un traceparent son 55 bytes, y un tracestate típicamente agrega unos cientos más: nada al lado de un handshake TLS o de cualquier payload real. El costo real de las trazas está en exportar y almacenar los spans muestreados, no en llevar cabeceras de trazas distribuidas por el cable.

Etiquetas: distributed-tracing opentelemetry observability http-headers w3c

Artículos relacionados

Ver todos los artículos