LoggerAdapter merge_extra: contexto dinámico en logs

Publicado el: 03/10/2026
Tempo de leitura: 5 minutos
Desarrollador configurando logs estructurados con LoggerAdapter merge_extra en Python

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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Pantalla de portátil con código para análisis TLS usando ssl keylog_filename en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ssl keylog_filename: analiza TLS en Wireshark

    Aprende ssl keylog_filename en Python para inspeccionar sesiones TLS autorizadas en Wireshark sin desactivar el cifrado.

    Ler mais

    Tempo de leitura: 5 minutos
    02/10/2026
    Portátil con código y base SQLite para sqlite3 autocommit en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    sqlite3 autocommit: controla transacciones en Python

    Aprende sqlite3 autocommit en Python para controlar transacciones, commits, rollbacks, compatibilidad y bloqueos de SQLite.

    Ler mais

    Tempo de leitura: 5 minutos
    02/10/2026
    Programador trabajando con objetos inmutables y copy.replace en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    copy.replace: actualiza objetos inmutables en Python

    Aprende copy.replace en Python para crear nuevas versiones de objetos con cambios puntuales, inmutabilidad y validación segura.

    Ler mais

    Tempo de leitura: 5 minutos
    01/10/2026
    Estructura de archivos y código para pathlib.Path.info en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pathlib.Path.info: caché de metadatos de archivos

    Aprende pathlib.Path.info en Python para clasificar archivos con metadatos en caché y optimizar recorridos de directorios.

    Ler mais

    Tempo de leitura: 5 minutos
    01/10/2026
    Portátil con material de pruebas en Python para loop_factory y asyncio
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    loop_factory: aísla event loops en pruebas asyncio

    Aprende loop_factory en IsolatedAsyncioTestCase para pruebas asyncio aisladas, predecibles y con limpieza segura.

    Ler mais

    Tempo de leitura: 5 minutos
    30/09/2026
    Desarrolladora navegando archivos ZIP con zipfile.Path en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    zipfile.Path: navega ZIPs sin extraer archivos

    Aprende zipfile.Path en Python para navegar, leer y validar archivos dentro de ZIPs sin extraer todo.

    Ler mais

    Tempo de leitura: 4 minutos
    30/09/2026