Los print() de depuración no escalan: desaparecen en producción, no tienen nivel de severidad y no dicen de dónde viene el mensaje. El módulo logging resuelve todo eso: niveles, formato configurable, múltiples destinos y jerarquía de loggers que permite activar el nivel correcto por módulo sin cambiar el código.
Uso básico
import logging
# Configuración mínima: nivel y formato
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)
logger.debug("Mensaje de depuración (solo en desarrollo)")
logger.info("El servidor arrancó en el puerto 8080")
logger.warning("La caché está al 90% de capacidad")
logger.error("No se pudo conectar a la base de datos")
logger.critical("El proceso va a terminar")
# 2024-03-15 10:00:01 DEBUG __main__ Mensaje de depuración
# 2024-03-15 10:00:01 INFO __main__ El servidor arrancó en el puerto 8080
# ...
Jerarquía de loggers
Cada módulo debe obtener su propio logger con getLogger(__name__). Así el sistema de logging sabe de dónde viene cada mensaje y puedes configurar niveles distintos por módulo.
# modulo/base_datos.py
import logging
logger = logging.getLogger(__name__) # nombre: "modulo.base_datos"
def conectar(host: str):
logger.debug(f"Intentando conectar a {host}")
# ...
logger.info(f"Conexión establecida con {host}")
# modulo/api.py
import logging
logger = logging.getLogger(__name__) # nombre: "modulo.api"
def procesar_peticion(path: str):
logger.info(f"Petición recibida: {path}")
# Configurar niveles distintos por módulo:
# logging.getLogger("modulo.base_datos").setLevel(logging.DEBUG)
# logging.getLogger("modulo.api").setLevel(logging.WARNING)
Handlers: múltiples destinos
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("mi_app")
logger.setLevel(logging.DEBUG)
# Handler para consola (solo WARNING y superior)
consola = logging.StreamHandler()
consola.setLevel(logging.WARNING)
consola.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
# Handler para fichero (todos los niveles, rotación automática)
fichero = RotatingFileHandler(
"logs/app.log",
maxBytes=5 * 1024 * 1024, # 5 MB
backupCount=3 # mantener 3 ficheros históricos
)
fichero.setLevel(logging.DEBUG)
fichero.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s"
))
logger.addHandler(consola)
logger.addHandler(fichero)
logger.debug("Solo en fichero") # no aparece en consola
logger.warning("En ambos lados") # aparece en consola y en fichero
Logging de excepciones
import logging
logger = logging.getLogger(__name__)
def dividir(a: float, b: float) -> float:
try:
return a / b
except ZeroDivisionError:
logger.error("División por cero: a=%s, b=%s", a, b, exc_info=True)
# exc_info=True incluye el traceback completo en el log
raise
def procesar():
try:
resultado = dividir(10, 0)
except ZeroDivisionError:
logger.exception("Error en procesar()")
# logger.exception() es equivalente a logger.error(..., exc_info=True)
dictConfig para configuración por entorno
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detallado": {
"format": "%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s",
},
"simple": {
"format": "%(levelname)s: %(message)s",
},
},
"handlers": {
"consola": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
"stream": "ext://sys.stdout",
},
"fichero": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detallado",
"filename": "logs/app.log",
"maxBytes": 5242880,
"backupCount": 3,
"encoding": "utf-8",
},
},
"loggers": {
"mi_app": {
"level": "DEBUG",
"handlers": ["consola", "fichero"],
"propagate": False,
},
"mi_app.base_datos": {
"level": "WARNING", # menos verboso para BD
},
},
"root": {
"level": "WARNING",
"handlers": ["consola"],
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("mi_app")
logger.info("Servidor arrancado")
Logging estructurado (JSON) para sistemas modernos
# pip install python-json-logger
import logging
from pythonjsonlogger import jsonlogger
logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
fmt="%(asctime)s %(name)s %(levelname)s %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Añadir campos extra
logger.info("Petición procesada", extra={
"path": "/api/usuarios",
"metodo": "GET",
"duracion_ms": 23,
"usuario_id": 42
})
# {"asctime": "2024-03-15...", "levelname": "INFO", "message": "...", "path": "/api/usuarios", ...}
Las reglas fundamentales: siempre usa getLogger(__name__) en cada módulo (nunca el logger root directamente en producción); configura el logging una sola vez en el punto de entrada de la aplicación con dictConfig(); usa logger.exception() dentro de bloques except para capturar el traceback automáticamente; y nunca construyas el mensaje de log con concatenación de strings (f"valor: {variable}"), sino con el formato de % o pasando el mensaje y los argumentos por separado, para que Python evite construir el string si el nivel está desactivado.
