Observabilidad en Python con structlog

Publicado el: 24/07/2026
Tempo de leitura: 5 minutos
Desarrollador monitoreando registros estructurados en Python

La observabilidad permite comprender qué está haciendo una aplicación a partir de las señales que produce. En proyectos Python, una de las señales más útiles es el registro de eventos. Los mensajes de texto simples funcionan al principio, pero se vuelven difíciles de buscar y relacionar cuando el sistema crece. La observabilidad en Python con structlog mejora esta situación al representar cada evento como datos estructurados con campos estables.

En esta guía aprenderás a configurar structlog, añadir contexto de solicitudes, integrar la biblioteca estándar logging, proteger información confidencial, probar eventos importantes y preparar salidas diferentes para desarrollo y producción. También puedes consultar calidad de código con Ruff, descriptors avanzados en Python, búsqueda en PDF con RAG y Pydantic Settings.

Por qué importan los eventos estructurados

Un mensaje tradicional puede indicar que una operación falló para cierto usuario. Una persona entiende la frase, pero una herramienta debe analizar el texto para encontrar el identificador, el servicio, la duración y la categoría del error. Un evento estructurado guarda cada dato en un campo separado. Esto facilita filtros, paneles, alertas y correlación entre componentes.

Los nombres de eventos deben ser estables. Usa valores como request_started, order_created o job_completed. Los detalles variables deben ir en campos como request_id, order_id y duration_ms. Así, las búsquedas no dependen de frases que cambian con el tiempo.

Instalación y configuración inicial

python -m venv .venv
source .venv/bin/activate
pip install structlog

En Windows, activa el entorno con .venv\Scripts\activate. Una configuración práctica usa procesadores para añadir nivel, fecha, contexto y un renderizador JSON.

import logging
import structlog

logging.basicConfig(level=logging.INFO, format="%(message)s")

structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso", utc=True),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.JSONRenderer(),
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
    logger_factory=structlog.PrintLoggerFactory(),
    cache_logger_on_first_use=True,
)

log = structlog.get_logger("api")
log.info("service_started", version="1.0.0")

La documentación oficial de structlog explica procesadores e integraciones. La documentación de logging en Python describe niveles, handlers, filtros y formateadores.

Añadir contexto con bind

No es necesario repetir los mismos campos en cada llamada. Un logger enlazado conserva contexto para varios eventos relacionados.

request_log = log.bind(
    request_id="req-8f2a",
    user_id=42,
    endpoint="/orders",
)

request_log.info("request_received", method="POST")
request_log.info("order_created", order_id=991)
request_log.info("request_finished", duration_ms=83)

Los tres eventos comparten los mismos identificadores. Un operador puede filtrar por request_id y reconstruir el recorrido completo. Cuando el contexto cambia, crea otro logger o elimina campos específicos.

Context variables para código asíncrono

Los servidores asíncronos procesan muchas solicitudes al mismo tiempo. Un contexto global mutable puede mezclar datos entre tareas. Las variables de contexto de Python mantienen valores separados para cada ejecución.

from structlog.contextvars import bind_contextvars, clear_contextvars

async def handle_request(request):
    clear_contextvars()
    bind_contextvars(
        request_id=request.headers.get("X-Request-ID"),
        path=request.url.path,
    )
    log.info("request_started")
    try:
        return await process(request)
    finally:
        log.info("request_finished")
        clear_contextvars()

En FastAPI u otro framework ASGI, este patrón puede colocarse en un middleware. Limpiar el contexto al inicio y al final evita que información de una solicitud aparezca en otra.

Integrar logging estándar

Muchas dependencias escriben mediante logging.getLogger(). Conviene procesar esos registros con el mismo formato. ProcessorFormatter permite compartir procesadores entre eventos de structlog y registros estándar.

shared = [
    structlog.contextvars.merge_contextvars,
    structlog.processors.add_log_level,
    structlog.processors.TimeStamper(fmt="iso", utc=True),
]

formatter = structlog.stdlib.ProcessorFormatter(
    processor=structlog.processors.JSONRenderer(),
    foreign_pre_chain=shared,
)

handler = logging.StreamHandler()
handler.setFormatter(formatter)
root = logging.getLogger()
root.handlers.clear()
root.addHandler(handler)
root.setLevel(logging.INFO)

De esta forma, los eventos del servidor web, el controlador de base de datos y la aplicación pueden buscarse en un mismo flujo. Las bibliotecas demasiado ruidosas pueden configurarse con un nivel distinto.

Salida legible en desarrollo

JSON es adecuado para sistemas de ingestión, pero una consola legible ayuda durante el desarrollo. Cambia únicamente el renderizador final según el entorno.

import os

is_dev = os.getenv("APP_ENV", "development") == "development"
renderer = (
    structlog.dev.ConsoleRenderer(colors=True)
    if is_dev
    else structlog.processors.JSONRenderer()
)

Los nombres y campos deben mantenerse iguales en todos los entornos. Si producción usa una estructura diferente, los problemas pueden aparecer después del despliegue y las pruebas pierden valor.

Registrar excepciones con contexto útil

Registra una excepción en la capa que decide si debe reintentarse, convertirse o propagarse. Evita escribir el mismo stack trace en varias funciones.

try:
    result = process_document(document_id)
except DocumentError:
    log.exception(
        "document_processing_failed",
        document_id=document_id,
    )
    raise

El procesador format_exc_info incorpora los detalles de la excepción. Añade identificadores y el nombre de la operación, pero evita adjuntar objetos completos cuando bastan unos pocos campos.

Proteger información confidencial

Los registros no deben incluir credenciales, valores de sesión, cabeceras privadas ni datos personales innecesarios. Es mejor declarar campos concretos que registrar cuerpos completos de solicitudes o configuraciones.

PRIVATE_FIELDS = {"secret", "credential", "session_value"}

def remove_private_fields(logger, method_name, event_dict):
    for key in PRIVATE_FIELDS:
        if key in event_dict:
            event_dict[key] = "[REMOVED]"
    return event_dict

Coloca este procesador antes del renderizador. Es una protección adicional, no un sustituto de un diseño cuidadoso. Revisa nuevos eventos durante el code review y establece reglas de retención.

Probar eventos estructurados

Los eventos importantes forman parte del comportamiento observable. structlog incluye una utilidad de captura.

import structlog

def create_report(report_id):
    structlog.get_logger().info("report_created", report_id=report_id)

def test_report_event():
    with structlog.testing.capture_logs() as events:
        create_report(17)

    assert events[0]["event"] == "report_created"
    assert events[0]["report_id"] == 17

Comprueba campos estables y evita depender del orden de claves JSON o de timestamps exactos. Añade pruebas que confirmen la ausencia de campos confidenciales.

Controlar volumen y coste

Cada evento consume CPU, serialización, red y almacenamiento. Evita payloads grandes y registros repetidos dentro de bucles intensivos. Usa métricas para cantidades agregadas y logs para contexto detallado. Los eventos de éxito muy frecuentes pueden muestrearse.

Supervisa el volumen después de cada despliegue. Un cambio pequeño puede multiplicar la ingestión diaria. Configura retención, rotación, compresión y niveles apropiados para cada entorno.

Definir un vocabulario común

Campos habituales son service, environment, version, event, level, timestamp, request_id, trace_id y duration_ms. No todos deben aparecer siempre.

Documenta los esquemas importantes como una pequeña API interna. Cambiar el nombre de un campo puede romper alertas y paneles. En sistemas distribuidos, propaga el mismo identificador entre llamadas HTTP, tareas y servicios.

Checklist para producción

  • Usar timestamps UTC en formato ISO 8601.
  • Emitir un objeto JSON por línea.
  • Mantener nombres de eventos estables.
  • Añadir contexto con context variables.
  • Integrar logging estándar.
  • Eliminar información confidencial.
  • Probar eventos críticos.
  • Controlar bibliotecas ruidosas.
  • Supervisar volumen y retención.
  • Propagar identificadores de correlación.

Conclusión

La observabilidad en Python con structlog convierte mensajes aislados en eventos consistentes y fáciles de buscar. Los procesadores añaden nivel y fecha, los loggers enlazados incorporan contexto, las variables de contexto soportan aplicaciones asíncronas y JSON facilita la integración con plataformas de monitoreo.

Empieza por límites de solicitudes, operaciones de negocio, llamadas externas, tareas en segundo plano y excepciones. Mantén un vocabulario pequeño y estable. Añade campos cuando respondan preguntas operativas reales. Un buen sistema de logs no registra todo: registra el contexto correcto para diagnosticar problemas con rapidez.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Configuración segura de aplicaciones Python con Pydantic Settings
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Pydantic Settings: configuración segura

    Aprende a validar variables de entorno, organizar configuraciones y proteger secretos en proyectos Python con Pydantic Settings.

    Ler mais

    Tempo de leitura: 5 minutos
    23/07/2026
    Desenvolvedor programando em Python com Ruff para lint e formatação
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Ruff en Python: lint y formato de código paso a paso

    Aprende Ruff en Python para lint, formato, correcciones automáticas, pyproject.toml, VS Code y CI con una configuración práctica.

    Ler mais

    Tempo de leitura: 10 minutos
    22/07/2026
    Dicas para melhorar performance de scripts Python lentos
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Por qué Python puede ser lento y cómo mejorar su rendimiento

    Descubre por qué Python puede ser lento y mejora su rendimiento con cProfile, algoritmos, sets, generadores, NumPy, caché y concurrencia.

    Ler mais

    Tempo de leitura: 5 minutos
    12/07/2026
    Leitura segura de senhas no terminal usando Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Cómo leer contraseñas de forma segura en el terminal con Python

    Lee contraseñas de forma segura con getpass, valida entradas, evita logs y texto plano y almacena credenciales con hashing adecuado.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Exemplo de testes unitários em Python com código de unittest para validação automatizada
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Tests unitarios en Python: unittest, pytest y mocks

    Aprende tests unitarios en Python con unittest, pytest, fixtures, parametrización, mocks, cobertura y buenas prácticas para evitar regresiones.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Proteção de API Flask usando autenticação JWT em Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Cómo proteger una API Flask con JWT

    Protege una API Flask con JWT, access y refresh tokens, bcrypt, roles, revocación, variables de entorno, HTTPS y pruebas de

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026