logging en Python: handlers, formatters, loggers jerárquicos y configuración por entorno

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP