Aprende leyendo en orden

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).

Cómo logging procesa una llamada
Códigologger.info(...)LoggerFiltra por nivelHandlerElige destinoFormatterEnsambla formato
Cuando llamas a logger.info(...) desde tu código, el flujo es Logger filtra por nivelHandler decide el destinoFormatter 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.

Los 5 niveles de logging
DEBUGDetalleINFOProgresoWARNINGPrecauciónERRORFalloCRITICALFatal
DEBUG (detalle) / INFO (progreso normal) / WARNING (precaución) / ERROR (fallo) / CRITICAL (catastrófico). Solo los mensajes en el nivel del Logger o por encima se emiten — DEBUG en desarrollo, INFO/WARNING en producción es el patrón estándar.

Primero, configura formato y nivel con basicConfig y emite un log INFO.

① Importa logging y configura basicConfig con level=logging.INFO, format="[%(levelname)s] %(message)s", force=True

② Obtén el logger vía logger = logging.getLogger("app")

③ Emite un log vía logger.info("App iniciada")

(Si tu código se ejecuta correctamente, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

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".

Reutiliza la configuración de logger de la Práctica 1 y emite en tres niveles. Nota: con INFO, DEBUG no aparece.

① Emite WARNING vía logger.warning("Archivo de config obsoleto")

② Emite ERROR vía logger.error("Falló la conexión a BD")

③ Llama a logger.debug("Traza detallada") — en nivel INFO, no debe imprimir nada

Editor Python

Ejecutar el código para ver el resultado

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.

Salida a pantalla vs. salida a archivo
logger.info(...)(solo pantalla)Terminal se cierra→ Logs perdidoslogger.info(...)filename=app.loggrep posterior / ingestaen herramientas de monitor
La salida a pantalla desaparece cuando el terminal se cierra, pero la salida a archivo sobrevive para grep post mortem e ingestión en herramientas de monitorización. Con 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).

Combinaciones de filemode y Handler
filemode="a" append→ Conserva historial (prod)filemode="w" sobrescribir→ Empezar de cero (dev)StreamHandler→ Salida a pantallaFileHandler→ Salida a archivo
filemode="a" añade al final y conserva historial (producción), mientras que filemode="w" sobrescribe para empezar de cero en cada ejecución (desarrollo). Para enviar tanto a pantalla como a archivo, construye 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="")

Pasa filename a basicConfig para cambiar logs a un archivo, luego lee el contenido de vuelta del disco.

① Usa os.makedirs("logs", exist_ok=True) para crear primero la carpeta padre (FileHandler no la crea automáticamente)

② Importa logging y llama a basicConfig con level=logging.INFO, format="[%(levelname)s] %(message)s", filename="logs/app.log", filemode="w", force=True

③ Obtén el logger con logger = logging.getLogger("app")

④ Emite 3 logs en INFO / WARNING / ERROR con los mensajes "Iniciado", "Config obsoleta", "Falló la conexión a BD"

⑤ Abre logs/app.log para lectura, luego bajo el encabezado --- contenido de logs/app.log --- imprime el contenido sin nueva línea final (print(content, end=""))

Editor Python

Ejecutar el código para ver el resultado

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.

Centraliza log_setup; los módulos importan desde él
log_setup.py(formato, destino, nivel)main.pyorders.pyusers.pyimportimportimport
log_setup.py es la fuente única para la configuración del logger (formato, destino, nivel). Cada módulo (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")

log_setup.py está provisto (abre el panel de archivos 📂 a la izquierda de la consola para inspeccionarlo). Desde main.py, importa setup_logger y emite 3 logs con el logger nombrado "orders".

① Importa la función vía from log_setup import setup_logger

② Obtén un logger con logger = setup_logger("orders")

③ Emite 3 logs en INFO / WARNING / ERROR: "Pedido recibido", "Stock bajo", "La API de pago devolvió un error"

Editor Python

Ejecutar el código para ver el resultado

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ódigoQué imprimeEjemplo
%(asctime)sTimestamp2024-12-01 10:30:45
%(name)sNombre del logger (separado por puntos para jerarquía)app.orders
%(levelname)sNivel del logINFO / WARNING / ERROR
%(funcName)sNombre de la función llamadoraprocess_order
%(lineno)dNúmero de línea llamadora42
%(message)sCuerpo del mensajeProcesando 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

Usa 3 nombres de logger jerárquicos — app / app.orders / app.orders.payment — y muestra de qué módulo viene cada log con %(name)s. En producción añadirías %(asctime)s para el timestamp, pero aquí nos quedamos solo con la jerarquía para que la salida sea determinista.

① Importa logging y configura basicConfig con level=logging.INFO, format="[%(name)s] [%(levelname)s] %(message)s", force=True

② Obtén 3 loggers: logging.getLogger("app"), logging.getLogger("app.orders"), logging.getLogger("app.orders.payment")

③ Emite un log de cada uno: app llama a info("App iniciada"), app.orders llama a info("Pedido recibido"), app.orders.payment llama a error("Error de API de pago")

Editor Python

Ejecutar el código para ver el resultado

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.

Flujo archivo de configuración yaml → dictConfig
logging.ymlformato / handler / nivelyaml.safe_load()+ dictConfig()Logger configuradologger.info(...)
Escribe formato / handler / nivel en logging.yml, luego conviértelo a un dict vía yaml.safe_load() y pásalo a logging.config.dictConfig(...). El lado Python solo carga el yaml y llama a dictConfig, y el logger queda configurado.
# 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
ClaveRolNotas
versionVersión del esquema dictConfigActualmente solo 1. Obligatorio.
disable_existing_loggersSi deshabilitar los loggers existentesfalse recomendado (true silencia loggers como app.orders de antes)
formattersLista nombrada de FormattersDefine con cualquier nombre (p. ej., default), referenciado desde handlers
formatters.<name>.formatCadena de formato (códigos %(...)s)Igual que el argumento de Formatter en código
handlersLista nombrada de Handlers (destinos)Define console / file / mail, etc.
handlers.<name>.classNombre completamente cualificado de la clase Handlerlogging.StreamHandler / logging.FileHandler / logging.handlers.RotatingFileHandler, etc.
handlers.<name>.formatterNombre del Formatter a aplicarUsa una clave definida bajo formatters
handlers.<name>.levelNivel por HandlerFiltra la salida más finamente que en el nivel del Logger
loggersLista nombrada de Loggersloggers.app se obtiene vía logging.getLogger("app")
loggers.<name>.handlersLista de nombres de Handler asociados a este loggerMúltiples OK, como [console, file]
loggers.<name>.propagateSi propagar a loggers padresfalse detiene la propagación al root logger (evita doble salida)

logging.yml está provisto (abre el panel de archivos 📂 para inspeccionarlo). Parsea a un dict vía yaml.safe_load, luego pásalo a logging.config.dictConfig para configurar el logger.

① Importa logging, logging.config y yaml

② Abre con with open("logging.yml") as f: y convierte a un dict vía yaml.safe_load(f)

③ Pasa el dict a logging.config.dictConfig(...) para aplicar

④ Obtén el logger vía logger = logging.getLogger("app") y emite 1 INFO y 1 WARNING: "Iniciado con config yaml" y "WARNING vía yaml"

Editor Python

Ejecutar el código para ver el resultado

Rotación de logs

Si los logs siguen creciendo, el archivo se infla indefinidamente, así que producción necesita rotaciónrenombrar 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.

Dos tipos: basado en tamaño y basado en tiempo
RotatingFileHandlerBasado en tamaño(rota cuando se excede maxBytes)TimedRotatingFileHandlerBasado en tiempo(rota en los límites de when)
Basado en tamaño encaja con logs de depuración donde quieres un tope estricto del uso de disco. Basado en tiempo encaja con logs de acceso que quieres dividir por fecha para análisis o herramientas de monitorización.

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.

Cómo funciona RotatingFileHandler
app.logEscribiendomaxBytes excedido→ rotaapp.log.1app.log.2 ...(backups)Más allá de backupCount→ Más antiguo eliminado
Cuando el archivo activo app.log se acerca a 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"))

rotate_logging.yml está provisto (abre el panel de archivos 📂 para inspeccionarlo). La configuración de RotatingFileHandler (maxBytes=60 / backupCount=2) vive en yaml, y el código simplemente lo carga y lo usa.

① Importa os / shutil, elimina y recrea la carpeta rotate/. También limpia todos los handlers del logger rotate_demo con close + removeHandler (evita interferencias en re-ejecuciones)

② Abre el yaml con with open("rotate_logging.yml"), convierte vía yaml.safe_load(f), y pásalo a logging.config.dictConfig(...) para configurar el handler

③ Obtén logger = logging.getLogger("rotate_demo") y emite 15 logs en un bucle: for i in range(15): logger.info(f"event {i:02d}")

④ Desde os.listdir("rotate"), toma solo los archivos que empiezan con app.log, ordénalos, y bajo el encabezado Archivos tras rotación: ◯ imprime cada nombre de archivo como - nombre_archivo en su propia línea

Editor Python

Ejecutar el código para ver el resultado

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.

Cómo funciona TimedRotatingFileHandler
app.logEscribiendoPróximo when (tiempo)→ rotaapp.log.2024-12-01app.log.2024-12-02(sufijo de timestamp)Más allá de backupCount→ Más antiguo eliminado
En el momento especificado por when (cada día a las 0:00, etc.), el app.log activo se renombra con un nombre con sufijo de timestamp (p. ej., app.log.2024-12-01_00-00-00) y se abre un nuevo 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")
)

time_logging.yml está provisto. La configuración de TimedRotatingFileHandler (when: S (por segundo) / interval: 1 / backupCount: 3) vive en yaml, y el código Python usa time.sleep para simular el paso del tiempo y disparar rotaciones.

① Importa logging / os / shutil / time. Limpia todos los handlers del logger trotate_demo con close + removeHandler, luego elimina y recrea la carpeta trotate/

② Abre time_logging.yml, convierte a un dict vía yaml.safe_load(f), y pásalo a logging.config.dictConfig(...) para configurar

③ Obtén el logger y emite 3 logs con sleep entre ellos: logger.info(...)time.sleep(1.2)logger.info(...)time.sleep(1.2)logger.info(...), disparando 2 rotaciones

④ Desde os.listdir("trotate"), separa app.log (log actual) del resto (backups con timestamp), luego imprime Log actual presente: True/False y Cantidad de backups: ◯

Editor Python

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Con logging.basicConfig(level=logging.INFO), ¿qué nivel se suprime?

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?