Correlación de Señales — Uniendo los Tres Pilares

Por: Artiko
correlacionlogsmetricastrazasobservabilidadincidentestrace-id

Correlación de Señales — Uniendo los Tres Pilares

Por qué los tres pilares solos no son suficientes

Tienes logs excelentes, métricas bien diseñadas, trazas detalladas. Pero si no puedes navegar entre ellos durante un incidente, tienes tres sistemas de información inconexos en lugar de una plataforma de observabilidad.

La correlación de señales es la capacidad de pasar fluidamente de una métrica que detectó un problema, a la traza que muestra dónde ocurrió, a los logs que revelan exactamente qué pasó. Es la diferencia entre observabilidad real y una colección de herramientas costosas.


El trace_id: el hilo conductor universal

El mecanismo fundamental de correlación es el trace_id. Este identificador único debe estar presente en:

graph LR
    TID[trace_id\na1b2c3d4] --> TRACE[Traza\nSpans del viaje completo]
    TID --> LOG[Logs\nCada línea tiene trace_id]
    TID --> EVENT[Métricas de ejemplo\nExemplars en Prometheus]

    TRACE --> DASHBOARD[Dashboard\ncon link a logs]
    LOG --> TRACE_LINK[Log con link\na la traza]

Cuando tu sistema emite los tres tipos de señales con el mismo trace_id, puedes:

  1. Ver una alerta en tus métricas
  2. Hacer clic en un datapoint específico y ver las trazas de ese momento
  3. Dentro de la traza, hacer clic en un span con error y ver los logs de ese span específico

Esto es navegación fluida entre señales. Esto es observabilidad.


Flujo típico de correlación durante un incidente

Escenario: error rate aumentó de 0.1% a 8%

flowchart TD
    A1[Alerta: error_rate > 5%\nen payment-service] --> A2[Abrir dashboard\nde métricas]

    A2 --> B1[Métrica muestra: errores\nempezaron hace 12 minutos]
    B1 --> B2{¿Qué tipo de errores?}
    B2 --> B3[Breakdeown: 94% son\nTimeoutException]
    
    B3 --> C1[Ir a Traces:\nbuscar trazas con error\nen últimos 12 min]
    C1 --> C2[Filtrar por:\nerror=true, service=payment]
    C2 --> C3[Ver 3 trazas representativas]
    
    C3 --> D1[Span problemático:\nstripe-client → POST api.stripe.com\nlatencia: 8000ms, status: ERROR]
    D1 --> D2{¿Es siempre Stripe\no solo algunos pagos?}
    D2 --> D3[100% de las trazas con error\ntienen el mismo span lento]
    
    D3 --> E1[Ir a Logs de stripe-client\ncon trace_id de una traza afectada]
    E1 --> E2[Log revela:\nStripe API devuelve 529 Too Many Requests]
    E2 --> E3[Causa raíz: rate limiting\nde Stripe. Nuestra carga\naumentó 3x en deploy de hace 15 min]
    
    E3 --> FIX[Acción: rollback del deploy +\nalertar a Stripe + implementar\nexponential backoff]

Este flujo, con buena correlación, toma 5-10 minutos. Sin ella, podría tomar horas.


Implementando la correlación: el baggage

Además del trace_id para correlacionar telemetría técnica, OpenTelemetry tiene el concepto de baggage (equipaje): información de contexto que se propaga a través de todos los servicios y puede enriquecer todos los spans y logs.

import { propagation, context } from '@opentelemetry/api';

// En el API Gateway, al recibir la request autenticada:
const baggage = propagation.createBaggage({
  'user.id': { value: user.id },
  'user.tier': { value: user.tier },
  'request.feature_flags': { value: JSON.stringify(featureFlags) }
});

// Propagar a todos los servicios downstream
const ctx = propagation.setBaggage(context.active(), baggage);

Ahora en cualquier servicio downstream, puedes:

const baggage = propagation.getBaggage(context.active());
const userId = baggage?.getEntry('user.id')?.value;

// Agregar a todos los spans automáticamente
span.setAttribute('user.id', userId);
// O incluir en todos los logs
logger.info('Processing payment', { userId, ...otherContext });

Exemplars: conectando métricas a trazas

Un exemplar es un dato adjunto a una observación de métrica que apunta a una traza específica. Es el puente directo entre métricas e histogramas de trazas.

# Prometheus con exemplars
http_request_duration_seconds_bucket{le="0.1"} 9876 # {trace_id="a1b2c3d4"} 0.0823 1680617020.123

En Grafana, cuando ves un histograma con exemplars habilitados, puedes hacer clic en un datapoint y saltar directamente a la traza correspondiente.

sequenceDiagram
    participant G as Grafana Dashboard
    participant P as Prometheus
    participant T as Jaeger/Tempo

    G->>P: query: http_request_duration_seconds_bucket
    P-->>G: Histograma + exemplars con trace_ids
    Note over G: Usuario ve un pico en el p99
    G->>G: Usuario hace clic en el pico
    G->>T: GET trace/a1b2c3d4
    T-->>G: Traza completa del pico
    Note over G: Ingeniero ve exactamente\nqué request fue lenta

Para habilitar exemplars en Prometheus + OpenTelemetry:

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'myapp'
    static_configs:
      - targets: ['app:8080']
    # Habilitar exemplars
    exemplars:
      enabled: true

Correlación de logs y trazas en Grafana

La suite de Grafana (Loki para logs, Tempo para trazas, Prometheus para métricas) es el stack open source más completo para correlación de señales:

graph LR
    P[Prometheus\nMétricas] --> G[Grafana UI]
    L[Loki\nLogs] --> G
    T[Tempo\nTrazas] --> G

    G --> |Exemplars\ntrace_id en métricas| T
    G --> |LogQL query\ncon trace_id| L
    G --> |TraceQL\nbuscar spans| T
    L --> |trace_id link\nen log line| T
    T --> |span logs link| L

En Grafana, la correlación se configura así:

  1. Loki → Tempo: En Loki, definir “Derived fields” que detectan trace_id en los logs y crean un link automático a Tempo.

  2. Tempo → Loki: En Tempo, configurar el datasource de Loki para ver los logs asociados a un trace.

  3. Prometheus → Tempo: Habilitar exemplars en el datasource de Prometheus.


La correlación en la práctica: un sistema real

Veamos cómo se ve la correlación completa en un sistema bien instrumentado:

Lo que el ingeniero ve en cada herramienta

En Grafana (Métricas):

Gráfica de error rate en payment-service
Hay un pico a las 14:23 - 14:35

[Clic en el pico] → Link a traza del pico: trace_id a1b2c3d4

En Tempo (Traza):

Trace: a1b2c3d4
├── [ROOT] api-gateway POST /checkout - 8234ms ❌
│   ├── order-service createOrder - 45ms ✅
│   └── payment-service processPayment - 8180ms ❌
│       └── stripe-client charge - 8170ms ❌
│           └── http POST api.stripe.com - 8165ms ❌ (HTTP 529)

[Clic en "Ver logs de este span"] → Link a Loki con trace_id + span_id + time range

En Loki (Logs):

// Logs del span de stripe-client
{"trace_id":"a1b2c3d4","span_id":"c3d4e5f6","service":"payment-service",
 "level":"error","event":"stripe_request_failed","http_status":529,
 "message":"Stripe API returned 429/529: too many requests",
 "retry_after_seconds":30,"request_id":"stripe_req_789"}

{"trace_id":"a1b2c3d4","span_id":"c3d4e5f6","service":"payment-service",
 "level":"warn","event":"rate_limit_detected","message":"Stripe rate limiting active",
 "orders_affected":847,"backoff_ms":30000}

En 5 minutos, sin SSH, sin grep, sin reuniones: causa raíz identificada.


Construyendo el pipeline de correlación

Para implementar correlación real en tu sistema:

1. Propagación de trace_id en todos los logs

// middleware de logging que inyecta trace_id en cada log
import { trace } from '@opentelemetry/api';

export function createContextualLogger(baseLogger: Logger) {
  return {
    info: (message: string, data?: object) => {
      const span = trace.getActiveSpan();
      const traceContext = span ? {
        trace_id: span.spanContext().traceId,
        span_id: span.spanContext().spanId,
      } : {};
      baseLogger.info(message, { ...traceContext, ...data });
    },
    // ...error, warn, debug
  };
}

2. Enrichment de spans con contexto de negocio

// En el middleware de autenticación
app.use((req, res, next) => {
  const span = trace.getActiveSpan();
  if (span && req.user) {
    span.setAttribute('user.id', req.user.id);
    span.setAttribute('user.role', req.user.role);
    span.setAttribute('user.tenant_id', req.user.tenantId);
  }
  next();
});

A veces, procesos asíncronos crean nuevas trazas pero quieres relacionarlas con la traza original:

// Cuando encolas un job de procesamiento asíncrono
const parentSpanContext = trace.getActiveSpan()?.spanContext();

await queue.send({
  jobType: 'process-payment',
  payload: { orderId },
  // Incluir el contexto de la traza original en el mensaje
  traceContext: {
    traceId: parentSpanContext?.traceId,
    spanId: parentSpanContext?.spanId,
  }
});

// En el worker que procesa el job
const tracer = trace.getTracer('payment-worker');
const span = tracer.startSpan('processPaymentJob', {
  links: [{
    context: {
      traceId: job.traceContext.traceId,
      spanId: job.traceContext.spanId,
      traceFlags: TraceFlags.SAMPLED,
    }
  }]
});

Métricas derivadas de trazas

En sistemas modernos, es posible generar métricas automáticamente desde las trazas. OpenTelemetry Collector puede hacer esto con el procesador spanmetrics:

# otel-collector-config.yaml
processors:
  spanmetrics:
    metrics_exporter: prometheus
    latency_histogram_buckets: [5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s]
    dimensions:
      - name: http.method
      - name: http.status_code
      - name: service.name
      - name: user.tier  # custom attribute

Esto genera métricas como:

calls_total{service="payment-service", http.method="POST", http.status_code="200"} 12345
latency_bucket{service="payment-service", le="0.1"} 11000

Sin instrumentar las métricas por separado. Las trazas son la fuente de verdad y las métricas se derivan automáticamente.


Observabilidad sin correlación vs. con correlación

graph TD
    subgraph "Sin correlación - MTTD alto"
        I1[Incidente detectado] --> M1[Ver métricas: algo está mal]
        M1 --> T1[Buscar en logs manualmente\ntardas 20 min]
        T1 --> R1[Buscar trazas\naisladas por servicio]
        R1 --> C1[Causa raíz después\nde 1-2 horas]
    end

    subgraph "Con correlación - MTTD bajo"
        I2[Incidente detectado] --> M2[Ver métrica + clic en exemplar]
        M2 --> T2[Traza completa\nen 10 segundos]
        T2 --> R2[Clic en span con error\nver logs del span]
        R2 --> C2[Causa raíz en\n5-10 minutos]
    end

Referencias