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 structlogEn 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,
)
raiseEl 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_dictColoca 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"] == 17Comprueba 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.







