La jerarquía de loggers y por qué print no escala

print escribe en stdout y punto. No sabe si el mensaje es crítico o solo informativo, no puede redirigirse a un archivo sin manipular el proceso, y no hay forma de silenciar solo los prints de un módulo específico sin comentar líneas. En un script de 50 líneas eso es tolerable. En un sistema con diez módulos corriendo en producción, es un problema serio.

El módulo logging de la biblioteca estándar resuelve exactamente eso. La pieza central es el logger, un objeto nombrado que pertenece a una jerarquía de árbol. Cada logger tiene un nombre, y ese nombre define su posición en el árbol. El separador es el punto: un logger llamado myapp.database es hijo de myapp, que a su vez es hijo del root logger (la raíz de toda la jerarquía). Cuando un logger emite un mensaje, ese mensaje sube por el árbol —lo que se llama propagation— hasta encontrar un Handler que sepa qué hacer con él: escribir a un archivo, enviarlo a stdout, mandarlo a un servicio externo.

El patrón correcto para crear un logger en cualquier módulo es:

logger = logging.getLogger(__name__)

__name__ en un módulo Python es automáticamente su nombre completo (myapp.database, myapp.services.email, etc.). Usarlo como nombre del logger no es solo convención: es lo que hace que la jerarquía refleje exactamente la estructura de tu paquete. Puedes configurar el logger myapp para que filtre DEBUG en producción, y todos sus hijos heredan esa decisión sin que tengas que tocar cada módulo individualmente.

Si en cambio haces logging.getLogger("mi_logger_especial"), ese logger queda suelto en la jerarquía —sigue conectado al root, pero pierde la relación con la estructura real de tu código. Perderás visibilidad de qué módulo genera qué mensajes.

# myapp/__init__.py
import logging

# Configura el handler una sola vez, en el punto de entrada de la aplicación.
# Los módulos hijos nunca deben configurar handlers propios.
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
    datefmt="%H:%M:%S",
)
# myapp/database.py
import logging

logger = logging.getLogger(__name__)  # nombre: "myapp.database"


def connect(host: str, port: int) -> None:
    logger.debug("Intentando conexión a %s:%d", host, port)  # lazy interpolation
    try:
        # simulamos la conexión
        if port <= 0:
            raise ValueError(f"Puerto inválido: {port}")
        logger.info("Conexión establecida con %s:%d", host, port)
    except ValueError:
        # exc_info=True añade el traceback completo al registro
        logger.error("Fallo de conexión", exc_info=True)
        raise
# myapp/services/email.py
import logging

logger = logging.getLogger(__name__)  # nombre: "myapp.services.email"


def send(to: str, subject: str) -> None:
    logger.debug("Preparando envío a %s", to)
    logger.info("Email enviado: subject=%r to=%s", subject, to)
# main.py
import logging
import myapp.database as db
import myapp.services.email as email

# Importar __init__.py aplica basicConfig.
# Desde aquí controlamos el nivel de todo el árbol "myapp".
logging.getLogger("myapp").setLevel(logging.WARNING)

db.connect("localhost", 5432)
email.send("user@example.com", "Bienvenido")

# Ahora reactivamos solo el subárbol de base de datos para depurar un problema.
logging.getLogger("myapp.database").setLevel(logging.DEBUG)
db.connect("localhost", -1)

Lo que hace el árbol por ti

Cuando myapp/database.py llama a logger.debug(...), el mensaje tiene nivel DEBUG y nombre myapp.database. El sistema busca en ese logger si tiene un handler propio —no lo tiene— y lo propaga al padre myapp. Si myapp tampoco tiene handler, sube al root logger, que sí tiene el handler configurado en basicConfig.

La propagación es lo que te permite centralizar toda la configuración de handlers en un solo lugar (el punto de entrada de la aplicación) mientras cada módulo simplemente escribe mensajes. Si un módulo añade su propio handler y deja propagate=True (el valor por defecto), el mensaje llega dos veces al root —ese es el bug más común con este sistema.

La llamada logger.debug("msg %s", valor) en lugar de logger.debug(f"msg {valor}") también importa: si el nivel del logger descarta el mensaje, la interpolación de la f-string ocurre de todas formas antes de llamar al método. Con el formato de %-style, la interpolación solo ocurre si el mensaje realmente se va a emitir.

La capacidad de hacer logging.getLogger("myapp.database").setLevel(logging.DEBUG) en caliente —sin reiniciar el proceso— mientras el resto de la aplicación sigue en WARNING es exactamente lo que print nunca podrá darte.

Errores que debes conocer

Error: configurar basicConfig dentro de un módulo importable en lugar del punto de entrada, lo que hace que el primer import que llegue al módulo configure los handlers de toda la aplicación.

# ❌ Wrong — en myapp/database.py
import logging
logging.basicConfig(level=logging.DEBUG)  # afecta el root logger globalmente
logger = logging.getLogger(__name__)

# ✅ Right — basicConfig solo en main.py o en el __main__ block
import logging
logger = logging.getLogger(__name__)
# la configuración la decide quien arranca la aplicación, no la biblioteca

basicConfig es idempotente: si el root logger ya tiene handlers, no hace nada. Eso significa que el orden de imports decide qué configuración “gana”, y depurar eso en una base de código grande es una pesadilla.

Error: dejar propagate=True después de añadir un handler propio a un logger intermedio, duplicando todos los mensajes.

# ❌ Wrong
import logging
logger = logging.getLogger("myapp.database")
handler = logging.FileHandler("db.log")
logger.addHandler(handler)
# los mensajes van a db.log Y suben al root logger → duplicados

# ✅ Right
logger.addHandler(handler)
logger.propagate = False  # este logger es un destino final, no retransmite

Cuando añades un handler a un logger intermedio y quieres que sea su destino exclusivo, corta la propagación explícitamente.

160

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio