---
name: monitoring-alertas
description: >
  Observabilidad y alertas: Prometheus, Grafana, OpenTelemetry, logging estructurado,
  métricas RED/USE, SLIs/SLOs/SLAs, reglas de alerting, runbooks, dashboards,
  distributed tracing. Anti-patrones de alerting.
version: "1.0.1"
evolved: true
evolved-from: "5.0.3"
evolved-at: "2026-04-23"
evolved-by: "aprender"
evolved-note: "Patrón nuevo: logging.Filter para silenciar 404 esperados en endpoints semánticos"
herramientasPermitidas: [Read, Grep]
evolvable: true  # default para skill estandar
exclusiones:
  - "No cargar para configuración de infraestructura de monitoreo en la nube (CloudWatch Dashboards, Azure Monitor, GCP Cloud Monitoring) sin relación con la aplicación — para cloud monitoring nativo cargar el skill de la nube correspondiente."
  - "No cargar para notificaciones de agentes SWL hacia el usuario (Telegram, desktop notifications) — para notificaciones de agentes cargar `notificaciones-multicanal`."
  - "No cargar para profiling de rendimiento de aplicación (flamegraphs, CPU profiling, memory leaks) — para profiling cargar `performance-baseline`."
  - "No cargar para logs de auditoría de negocio (quién aprobó una factura, historial de cambios de usuario) — para audit logs de dominio usar el patrón de event log de la app, no Prometheus."
---
# Observabilidad y Alertas — Guía de Producción

## Cuándo NO cargar

- La tarea es configurar CloudWatch, Azure Monitor o GCP Cloud Monitoring sin relación con la instrumentación de la aplicación: cargar el skill de la nube correspondiente.
- La tarea es notificaciones de agentes SWL al usuario (Telegram, desktop): cargar `notificaciones-multicanal`.
- La tarea es profiling de rendimiento (flamegraphs, CPU profiling, memory leaks): cargar `performance-baseline`.
- La tarea es audit log de negocio (historial de cambios de dominio): usar el patrón de event log de la aplicación, no Prometheus.

## Los Tres Pilares de la Observabilidad

La observabilidad responde la pregunta: **¿por qué se comporta el sistema así?**

| Pilar | Qué captura | Herramientas |
|-------|------------|--------------|
| **Métricas** | Valores numéricos en el tiempo | Prometheus, DataDog |
| **Logs** | Eventos discretos con contexto | Loki, CloudWatch, ELK |
| **Trazas** | Flujo de una petición a través de servicios | Jaeger, Tempo, X-Ray |

---

## Reglas Obligatorias

1. **Cada endpoint instrumentado con métricas RED** (Rate, Errors, Duration).
2. **Trazas OpenTelemetry propagadas** de entrada a salida (incluyendo BD y servicios externos).
3. **Logs estructurados en JSON** con `request_id` en cada línea.
4. **Cada alerta tiene `for` clause, severidad y URL de runbook** — alertas sin runbook no se crean.
5. **NUNCA alertar "latencia promedio"** — el promedio oculta percentiles altos. Usar p99 SIEMPRE.
6. **Solo alertar lo que requiere acción humana** — demasiadas alertas causa alert fatigue.
7. **Crítico = acción ahora, Advertencia = acción en 24h** — no todo es "crítico".

---

## Metodologías de Métricas

### RED (para servicios orientados a peticiones)

| Métrica | Descripción | Ejemplo PromQL |
|---------|-------------|----------------|
| **Rate** | Peticiones por segundo | `rate(http_requests_total[5m])` |
| **Errors** | Tasa de errores | `rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])` |
| **Duration** | Latencia p50, p95, p99 | `histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))` |

### USE (para recursos de infraestructura)

| Métrica | Descripción | Ejemplo |
|---------|-------------|---------|
| **Utilization** | % de tiempo ocupado | CPU idle rate |
| **Saturation** | Cola / trabajo pendiente | `node_load1` |
| **Errors** | Errores del recurso | disk I/O errors |

---

## SLIs, SLOs y SLAs

```
SLA (Service Level Agreement)  — Contrato legal con el cliente
  +-- SLO (Service Level Objective) — Meta interna (más estricto que el SLA)
        +-- SLI (Service Level Indicator) — Métrica que mide el cumplimiento
```

### Definir SLIs correctamente

```yaml
# MAL — SLI vago
sli: "el servicio debe estar disponible"

# BIEN — SLI medible y específico
slis:
  disponibilidad:
    definicion: >
      Proporción de peticiones HTTP al endpoint /api/* que retornan
      código de estado < 500 en los últimos 30 días.
    query: >
      sum(rate(http_requests_total{status!~"5.."}[30d]))
      / sum(rate(http_requests_total[30d]))
```

### Presupuesto de Error

```
Error Budget = 1 - SLO

Si SLO = 99.9% disponibilidad en 30 días:
Error Budget = 0.1% = 43.8 minutos de caída permitida

Si el budget está al 50% a mitad del mes -> congelar deploys riesgosos
Si el budget está al 0% -> modo emergencia, solo fixes críticos
```

---

## Anti-Patrones de Alerting

| Anti-patrón | Problema | Solución |
|-------------|----------|----------|
| **Alertas sin runbook** | El oncall no sabe qué hacer | Cada alerta tiene URL de runbook |
| **Umbral fijo sin `for`** | Falsos positivos por picos | Usar `for: 5m` para confirmar |
| **Alerta de "latencia promedio"** | Oculta percentiles altos | Usar p99 siempre |
| **Demasiadas alertas** | Alert fatigue — se ignoran todas | Solo alertar acción humana |
| **Alertas de síntoma, no de causa** | "CPU alto" no dice acción | Alertar efectos en el usuario |
| **Severidades mal usadas** | Todo es "crítico" | Crítico = ahora, Advertencia = 24h |

---

## Dashboard Grafana — Paneles Mínimos

1. **Panel de disponibilidad** — Tasa de éxito (gauge + serie de tiempo).
2. **Panel RED** — Rate, Errors, Duration en una vista.
3. **Panel de latencia** — p50, p95, p99 en serie de tiempo.
4. **Panel de saturación** — CPU, memoria, conexiones activas.
5. **Panel de errores por tipo** — 4xx vs 5xx separados.
6. **Anotaciones de deploys** — Marcas verticales en cada despliegue.
7. **Panel de error budget** — Consumo del presupuesto de error del SLO.

---

## Checklist de Revisión — Observabilidad

- [ ] Cada endpoint instrumentado con métricas RED.
- [ ] Trazas OpenTelemetry propagadas de entrada a salida.
- [ ] Logs estructurados en JSON con `request_id`.
- [ ] SLIs definidos como queries PromQL verificables.
- [ ] SLOs documentados con su error budget calculado.
- [ ] Cada alerta tiene `for` clause, severidad y URL de runbook.
- [ ] No hay alertas de "síntoma" sin correlación con impacto en usuario.
- [ ] Runbooks actualizados y probados en el último trimestre.
- [ ] Dashboard de servicio con los 7 paneles mínimos.

---

Para ejemplos completos de instrumentación Prometheus en Python, trazas OpenTelemetry, logging estructurado con structlog, reglas de alerting para Alertmanager y plantilla de runbook, ver [recursos/instrumentacion-y-alertas.md](recursos/instrumentacion-y-alertas.md).

## Gotchas / Errores comunes no obvios

**`histogram_quantile(0.99, ...)` en Prometheus produce percentiles incorrectos cuando los buckets del histograma no cubren el rango real de valores observados**: si el histograma tiene buckets hasta `le="1.0"` segundos y hay requests que tardan 3 segundos, Prometheus reporta el p99 como "aproximadamente 1.0s" (el bucket máximo) en lugar del valor real. Causa: Prometheus calcula percentiles por interpolación dentro de los buckets definidos. Si el valor real excede todos los buckets, lo aplana al último. Fix: definir los buckets explícitamente según el perfil de latencia esperado del servicio: `buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]`. Verificar con `histogram_quantile(1, ...)` que no devuelve `+Inf` en producción.

**Las trazas OpenTelemetry se propagan correctamente en requests HTTP pero se pierden al cruzar un mensaje de cola (Kafka, RabbitMQ) porque el consumidor no extrae el contexto del header del mensaje**: el `traceparent` y `tracestate` se envían como headers del mensaje pero el handler del consumidor crea un nuevo span sin parent, rompiendo la traza distribuida. Causa: las librerías de OTel para brokers de mensajes requieren extracción explícita del contexto: `tracer.extract(carrier=message.headers)`. Fix: en el consumidor de la cola, extraer el contexto antes de crear el span: `ctx = propagate.extract(dict(message.headers))` y pasar ese contexto al `tracer.start_as_current_span()`.

**Las alertas con `for: 5m` no se disparan cuando Prometheus tiene un `scrape_interval` de 30 segundos y la condición se cumple durante exactamente 5 minutos pero con gaps de datos**: si el target no responde en 2 de los 10 scrapes dentro de los 5 minutos, Prometheus resetea el contador de `for` y la alerta nunca llega a `FIRING`. Causa: `for` requiere que la condición sea verdadera en TODOS los scrapes del período, sin gaps. Fix: usar `for` con al menos 2× el `scrape_interval`. Para `scrape_interval: 30s`, usar `for: 3m` como mínimo. Documentar en el runbook el tiempo real de propagación esperado.

**Un dashboard de Grafana con `refresh: 5s` en producción genera una carga de queries sobre Prometheus que puede afectar el rendimiento del propio sistema monitoreado**: si el dashboard tiene 20 paneles y cada panel hace 3 queries, se ejecutan 60 queries por refresh = 720 queries/minuto por usuario del dashboard abierto. Con 5 usuarios simultáneos viendo el dashboard, son 3,600 queries/minuto. Causa: el intervalo de refresh corto es intuitivo para debugging pero destructivo en producción con dashboards complejos. Fix: usar `refresh: 30s` o `60s` en dashboards de producción. Para debugging en tiempo real, crear un dashboard separado simplificado con pocas métricas y refresh corto.

## Patrón — Filtro de log para 404 REST esperados (auto-fetch de recuperación)

### SIEMPRE: silenciar con `logging.Filter` los 404 que la semántica REST requiere como normales

Cuando un endpoint `GET` se usa como "pregunta de existencia" (ej: recuperar el resultado anterior al recargar la UI, verificar si un recurso existe antes de operar), el 404 es la respuesta esperada al inicio de cada sesión y ensucia los logs del desarrollador con mensajes rojos sin información.

**Regla**: NO cambiar el status code a 200 para acallar el log (rompe la semántica REST). En lugar de eso, agregar un filtro de logging específico que silencie el combo exacto "método + path + status" esperado.

```python
import logging
import re


class _Silenciar404AutoFetch(logging.Filter):
    """Silencia el 404 esperado del auto-fetch de recuperación.

    Solo silencia el combo exacto: GET <path> → 404. Cualquier otro 404
    (de otro endpoint o con otro método) sigue apareciendo en el log.
    """
    _PATRON = re.compile(
        r'"GET\s+/api/v1/cases/[^/]+/analisis(?:\?[^"\s]*)?\s+HTTP/[\d.]+"\s+404\b'
    )

    def filter(self, record: logging.LogRecord) -> bool:
        return not self._PATRON.search(record.getMessage())


logging.getLogger("uvicorn.access").addFilter(_Silenciar404AutoFetch())
```

**Criterios para aplicar este filtro** (cumplir los 4):

1. El 404 es la respuesta **esperada** en el flujo normal (no bug).
2. La semántica REST del endpoint requiere 404 (ej: `GET /items/{id}` de existencia).
3. El volumen de 404 esperados es alto (≥1 por sesión de usuario).
4. El 404 no requiere acción del ops team (no hay impacto al usuario).

**Anti-patrón opuesto**: NO cambiar el endpoint a devolver `200 {"exists": false}` solo para que el log quede limpio — rompe HATEOAS y los clientes que esperan el contrato REST estándar. Arreglar el síntoma en el canal correcto (filtro de log), no cambiar la semántica del API.
