49 · Configuración multi-logger: handlers, formatters, dictConfig
Un logger, múltiples destinos — conecta un StreamHandler para la terminal y un FileHandler para logs persistentes, cada uno con su propio nivel y formato. Luego reemplaza el cableado manual con logging.config.dictConfig para el enfoque declarativo listo para producción.
Un `Logger` es un despachador nombrado — no sabe nada sobre *adónde* van los registros. Los `Handler`s deciden el destino (terminal, archivo, endpoint HTTP); los `Formatter`s deciden la forma. Conectar múltiples handlers a un logger significa que el mismo registro se entrega a varios lugares simultáneamente — una capacidad crítica para cualquier pipeline que corra sin supervisión. `logging.config.dictConfig({...})` te permite declarar todo el árbol de logging en un único diccionario al inicio, haciendo la configuración auditable, versionable y intercambiable por entorno sin tocar el código de la aplicación.
Sin esto:
Sin configuración multi-handler estás forzado a elegir: o ves los logs en la terminal (y desaparecen cuando el proceso termina) o escribes en un archivo (y pierdes visibilidad en tiempo real). `dictConfig` te permite tener ambos, además de diferentes formatos y niveles de verbosidad por destino, declarados una vez y compartidos en todo el programa.
¿Por qué múltiples handlers?
En la lección anterior cada registro de log iba a un solo lugar — la terminal (stderr) a través del StreamHandler predeterminado del root logger. Las pipelines reales necesitan más:
- Terminal (
StreamHandler) — inspección humana durante el desarrollo. Normalmente solo quieresWARNINGy superior para que la consola siga siendo legible. - Archivo (
FileHandler/RotatingFileHandler) — salidaDEBUGcompleta escrita en disco para análisis post-mortem. - Remoto (
HTTPHandler,SocketHandlero un sink de terceros) — reenviado a un agregador de logs como Datadog, Elastic o CloudWatch.
Cada handler tiene su propio nivel (un segundo filtro después del nivel del logger) y su propio Formatter (la plantilla de cadena). Un registro debe pasar tanto el nivel del logger como el nivel del handler para ser emitido.
La cadena de filtrado
se registra un mensaje
│
▼
Nivel del Logger ──── ¿demasiado bajo? ──► descartado silenciosamente
│
▼ (para cada Handler conectado)
Nivel del Handler ──── ¿demasiado bajo? ──► omitido para este handler
│
▼
Formatter → cadena final → destino
Tres formas de configurar el logging
- API manual —
logger.addHandler(handler)— útil para entender el modelo; incómodo a escala. logging.config.dictConfig(d)— declara la configuración completa como un dict de Python; el enfoque idiomático para producción.logging.config.fileConfig(ruta)— archivo de estilo INI más antiguo; menos expresivo, raramente usado en código nuevo.
Python (in browser)
La consola imprime solo `WARNING` y `ERROR` — los dos registros que superaron tanto el nivel del logger (`DEBUG`) como el nivel del handler (`WARNING`). El archivo contiene los cuatro porque el nivel del handler es `DEBUG`. Mismo logger, misma llamada `logger.debug(...)`, diferentes destinos.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`dictConfig` produce el mismo comportamiento que la API manual — la diferencia es operacional: el dict de configuración puede vivir en un archivo de configuración YAML/JSON, ser sobreescrito por entorno (dev/staging/prod) y ser leído por herramientas DevOps sin tocar el código fuente de Python. `"propagate": False` evita que el registro también llegue al root logger (lo que causaría impresión doble).
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
La convención de `NullHandler` se aplica en bibliotecas populares como `requests`, `boto3` y `httpx` — todas declaran `logging.getLogger(__name__).addHandler(logging.NullHandler())` y dejan las decisiones de enrutamiento completamente a la aplicación.
Configuras un logger con `logger.setLevel(logging.DEBUG)` y conectas un único `StreamHandler` con `handler.setLevel(logging.WARNING)`. Luego llamas a `logger.info("checkpoint guardado")`. ¿Qué sucede?
- Un `Logger` puede tener múltiples `Handler`s — cada uno con su propio nivel y `Formatter`. Un registro debe superar tanto el nivel del logger como el nivel del handler para ser emitido.
- Handlers integrados: `StreamHandler` (stderr), `FileHandler` (archivo), `RotatingFileHandler` (rotación automática por tamaño), `TimedRotatingFileHandler` (rotación automática por tiempo).
- `logging.config.dictConfig({...})` es la forma idiomática de producción para configurar el árbol de logging completo: formatters, handlers y loggers declarados una vez en un único dict.
- Autores de bibliotecas: solo añade `logging.NullHandler()` a tu logger. Nunca llames a `basicConfig` ni añadas handlers reales — ese es el trabajo del punto de entrada de la aplicación.
- Fija `"propagate": False` en los loggers de `dictConfig` para evitar que los registros suban al root logger y se impriman dos veces.
Los servicios ML de producción usan configuraciones multi-handler por defecto: un `StreamHandler` en `WARNING` para inspección humana en la terminal durante el desarrollo, un `StreamHandler` en `DEBUG` emitiendo JSON a stdout para agregación de logs en la nube (Cloud Logging, CloudWatch, Datadog) y un `RotatingFileHandler` para datos de depuración de alto volumen que se archivan en S3 o GCS. El diccionario `dictConfig` usualmente vive en `config/logging.yaml`, cargado al inicio — un cambio y cada logger en la aplicación sigue las nuevas reglas sin un despliegue de código.
Si lo quitas: Sin configuración multi-handler estás forzado a elegir un destino por ejecución del script. Las sesiones de depuración requieren cambiar el código (añadir un FileHandler manualmente, luego eliminarlo), y no hay una forma limpia de que la terminal muestre solo warnings mientras el archivo captura el detalle de depuración completo. A escala esto significa terminales ruidosas o puntos ciegos en tus logs.