lección 7
Alertas y monitorización: enterarte ANTES de que el CEO pregunte
Qué monitorizar (frescura, volumen, distribuciones), cómo alertar (Slack, email), umbrales estáticos y dinámicos.
⏱ 50 min
### El pipeline perfecto que nadie vigila
Tienes validaciones. Tienes contratos. Tienes Great Expectations ejecutándose cada día. Genial. Pero... ¿quién VE los resultados? Si tu suite de GE falla a las 3 de la mañana un sábado y el resultado queda en un log que nadie lee hasta el lunes, ¿de qué sirve? De nada. Una validación sin alerta es como una alarma de incendios sin sirena — detecta el fuego pero nadie se entera.
La monitorización de datos es el último eslabón de la cadena de calidad. Completa el ciclo: defines expectativas (contrato) → verificas que se cumplen (validación) → alertas cuando NO se cumplen (monitorización). Sin los tres eslabones, la cadena se rompe.
Analogía: imagina un hospital. Los pacientes están monitorizados con máquinas que miden pulso, presión, oxígeno. Si algo se sale del rango normal, la máquina PITA. No espera a que una enfermera pase por ahí y mire la pantalla — avisa activamente. Tus datos necesitan lo mismo: monitorización activa con alertas inmediatas.
### ¿Qué monitorizar? Las 4 señales vitales de los datos
- 01.FRESCURA: ¿los datos están actualizados? Si tu pipeline debería ejecutarse cada hora, verifica que realmente lo hizo. Si los datos tienen más de X horas, alerta.
- 02.VOLUMEN: ¿llegó la cantidad esperada de datos? Si normalmente recibes 10.000 pedidos diarios y hoy llegaron 200, algo está mal. Si llegaron 500.000, también.
- 03.DISTRIBUCIÓN: ¿los valores se comportan "normalmente"? Si el importe medio siempre es ~50€ y hoy es 5.000€, hay un problema. Si el % de nulos sube del 1% habitual al 30%, hay un problema.
- 04.SCHEMA: ¿la estructura cambió? Si apareció una columna nueva o desapareció una existente, alerta (posible breaking change del upstream).
### Umbrales estáticos vs dinámicos
Un umbral estático es simple: "si hay más del 5% de nulos, alerta". Funciona, pero tiene un problema: no se adapta. Si un dataset normalmente tiene 0.1% de nulos y sube a 3%, el umbral estático no se dispara (está por debajo del 5%), pero claramente algo cambió. Los umbrales dinámicos se basan en el HISTORIAL: "si el % de nulos es 3 desviaciones estándar mayor que la media de los últimos 30 días, alerta".
1import pandas as pd2import numpy as np3from datetime import datetime, timedelta45class MonitorDatos:6 """Monitor de calidad con umbrales estáticos y dinámicos."""78 def __init__(self, nombre_dataset: str):9 self.nombre = nombre_dataset10 self.historial: list[dict] = []1112 def registrar_metricas(self, metricas: dict):13 """Registra las métricas del día (simula historial)."""14 metricas['timestamp'] = datetime.now()15 self.historial.append(metricas)1617 def check_estatico(self, metrica: str, valor: float, umbral: float) -> dict:18 """Check simple: ¿el valor supera el umbral fijo?"""19 dispara = valor > umbral20 return {21 'tipo': 'estático',22 'metrica': metrica,23 'valor': valor,24 'umbral': umbral,25 'alerta': dispara,26 'mensaje': f"{metrica} = {valor:.2f} ({'[CRITICO] > ' if dispara else '[OK] <= '}{umbral})",27 }2829 def check_dinamico(self, metrica: str, valor_actual: float,30 historial_valores: list[float], sigma: float = 3.0) -> dict:31 """Check dinámico: ¿el valor se desvía del comportamiento histórico?"""32 if len(historial_valores) < 7:33 return {'tipo': 'dinámico', 'metrica': metrica, 'alerta': False,34 'mensaje': 'Historial insuficiente (< 7 días)'}3536 media = np.mean(historial_valores)37 std = np.std(historial_valores)3839 if std == 0:40 limite_superior = media * 1.541 limite_inferior = media * 0.542 else:43 limite_superior = media + sigma * std44 limite_inferior = media - sigma * std4546 fuera = valor_actual > limite_superior or valor_actual < limite_inferior4748 return {49 'tipo': 'dinámico',50 'metrica': metrica,51 'valor': valor_actual,52 'media_historica': round(media, 2),53 'rango_normal': f"[{limite_inferior:.1f}, {limite_superior:.1f}]",54 'alerta': fuera,55 'mensaje': f"{metrica} = {valor_actual:.1f} vs media {media:.1f} "56 f"{'[CRITICO] ANOMALÍA' if fuera else '[OK] Normal'}",57 }5859# Ejemplo: monitorizar volumen diario60monitor = MonitorDatos("pedidos_diarios")6162# Historial de los últimos 14 días (volumen normal: ~10000 ± 1000)63historial_volumen = [9800, 10200, 10100, 9900, 10500, 10300, 9700,64 10100, 10400, 9600, 10200, 10000, 10300, 9800]6566# Hoy: solo llegaron 3200 pedidos (¡anomalía!)67volumen_hoy = 32006869# Check estático (umbral fijo: mínimo 5000)70resultado_estatico = monitor.check_estatico("volumen", volumen_hoy, 5000)71print(f"Estático: {resultado_estatico['mensaje']}")7273# Check dinámico (basado en historial)74resultado_dinamico = monitor.check_dinamico("volumen", volumen_hoy, historial_volumen)75print(f"Dinámico: {resultado_dinamico['mensaje']}")76print(f" Rango normal: {resultado_dinamico.get('rango_normal', 'N/A')}")
El umbral dinámico detecta la anomalía aunque el estático no se dispare — se adapta al comportamiento real
### Implementar alertas: Slack, email y más
En producción real, las alertas se envían a canales donde el equipo las vea inmediatamente. Los más comunes son: Slack (un canal #data-alerts), email (al equipo on-call), PagerDuty (para incidentes críticos a las 3 AM). Aquí vamos a implementar un sistema de alertas SIMULADO que muestra la arquitectura correcta — en producción solo cambiarías el "enviar" por la integración real.
1from dataclasses import dataclass2from datetime import datetime3from enum import Enum45class Severidad(Enum):6 INFO = "info"7 WARNING = "warning"8 CRITICAL = "critical"910@dataclass11class Alerta:12 """Una alerta de calidad de datos."""13 dataset: str14 metrica: str15 mensaje: str16 severidad: Severidad17 valor_actual: float18 valor_esperado: float19 timestamp: datetime = None2021 def __post_init__(self):22 if self.timestamp is None:23 self.timestamp = datetime.now()2425class SistemaAlertas:26 """Sistema de alertas multi-canal."""2728 def __init__(self):29 self.alertas_enviadas: list[Alerta] = []3031 def enviar(self, alerta: Alerta):32 """Enruta la alerta al canal apropiado según severidad."""33 self.alertas_enviadas.append(alerta)3435 if alerta.severidad == Severidad.CRITICAL:36 self._enviar_slack(alerta)37 self._enviar_email(alerta)38 self._enviar_pagerduty(alerta)39 elif alerta.severidad == Severidad.WARNING:40 self._enviar_slack(alerta)41 else:42 self._log(alerta)4344 def _enviar_slack(self, alerta: Alerta):45 """Simula envío a Slack (en producción: webhook o SDK)."""46 emoji = "[CRITICO]" if alerta.severidad == Severidad.CRITICAL else "[AVISO]"47 print(f"[SLACK #data-alerts] {emoji} {alerta.dataset}: {alerta.mensaje}")48 print(f" Valor: {alerta.valor_actual} (esperado: {alerta.valor_esperado})")49 # En producción:50 # requests.post(SLACK_WEBHOOK_URL, json={"text": mensaje})5152 def _enviar_email(self, alerta: Alerta):53 """Simula envío de email (en producción: SMTP o SES)."""54 print(f"[EMAIL → data-oncall@empresa.com] CRÍTICO: {alerta.dataset}")55 print(f" {alerta.mensaje}")56 # En producción:57 # import smtplib / boto3.client('ses').send_email(...)5859 def _enviar_pagerduty(self, alerta: Alerta):60 """Simula envío a PagerDuty (en producción: API)."""61 print(f"[PAGERDUTY] Incidente creado: {alerta.dataset} — {alerta.metrica}")62 # En producción:63 # requests.post("https://events.pagerduty.com/v2/enqueue", json=payload)6465 def _log(self, alerta: Alerta):66 """Solo log para alertas informativas."""67 print(f"[LOG] {alerta.dataset}: {alerta.mensaje}")6869# Uso70alertas = SistemaAlertas()7172# Alerta crítica: pipeline no se ejecutó73alertas.enviar(Alerta(74 dataset="pedidos_diarios",75 metrica="frescura",76 mensaje="Datos no actualizados en 6 horas (SLA: 1h)",77 severidad=Severidad.CRITICAL,78 valor_actual=6.0,79 valor_esperado=1.0,80))8182print()8384# Alerta warning: volumen bajo85alertas.enviar(Alerta(86 dataset="pedidos_diarios",87 metrica="volumen",88 mensaje="Volumen 40% menor que la media histórica",89 severidad=Severidad.WARNING,90 valor_actual=6000,91 valor_esperado=10000,92))
La severidad determina el canal: INFO → log, WARNING → Slack, CRITICAL → Slack + email + PagerDuty
Consejo de senior sobre alertas: la regla de oro es "si una alerta no requiere ACCIÓN, no debería ser una alerta". Las alertas informativas que nadie mira generan "fatiga de alertas" — el equipo deja de prestarles atención y cuando llega una real, la ignoran. Menos alertas, más accionables = mejor.
Error fatal que he visto múltiples veces: enviar TODAS las alertas al mismo canal con la misma severidad. Si el canal de Slack tiene 50 mensajes diarios de "todo OK" mezclados con 1 mensaje de "pipeline roto", nadie va a ver el importante. Separa los canales por severidad y SÍ SOLO envía al canal lo que requiere atención.
### Anatomía de una buena alerta
Una alerta sin contexto es inútil. "Pipeline falló" no le dice nada al ingeniero que la recibe a las 3 AM. Una buena alerta incluye:
- 01.QUÉ falló: nombre del dataset y métrica específica
- 02.CUÁNDO: timestamp exacto del fallo
- 03.CUÁNTO se desvía: valor actual vs esperado (no solo "está mal" sino "es un 40% menor")
- 04.DESDE CUÁNDO: ¿es nuevo o lleva horas/días?
- 05.IMPACTO: ¿a quién afecta? (dashboard X, informe Y, equipo Z)
- 06.RUNBOOK: link a documentación de cómo investigar/resolver este tipo de fallo
Lo que le diría a mi yo de hace 5 años: crea un runbook para CADA tipo de alerta. Un runbook es un documento que dice: "si recibes esta alerta, haz estos pasos: 1) verifica X, 2) mira el log Y, 3) si es Z reinicia el pipeline, 4) si no se resuelve escala a fulano". A las 3 AM no quieres estar pensando desde cero.
Con validaciones, contratos y alertas tienes el stack completo de calidad de datos. En la próxima (y última) lección lo vamos a juntar TODO en un proyecto real: un pipeline completo con validación, alertas y reporte de calidad.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...