lección 6
Logging y manejo de errores en producción
Cuando tu pipeline falla a las 3AM, los logs son tu única pista. Aprende a dejar rastros útiles.
⏱ 45 min
### El pipeline invisible
Tu pipeline funciona. Corre cada noche a las 2AM mientras duermes. Nadie lo mira, nadie lo vigila. Hasta que un martes a las 7AM, el equipo de BI reporta que la tabla de ventas no se actualizó. ¿Qué pasó? ¿Falló la lectura? ¿La API devolvió un error? ¿Se llenó el disco? Sin logs, no tienes NI IDEA. Con logs buenos, sabes exactamente qué falló, cuándo, y por qué en menos de 2 minutos.
La analogía: los logs son como la caja negra de un avión. Cuando todo va bien, nadie los mira. Cuando algo explota, son lo ÚNICO que tienes para reconstruir qué pasó. Un pipeline sin logs es un avión sin caja negra: vuela bien hasta que no vuela, y entonces nadie sabe qué pasó.
### print() NO es logging
Sé lo que estás pensando: "pero yo pongo print() en mi código para ver qué pasa". El problema de print: no tiene timestamp (¿cuándo pasó?), no tiene niveles (¿es un error o solo información?), no se puede redirigir a un archivo, no se puede filtrar, y se pierde cuando cierras la terminal. Para un script de 10 líneas que ejecutas a mano, print vale. Para un pipeline de producción, necesitas logging de verdad.
1import logging23# Configuración básica (haz esto UNA vez al inicio del script)4logging.basicConfig(5 level=logging.INFO,6 format='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',7 datefmt='%Y-%m-%d %H:%M:%S'8)910logger = logging.getLogger('mi_pipeline')1112# Los 5 niveles de logging13logger.debug("Entrando en la función X con parámetro Y") # Para debugging detallado14logger.info("Procesadas 15,000 filas correctamente") # Progreso normal15logger.warning("El archivo tiene 3 columnas menos de lo esperado") # Algo raro pero no fatal16logger.error("No se pudo conectar a la base de datos") # Algo falló17logger.critical("Pipeline abortado: datos corruptos detectados") # Todo roto
Los 5 niveles: debug < info < warning < error < critical
Consejo de senior: pon logs desde el DÍA 1. No esperes a que algo falle. El coste de añadir un logger.info() es cero. El coste de no tenerlo cuando necesitas diagnosticar un error a las 3AM es enorme. Lo que le diría a mi yo de hace 5 años: "pon un log en cada entrada de función, cada lectura de archivo y cada escritura a base de datos".
### Logging a archivo: logs que sobreviven al reinicio
1import logging23# Configuración con salida a archivo Y a consola4logging.basicConfig(5 level=logging.INFO,6 format='%(asctime)s | %(levelname)-8s | %(message)s',7 datefmt='%Y-%m-%d %H:%M:%S',8 handlers=[9 logging.FileHandler('pipeline.log'), # Guarda en archivo10 logging.StreamHandler() # También muestra en consola11 ]12)1314logger = logging.getLogger('pipeline_ventas')1516# Ahora todos los logs van a pipeline.log Y a la consola17logger.info("Inicio del pipeline de ventas")18logger.info("Conectando a base de datos...")
FileHandler guarda logs en disco: sobreviven reinicios y cierres
En producción los archivos de log crecen sin fin. RotatingFileHandler (de logging.handlers) los parte automáticamente: cuando el archivo llega a un tamaño (por ejemplo 5 MB), lo renombra a pipeline.log.1 y empieza uno nuevo. TimedRotatingFileHandler hace lo mismo pero por tiempo (uno al día, uno a la semana). Sin rotación, un pipeline que corre 24/7 llena el disco en semanas.
### Manejo de errores: try/except profesional
Ya conoces try/except de la Skill 3. Ahora vamos a usarlo como un profesional: con logging, con tipos específicos de error, y con la lógica de "qué hago cuando algo falla" que necesita un pipeline de producción.
1import logging2import pandas as pd3from sqlalchemy import create_engine45logger = logging.getLogger('pipeline')67def extraer_datos(query: str, engine) -> pd.DataFrame:8 """Extrae datos de la DB con manejo de errores profesional."""9 try:10 logger.info(f"Ejecutando query: {query[:50]}...")11 df = pd.read_sql(query, engine)12 logger.info(f"Extraídas {len(df)} filas correctamente")13 return df1415 except ConnectionError as e:16 logger.error(f"No se pudo conectar a la base de datos: {e}")17 raise # Re-lanzar para que el caller decida qué hacer1819 except pd.errors.DatabaseError as e:20 logger.error(f"Error SQL: {e}")21 raise2223 except Exception as e:24 logger.critical(f"Error inesperado en extracción: {type(e).__name__}: {e}")25 raise
Try/except profesional: captura específica, log del error, y re-lanzar
NUNCA hagas except: pass (capturar y silenciar). Es la forma más rápida de tener bugs invisibles. Si capturas un error, al mínimo loguéalo. Mejor aún: loguéalo Y re-lánzalo (raise) para que el nivel superior decida qué hacer.
### Patrón: pipeline con logging completo
Este es el patrón que usarás en todos tus pipelines. Cada paso está logueado: inicio, progreso, resultado y errores. Cuando algo falle a las 3AM, abrirás el log y en 30 segundos sabrás exactamente dónde y por qué.
1import logging2import pandas as pd3import time45logging.basicConfig(6 level=logging.INFO,7 format='%(asctime)s | %(levelname)-8s | %(message)s',8 datefmt='%Y-%m-%d %H:%M:%S',9 handlers=[logging.FileHandler('pipeline.log'), logging.StreamHandler()]10)11logger = logging.getLogger('pipeline_ventas')1213def ejecutar_pipeline():14 """Pipeline completo con logging profesional."""15 inicio = time.time()16 logger.info("=" * 50)17 logger.info("INICIO: Pipeline de ventas diarias")18 logger.info("=" * 50)1920 try:21 # Paso 1: Extracción22 logger.info("[1/3] Extrayendo datos de la API...")23 df = extraer_de_api()24 logger.info(f"[1/3] Extraídas {len(df)} filas en {time.time()-inicio:.1f}s")2526 # Paso 2: Limpieza27 logger.info("[2/3] Limpiando datos...")28 filas_antes = len(df)29 df = limpiar(df)30 filas_despues = len(df)31 logger.info(f"[2/3] Limpieza: {filas_antes} → {filas_despues} filas ({filas_antes - filas_despues} eliminadas)")3233 if filas_despues < filas_antes * 0.5:34 logger.warning(f"Se eliminó más del 50% de datos. Revisar fuente.")3536 # Paso 3: Carga37 logger.info("[3/3] Cargando en base de datos...")38 cargar_en_db(df)39 logger.info(f"[3/3] Cargadas {len(df)} filas correctamente")4041 duracion = time.time() - inicio42 logger.info(f"ÉXITO: Pipeline completado en {duracion:.1f}s")4344 except Exception as e:45 logger.critical(f"FALLO: Pipeline abortado tras {time.time()-inicio:.1f}s")46 logger.critical(f"Error: {type(e).__name__}: {e}")47 raise4849if __name__ == '__main__':50 ejecutar_pipeline()
Pipeline con logging completo: cada paso documentado, tiempos medidos, alertas de anomalías
### Qué loguear y qué NO loguear
- SÍ: Inicio y fin de cada paso con duración
- SÍ: Número de filas procesadas/eliminadas/insertadas
- SÍ: Errores con contexto (qué archivo, qué tabla, qué query)
- SÍ: Anomalías y warnings (más nulos de lo normal, archivo más pequeño de lo esperado)
- NO: Contraseñas, tokens, API keys (NUNCA loguees secretos)
- NO: Datos personales (emails, nombres) en texto claro
- NO: Cada fila individual (millones de logs = ilegible e inútil)
### Custom exceptions: errores con significado
En pipelines complejos, Exception genérica no dice mucho. Crea excepciones propias que describan el problema de negocio:
1class PipelineError(Exception):2 """Error base del pipeline."""3 pass45class ExtraccionError(PipelineError):6 """La fuente de datos no respondió o devolvió datos inválidos."""7 pass89class ValidacionError(PipelineError):10 """Los datos no pasan las reglas de calidad."""11 pass1213class CargaError(PipelineError):14 """Error al escribir en el destino."""15 pass1617# Uso18def validar_datos(df):19 if df.empty:20 raise ValidacionError("DataFrame vacío: la fuente no devolvió datos")21 if df['importe'].isnull().sum() > len(df) * 0.1:22 raise ValidacionError(f"Más del 10% de importes son nulos ({df['importe'].isnull().sum()}/{len(df)})")
Excepciones personalizadas: errores que cuentan una historia
Las excepciones personalizadas tienen dos ventajas: 1) El nombre describe el problema (ValidacionError dice más que Exception), 2) Puedes capturarlas selectivamente: except ExtraccionError → reintentar, except ValidacionError → alertar y no cargar datos sucios.
### pytest: comprobar que tu código hace lo que dice
pytest es la herramienta estándar en Python para escribir tests automáticos. Un test es una función que empieza por test_ y que comprueba algo concreto: "si le paso un DataFrame vacío, ¿lanza ValidacionError?". Si la condición se cumple, el test pasa (verde); si no, falla (rojo con el motivo exacto). Lo instalas con pip install pytest y lo ejecutas con pytest archivo.py -v. En el ejercicio siguiente lo usarás para verificar que tus excepciones se lanzan cuando deben.
1# Instalar pytest (una vez)2pip install pytest34# Ejecutar los tests de un archivo5pytest mi_archivo.py -v67# Ejemplo de salida:8# test_valida_df_vacio PASSED9# test_valida_importes_negativos PASSED10# 2 passed in 0.03s
pytest se instala con pip y se lanza desde terminal
La función pytest.raises(ErrorEsperado, match="texto") es el patrón para comprobar que un error SÍ se lanza. Si la función no lanza nada, el test falla. Si lanza otro error, también falla. Solo pasa si lanza exactamente el error esperado con un mensaje que contiene "texto".
## ejercicios
Añadir logging profesional a un pipeline
Tienes un pipeline sin logs. Añade logging completo: configuración con FileHandler, log de inicio/fin de cada paso, medición de tiempo total, y manejo de errores con log del error.
Crear excepciones de validación de datos
Crea una clase ValidacionError y una función validar_ventas(df) que lance errores descriptivos si: el DataFrame está vacío, tiene importes negativos, o tiene más del 10% de nulos en "importe".
Detectar y loguear anomalías
Escribe una función verificar_calidad(df) que loguee warnings si: el número de filas es menor a 100, hay más del 5% de nulos en cualquier columna, o hay outliers extremos en importe (usando IQR, como en la lección de limpieza).
💡 Resultado esperado
WARNING | Solo 11 filas (esperado >= 100). ¿Datos incompletos? WARNING | Columna 'importe' tiene 9.1% nulos (umbral: 5%) WARNING | 1 importes por encima de 166 (IQR). Revisar INFO | Verificación completada: 11 filas, 2 columnas
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...