OpenTelemetry en Producción: Trazas, Contexto y Lo que Realmente Importa
Patrones de OpenTelemetry en producción. Propagación de contexto entre colas y workers, trazado de llamadas LLM, estrategias de sampling para cargas de IA, spans seguros para privacidad y la Baggage API.
Por Qué Ganó OpenTelemetry
Hace tres años, el panorama de observabilidad estaba fragmentado. Jaeger para trazas, Prometheus para métricas, Fluentd para logs, cada uno con su propio SDK, su propio protocolo, su propio vendor lock-in. OpenTelemetry los unificó en un solo estándar: un SDK, un protocolo (OTLP), un collector que enruta hacia cualquier backend.
Adoptamos OpenTelemetry en nuestros sistemas de producción. Este artículo cubre los patrones que realmente importan en producción, no el tutorial de setup. Para la estrategia de observabilidad más amplia (cuándo alertar, qué loguear, cómo estructurar métricas), consulta nuestra guía de observabilidad de IA. Este artículo profundiza en la implementación específica de OpenTelemetry.
Propagación de Contexto: La Parte Más Difícil
Una sola solicitud de usuario puede cruzar 5 servicios, 3 colas de mensajes, 2 procesos worker y una llamada LLM. La propagación de contexto asegura que la traza siga a la solicitud a través de todos ellos.
Propagación HTTP (Fácil)
OpenTelemetry auto-instrumenta clientes y servidores HTTP. El contexto de la traza se propaga mediante headers traceparent y tracestate. Esto funciona directamente sin configuración adicional.
// Auto-instrumentado: no se necesita código para la propagación HTTP
// El SDK agrega el header traceparent a las solicitudes salientes
// El servicio receptor lo extrae y continúa la traza
Propagación en Colas (Difícil)
Las colas de mensajes rompen la propagación automática. Cuando encolas un mensaje, el contexto de la traza debe serializarse en los headers del mensaje. Cuando un worker lo desencola, el contexto debe extraerse y la traza debe continuarse.
// Productor: inyectar contexto de traza en headers del mensaje
import { context, propagation } from '@opentelemetry/api';
async function enqueueMessage(queue: string, payload: any) {
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
await messageQueue.send(queue, {
body: payload,
headers: carrier, // Contiene traceparent, tracestate
});
}
// Consumidor: extraer contexto de traza de los headers del mensaje
async function processMessage(message: QueueMessage) {
const parentContext = propagation.extract(context.active(), message.headers);
await context.with(parentContext, async () => {
const span = tracer.startSpan('process_message', {
attributes: {
'messaging.system': 'rabbitmq',
'messaging.operation': 'process',
'messaging.destination': message.queue,
},
});
try {
await handleMessage(message.body);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
});
}
Este patrón funciona para RabbitMQ, Kafka, BullMQ, SQS y Symfony Messenger. Los headers del mensaje transportan el contexto de la traza. El consumidor lo extrae y crea spans hijos bajo la traza original.
Propagación en Procesos Worker
Para los workers BullMQ de Vendure, los workers Symfony Messenger de Pimcore y sistemas similares de jobs en background, el patrón es el mismo: serializar el contexto en el payload del job, extraerlo en el lado del worker.
// BullMQ: agregar contexto de traza a los datos del job
async function addJob(queue: Queue, data: any) {
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
await queue.add('process', {
...data,
_traceContext: carrier,
});
}
// BullMQ: extraer contexto de traza en el worker
worker.on('process', async (job) => {
const parentContext = propagation.extract(context.active(), job.data._traceContext || {});
await context.with(parentContext, async () => {
const span = tracer.startSpan(`job:${job.name}`);
try {
await processJob(job.data);
} finally {
span.end();
}
});
});
Trazado de Llamadas LLM
Las llamadas LLM son las operaciones más costosas en sistemas de IA. Trazarlas con atributos apropiados permite el seguimiento de costos, análisis de latencia y monitoreo de calidad.
async function tracedLlmCall(prompt: string, options: LlmOptions): Promise<string> {
const span = tracer.startSpan('llm.generate', {
attributes: {
'llm.provider': options.provider, // "openai", "anthropic"
'llm.model': options.model, // "gpt-4o", "claude-sonnet-4-20250514"
'llm.temperature': options.temperature,
'llm.max_tokens': options.maxTokens,
'llm.prompt_tokens': estimateTokens(prompt), // estimación antes de la llamada
},
});
try {
const response = await llmClient.generate(prompt, options);
span.setAttributes({
'llm.response_tokens': response.usage.completionTokens,
'llm.total_tokens': response.usage.totalTokens,
'llm.finish_reason': response.finishReason,
'llm.cost_usd': calculateCost(response.usage, options.model),
});
span.setStatus({ code: SpanStatusCode.OK });
return response.text;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.setAttribute('llm.error_type', error.constructor.name);
throw error;
} finally {
span.end();
}
}
No pongas texto de prompts en atributos de span. Los prompts contienen PII. Los atributos de span se envían a tu backend de observabilidad (Jaeger, Grafana Tempo, Datadog). En su lugar, loguea el hash del prompt o el conteo de tokens. Para la arquitectura completa de logging seguro con PII, consulta nuestra guía de prevención de fuga de datos en IA.
Convención de Atributos de Span para LLM
| Atributo | Tipo | Ejemplo |
|---|---|---|
llm.provider | string | "openai" |
llm.model | string | "gpt-4o" |
llm.temperature | float | 0.7 |
llm.prompt_tokens | int | 1250 |
llm.response_tokens | int | 340 |
llm.total_tokens | int | 1590 |
llm.finish_reason | string | "stop" |
llm.cost_usd | float | 0.023 |
llm.error_type | string | "RateLimitError" |
llm.cache_hit | boolean | false |
Estrategias de Sampling
En sistemas de IA de alto volumen, trazar cada solicitud es demasiado costoso. El sampling reduce el volumen mientras preserva la visibilidad sobre trazas importantes.
Sampling Head-Based
Decide al inicio de la traza si muestrearla. Simple pero con pérdidas.
// Muestrear 10% de todas las trazas
const sampler = new TraceIdRatioBased(0.1);
// Siempre muestrear errores (sobreescribir ratio para trazas con error)
const compositeSampler = new ParentBasedSampler({
root: new TraceIdRatioBased(0.1),
// Los errores siempre se muestrean vía span processor
});
Sampling Tail-Based (Recomendado para IA)
Decide después de que la traza se complete si conservarla. Mantiene todas las trazas interesantes (errores, respuestas lentas, alto costo) y descarta las rutinarias.
// OpenTelemetry Collector: configuración de tail-based sampling
processors:
tail_sampling:
decision_wait: 10s
policies:
# Mantener todos los errores
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
# Mantener trazas lentas (> 5 segundos)
- name: slow
type: latency
latency: { threshold_ms: 5000 }
# Mantener llamadas LLM costosas (> $0.10)
- name: expensive_llm
type: string_attribute
string_attribute:
key: llm.cost_usd
values: [] # Custom: filtrar en el pipeline
enabled_regex_matching: true
# Muestrear 5% de todo lo demás
- name: baseline
type: probabilistic
probabilistic: { sampling_percentage: 5 }
El tail-based sampling requiere el OpenTelemetry Collector. El collector almacena trazas completas en buffer, evalúa políticas y reenvía solo las trazas muestreadas al backend. Esto agrega latencia (el período decision_wait) pero reduce drásticamente los costos de almacenamiento mientras conserva todos los datos interesantes.
Spans Seguros para Privacidad
Los atributos de span, los nombres de span y los eventos de span se envían a tu backend de observabilidad. Si alguno de estos contiene PII, tu infraestructura de trazado se convierte en un riesgo de protección de datos.
// MAL: PII en atributos de span
span.setAttribute('user.email', 'sara.mustermann@beispiel.de');
span.setAttribute('user.name', 'Sara Mustermann');
span.setAttribute('request.body', JSON.stringify(requestBody)); // Contiene PII
// BIEN: solo IDs opacos y datos agregados
span.setAttribute('user.id', 'usr_abc123'); // ID opaco, no PII
span.setAttribute('entities.detected', 3);
span.setAttribute('entities.types', ['person', 'email', 'phone']);
span.setAttribute('policy.applied', 'german-support');
Reglas para trazado seguro de privacidad:
- IDs de usuario: solo identificadores opacos (no emails, no nombres)
- Cuerpos de solicitud: nunca incluir contenido raw. Loguear conteos de entidades y tipos.
- Prompts de LLM: nunca incluir. Loguear conteos de tokens y hash del prompt.
- Mensajes de error: sanitizar antes de adjuntar a spans. Eliminar cualquier dato de usuario.
Para la arquitectura completa de cumplimiento de privacidad en sistemas de IA, revisa nuestra guía de cumplimiento GDPR para IA.
La Baggage API
OpenTelemetry Baggage transporta pares clave-valor a través de límites de servicio. A diferencia de los atributos de span (que permanecen en el span), el baggage se propaga a todos los servicios downstream automáticamente.
import { propagation, context, baggage } from '@opentelemetry/api';
// Establecer baggage en el API gateway
const bag = propagation.createBaggage({
'tenant.id': { value: 'tenant_acme' },
'request.priority': { value: 'high' },
'feature.flags': { value: 'new-checkout,beta-search' },
});
const ctx = propagation.setBaggage(context.active(), bag);
// Los servicios downstream pueden leer el baggage
const tenantId = propagation.getBaggage(context.active())?.getEntry('tenant.id')?.value;
Útil para:
- Propagación de tenant ID (cada servicio downstream sabe qué tenant es)
- Feature flags (propagar asignaciones de experimentos entre servicios)
- Enrutamiento por prioridad (solicitudes de alta prioridad reciben tratamiento diferente en colas)
- Marcadores de debug (marcar solicitudes específicas para logging verboso)
El baggage viaja con el contexto de la traza en los headers HTTP y metadata de mensajes. Cada servicio que extrae el contexto de la traza también obtiene el baggage.
Arquitectura del Collector
El OpenTelemetry Collector es la capa central de enrutamiento entre tus aplicaciones y tus backends de observabilidad.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service A │ │ Service B │ │ Worker C │
│ (OTLP gRPC) │ │ (OTLP HTTP) │ │ (OTLP gRPC) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ OTel Collector │
│ │
│ Receivers: OTLP (gRPC + HTTP) │
│ Processors: batch, tail_sampling, attributes │
│ Exporters: Tempo, Prometheus, Loki │
└─────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Grafana │ │ Prometheus │ │ Grafana │
│ Tempo │ │ │ │ Loki │
│ (trazas) │ │ (métricas) │ │ (logs) │
└──────────────┘ └──────────────┘ └──────────────┘
El collector maneja batching (reduce llamadas de red), sampling (reduce almacenamiento), procesamiento de atributos (agrega/elimina atributos) y enrutamiento (diferentes señales a diferentes backends). Despliégalo como sidecar o como servicio central dependiendo de tu infraestructura.
Para patrones de despliegue cloud incluyendo infraestructura de observabilidad, esa página cubre nuestro enfoque.
Errores Comunes
-
Sin propagación de contexto entre colas. La propagación HTTP es automática. La propagación en colas no lo es. Si no inyectas/extraes contexto en los headers del mensaje, las trazas se rompen en cada límite de cola.
-
PII en atributos de span. Tu backend de trazado indexa todo. Si los spans contienen emails, nombres o cuerpos de solicitud, tu cluster de Grafana Tempo es un almacén de PII.
-
Trazar cada solicitud en producción. A 1000 RPS, el trazado completo genera terabytes de datos. Usa tail-based sampling para conservar errores, trazas lentas y operaciones costosas.
-
Sin atributos específicos de LLM. Sin conteos de tokens, costo, model ID y finish reason en los spans de LLM, no puedes rastrear costos de IA ni diagnosticar problemas de calidad.
-
Sampling head-based descartando errores. Si muestreas el 10% de las trazas y un error ocurre en el 90% que descartas, nunca lo ves. Usa tail-based sampling o políticas de siempre-muestrear-errores.
-
Baggage para payloads grandes. El baggage viaja con cada solicitud. Valores grandes aumentan el tamaño de los headers en cada llamada HTTP. Mantén los valores de baggage pequeños (IDs, flags, prioridades).
Conclusiones Clave
-
La propagación de contexto entre colas es la parte más difícil. HTTP es automático. Las colas requieren inject/extract manual del contexto de traza en los headers del mensaje. Aquí es donde la mayoría de las implementaciones de trazado distribuido se rompen.
-
Traza las llamadas LLM con atributos de costo y tokens. Modelo, proveedor, conteos de tokens, costo, finish reason. Estos atributos habilitan dashboards de costos de IA y monitoreo de calidad.
-
Tail-based sampling para cargas de trabajo de IA. Conserva todos los errores, trazas lentas y operaciones costosas. Descarta trazas rutinarias. Reduce el almacenamiento en más del 90% mientras conserva todos los datos interesantes.
-
Sin PII en spans. IDs de usuario opacos, conteos de entidades, tipos de tokens. Nunca contenido raw, emails, nombres ni cuerpos de solicitud.
-
Baggage propaga el contexto de tenant. Establece el tenant ID, feature flags y prioridad en el edge. Cada servicio downstream lo lee del baggage sin necesidad de pasar parámetros explícitamente.
Implementamos OpenTelemetry en nuestros servicios de IA, software a medida e infraestructura cloud. Si estás construyendo observabilidad para un sistema distribuido, habla con nuestro equipo o solicita un presupuesto.
Temas cubiertos
Guías relacionadas
Guía Empresarial de Sistemas de IA Agéntica
Guia tecnica de sistemas de IA agentica en entornos empresariales. Descubre la arquitectura, capacidades y aplicaciones de agentes IA autonomos.
Leer guíaComercio Agéntico: Cómo Dejar que los Agentes IA Compren de Forma Segura
Cómo diseñar comercio iniciado por agentes IA con gobernanza. Motores de políticas, puertas de aprobación HITL, recibos HMAC, idempotencia, aislamiento de tenants y el Agentic Checkout Protocol completo.
Leer guíaLos 9 Puntos Donde Tu Sistema de IA Filtra Datos (y Cómo Sellar Cada Uno)
Un mapa sistemático de cada lugar donde se filtran datos en sistemas de IA. Prompts, embeddings, logs, llamadas a herramientas, memoria de agentes, mensajes de error, caché, datos de fine-tuning y handoffs entre agentes.
Leer guía¿Listo para construir sistemas de IA listos para producción?
Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.
Iniciar una conversación