Trazas Distribuidas — Siguiendo el Flujo

Por: Artiko
trazasdistributed-tracingspansjaegerzipkinopentelemetrymicroservicios

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:

¿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:

{
  "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:

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:

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:

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:

  1. Encontrar todas las trazas con error en el período del incidente
  2. Agrupar por el span que falló primero
  3. Identificar el servicio raíz del problema
  4. 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:


Herramientas del ecosistema

Open Source Backends:

Instrumentación:

SaaS:


Referencias