9 min read
Un cliente escribe a soporte: “Intenté hacer un pedido a las 3:42pm y me apareció un error”.
Vos abrís los logs.
El API Gateway tiene miles de líneas a esa hora. El servicio de órdenes también. El de pagos también. El de inventario también.
Cada servicio tiene su propio archivo de logs, y ninguna línea dice a qué solicitud pertenece.
Terminás buscando por la hora, el monto y el correo del cliente, tratando de adivinar qué líneas de qué servicios son parte del mismo pedido.
En una arquitectura de microservicios, una sola solicitud del usuario atraviesa varios servicios.
Sin una forma de unir esas piezas, depurar un error se convierte en trabajo de detective.
El Correlation ID (Identificador de Correlación) es la solución más simple y de mayor retorno para este problema, y es la base de lo que después se convierte en trazabilidad distribuida completa.
¿Qué es un Correlation ID?
Es un identificador único que se genera una sola vez, en el borde del sistema (normalmente en el API Gateway), cuando entra la solicitud.
Ese ID viaja en un header HTTP (Hypertext Transfer Protocol — Protocolo de Transferencia de Hipertexto), típicamente X-Correlation-Id, a través de cada llamada entre servicios, y cada servicio lo incluye en todas sus líneas de log.
Después, una sola búsqueda por ese ID en tu sistema de logs reconstruye el recorrido completo de la solicitud.
Es un patrón simple, pero requiere disciplina en tres puntos: generarlo una sola vez, propagarlo en cada llamada saliente, y registrarlo en cada línea de log.
El diagrama

Guía paso a paso
Paso 1 — Generar el ID en el borde
El API Gateway (o el primer servicio que recibe la solicitud) revisa si el request ya trae un X-Correlation-Id. Si no lo trae, genera uno nuevo, por ejemplo un UUID (Universally Unique Identifier — Identificador Único Universal) corto.
Paso 2 — Guardarlo en un contexto accesible
Cada servicio, al recibir la solicitud, guarda el ID en un contexto asociado al hilo de ejecución actual. En Java con SLF4J (Simple Logging Facade for Java) esto se llama MDC (Mapped Diagnostic Context — Contexto de Diagnóstico Mapeado).
Paso 3 — Incluirlo automáticamente en cada log
Configurá el formato de logs para que imprima el ID en cada línea, sin que el desarrollador tenga que acordarse de agregarlo a mano.
Paso 4 — Propagarlo en cada llamada saliente
Cuando un servicio llama a otro, copia el ID al header de la solicitud saliente. Esto aplica a HTTP, y también a los mensajes asíncronos (como headers de un mensaje de Kafka).
Paso 5 — Devolverlo al cliente
Incluí el ID en un header de la respuesta. Cuando el usuario reporte un error, pedile ese ID: soporte llega directo al problema en lugar de adivinar.
El código
Simulamos un API Gateway que llama a tres servicios (Orders, Payments e Inventory), cada uno con su propio logger.
El segundo request falla en inventario, y al final reconstruimos el recorrido completo buscando por su Correlation ID, como lo haría soporte en producción.
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
// --- Contexto del hilo actual (en producción: MDC de SLF4J) ---
class CorrelationContext {
static final String HEADER = "X-Correlation-Id";
private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();
// Filtro de entrada: usa el ID que viene en el header o genera uno nuevo
static void fromHeaders(Map<String, String> headers) {
String id = headers.get(HEADER);
if (id == null) {
id = UUID.randomUUID().toString().substring(0, 8);
}
CURRENT.set(id);
}
static String get() {
return CURRENT.get();
}
// Headers para las llamadas salientes hacia otros servicios
static Map<String, String> outboundHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put(HEADER, CURRENT.get());
return headers;
}
static void clear() {
CURRENT.remove();
}
}
// --- Una línea de log estructurada ---
@Getter
@AllArgsConstructor
class LogEntry {
private final String correlationId;
private final String service;
private final String level;
private final String message;
}
// --- Almacén centralizado de logs (en producción: Elasticsearch, Loki, Datadog...) ---
class LogStore {
private static final List<LogEntry> ENTRIES = new ArrayList<>();
static synchronized void add(LogEntry entry) {
ENTRIES.add(entry);
}
static synchronized List<LogEntry> findByCorrelationId(String id) {
return ENTRIES.stream()
.filter(entry -> entry.getCorrelationId().equals(id))
.collect(Collectors.toList());
}
}
// --- Logger que agrega el Correlation ID automáticamente en cada línea ---
@AllArgsConstructor
class ServiceLogger {
private final String service;
void info(String message) {
log("INFO", message);
}
void error(String message) {
log("ERROR", message);
}
private void log(String level, String message) {
LogEntry entry = new LogEntry(CorrelationContext.get(), service, level, message);
LogStore.add(entry);
System.out.println("[" + entry.getCorrelationId() + "] " + level + " " + service + " - " + message);
}
}
// --- Los servicios ---
class InventoryService {
private final ServiceLogger log = new ServiceLogger("inventory-service");
void reserve(Map<String, String> headers, String productId) {
CorrelationContext.fromHeaders(headers);
log.info("Reservando stock de " + productId);
if (productId.equals("PROD-AGOTADO")) {
log.error("Sin stock para " + productId);
throw new IllegalStateException("Sin stock");
}
log.info("Stock reservado");
}
}
class PaymentService {
private final ServiceLogger log = new ServiceLogger("payment-service");
void charge(Map<String, String> headers, double amount) {
CorrelationContext.fromHeaders(headers);
log.info("Cobrando $" + amount);
log.info("Pago aprobado");
}
}
class OrderService {
private final ServiceLogger log = new ServiceLogger("order-service");
private final PaymentService payments = new PaymentService();
private final InventoryService inventory = new InventoryService();
void createOrder(Map<String, String> headers, String productId, double amount) {
CorrelationContext.fromHeaders(headers);
log.info("Creando orden para " + productId);
try {
payments.charge(CorrelationContext.outboundHeaders(), amount);
inventory.reserve(CorrelationContext.outboundHeaders(), productId);
log.info("Orden creada con éxito");
} catch (Exception e) {
log.error("Orden fallida: " + e.getMessage());
}
}
}
// --- El borde del sistema: aquí nace el Correlation ID ---
class ApiGateway {
private final ServiceLogger log = new ServiceLogger("api-gateway");
private final OrderService orders = new OrderService();
String handle(String productId, double amount) {
CorrelationContext.fromHeaders(new HashMap<>()); // el cliente no trae ID: lo generamos
String correlationId = CorrelationContext.get();
log.info("POST /orders recibido");
orders.createOrder(CorrelationContext.outboundHeaders(), productId, amount);
CorrelationContext.clear();
return correlationId; // en producción, se devuelve al cliente en un header
}
}
// --- Main ---
public class CorrelationIdDemo {
public static void main(String[] args) {
ApiGateway gateway = new ApiGateway();
System.out.println("=== Request 1 (exitoso) ===");
gateway.handle("PROD-1", 120.0);
System.out.println("\n=== Request 2 (falla en inventario) ===");
String failedId = gateway.handle("PROD-AGOTADO", 75.0);
System.out.println("\n=== Soporte: buscamos por el Correlation ID " + failedId + " ===");
for (LogEntry entry : LogStore.findByCorrelationId(failedId)) {
System.out.println(entry.getService() + " | " + entry.getLevel() + " | " + entry.getMessage());
}
}
}
Output esperado (los IDs cambian en cada corrida):
=== Request 1 (exitoso) ===
[3f9a1c2e] INFO api-gateway - POST /orders recibido
[3f9a1c2e] INFO order-service - Creando orden para PROD-1
[3f9a1c2e] INFO payment-service - Cobrando $120.0
[3f9a1c2e] INFO payment-service - Pago aprobado
[3f9a1c2e] INFO inventory-service - Reservando stock de PROD-1
[3f9a1c2e] INFO inventory-service - Stock reservado
[3f9a1c2e] INFO order-service - Orden creada con éxito
=== Request 2 (falla en inventario) ===
[b71d04aa] INFO api-gateway - POST /orders recibido
[b71d04aa] INFO order-service - Creando orden para PROD-AGOTADO
[b71d04aa] INFO payment-service - Cobrando $75.0
[b71d04aa] INFO payment-service - Pago aprobado
[b71d04aa] INFO inventory-service - Reservando stock de PROD-AGOTADO
[b71d04aa] ERROR inventory-service - Sin stock para PROD-AGOTADO
[b71d04aa] ERROR order-service - Orden fallida: Sin stock
=== Soporte: buscamos por el Correlation ID b71d04aa ===
api-gateway | INFO | POST /orders recibido
order-service | INFO | Creando orden para PROD-AGOTADO
payment-service | INFO | Cobrando $75.0
payment-service | INFO | Pago aprobado
inventory-service | INFO | Reservando stock de PROD-AGOTADO
inventory-service | ERROR | Sin stock para PROD-AGOTADO
order-service | ERROR | Orden fallida: Sin stock
Con una sola búsqueda aparece el recorrido completo, y además revela algo importante: el pago se aprobó y el inventario falló después.
Ese es un caso para una compensación, como vimos en el post del Patrón Saga.
Sin el Correlation ID, ese hallazgo habría costado horas.
Para tener en cuenta en producción
- No reinventes la rueda: usá OpenTelemetry: el estándar actual para trazabilidad distribuida es OpenTelemetry, y en Spring Boot 3/4 Micrometer Tracing lo integra casi sin código. El Correlation ID de este post es el concepto base; OpenTelemetry lo extiende con trazas, spans (tramos) con tiempos, y el header estándar
traceparentdel W3C (World Wide Web Consortium) Trace Context. - El MDC no cruza hilos automáticamente: si procesás algo en un
ExecutorServiceo con@Async, el contexto del hilo original no se copia solo, y los logs de ese hilo salen sin ID. Hay que propagarlo explícitamente (o usar utilidades del framework que lo hagan por vos). - Propagalo también en mensajes asíncronos: cuando publicás un evento a Kafka o RabbitMQ (por ejemplo con el Outbox Pattern), incluí el ID como header del mensaje, para que el consumidor continúe el mismo recorrido.
- Logs estructurados facilitan todo: en lugar de texto libre, emití logs en JSON con el campo
correlationIdseparado. Buscar y filtrar por un campo es mucho más rápido y confiable que buscar texto dentro de una línea. - Devolvelo al usuario: incluir el ID en un header de respuesta (y mostrarlo en pantallas de error) convierte el “me salió un error” del usuario en algo que soporte puede rastrear en segundos.
Conclusión
Un Correlation ID cuesta poco: unas pocas líneas de configuración y un poco de disciplina al propagarlo.
A cambio, convierte la pregunta “¿qué le pasó a este usuario?” de una investigación de horas en una búsqueda de segundos.
Es de esas prácticas que se agradecen una sola vez, la primera vez que hay un incidente real a las 2am.
Recordá suscribirte aquí para recibir los próximos posts directamente en tu correo.
Referencias:
OpenTelemetry Documentation — https://opentelemetry.io/docs/
W3C Trace Context — https://www.w3.org/TR/trace-context/
Distributed Tracing in Practice — Austin Parker et al.