Pregunta 1Con logging.basicConfig(level=logging.INFO), ¿qué nivel se suprime?
logging — Registrar en lugar de print
Aprende los 5 niveles de log y la configuración de formato con basicConfig, códigos como %(levelname)s, y salida a archivo con FileHandler para abandonar la depuración a base de print.
logging es el módulo estándar para registrar el comportamiento de tu app por etapas. Escribir salida de depuración con print da problemas: olvidas eliminarlo en producción, no puedes distinguir lo que es importante, y cambiar a salida a archivo después significa reescribir todo. Con logging, obtienes niveles de importancia (DEBUG / INFO / WARNING / ERROR / CRITICAL) para filtrar la salida, Formatter para formato unificado, y Handler para cambiar destinos (stdout / archivo / remoto).
logger.info(...) desde tu código, el flujo es Logger filtra por nivel → Handler decide el destino → Formatter ensambla el formato. Como las tres responsabilidades están separadas, cambiar el formato o el destino en un solo sitio cambia todo el comportamiento.Niveles de log — 5 grados de importancia
logging tiene 5 niveles, y solo se emiten los mensajes en el nivel definido en el Logger o por encima. La práctica estándar es DEBUG durante el desarrollo y INFO o WARNING en producción — y poder cambiar el volumen de salida desde una sola configuración sin tocar el código es la mayor diferencia con print.
Eligiendo entre WARNING / ERROR / DEBUG
Con el logger configurado, llama a niveles distintos de INFO y observa la salida. Como el nivel está fijado en INFO, DEBUG no produce salida alguna — confirma eso también. Esta es la base del comportamiento de logging de "cambia el volumen de salida desde una sola configuración".
Emitir logs a un archivo
Los proyectos reales casi siempre necesitan logs persistidos en un archivo, no solo en pantalla. Más tarde querrás grep para investigaciones post mortem o llevarlos a una herramienta de monitorización. Pasa filename="app.log" a basicConfig y el destino del root logger cambia a un archivo — hacer lo mismo con print significaría reescribir cada llamada a print, pero con logging cambias una línea de configuración para redirigir.
logging, alternas entre ambas en una línea de configuración.filemode es "a" (por defecto, append) o "w" (sobrescribir). Append es la elección de producción — conserva el historial. Sobrescribir suele ser más cómodo durante el desarrollo — empezar de cero cada vez. Para emitir tanto a pantalla como a archivo, te saltas basicConfig y combinas StreamHandler + FileHandler explícitamente (cubierto más adelante).
StreamHandler y FileHandler por separado y haz addHandler de cada uno.import logging
import os
# Crea primero la carpeta padre (FileHandler no la crea automáticamente)
os.makedirs("logs", exist_ok=True)
# Pasa filename a basicConfig para cambiar el destino a un archivo
logging.basicConfig(
level=logging.INFO,
format="[%(levelname)s] %(message)s",
filename="logs/app.log", # Salida a archivo
filemode="w", # "a" (append) o "w" (sobrescribir)
force=True,
)
logger = logging.getLogger("app")
logger.info("Iniciado")
logger.warning("Config obsoleta")
logger.error("Falló la conexión a BD")
# Lee el archivo para confirmar el contenido
with open("logs/app.log") as f:
print(f.read(), end="")
Define handlers en un archivo aparte
A medida que tu app crece, separa la configuración del logger (formato, nivel, destino) en un módulo dedicado y haz que cada módulo import desde él. Las configuraciones del logger ya no se duplican, y cambias formato/destino en un sitio. El patrón: construye un StreamHandler directamente, asocia un Formatter, regístralo con logger.addHandler(...) — todo eso vive en un archivo aparte.
main.py / orders.py / users.py) solo llama a setup_logger(__name__) para obtener un logger configurado. Para cambiar formato o destino, edita solo log_setup.py.# log_setup.py — módulo dedicado para la configuración del logger
import logging
def setup_logger(name):
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
if not logger.handlers: # Previene registro duplicado
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter("[%(name)s] [%(levelname)s] %(message)s")
)
logger.addHandler(handler)
logger.propagate = False # No propagar al root
return logger
# main.py — importa log_setup y lo usa
from log_setup import setup_logger
logger = setup_logger("orders")
logger.info("Pedido recibido")
Formato más detallado — timestamp / módulo / línea
La cadena de formato del Formatter admite códigos de reemplazo estilo %(...)s, permitiéndote empaquetar la información operativamente relevante en una sola línea. Aquí están los seis códigos más usados con ejemplos. Los nombres de logger pueden ser jerárquicos vía forma separada por puntos como padre.hijo.nieto — llamar a getLogger(__name__) desde cada módulo registra automáticamente "qué módulo emitió este log".
| Código | Qué imprime | Ejemplo |
|---|---|---|
| %(asctime)s | Timestamp | 2024-12-01 10:30:45 |
| %(name)s | Nombre del logger (separado por puntos para jerarquía) | app.orders |
| %(levelname)s | Nivel del log | INFO / WARNING / ERROR |
| %(funcName)s | Nombre de la función llamadora | process_order |
| %(lineno)d | Número de línea llamadora | 42 |
| %(message)s | Cuerpo del mensaje | Procesando pedido |
import logging
# Formato detallado para producción
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] [%(levelname)s] %(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
force=True,
)
logger = logging.getLogger("app.orders")
def process_order(order_id):
logger.info(f"Procesando pedido {order_id}")
process_order(1234)
# Salida de muestra:
# 2024-12-01 10:30:45 [app.orders] [INFO] process_order:13 - Procesando pedido 1234
Separa la configuración en un archivo yaml
Una vez que la configuración del logger se vuelve compleja, en lugar de hard-codear formato / handler / nivel en Python, sepáralos en un archivo de configuración yaml. logging.config.dictConfig(...) acepta una configuración en dict, así que solo parsea el yaml y pásalo para configurar todo el setup del logger. Tu base de código de repente gestiona "diferentes archivos yaml para producción / staging / desarrollo" sin cambios.
# logging.yml — fuente única para la configuración del logger
version: 1
disable_existing_loggers: false
formatters:
default:
format: "[%(name)s] [%(levelname)s] %(message)s"
handlers:
console:
class: logging.StreamHandler
formatter: default
level: INFO
loggers:
app:
level: INFO
handlers: [console]
propagate: false
| Clave | Rol | Notas |
|---|---|---|
| version | Versión del esquema dictConfig | Actualmente solo 1. Obligatorio. |
| disable_existing_loggers | Si deshabilitar los loggers existentes | false recomendado (true silencia loggers como app.orders de antes) |
| formatters | Lista nombrada de Formatters | Define con cualquier nombre (p. ej., default), referenciado desde handlers |
| formatters.<name>.format | Cadena de formato (códigos %(...)s) | Igual que el argumento de Formatter en código |
| handlers | Lista nombrada de Handlers (destinos) | Define console / file / mail, etc. |
| handlers.<name>.class | Nombre completamente cualificado de la clase Handler | logging.StreamHandler / logging.FileHandler / logging.handlers.RotatingFileHandler, etc. |
| handlers.<name>.formatter | Nombre del Formatter a aplicar | Usa una clave definida bajo formatters |
| handlers.<name>.level | Nivel por Handler | Filtra la salida más finamente que en el nivel del Logger |
| loggers | Lista nombrada de Loggers | loggers.app se obtiene vía logging.getLogger("app") |
| loggers.<name>.handlers | Lista de nombres de Handler asociados a este logger | Múltiples OK, como [console, file] |
| loggers.<name>.propagate | Si propagar a loggers padres | false detiene la propagación al root logger (evita doble salida) |
Rotación de logs
Si los logs siguen creciendo, el archivo se infla indefinidamente, así que producción necesita rotación — renombrar archivos antiguos a un nombre separado y eventualmente eliminarlos. logging.handlers viene con dos sabores — basado en tamaño y basado en tiempo — y eliges según el caso de uso.
RotatingFileHandler — basado en tamaño
RotatingFileHandler(filename, maxBytes, backupCount) es un Handler que cambia a un archivo nuevo cuando se excedería maxBytes. Los archivos antiguos se renombran con un sufijo numérico (app.log.1 / app.log.2), y una vez excedido backupCount el más antiguo se elimina. Con maxBytes=10_000_000 (10 MB) + backupCount=5, tus logs reciclan dentro de un techo de 60 MB.
maxBytes, se renombra app.log → app.log.1, y se crea un app.log nuevo para seguir escribiendo. Si app.log.N ya existe, se incrementa a app.log.N → app.log.N+1, y el archivo más antiguo más allá de backupCount se elimina.import logging
from logging.handlers import RotatingFileHandler
# Basado en tamaño: rota cuando se excede maxBytes
size_handler = RotatingFileHandler(
"app.log",
maxBytes=10_000_000, # Rota cuando se excedería esto (10 MB)
backupCount=5, # Conserva app.log.1 hasta app.log.5 (máx 60 MB total)
)
size_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
TimedRotatingFileHandler — basado en tiempo
TimedRotatingFileHandler(filename, when, interval, backupCount) rota en el límite de tiempo especificado por when. Usa when="midnight" (cada día a las 0:00), "H" (por hora), "M" (minuto), "S" (segundo), "D" (diario), etc. Los backups llevan sufijos de timestamp como app.log.2024-12-01_00-00-00, así que el nombre del archivo te dice "cuándo se escribió este log" directamente.
app.log. Los archivos timestamped más antiguos más allá de backupCount se eliminan automáticamente.import logging
from logging.handlers import TimedRotatingFileHandler
# Basado en tiempo: rota cada día a medianoche
day_handler = TimedRotatingFileHandler(
"app.log",
when="midnight", # "S" seg / "M" min / "H" hora / "D" día / "midnight", etc.
interval=1, # Cada N unidades (when="H", interval=6 significa cada 6 horas)
backupCount=30, # Conserva 30 días de historial
)
day_handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
)
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Cuando pasas filename="app.log" a logging.basicConfig, ¿adónde van los logs?
Pregunta 3¿Cuál es el principal beneficio de separar la configuración del logger (formato / nivel / handler) en un módulo aparte?