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.
![]()
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.
| Concepto | Descripcion |
|---|---|
| Thread-Local Storage | Cada hilo tiene su propia copia del MDC, evitando colisiones entre requests concurrentes |
| Clave-Valor | Se almacenan pares como traceId=abc123, userId=42, sessionId=xyz |
| Transparente | Una vez configurado, cada log.info() incluye automaticamente los valores del MDC sin cambiar el codigo |
| Propagable | Se puede propagar entre microservicios via headers HTTP |
Equivalentes en cada lenguaje
| Lenguaje | Mecanismo | Libreria / API |
|---|---|---|
| Java | ThreadLocal (MDC nativo) | SLF4J + Logback / Log4j2 |
| Python | contextvars (PEP 567) | logging + Filter personalizado |
| Node.js | AsyncLocalStorage | node:async_hooks (built-in) |
¿Por que es importante para la trazabilidad?
Sin MDC, tus logs se ven asi:
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:
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

Fuente: Luke Chesser — Unsplash
Implementacion en Spring Boot (Java / SLF4J + Logback)

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
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
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)
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}
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)

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
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
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)
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
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)
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)

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
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
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)
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
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});
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:
- El API Gateway genera el
traceIdinicial y lo pone en el headerX-Trace-Id - Cada servicio lee el header, lo pone en su MDC, y lo propaga en las llamadas salientes
- Todos los logs de todos los servicios comparten el mismo
traceId

Fuente: NASA — Unsplash
Con la propagacion implementada en las secciones anteriores, un flujo de compra genera logs asi en los tres servicios:
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.
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():
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:
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:
| Herramienta | Java (Spring Boot) | Python | Node.js |
|---|---|---|---|
| ELK Stack | LogstashEncoder | python-json-logger | winston + JSON format |
| Datadog | dd-trace-java (auto) | ddtrace (auto) | dd-trace-js (auto) |
| OpenTelemetry | otel-java-agent | opentelemetry-python | @opentelemetry/node |
| Grafana Loki | Labels desde MDC | Labels desde contextvars | Labels desde store |
| AWS CloudWatch | JSON structured logs | aws-lambda-powertools | @aws-lambda-powertools |
Ejemplo: JSON logging para ELK en cada lenguaje
1// Java: logback con LogstashEncoder
2// pom.xml: net.logstash.logback:logstash-logback-encoder
3// Output: {"timestamp":"...","level":"INFO","traceId":"a1b2c3","message":"..."}
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":"..."}
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 usartraceId - 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.
Comments
Sign in to leave a comment
No comments yet. Be the first!