Trazas Distribuidas — Siguiendo el Flujo
Trazas Distribuidas — Siguiendo el Flujo
El problema que las trazas resuelven
Imagina que eres el on-call de una empresa de e-commerce. Un usuario reporta que su checkout tardó 12 segundos. Tienes:
- 8 microservicios involucrados en el checkout
- Cada uno emite métricas de latencia propias
- Los logs existen pero no están correlacionados entre servicios
¿Cómo encuentras cuál de los 8 servicios causó el problema? ¿Fue una sola llamada lenta o varias lentas en secuencia? ¿Fue una dependencia externa?
Sin distributed tracing, esto puede tomar horas. Con trazas bien instrumentadas, toma minutos.
Las trazas distribuidas son el mapa del viaje de una request a través de todos los servicios de tu sistema. Son el tercer pilar de la observabilidad y el más poderoso para entender performance y dependencias en arquitecturas de microservicios.
Conceptos fundamentales
Trace (Traza)
Una traza es el registro completo del viaje de una request a través del sistema, desde que entra hasta que sale. Tiene un identificador único llamado trace_id.
trace_id: a1b2c3d4e5f6g7h8i9j0
[API Gateway] → [Order Service] → [Inventory Service]
→ [Payment Service] → [Stripe API]
→ [Notification Service]
Span
Un span es la unidad básica de una traza. Representa una operación específica en un servicio específico, con:
- Un
span_idúnico - El
trace_idde la traza padre - El
parent_span_id(el span que lo originó) - Timestamp de inicio
- Duración
- Nombre de la operación
- Atributos (metadata)
- Eventos (puntos de interés dentro del span)
- Estado (OK, ERROR)
{
"trace_id": "a1b2c3d4e5f6g7h8",
"span_id": "f1e2d3c4",
"parent_span_id": "b5a6c7d8",
"name": "POST /api/payment/process",
"service": "payment-service",
"start_time": "2026-04-04T14:23:45.100Z",
"duration_ms": 2340,
"status": "ERROR",
"attributes": {
"http.method": "POST",
"http.url": "/api/payment/process",
"payment.gateway": "stripe",
"payment.amount": 14999,
"user.id": "usr_12345"
},
"events": [
{
"name": "retry.attempt",
"timestamp": "2026-04-04T14:23:46.200Z",
"attributes": {"attempt": 2}
}
]
}
La relación entre Traces y Spans
graph TD
subgraph "Trace: a1b2c3d4e5f6g7h8"
ROOT["Span: api-gateway\nPOST /checkout\n0ms → 2400ms"] --> S1["Span: order-service\ncreateOrder\n50ms → 150ms"]
ROOT --> S2["Span: payment-service\nprocessPayment\n200ms → 2380ms"]
ROOT --> S3["Span: notification\nsendEmail\n2350ms → 2400ms"]
S1 --> S1A["Span: postgres\nINSERT orders\n80ms → 140ms"]
S2 --> S2A["Span: stripe-client\ncharge\n210ms → 2370ms"]
S2A --> S2A1["Span: http-client\nPOST api.stripe.com\n215ms → 2365ms"]
end
La visualización Waterfall
La visualización más común de una traza es el diagrama de cascada (waterfall), que muestra los spans en orden temporal y anidados según su jerarquía:
gantt
title Waterfall de una Traza de Checkout
dateFormat x
axisFormat %Lms
section API Gateway
POST /checkout :0, 2400
section Order Service
createOrder :50, 100
section Order DB
INSERT orders :80, 60
section Payment Service
processPayment :200, 2180
section Stripe Client
charge() :210, 2155
section HTTP Client
POST api.stripe.com :215, 2150
section Notification
sendEmail :2350, 50
De un vistazo, ves que:
- El total fue 2400ms
- El tiempo en el API Gateway fue mínimo (overhead de routing)
- Order Service fue rápido (150ms total, 60ms en DB)
- Payment Service consumió 2180ms — aquí está el problema
- La llamada a Stripe tardó 2150ms — causa raíz: Stripe API lenta
- Notification service fue secuencial (esperó a payment) — podría ser asíncrono
Sin la traza, habrías necesitado revisar logs de cada servicio manualmente.
Context Propagation: el mecanismo clave
Para que las trazas funcionen entre servicios, cada servicio debe pasar el contexto de la traza a todos los servicios que llame. Este mecanismo se llama context propagation.
El estándar moderno es W3C TraceContext, que define headers HTTP:
POST /api/payment/process HTTP/1.1
Host: payment-service.internal
traceparent: 00-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6-f1e2d3c4b5a69788-01
tracestate: vendor1=value1
El header traceparent contiene:
00— versión del formatoa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6— trace_id (16 bytes, hex)f1e2d3c4b5a69788— parent_span_id (8 bytes, hex)01— trace flags (01 = sampled)
sequenceDiagram
participant C as Cliente
participant A as API Gateway
participant O as Order Service
participant P as Payment Service
C->>A: POST /checkout (sin headers de traza)
Note over A: Genera trace_id: abc123<br/>Crea root span
A->>O: POST /orders<br/>traceparent: 00-abc123-span1-01
Note over O: Extrae trace_id: abc123<br/>Crea child span con parent=span1
O->>P: POST /payments<br/>traceparent: 00-abc123-span2-01
Note over P: Extrae trace_id: abc123<br/>Crea child span con parent=span2
P-->>O: 200 OK
O-->>A: 200 OK
A-->>C: 200 OK
Note over A,P: Todos los spans enviados\nal backend de trazas
Sampling: no puedes guardar todo
En un sistema de producción con miles de requests por segundo, guardar cada traza completa es prohibitivamente caro. Una sola traza puede tener 20-50 spans, cada uno con muchos atributos. El volumen es enorme.
La solución es el sampling: solo guardar una muestra representativa de las trazas.
Head-based sampling (muestreo al inicio)
La decisión de samplear se toma al inicio de la traza, en el primer servicio. Todos los spans de esa traza siguen la misma decisión.
flowchart LR
REQ[Request entrante] --> DICE{Samplear?\n10% probabilidad}
DICE -->|Sí 10%| SAMPLE[Traza completa\nguardada]
DICE -->|No 90%| DROP[Traza descartada\nsolo se propaga contexto]
Ventaja: Simple, bajo overhead.
Desventaja: El 90% de las trazas interesantes (errores, lentas) también se descartan.
Tail-based sampling (muestreo al final)
La decisión se toma después de que la traza completa llega al collector. Puedes samplear inteligentemente:
- Guardar el 100% de trazas con errores
- Guardar el 100% de trazas con latencia > p99
- Guardar solo el 1% de las trazas “normales”
flowchart LR
ALL[Todas las trazas\nalmacenadas temporalmente] --> COLLECTOR[Trace Collector]
COLLECTOR --> Q1{¿Tiene ERROR?}
Q1 -->|Sí| KEEP1[Guardar 100%]
Q1 -->|No| Q2{¿Latencia > p99?}
Q2 -->|Sí| KEEP2[Guardar 100%]
Q2 -->|No| Q3{Random sample\n1%}
Q3 -->|Sí| KEEP3[Guardar]
Q3 -->|No| DROP[Descartar]
Ventaja: No pierdes las trazas importantes.
Desventaja: Requiere buffer temporal de todas las trazas, más complejo.
OpenTelemetry Collector soporta tail-based sampling. Herramientas como Grafana Tempo y Jaeger lo implementan.
Atributos de span: el contexto que importa
Los atributos son metadata que enriquece los spans y los hace consultables. OpenTelemetry define convenciones semánticas para los atributos más comunes:
HTTP (OpenTelemetry Semantic Conventions)
{
"http.method": "POST",
"http.url": "https://api.example.com/checkout",
"http.status_code": 200,
"http.request_content_length": 1234,
"http.response_content_length": 567,
"net.peer.name": "api.example.com",
"net.peer.port": 443
}
Base de datos
{
"db.system": "postgresql",
"db.name": "orders_db",
"db.operation": "SELECT",
"db.statement": "SELECT * FROM orders WHERE user_id = $1",
"db.sql.table": "orders"
}
Contexto de negocio (custom)
{
"user.id": "usr_12345",
"user.tier": "premium",
"order.id": "ord_98765",
"order.total_cents": 14999,
"checkout.step": "payment_validation"
}
Los atributos de negocio son lo que permite queries como “muéstrame todas las trazas lentas de usuarios premium en los últimos 24 horas” — esto es observabilidad de alta cardinalidad.
Instrumentación: automática vs. manual
Instrumentación automática
Los frameworks modernos y las librerías de OpenTelemetry ofrecen instrumentación automática: con una sola línea de código (o cero, vía agentes), instrumentas todas las llamadas HTTP, consultas de base de datos, mensajes de colas, etc.
// Node.js — instrumentación automática con OpenTelemetry
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
traceExporter: ...,
instrumentations: [getNodeAutoInstrumentations()] // Una línea, todo instrumentado
});
sdk.start();
// Ahora Express, Axios, PG, Redis, etc. emiten spans automáticamente
Instrumentación manual
Para lógica de negocio específica, necesitas instrumentación manual:
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('payment-service');
async function processPayment(orderId: string, amount: number) {
// Crear span manualmente
return await tracer.startActiveSpan('processPayment', async (span) => {
span.setAttribute('order.id', orderId);
span.setAttribute('payment.amount_cents', amount);
try {
const result = await chargeStripe(amount);
span.setAttribute('payment.transaction_id', result.transactionId);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
Casos de uso avanzados
Análisis de dependencias
Las trazas permiten generar automáticamente mapas de dependencias entre servicios. Si tienes millones de trazas, puedes visualizar exactamente qué servicio llama a qué otro y con qué frecuencia:
graph LR
API[API Gateway] -->|12k req/min| ORDER[Order Service]
API -->|5k req/min| USER[User Service]
ORDER -->|12k req/min| PAYMENT[Payment Service]
ORDER -->|12k req/min| INVENTORY[Inventory Service]
PAYMENT -->|12k req/min| STRIPE[Stripe API]
ORDER -->|12k req/min| NOTIFICATION[Notification Service]
NOTIFICATION -->|12k req/min| SENDGRID[SendGrid API]
style STRIPE fill:#ffcccc
style SENDGRID fill:#ffcccc
Análisis de causa raíz
Con un sistema de trazas maduro, cuando hay un incidente puedes:
- Encontrar todas las trazas con error en el período del incidente
- Agrupar por el span que falló primero
- Identificar el servicio raíz del problema
- Ver el contexto completo (usuario, acción, datos) del fallo
Performance profiling en producción
Las trazas en producción son el equivalente a un profiler, pero sin el overhead de un profiler y con contexto de negocio. Puedes identificar:
- Qué operaciones específicas son las más lentas para usuarios de cierto segmento
- Si hay diferencia de performance entre versiones del servicio
- Qué queries de base de datos consumen la mayor parte del tiempo
Herramientas del ecosistema
Open Source Backends:
- Jaeger — creado por Uber, estándar en Kubernetes
- Zipkin — creado por Twitter, más simple
- Grafana Tempo — integrado con Loki y Prometheus, eficiente en almacenamiento
- SigNoz — stack completo open source
Instrumentación:
- OpenTelemetry — el estándar moderno, agnóstico de vendor
SaaS:
- Honeycomb (pionero en high-cardinality), Datadog APM, New Relic, Lightstep, Dynatrace