Logging en Python: registro de eventos con el módulo logging
El módulo logging de la biblioteca estándar de Python es la solución correcta para registrar eventos en aplicaciones. Evita usar print() para depurar en producción: el logging permite controlar el nivel de detalle, escribir a múltiples destinos y deshabilitar mensajes sin borrar código.
1. Configuración básica
import logging
# Configuración mínima: nivel y formato
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Los cinco niveles estándar (de menor a mayor severidad)
logging.debug("Información de depuración detallada")
logging.info("Flujo normal de la aplicación")
logging.warning("Algo inesperado pero no crítico")
logging.error("Error que impide una operación")
logging.critical("Error grave — la aplicación puede no funcionar")
Niveles y cuándo usarlos
| Nivel | Valor | Cuándo usarlo |
|---|---|---|
| DEBUG | 10 | Diagnóstico detallado durante desarrollo |
| INFO | 20 | Confirmación de que las cosas funcionan |
| WARNING | 30 | Algo inesperado; la app sigue funcionando |
| ERROR | 40 | Error en una función; no se completa la tarea |
| CRITICAL | 50 | Error grave; la app puede terminar |
2. Loggers con nombre (jerarquía)
import logging
# Cada módulo debería tener su propio logger
logger = logging.getLogger(__name__) # __name__ = nombre del módulo
def procesar_archivo(ruta):
logger.info("Procesando: %s", ruta)
try:
# ... lógica ...
logger.debug("Archivo procesado correctamente: %s", ruta)
except FileNotFoundError:
logger.error("Archivo no encontrado: %s", ruta)
except Exception as e:
logger.exception("Error inesperado al procesar %s", ruta)
# logger.exception incluye el traceback automáticamente
Jerarquía de loggers
logging.root ← logger raíz
└── mi_paquete ← logging.getLogger("mi_paquete")
└── mi_paquete.modulo ← logging.getLogger("mi_paquete.modulo")
Los mensajes suben por la jerarquía (propagate=True por defecto).
3. Handlers: múltiples destinos
import logging
logger = logging.getLogger("mi_app")
logger.setLevel(logging.DEBUG)
# Handler 1: consola (solo WARNING y superior)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s"
))
# Handler 2: archivo (todos los niveles)
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d — %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
))
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.debug("Solo en archivo")
logger.warning("En consola y archivo")
logger.error("En consola y archivo")
4. Rotación de archivos
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
logger = logging.getLogger("rotacion")
logger.setLevel(logging.DEBUG)
# Por tamaño: máximo 5 MB, guardar 3 backups
size_handler = RotatingFileHandler(
"app.log",
maxBytes=5 * 1024 * 1024, # 5 MB
backupCount=3,
encoding="utf-8",
)
# Crea: app.log, app.log.1, app.log.2, app.log.3
# Por tiempo: un nuevo archivo cada medianoche
time_handler = TimedRotatingFileHandler(
"app_diario.log",
when="midnight", # 'S','M','H','D','W0'-'W6','midnight'
interval=1,
backupCount=7, # Conservar 7 días
encoding="utf-8",
)
formateador = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
size_handler.setFormatter(formateador)
time_handler.setFormatter(formateador)
logger.addHandler(size_handler)
logger.addHandler(time_handler)
for i in range(100):
logger.info("Mensaje de prueba %d", i)
5. Configuración por diccionario (recomendado)
import logging
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detallado": {
"format": "%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d — %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"simple": {
"format": "[%(levelname)s] %(message)s",
},
},
"handlers": {
"consola": {
"class": "logging.StreamHandler",
"level": "WARNING",
"formatter": "simple",
"stream": "ext://sys.stdout",
},
"archivo": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detallado",
"filename": "app.log",
"maxBytes": 10485760, # 10 MB
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"mi_app": {
"level": "DEBUG",
"handlers": ["consola", "archivo"],
"propagate": False,
},
},
"root": {
"level": "WARNING",
"handlers": ["consola"],
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("mi_app")
logger.info("Configuración de logging aplicada")
6. Logging estructurado en JSON
Para sistemas modernos (ELK, Datadog, Grafana Loki) es útil emitir JSON:
pip install python-json-logger
import logging
from pythonjsonlogger import jsonlogger
logger = logging.getLogger("mi_servicio")
handler = logging.StreamHandler()
formateador = jsonlogger.JsonFormatter(
"%(asctime)s %(name)s %(levelname)s %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
handler.setFormatter(formateador)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Añadir campos extra
logger.info("Usuario autenticado", extra={
"user_id": 42,
"ip": "192.168.1.1",
"action": "login",
})
# Salida: {"asctime": "2025-06-15T14:30:00", "name": "mi_servicio",
# "levelname": "INFO", "message": "Usuario autenticado",
# "user_id": 42, "ip": "192.168.1.1", "action": "login"}
7. Contexto con LoggerAdapter
import logging
class LoggerConContexto(logging.LoggerAdapter):
def process(self, msg, kwargs):
extra = self.extra.copy()
extra.update(kwargs.get("extra", {}))
kwargs["extra"] = extra
return f"[request={self.extra.get('request_id','?')}] {msg}", kwargs
base_logger = logging.getLogger("mi_app")
def manejar_peticion(request_id):
logger = LoggerConContexto(base_logger, {"request_id": request_id})
logger.info("Petición recibida")
logger.warning("Token próximo a expirar")
logger.info("Petición completada")
manejar_peticion("abc-123")
8. Capturar excepciones correctamente
import logging
logger = logging.getLogger(__name__)
# MAL: perder el traceback
try:
resultado = 1 / 0
except ZeroDivisionError:
logger.error("División por cero") # Sin traceback
# BIEN: logger.exception incluye el traceback completo
try:
resultado = 1 / 0
except ZeroDivisionError:
logger.exception("División por cero")
# También válido con exc_info=True
try:
resultado = int("no-es-numero")
except ValueError as e:
logger.error("Error de conversión: %s", e, exc_info=True)
9. Suprimir logging de bibliotecas ruidosas
import logging
# Silenciar urllib3 y requests (muy verbosos en DEBUG)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("PIL").setLevel(logging.WARNING)
# O silenciar todos excepto el tuyo
logging.basicConfig(level=logging.WARNING) # Todo en WARNING
logging.getLogger("mi_app").setLevel(logging.DEBUG) # Tu app en DEBUG
10. Buenas prácticas
- Usa
logging.getLogger(__name__)en cada módulo — nunca el logger raíz directamente. - Usa
%sen lugar de f-strings:logger.info("Valor: %s", valor)— evita formatear si el mensaje no se va a emitir. - Usa
logger.exception()dentro de bloquesexceptpara incluir el traceback. - No uses
print()en código de producción — usa logging con el nivel apropiado. - Configura desde el punto de entrada (main.py, app.py), no en módulos internos.
- Rota los archivos de log para evitar llenar el disco.
- JSON en producción, texto legible en desarrollo.
Resumen de handlers
| Handler | Uso |
|---|---|
StreamHandler |
Consola (stdout/stderr) |
FileHandler |
Archivo simple |
RotatingFileHandler |
Rotación por tamaño |
TimedRotatingFileHandler |
Rotación por tiempo |
SMTPHandler |
Email en errores críticos |
SysLogHandler |
Syslog del sistema |
MemoryHandler |
Buffer en memoria |
Conversiones relacionadas
Conversiones frecuentes del catálogo: