Los registros útiles necesitan contexto. Un mensaje como “falló el pago” resulta mucho más práctico cuando incluye el identificador de la solicitud, el usuario, el servicio, el entorno, el pago y la etapa del proceso. logging.LoggerAdapter permite añadir contexto compartido a muchos registros. En versiones recientes de Python, el parámetro merge_extra facilita combinar ese contexto permanente con el diccionario extra enviado en una llamada concreta.
En esta guía aprenderás qué cambia merge_extra, cómo usarlo en logs estructurados, cómo manejar colisiones de claves y cómo evitar que información sensible termine en los registros.
Qué hace LoggerAdapter
LoggerAdapter envuelve un logger normal y agrega campos a cada registro. Es útil cuando una secuencia de mensajes pertenece a la misma solicitud, tarea, cliente o transacción.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s %(request_id)s %(message)s",
)
base_logger = logging.getLogger("api")
logger = logging.LoggerAdapter(
base_logger,
{"request_id": "req-123"},
)
logger.info("solicitud recibida")
El adaptador coloca su diccionario en el LogRecord. Después, un formateador puede incluir esos valores en texto, JSON u otro formato.
Por qué merge_extra es importante
Sin fusión, el contexto guardado en el adaptador puede impedir que los campos específicos de una llamada se combinen como el desarrollador espera. Antes era común crear un adaptador nuevo, copiar diccionarios manualmente o implementar una subclase.
Con merge_extra=True, los campos permanentes y los campos enviados en una llamada se combinan. Este patrón es ideal cuando todos los eventos necesitan datos de correlación compartidos, pero cada evento también posee detalles propios.
Ejemplo básico
import logging
logging.basicConfig(
level=logging.INFO,
format=(
"%(levelname)s request=%(request_id)s "
"user=%(user_id)s order=%(order_id)s %(message)s"
),
)
base_logger = logging.getLogger("checkout")
logger = logging.LoggerAdapter(
base_logger,
{"request_id": "req-987", "user_id": "u-42"},
merge_extra=True,
)
logger.info(
"pedido validado",
extra={"order_id": "ord-1001"},
)
El registro contiene los tres identificadores. La solicitud y el usuario pertenecen al adaptador, mientras que el pedido pertenece únicamente a ese evento.
Colisiones de claves
Cuando una clave aparece en ambos diccionarios, el valor específico de la llamada puede sustituir al valor del adaptador. Puede ser útil, pero también puede romper el significado de un contexto que debería permanecer estable.
logger.info(
"acción realizada en nombre de otro usuario",
extra={"user_id": "u-admin", "order_id": "ord-1002"},
)
Si user_id identifica al usuario autenticado durante toda la solicitud, reemplazarlo genera registros engañosos. Es mejor usar nombres separados como authenticated_user_id, actor_user_id y target_user_id. El conjunto de campos de logging también funciona como una API y debe documentarse.
Logging por solicitud
Un patrón limpio consiste en crear un adaptador al inicio de la solicitud y pasarlo a los servicios internos. Así se evita el estado global mutable y las dependencias quedan explícitas.
def crear_logger_solicitud(base_logger, request_id, user_id):
return logging.LoggerAdapter(
base_logger,
{
"request_id": request_id,
"user_id": user_id,
"service": "payments",
},
merge_extra=True,
)
def procesar_pago(logger, payment_id):
logger.info(
"pago iniciado",
extra={"payment_id": payment_id, "stage": "start"},
)
Para ampliar estos conceptos, consulta los artículos de Academify sobre logging en Python, contextvars en Python, decoradores en Python y excepciones en Python.
Logs JSON estructurados
LoggerAdapter no genera JSON por sí solo. Enriquece el registro para que un formateador JSON pueda serializar los campos.
class JsonFormatter(logging.Formatter):
def format(self, record):
import json
payload = {
"level": record.levelname,
"message": record.getMessage(),
"request_id": getattr(record, "request_id", None),
"user_id": getattr(record, "user_id", None),
"order_id": getattr(record, "order_id", None),
}
return json.dumps(payload, ensure_ascii=False)
Los registros estructurados se filtran y agregan con facilidad en Elasticsearch, Loki, Datadog, CloudWatch y OpenSearch. La documentación oficial de logging de Python explica la API, y el Logging Cookbook muestra patrones avanzados.
Evita nombres reservados
Un LogRecord ya contiene atributos como name, levelname, filename, module y message. Intentar sobrescribir nombres reservados mediante extra puede producir una excepción. Define una convención estable, por ejemplo request_id, customer_id, job_id, operation y duration_ms.
Protección de datos sensibles
Nunca registres contraseñas, tokens de acceso, cookies completas, claves de API, datos de tarjetas o información personal innecesaria. Como la fusión facilita añadir diccionarios arbitrarios, también aumenta el riesgo de filtrar información. Usa una lista de campos permitidos.
CAMPOS_PERMITIDOS = {"order_id", "payment_id", "stage", "duration_ms"}
def extra_seguro(datos):
return {
clave: valor
for clave, valor in datos.items()
if clave in CAMPOS_PERMITIDOS
}
También conviene truncar textos largos y normalizar valores. Un objeto enorme o mal formado no debería producir registros gigantes ni bloquear el sistema de observabilidad.
Compatibilidad de versiones
Comprueba la versión mínima de Python del proyecto antes de pasar merge_extra. Un intérprete antiguo puede rechazar el argumento. Una función de compatibilidad puede inspeccionar el constructor y usar el adaptador tradicional como alternativa.
import inspect
import logging
def crear_adapter(logger, contexto):
parametros = inspect.signature(logging.LoggerAdapter).parameters
if "merge_extra" in parametros:
return logging.LoggerAdapter(
logger,
contexto,
merge_extra=True,
)
return logging.LoggerAdapter(logger, contexto)
Si la fusión por llamada es imprescindible en versiones antiguas, crea una subclase pequeña cuyo método process combine los diccionarios de forma explícita. Prueba especialmente la prioridad en caso de colisión.
Pruebas automatizadas
Las pruebas deben verificar el mensaje y los campos adjuntos. En pytest, caplog permite inspeccionar los objetos LogRecord capturados.
def test_contexto_log(caplog):
base = logging.getLogger("test")
adapter = logging.LoggerAdapter(
base,
{"request_id": "req-test"},
merge_extra=True,
)
with caplog.at_level(logging.INFO):
adapter.info("ok", extra={"order_id": "ord-test"})
registro = caplog.records[0]
assert registro.request_id == "req-test"
assert registro.order_id == "ord-test"
También es útil probar colisiones, campos opcionales ausentes, redacción y serialización JSON. El código de logging suele ejecutarse durante errores, por lo que no debe lanzar una segunda excepción que oculte el problema original.
Uso en código asíncrono
Pasar el adaptador de manera explícita funciona bien en muchas aplicaciones async. En frameworks con muchas capas, contextvars puede ser una forma más cómoda de mantener valores locales a la solicitud. Un filtro de logging puede copiar esos valores a cada registro. Ambas técnicas se complementan: las variables de contexto aportan datos estables y merge_extra añade campos específicos del evento.
Cuándo usar merge_extra
Úsalo cuando varios registros comparten contexto estable y cada evento necesita atributos adicionales. Casos comunes son solicitudes HTTP, tareas en colas, operaciones de CLI, pipelines de datos, tareas programadas y transacciones distribuidas.
Un logger simple es suficiente para scripts pequeños. Un filtro puede ser mejor para campos globales del proceso. Una variable de contexto puede ser mejor para contexto async implícito. El adaptador destaca cuando deseas contexto explícito, comprobable y ligado a una operación.
Lista de comprobación
- Define nombres y significados estables.
- Evita atributos reservados de
LogRecord. - Decide si una llamada puede sobrescribir valores del adaptador.
- Usa una lista permitida para datos de usuarios o solicitudes.
- Prueba la salida formateada y los atributos del registro.
- Confirma la compatibilidad con la versión de Python.
- Evita que un fallo de logging oculte el error de la aplicación.
Conclusión
LoggerAdapter con merge_extra es una base práctica para logs estructurados en Python. Combina campos permanentes de correlación con detalles específicos del evento, reduce la manipulación repetitiva de diccionarios y mantiene el contexto visible en el código. Con nombres claros, reglas de colisión, pruebas, compatibilidad y protección de datos sensibles, los registros resultan más fáciles de buscar, depurar y confiar.







