Los logs estructurados representan cada evento como campos, no como una frase que debe interpretarse después. Así es posible filtrar por request_id, operación, duración y resultado.

Configurar JSON

import logging
import structlog

logging.basicConfig(level=logging.INFO)
structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso", utc=True),
        structlog.processors.JSONRenderer(),
    ]
)

log = structlog.get_logger("pagos")
log.info("pago_consultado", pago_id="pg_123", duracion_ms=42)

Usa nombres de evento estables y campos con tipos coherentes. El tutorial de logging en Python explica niveles y handlers.

Añadir contexto con seguridad

Asocia un request_id al inicio mediante structlog.contextvars y limpia el contexto al finalizar. Verifica la propagación cuando el trabajo pasa a otro hilo o proceso.

Los IDs internos pueden ser útiles, pero las contraseñas, tokens, cookies y datos personales no deben registrarse. El almacenamiento de logs suele tener otra retención y permisos.

Captura eventos en pruebas y verifica nombre y campos obligatorios, no el orden del JSON. Trata cambios del schema como cambios de interfaz si existen dashboards o alertas.

La documentación oficial de structlog, consultada el 22 de julio de 2026, cubre procesadores, JSON, integración con logging y variables de contexto. La estructura facilita las consultas, pero aún necesitas eventos claros, cardinalidad controlada y privacidad.

Comprender la cadena de procesadores

Cada llamada comienza como un diccionario de evento. Los procesadores lo reciben en orden, añaden o transforman campos y un renderer genera la salida final. El orden importa: nivel y timestamp deben agregarse antes de JSONRenderer, que termina la cadena.

En desarrollo puedes cambiar solo el renderer:

import os

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

Mantén iguales los campos anteriores al renderer en todos los entornos. De lo contrario, un dashboard puede esperar duration_ms cuando el equipo solo ha visto elapsed. Los colores ayudan en un terminal interactivo, pero conviene desactivarlos al redirigir la salida.

Añade procesadores de pila y excepciones cuando hagan falta. Registrar únicamente str(exc) pierde el tipo y el traceback. Dentro del manejador usa log.exception("pedido_fallido", pedido_id=pedido_id). Conserva un nombre de evento estable y coloca detalles variables en campos.

Integrar logging estándar

Las dependencias suelen escribir mediante logging. Si la aplicación configura una salida independiente para structlog, los eventos externos tendrán otro formato. ProcessorFormatter permite procesar ambos caminos de forma coherente. La configuración exacta depende de handlers y servidor, pero debe realizarse una vez en el punto de entrada.

Las líneas duplicadas suelen indicar que un logger tiene handler propio y además propaga al logger raíz. Examina handlers, propagate y la configuración del servidor ASGI antes de añadir filtros. Una biblioteca reutilizable no debería llamar a basicConfig() durante la importación, pues la aplicación decide destino y nivel.

Los niveles forman parte del contrato. Usa debug para diagnóstico detallado, info para hitos normales, warning para una condición recuperable, error si falla la operación y exception cuando se necesita traceback. Una validación esperada no debe crear una traza alarmante por petición.

Propagar contexto

Limpia el contexto al entrar en cada petición para que un worker reutilizado no conserve campos anteriores. Después vincula identificadores seguros:

from structlog.contextvars import bind_contextvars, clear_contextvars

async def middleware(request, call_next):
    clear_contextvars()
    request_id = request.headers.get("X-Request-ID") or crear_id()
    bind_contextvars(request_id=request_id, method=request.method)
    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    return response

Valida el ID recibido y limita su longitud. Un cliente no debe insertar saltos de línea ni miles de caracteres. Vincula campos como order_id solo durante la operación correspondiente y elimínalos después para no contaminar eventos posteriores.

Las variables de contexto acompañan tareas asíncronas dentro del mismo contexto, pero no siempre pasan automáticamente entre hilos, colas y procesos. Incluye el ID de correlación en el mensaje y vuelve a vincularlo en el worker. No copies todo el contexto sin revisarlo porque puede contener datos inadecuados para el sistema receptor.

Definir un schema de eventos

Elige nombres y tipos previsibles: duration_ms siempre numérico, success booleano y timestamps UTC. No alternes entre "42 ms", 42 y 0.042. Un catálogo breve de eventos críticos ayuda a crear alertas y reduce diferencias entre servicios.

Controla la cardinalidad. Una ruta normalizada como "/users/{id}" se agrega mejor que cada URL concreta. Los identificadores pueden servir para investigar en el cuerpo consultable, sin convertirse en labels indexados. Controla también el volumen: un resumen del lote con cantidad, fallos y duración suele aportar más que un evento por elemento.

Proteger información sensible

Prefiere una lista positiva de campos permitidos en operaciones sensibles. Intentar eliminar después todas las variantes de contraseña, token o autorización es frágil. Nunca registres headers completos, cadenas de conexión, cuerpos enteros ni objetos de usuario. Omite identificadores personales innecesarios o aplica la seudonimización aprobada.

La redacción debe ocurrir antes del renderer y de cualquier handler externo. Un procesador puede sustituir claves conocidas por "[REDACTED]", pero debe contemplar estructuras anidadas y tener pruebas. El almacenamiento también necesita retención, controles de acceso y procedimientos de eliminación apropiados.

Probar y operar

structlog.testing.capture_logs() sirve para pruebas pequeñas, aunque omite algunos procesadores configurados. Para validar la cadena real, envía la salida a un stream, interpreta cada línea JSON y comprueba campos y tipos. Prueba excepciones, limpieza de contexto y ausencia de secretos representativos.

En producción suele ser más sencillo escribir JSON en la salida estándar y permitir que la plataforma lo recopile. La rotación, retención y entrega quedan fuera del proceso web. Si el destino falla, define si el logging bloquea, almacena temporalmente o descarta para que una avería de observabilidad no detenga el servicio inesperadamente.

Comienza con pocos eventos que respondan preguntas concretas: qué operación falló, para qué recurso, cuánto tardó y qué correlación conecta el trabajo. Revisa consultas y alertas periódicamente. El valor del logging estructurado está en acelerar el diagnóstico, no en acumular JSON.

Antes de desplegar, documenta quién consume cada campo crítico y prepara una consulta real con eventos de prueba. Esa revisión revela nombres ambiguos, tipos inconsistentes y datos que no deberían salir de la aplicación. También conviene definir qué ocurre cuando el colector está lento o no está disponible: el equipo debe conocer si el proceso bloquea, descarta o almacena temporalmente los registros.

La migración desde mensajes tradicionales puede hacerse por etapas. Empieza por operaciones de mayor impacto, conserva niveles coherentes y transforma dashboards solo cuando el nuevo evento ya esté llegando. No dupliques indefinidamente la línea antigua y el evento JSON, porque elevarás costes y podrías disparar dos alertas por el mismo fallo. Con un schema pequeño, pruebas automatizadas y una política de privacidad explícita, structlog se convierte en una interfaz operativa mantenible.