Cristhian Villegas
Backend15 min read13 views

Mejora tus Logs y Trazabilidad con MDC en Spring Boot, Python y Node.js

Mejora tus Logs y Trazabilidad con MDC en Spring Boot, Python y Node.js

Introduccion: el problema de encontrar una aguja en un pajar de logs

Imagina un sistema en produccion con 50 microservicios procesando miles de requests por segundo. Un usuario reporta un error. Abres los logs y te encuentras con millones de lineas entremezcladas de distintos hilos, servicios y usuarios. ¿Como encuentras exactamente las lineas relacionadas con ese request especifico?

La respuesta es MDC (Mapped Diagnostic Context): un mecanismo que permite agregar informacion contextual a cada linea de log automaticamente, sin modificar el codigo de negocio. En este articulo exploraremos que es MDC, por que es fundamental para la trazabilidad, y como implementarlo con ejemplos reales en Spring Boot, Python y Node.js.

Servidores en un centro de datos representando infraestructura de microservicios

Fuente: Taylor Vick — Unsplash

¿Que es MDC (Mapped Diagnostic Context)?

MDC es un mapa clave-valor asociado al hilo de ejecucion actual (thread-local) que los frameworks de logging pueden inyectar automaticamente en cada linea de log. Funciona como un diccionario invisible que viaja con cada request a traves de toda la cadena de ejecucion.

ConceptoDescripcion
Thread-Local StorageCada hilo tiene su propia copia del MDC, evitando colisiones entre requests concurrentes
Clave-ValorSe almacenan pares como traceId=abc123, userId=42, sessionId=xyz
TransparenteUna vez configurado, cada log.info() incluye automaticamente los valores del MDC sin cambiar el codigo
PropagableSe puede propagar entre microservicios via headers HTTP
📌 Analogia: MDC es como un brazalete de hospital. Una vez que se lo pones al paciente (request), cada doctor, enfermera o laboratorio (servicio, metodo, capa) puede leer la informacion sin tener que preguntar cada vez "¿quien es este paciente?".

Equivalentes en cada lenguaje

LenguajeMecanismoLibreria / API
JavaThreadLocal (MDC nativo)SLF4J + Logback / Log4j2
Pythoncontextvars (PEP 567)logging + Filter personalizado
Node.jsAsyncLocalStoragenode:async_hooks (built-in)

¿Por que es importante para la trazabilidad?

Sin MDC, tus logs se ven asi:

bash
1INFO  OrderService - Processing order
2INFO  PaymentService - Payment received
3ERROR OrderService - Failed to update inventory
4INFO  PaymentService - Sending confirmation email
5INFO  OrderService - Processing order
6ERROR PaymentService - Payment declined

¿Cual linea pertenece a cual request? Imposible saberlo. Con MDC, los mismos logs se ven asi:

bash
1INFO  [traceId=a1b2c3 userId=42] OrderService - Processing order
2INFO  [traceId=a1b2c3 userId=42] PaymentService - Payment received
3ERROR [traceId=a1b2c3 userId=42] OrderService - Failed to update inventory
4INFO  [traceId=d4e5f6 userId=99] PaymentService - Sending confirmation email
5INFO  [traceId=d4e5f6 userId=99] OrderService - Processing order
6ERROR [traceId=d4e5f6 userId=99] PaymentService - Payment declined

Ahora puedes filtrar por traceId=a1b2c3 y ver exactamente el flujo completo de ese request.

  • Debugging en produccion: Filtrar logs por traceId para reconstruir el flujo completo de un request
  • Correlacion entre servicios: Seguir un request a traves de multiples microservicios
  • Auditoria: Saber que usuario ejecuto que operacion y cuando
  • Metricas: Medir latencia por request, no por promedio global
  • Alertas inteligentes: Agrupar errores por traceId para evitar alertas duplicadas

Dashboard de monitoreo con graficas y metricas representando observabilidad

Fuente: Luke Chesser — Unsplash

Implementacion en Spring Boot (Java / SLF4J + Logback)

Logo de Spring Framework

Fuente: Wikimedia Commons

Spring Boot usa SLF4J como fachada de logging y Logback como implementacion por defecto. MDC esta integrado nativamente.

1. Filtro que inyecta el MDC

java
1import jakarta.servlet.Filter;
2import jakarta.servlet.FilterChain;
3import jakarta.servlet.ServletRequest;
4import jakarta.servlet.ServletResponse;
5import jakarta.servlet.http.HttpServletRequest;
6import org.slf4j.MDC;
7import org.springframework.stereotype.Component;
8import java.util.UUID;
9
10@Component
11public class MdcFilter implements Filter {
12
13    @Override
14    public void doFilter(ServletRequest request, ServletResponse response,
15                         FilterChain chain) throws Exception {
16        HttpServletRequest httpReq = (HttpServletRequest) request;
17        try {
18            String traceId = httpReq.getHeader("X-Trace-Id");
19            if (traceId == null || traceId.isBlank()) {
20                traceId = UUID.randomUUID().toString().substring(0, 8);
21            }
22            MDC.put("traceId", traceId);
23            MDC.put("userId", httpReq.getHeader("X-User-Id"));
24
25            chain.doFilter(request, response);
26        } finally {
27            MDC.clear(); // CRITICO: evitar fugas en thread pools
28        }
29    }
30}

2. Patron de Logback

xml
1<!-- logback-spring.xml -->
2<configuration>
3  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
4    <encoder>
5      <pattern>%d{HH:mm:ss.SSS} %-5level [%X{traceId:-no-trace}] [%X{userId:-anon}] %logger{36} - %msg%n</pattern>
6    </encoder>
7  </appender>
8  <root level="INFO">
9    <appender-ref ref="CONSOLE" />
10  </root>
11</configuration>

3. Propagacion a otros servicios (RestTemplate)

java
1public class MdcPropagationInterceptor implements ClientHttpRequestInterceptor {
2    @Override
3    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
4                                         ClientHttpRequestExecution execution) throws IOException {
5        String traceId = MDC.get("traceId");
6        if (traceId != null) {
7            request.getHeaders().set("X-Trace-Id", traceId);
8        }
9        return execution.execute(request, body);
10    }
11}
12
13// Registrar
14@Bean
15public RestTemplate restTemplate() {
16    RestTemplate rt = new RestTemplate();
17    rt.setInterceptors(List.of(new MdcPropagationInterceptor()));
18    return rt;
19}
🔐 Critico: Siempre llama MDC.clear() en un bloque finally. Los servidores reutilizan hilos, y sin limpieza un request puede "heredar" datos del anterior.

Implementacion en Python (logging + contextvars)

Logo de Python

Fuente: Python Software Foundation

Python no tiene MDC nativo, pero contextvars (Python 3.7+) ofrece exactamente lo mismo: variables de contexto que se propagan automaticamente en codigo async.

1. Modulo MDC reutilizable

python
1import logging
2import uuid
3from contextvars import ContextVar
4
5# Variables de contexto (equivalente a ThreadLocal en Java)
6trace_id_var: ContextVar[str] = ContextVar('trace_id', default='no-trace')
7user_id_var: ContextVar[str] = ContextVar('user_id', default='anon')
8
9
10class MDCFilter(logging.Filter):
11    """Inyecta variables de contexto en cada log record."""
12    def filter(self, record):
13        record.trace_id = trace_id_var.get()
14        record.user_id = user_id_var.get()
15        return True
16
17
18def setup_logging(name: str = 'app') -> logging.Logger:
19    """Configura un logger con MDC integrado."""
20    handler = logging.StreamHandler()
21    handler.setFormatter(logging.Formatter(
22        '%(asctime)s %(levelname)-5s [%(trace_id)s] [%(user_id)s] %(name)s - %(message)s'
23    ))
24    handler.addFilter(MDCFilter())
25
26    logger = logging.getLogger(name)
27    logger.addHandler(handler)
28    logger.setLevel(logging.INFO)
29    return logger
30
31
32logger = setup_logging()

2. Middleware para FastAPI

python
1from fastapi import FastAPI, Request
2from starlette.middleware.base import BaseHTTPMiddleware
3
4app = FastAPI()
5
6
7class MDCMiddleware(BaseHTTPMiddleware):
8    async def dispatch(self, request: Request, call_next):
9        trace_id = request.headers.get('X-Trace-Id', uuid.uuid4().hex[:8])
10        user_id = request.headers.get('X-User-Id', 'anon')
11
12        token_trace = trace_id_var.set(trace_id)
13        token_user = user_id_var.set(user_id)
14
15        try:
16            response = await call_next(request)
17            response.headers['X-Trace-Id'] = trace_id
18            return response
19        finally:
20            trace_id_var.reset(token_trace)
21            user_id_var.reset(token_user)
22
23
24app.add_middleware(MDCMiddleware)
25
26
27@app.get("/orders/{order_id}")
28async def get_order(order_id: int):
29    logger.info(f"Fetching order {order_id}")
30    return {"order_id": order_id}

3. Propagacion a otros servicios (httpx)

python
1import httpx
2
3async def call_payment_service(order_id: int):
4    """Propaga el traceId al siguiente microservicio."""
5    trace_id = trace_id_var.get()
6    user_id = user_id_var.get()
7
8    async with httpx.AsyncClient() as client:
9        response = await client.post(
10            "http://payment-service/api/pay",
11            json={"order_id": order_id},
12            headers={
13                "X-Trace-Id": trace_id,
14                "X-User-Id": user_id,
15            },
16        )
17    logger.info(f"Payment service responded: {response.status_code}")
18    return response.json()

4. Tambien funciona con Flask y Django

python
1# Flask middleware
2from flask import Flask, request, g
3
4app = Flask(__name__)
5
6@app.before_request
7def set_mdc():
8    trace_id = request.headers.get('X-Trace-Id', uuid.uuid4().hex[:8])
9    token = trace_id_var.set(trace_id)
10    g.mdc_token = token
11
12@app.teardown_request
13def clear_mdc(exc=None):
14    token = getattr(g, 'mdc_token', None)
15    if token:
16        trace_id_var.reset(token)
💡 Tip: En Python, contextvars se propaga automaticamente en funciones async, asyncio.create_task() y ThreadPoolExecutor (desde Python 3.12 con copy_context().run()). No necesitas decorators adicionales como en Java.

Implementacion en Node.js (AsyncLocalStorage)

Codigo JavaScript representando el ecosistema Node.js

Fuente: Gabriel Heinzer — Unsplash

Node.js ofrece AsyncLocalStorage (estable desde Node 16), que funciona como ThreadLocal para contextos asincronos — perfecto para MDC.

1. Modulo MDC reutilizable

typescript
1// src/mdc.ts
2import { AsyncLocalStorage } from 'node:async_hooks';
3
4interface MDCContext {
5  traceId: string;
6  userId: string;
7  [key: string]: string;  // extensible para mas campos
8}
9
10export const mdcStorage = new AsyncLocalStorage<MDCContext>();
11
12export function getMDC(): MDCContext {
13  return mdcStorage.getStore() ?? { traceId: 'no-trace', userId: 'anon' };
14}
15
16export function createLogger(name: string) {
17  const format = (level: string, msg: string, extra?: object) => {
18    const { traceId, userId } = getMDC();
19    return JSON.stringify({
20      timestamp: new Date().toISOString(),
21      level, traceId, userId, logger: name, message: msg, ...extra,
22    });
23  };
24
25  return {
26    info: (msg: string) => console.log(format('INFO', msg)),
27    warn: (msg: string) => console.warn(format('WARN', msg)),
28    error: (msg: string, err?: Error) =>
29      console.error(format('ERROR', msg, { stack: err?.stack })),
30  };
31}

2. Middleware para Express

typescript
1import express from 'express';
2import { randomUUID } from 'node:crypto';
3import { mdcStorage, createLogger } from './mdc';
4
5const app = express();
6const log = createLogger('OrderController');
7
8// MDC Middleware
9app.use((req, res, next) => {
10  const traceId = (req.headers['x-trace-id'] as string)
11    || randomUUID().substring(0, 8);
12  const userId = (req.headers['x-user-id'] as string) || 'anon';
13
14  res.setHeader('X-Trace-Id', traceId);
15
16  // Todo el request se ejecuta dentro del contexto MDC
17  mdcStorage.run({ traceId, userId }, () => next());
18});
19
20app.get('/orders/:id', (req, res) => {
21  log.info('Fetching order ' + req.params.id);
22  res.json({ orderId: req.params.id });
23});

3. Propagacion a otros servicios (fetch / axios)

typescript
1import { getMDC } from './mdc';
2
3// Con fetch nativo (Node 18+)
4async function callPaymentService(orderId: string) {
5  const { traceId, userId } = getMDC();
6  const log = createLogger('PaymentClient');
7
8  log.info('Calling payment service for order ' + orderId);
9
10  const response = await fetch('http://payment-service/api/pay', {
11    method: 'POST',
12    headers: {
13      'Content-Type': 'application/json',
14      'X-Trace-Id': traceId,
15      'X-User-Id': userId,
16    },
17    body: JSON.stringify({ orderId }),
18  });
19
20  log.info('Payment responded: ' + response.status);
21  return response.json();
22}
23
24// Con axios (interceptor global)
25import axios from 'axios';
26
27axios.interceptors.request.use((config) => {
28  const { traceId, userId } = getMDC();
29  config.headers['X-Trace-Id'] = traceId;
30  config.headers['X-User-Id'] = userId;
31  return config;
32});

4. Tambien funciona con NestJS y Fastify

typescript
1// NestJS: Guard o Interceptor
2import { Injectable, NestMiddleware } from '@nestjs/common';
3import { mdcStorage } from './mdc';
4
5@Injectable()
6export class MdcMiddleware implements NestMiddleware {
7  use(req: any, res: any, next: () => void) {
8    const traceId = req.headers['x-trace-id'] || randomUUID().substring(0, 8);
9    res.setHeader('X-Trace-Id', traceId);
10    mdcStorage.run({ traceId, userId: req.headers['x-user-id'] || 'anon' }, next);
11  }
12}
13
14// Fastify: plugin
15fastify.addHook('onRequest', (req, reply, done) => {
16  const traceId = req.headers['x-trace-id'] || randomUUID().substring(0, 8);
17  reply.header('X-Trace-Id', traceId);
18  mdcStorage.run({ traceId, userId: req.headers['x-user-id'] || 'anon' }, done);
19});
💡 Tip: AsyncLocalStorage se propaga automaticamente a traves de setTimeout, setInterval, Promises, async/await y EventEmitter. No necesitas hacer nada especial — el contexto "viaja" con el flujo asincrono.

Propagacion entre microservicios: el patron completo

El verdadero poder del MDC se muestra cuando propagas el traceId entre microservicios:

  1. El API Gateway genera el traceId inicial y lo pone en el header X-Trace-Id
  2. Cada servicio lee el header, lo pone en su MDC, y lo propaga en las llamadas salientes
  3. Todos los logs de todos los servicios comparten el mismo traceId

Red de conexiones representando la comunicacion entre microservicios

Fuente: NASA — Unsplash

Con la propagacion implementada en las secciones anteriores, un flujo de compra genera logs asi en los tres servicios:

bash
1# API Gateway (Node.js)
2INFO [traceId=a1b2c3] [user-42] Gateway - POST /api/orders received
3
4# Order Service (Spring Boot)
5INFO [traceId=a1b2c3] [user-42] OrderService - Creating order #1001
6INFO [traceId=a1b2c3] [user-42] OrderService - Calling payment service
7
8# Payment Service (Python/FastAPI)
9INFO [traceId=a1b2c3] [user-42] payment - Processing payment for order 1001
10INFO [traceId=a1b2c3] [user-42] payment - Payment approved, charging $99.99
11
12# Order Service (Spring Boot)
13INFO [traceId=a1b2c3] [user-42] OrderService - Order #1001 confirmed

Un solo grep a1b2c3 te muestra el flujo completo a traves de Gateway (Node), Order (Java) y Payment (Python).

Contextos asincronos y concurrencia

El MDC basado en ThreadLocal tiene limitaciones con concurrencia. Cada lenguaje lo resuelve de forma diferente:

Java: Virtual Threads (Java 21+)

Los Virtual Threads no heredan MDC automaticamente. Solucion: TaskDecorator.

java
1public class MdcTaskDecorator implements TaskDecorator {
2    @Override
3    public Runnable decorate(Runnable runnable) {
4        Map<String, String> ctx = MDC.getCopyOfContextMap();
5        return () -> {
6            try {
7                if (ctx != null) MDC.setContextMap(ctx);
8                runnable.run();
9            } finally {
10                MDC.clear();
11            }
12        };
13    }
14}

Python: asyncio y ThreadPoolExecutor

contextvars se propaga automaticamente en asyncio. Para threads, usa copy_context():

python
1import asyncio
2from contextvars import copy_context
3from concurrent.futures import ThreadPoolExecutor
4
5async def process_orders(order_ids: list[int]):
6    """contextvars se propaga automaticamente a tasks."""
7    tasks = [asyncio.create_task(process_one(oid)) for oid in order_ids]
8    await asyncio.gather(*tasks)  # Cada task hereda el contexto
9
10# Para threads (CPU-bound work)
11def run_in_thread(fn):
12    ctx = copy_context()
13    with ThreadPoolExecutor() as pool:
14        future = pool.submit(ctx.run, fn)
15        return future.result()

Node.js: Workers y child processes

AsyncLocalStorage se propaga en todo el event loop, pero no a Worker threads. Propaga manualmente:

typescript
1import { Worker } from 'node:worker_threads';
2import { getMDC } from './mdc';
3
4function runInWorker(scriptPath: string) {
5  const mdc = getMDC();
6  const worker = new Worker(scriptPath, {
7    workerData: { traceId: mdc.traceId, userId: mdc.userId },
8  });
9  return worker;
10}
11
12// Dentro del worker:
13import { workerData } from 'node:worker_threads';
14// workerData.traceId contiene el trace del padre

Integracion con herramientas de observabilidad

MDC se integra con todo el ecosistema de observabilidad, independientemente del lenguaje:

HerramientaJava (Spring Boot)PythonNode.js
ELK StackLogstashEncoderpython-json-loggerwinston + JSON format
Datadogdd-trace-java (auto)ddtrace (auto)dd-trace-js (auto)
OpenTelemetryotel-java-agentopentelemetry-python@opentelemetry/node
Grafana LokiLabels desde MDCLabels desde contextvarsLabels desde store
AWS CloudWatchJSON structured logsaws-lambda-powertools@aws-lambda-powertools

Ejemplo: JSON logging para ELK en cada lenguaje

java
1// Java: logback con LogstashEncoder
2// pom.xml: net.logstash.logback:logstash-logback-encoder
3// Output: {"timestamp":"...","level":"INFO","traceId":"a1b2c3","message":"..."}
python
1# Python: python-json-logger
2from pythonjsonlogger import jsonlogger
3
4handler = logging.StreamHandler()
5handler.setFormatter(jsonlogger.JsonFormatter(
6    '%(asctime)s %(levelname)s %(trace_id)s %(user_id)s %(name)s %(message)s'
7))
8# Output: {"asctime":"...","levelname":"INFO","trace_id":"a1b2c3","message":"..."}
typescript
1// Node.js: winston con JSON
2import winston from 'winston';
3import { getMDC } from './mdc';
4
5const logger = winston.createLogger({
6  format: winston.format.combine(
7    winston.format((info) => ({ ...info, ...getMDC() }))(),
8    winston.format.json(),
9  ),
10  transports: [new winston.transports.Console()],
11});
12// Output: {"level":"info","traceId":"a1b2c3","userId":"42","message":"..."}

Buenas practicas

  • Siempre limpiar el contexto: MDC.clear() en Java, .reset(token) en Python, automatico en Node.js (sale del scope de .run())
  • No almacenar datos sensibles en el MDC (passwords, tokens, PII sin cifrar)
  • Usar nombres consistentes entre servicios: si uno usa traceId, todos deben usar traceId
  • Limitar las claves: 3-5 son suficientes (traceId, userId, tenantId, sessionId)
  • JSON en produccion para que ELK/Datadog/Loki parseen los campos automaticamente
  • Headers estandar: X-Trace-Id, X-Request-Id, o W3C Trace Context
  • Testear la limpieza: verificar que el contexto esta vacio despues de cada request

Conclusion

MDC transforma tus logs de un flujo caotico en una narrativa rastreable. El patron es el mismo en los tres lenguajes: interceptar el request → inyectar contexto → propagar entre servicios → limpiar al final.

Ya sea con SLF4J + Logback en Spring Boot, contextvars en Python, o AsyncLocalStorage en Node.js, unas pocas lineas de configuracion enriquecen automaticamente cada log con trazabilidad. Combinado con ELK, Datadog o Grafana, MDC es la columna vertebral de la observabilidad en microservicios.

Implementalo hoy — tu yo del futuro, debuggeando a las 3 AM, te lo agradecera.

Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles