Correlation ID propagation across every downstream call, structured JSON logging, log-level discipline, the ELK/EFK stack, and scrubbing sensitive data before it ever hits a log line.
Published September 23, 2026
class CorrelationIdFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String correlationId = req.getHeader("X-Correlation-ID");
if (correlationId == null) correlationId = UUID.randomUUID().toString(); // generate at the EDGE if not already present
MDC.put("correlationId", correlationId); // thread-local, picked up automatically by every log statement
try {
chain.doFilter(req, res);
} finally {
MDC.clear(); // MUST clear — same pooled-thread leak risk as SecurityContextHolder
}
}
}
A correlation ID is generated once, at the edge (the API gateway or the first service a request hits) and propagated through every downstream synchronous call (as a header) and every asynchronous message (as metadata) for the request's entire journey. This is the single piece of infrastructure that turns "grep through a hundred services' logs hoping to find related lines" into "filter every log system-wide by one correlation ID" — directly solving Why Microservices Fail's "lack of observability makes failures impossible to localize" problem.
{"timestamp":"2026-09-23T10:15:00Z","level":"ERROR","correlationId":"abc-123","service":"order-service","message":"Payment failed","orderId":"ord-456","errorCode":"CARD_DECLINED"}
JSON-formatted logs (vs plain text) are machine-parseable — a log aggregator can filter, group, and query on specific fields (errorCode, service, correlationId) directly, rather than needing fragile regex parsing of free-text log lines. Plain text remains easier for a human reading logs directly on one machine; structured logging earns its slightly worse human-readability the moment logs are aggregated and queried across many services at once, which is essentially always the case past a handful of services.
A team without shared discipline on which level to use for what quickly ends up with either an unusably noisy production log stream (everything logged at INFO or above) or a blind spot (real errors logged at WARN, missed by ERROR-level alerting) — this is a genuinely common, avoidable operational problem worth establishing conventions for explicitly, not leaving to individual developer judgment call by call.
ELK (Elasticsearch, Logstash, Kibana): Logstash ingests and processes logs from many sources, Elasticsearch indexes them for fast search, Kibana provides the query/visualization UI. EFK swaps Logstash for Fluentd (a lighter-weight, more broadly-adopted log collector, especially common in Kubernetes environments) — same overall shape (collect → index → visualize), different collection component. Either stack is the standard answer to "where do all these structured JSON logs from every service actually go and get searched."
log.info("Processing payment for card ending in {}", maskCardNumber(cardNumber)); // NEVER log the raw PAN
PII (names, emails, addresses) and secrets (passwords, API keys, full card numbers) must never appear in log output — logs are typically retained for extended periods, often replicated to a centralized system with broader access than the originating service, and a leaked log stream containing raw sensitive data is a real, serious incident class. Log scrubbing/masking (either explicit at the call site, as shown, or via automated filters in the logging pipeline catching common sensitive-data patterns) is a required practice, directly connected to Payment — Security's PCI-DSS scope-reduction concerns — logging a raw card number would itself expand PCI compliance scope to every system that stores or processes that log.
Q: What happens to correlation ID propagation across an asynchronous message (not a synchronous HTTP call)? A: The correlation ID needs to travel as message metadata/headers (most message brokers support this), and the consuming service needs to extract it and populate its own MDC before processing — the mechanism differs slightly from HTTP header propagation, but the goal (one ID traceable across the entire request's journey, sync or async) is identical.
Q: Why use MDC (a thread-local) for correlation ID rather than passing it as an explicit parameter through every method call? A: Threading it through every method signature would be extremely invasive across an entire codebase — MDC lets logging statements automatically pick it up without every method needing an extra correlationId parameter, at the cost of the same pooled-thread-leak discipline required elsewhere (SecurityContextHolder, ThreadLocal generally) — it must be cleared after each request.
Q: How would centralized logging interact with a system also using distributed tracing (see Distributed Tracing)? A: They're complementary, often sharing the same correlation/trace ID — logs give you detailed, free-text context at each point; traces give you the timing/latency breakdown across hops — a mature observability setup links them (a trace view that can jump directly to the corresponding log lines for a specific span) rather than treating them as entirely separate tools.
Q: What's a practical way to enforce 'never log sensitive data' across a team, beyond code review vigilance? A: Automated static analysis rules (flagging suspicious log statements referencing known-sensitive field names) or a logging library wrapper that requires explicit masking annotations on sensitive fields — relying purely on developers remembering to mask manually in every log statement is a known-fragile approach at team scale.