Alertas, Cardinalidad y Ruido

Por: Artiko
alertascardinalidadalert-fatiguepagerdutyon-callsreprometheus

Alertas, Cardinalidad y Ruido

El problema de la alerta fatiga

Alert fatigue (fatiga de alertas) es uno de los problemas más graves y más ignorados en operaciones de software. Ocurre cuando el volumen y calidad de las alertas son tan malos que el equipo comienza a ignorarlas.

Las consecuencias son severas:

Un estudio de PagerDuty (2023) encontró que más del 30% de las páginas de on-call son falsos positivos, y que los equipos con alta tasa de alertas ruidosas tienen el doble de burnout que equipos con alertas bien calibradas.


Anatomía de una alerta efectiva

Una alerta efectiva tiene cinco características:

mindmap
  root((Alerta Efectiva))
    Accionable
      Hay algo que hacer cuando se dispara
      El runbook es claro
    Urgente correctamente
      Corresponde al impacto real
      No todo es P1
    Contextual
      Dice qué pasó y dónde
      Link al dashboard relevante
    Libre de falsos positivos
      No se dispara cuando no hay problema
      No falla por ruido estadístico
    Libre de falsos negativos
      Se dispara cuando hay un problema real
      No se silencia automáticamente

Alertas basadas en síntomas vs. causas

La regla más importante en el diseño de alertas: alerta en síntomas, no en causas.

Un síntoma es algo que el usuario experimenta. Una causa es un estado interno del sistema.

graph LR
    subgraph "❌ Alertas en Causas (Ruido)"
        C1[CPU > 90%]
        C2[Memory > 85%]
        C3[DB connections > 80%]
        C4[Disk > 70%]
        C5[Pod restarts > 5]
    end

    subgraph "✅ Alertas en Síntomas (Impacto)"
        S1[Error rate > 1% en 5min]
        S2[p99 latencia > 2s por 5min]
        S3[Checkout success rate < 99%]
        S4[SLO error budget > 10% burn en 1h]
    end

    C1 -->|puede causar| S1
    C2 -->|puede causar| S2
    C3 -->|puede causar| S1

El problema con las alertas en causas:

La ventaja de alertar en síntomas:


Diseño de alertas por severidad

No todas las alertas son iguales. Define niveles de severidad claros con procedimientos distintos:

Severidad crítica (P1) — Pager de noche

Criterio: Hay impacto significativo en usuarios ahora mismo. Alguien debe despertar a las 3AM.

Ejemplos:

Respuesta: Responder en menos de 5 minutos, resolver o escalar.

Severidad alta (P2) — Responder pronto

Criterio: Impacto parcial en usuarios, degradación significativa.

Ejemplos:

Respuesta: Responder en menos de 30 minutos, generalmente durante horario laboral.

Severidad media (P3) — Ticket

Criterio: Anomalía que no afecta usuarios actualmente pero podría escalar.

Ejemplos:

Respuesta: Ticket, resolver en el próximo sprint.

flowchart LR
    P1[P1 Critical] --> |PagerDuty\nWake everyone| I1[Respuesta en 5 min\n24/7]
    P2[P2 High] --> |Slack/SMS\nBusiness hours OK| I2[Respuesta en 30 min\nDurante horario laboral]
    P3[P3 Medium] --> |Ticket creado\nauto| I3[Resolver en próximo\nsprint]
    P4[P4 Low] --> |Log en sistema| I4[Backlog\ncuando sea posible]

Configurando alertas con Prometheus y Alertmanager

Anatomía de una alerta en Prometheus

groups:
  - name: checkout.rules
    rules:
      - alert: CheckoutHighErrorRate
        # La expresión que define cuándo se dispara
        expr: |
          (
            rate(http_requests_total{service="checkout", status=~"5.."}[5m]) /
            rate(http_requests_total{service="checkout"}[5m])
          ) > 0.05
        # Cuánto tiempo debe cumplirse antes de notificar (evita flapping)
        for: 5m
        # Metadata para routing y enriquecimiento
        labels:
          severity: critical
          team: checkout
          slo: checkout_availability
        # Información contextual para el on-call
        annotations:
          summary: "Checkout error rate above 5%"
          description: |
            Service {{ $labels.service }} has error rate {{ $value | humanizePercentage }}
            over the last 5 minutes. SLO target is 99.9% availability.
          dashboard: "https://grafana.example.com/d/checkout-service"
          runbook: "https://wiki.example.com/runbooks/checkout-errors"

Alertmanager: routing y deduplication

# alertmanager.yml
global:
  smtp_smarthost: 'smtp.example.com:587'

route:
  group_by: ['alertname', 'service']  # Agrupa alertas similares
  group_wait: 30s      # Espera antes de enviar el primer grupo
  group_interval: 5m   # Mínimo entre notificaciones del mismo grupo
  repeat_interval: 4h  # Re-notificar si no se resuelve en 4h
  receiver: 'slack-default'

  routes:
    - match:
        severity: critical
      receiver: 'pagerduty'
      continue: true  # Seguir evaluando otras rutas

    - match:
        team: checkout
      receiver: 'slack-checkout-team'

receivers:
  - name: 'pagerduty'
    pagerduty_configs:
      - service_key: 'tu-service-key'

  - name: 'slack-checkout-team'
    slack_configs:
      - channel: '#checkout-alerts'
        title: '{{ .GroupLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'

Cardinalidad: el problema que escala silenciosamente

Ya introdujimos la cardinalidad en el capítulo de métricas. Aquí exploramos cómo destruye sistemas en producción y cómo prevenirlo.

¿Qué es una explosión de cardinalidad?

Una explosión de cardinalidad ocurre cuando agregas etiquetas de alto cardinality a tus métricas, creando tantas series de tiempo únicas que el sistema de métricas se satura.

graph TD
    M1["http_requests_total\n{service, method, status}\n3 × 6 × 10 = 180 series"] --> OK1[✅ Manejable]
    
    M2["http_requests_total\n{service, method, status, endpoint}\n3 × 6 × 10 × 500 = 90,000 series"] --> OK2[⚠️ Vigilar]
    
    M3["http_requests_total\n{service, method, status, user_id}\n3 × 6 × 10 × 1,000,000 = 180M series"] --> BOOM[💥 Destroys Prometheus\nOOM, crash, dataloss]

Señales de que tienes un problema de cardinalidad

Detección con Prometheus

# Ver las métricas con más series de tiempo
topk(10, count by (__name__)({__name__=~".+"}))

# Ver las etiquetas con más valores únicos para una métrica
count(count by (user_id)(http_requests_total))

Solución para datos de alta cardinalidad

Si necesitas analizar por user_id u otros campos de alta cardinalidad, usa logs o trazas — no métricas.

flowchart LR
    D[Dato a analizar] --> Q{¿Cardinalidad?}
    Q --> |Baja\n<1000 valores| M[Métricas\nPrometheus/InfluxDB]
    Q --> |Alta\n>10000 valores| L[Logs + Trazas\ncon indexado por ese campo]

Anti-patrones de alertas más comunes

1. Alertas que siempre se disparan

Si una alerta se dispara más de una vez al día en promedio, está mal calibrada. Revisa el umbral o el for (duración antes de notificar).

2. Alertas sin runbook

Una alerta sin runbook obliga al on-call a investigar from scratch cada vez. El runbook debe existir antes de que la alerta llegue a producción.

Un runbook mínimo incluye:

3. Alertas por umbrales estáticos en sistemas dinámicos

Si tu tráfico varía entre 100 req/s en la madrugada y 10,000 req/s en horas pico, una alerta de “latencia > 500ms” puede dispararse falsamente durante el pico normal.

Solución: alertas basadas en tasas y comparaciones históricas:

# ❌ Umbral estático (se dispara en picos normales)
http_request_duration_seconds{quantile="0.99"} > 0.5

# ✅ Comparación con baseline histórico (mismo momento del día/semana)
(
  http_request_duration_seconds{quantile="0.99"}
) > (
  avg_over_time(http_request_duration_seconds{quantile="0.99"}[1w]) * 3
)

4. Alertas que no distinguen entre transientes y permanentes

# ❌ Se dispara con un spike de 1 minuto
expr: error_rate > 0.05
for: 0m  # Inmediato

# ✅ Solo se dispara si persiste (más accionable)
expr: error_rate > 0.05
for: 5m  # Debe persistir 5 minutos

5. Demasiadas alertas en la misma ruta de pager

Si 20 servicios distintos tienen alertas que llegan al mismo canal de Slack o al mismo PagerDuty schedule, el on-call está constantemente interrumpido.

La solución es routing claro: cada equipo tiene su canal, sus schedules, su escalation path.


Mantenimiento del sistema de alertas

Las alertas deben mantenerse activamente. Un conjunto de alertas creado hace 2 años y nunca revisado es casi seguro fuente de ruido.

Revisión periódica: las preguntas a hacer

Por cada alerta, trimestralmente:

flowchart TD
    Q1{¿La alerta se disparó\nalguna vez en 90 días?} --> |No| DELETE[Eliminar o convertir\nen alerta informativa]
    Q1 --> |Sí| Q2{¿Fue accionable\ncada vez que se disparó?}
    Q2 --> |No, muchos falsos positivos| TUNE[Ajustar umbral\no añadir for duration]
    Q2 --> |Sí| Q3{¿El runbook está\nactualizado?}
    Q3 --> |No| UPDATE[Actualizar runbook]
    Q3 --> |Sí| OK[Alerta saludable ✅]

Métricas de salud de tu sistema de alertas

Deberías medir la calidad de tus alertas:

Signal-to-noise ratio = alertas accionables / total de alertas
Objetivo: > 90%

MTTACK (Mean Time to Acknowledge) = tiempo promedio hasta que alguien acknowledges
Objetivo: < 5 minutos para P1

False positive rate = alertas que se resuelven sin ninguna acción
Objetivo: < 10%

Dead Man’s Switch: alertas inversas

Una técnica avanzada: el dead man’s switch es una alerta que se dispara cuando algo que debería ocurrir deja de ocurrir.

# Esta alerta se dispara si NO recibimos heartbeats en 5 minutos
# (es decir, si el job de backup dejó de correr)
alert: BackupJobMissing
expr: absent(backup_last_success_timestamp_seconds) or
      (time() - backup_last_success_timestamp_seconds > 86400)
for: 5m
annotations:
  summary: "Backup job hasn't run in 24 hours"

Casos de uso clásicos:


Referencias