A Mapped Diagnostic Context (MDC) is a per-thread map of key/value pairs that a logging framework adds to log messages, so we can distinguish the interleaved log output of different requests. Log output is interleaved when a server handles multiple clients at nearly the same time.
1. Stamping Requests with MDC
We use MDC to stamp each request, by putting the contextual information about the request into the MDC.
The class MDC provides the following static methods.
- void put(String key, String val) puts a context value, identified by the key, into the context map of the current thread. We can place as many key/value associations in the MDC as needed.
- String get(String key) returns the context value identified by the key parameter.
- void remove(String key) removes the context value identified by the key parameter.
- void clear() clears all entries in the MDC.
The following example stamps a request with the keys sessionId and userId.
MDC.put("sessionId", "abcd");
MDC.put("userId", "1234");
2. Printing MDC Values in Logs
To print the context information in the logs, we can use MDC keys in the log pattern string.
We refer to the MDC keys with the specifier %X, which prints the Nested Diagnostic Context (NDC) and the Mapped Diagnostic Context (MDC) of the current thread.
- Use %X to include the full contents of the Map.
- Use %X{key} to include the specified key.
- Use %x to include the full contents of the Stack.
For example, the following appender refers to the keys userId and sessionId that we put into the MDC earlier. At runtime, the appender appends the MDC information to every log message.
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout>
<Pattern>%d{DATE} %p %X{sessionId} %X{userId} %c - %m%n</Pattern>
</layout>
</appender>
3. Adding MDC using Servet Filter
We should not put MDC context information in several places of the application. Scattered MDC code is hard to maintain.
MDC is thread-local, and a servlet uses a single thread to process the whole request, so a servlet filter is a good place to add the MDC information. The MDC information then does not mix with other requests handled by the same handler or controller.
A servlet filter that the framework provides also frees us from thread-safety or synchronization concerns, because the framework handles both.
Servlet containers use one thread per request. For each HTTP request, the container creates a thread or takes one from a pool to serve it.
We must clear the MDC context after the request processing is complete. Otherwise, when the same thread serves another request, the stale MDC information creates wrong log entries.
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import org.slf4j.MDC;
//To convert to a Spring Boot Filter
//Uncomment @Component and Comment our @WebFilter annotation
//@Component
@WebFilter( filterName = "mdcFilter", urlPatterns = { "/*" } )
public class MDCFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter( final ServletRequest request,
final ServletResponse response, final FilterChain chain )
throws IOException, ServletException {
MDC.put( "sessionId", request.getParameter("traceId") );
try {
chain.doFilter( request, response );
} finally {
MDC.clear();
}
}
@Override
public void init( final FilterConfig filterConfig )
throws ServletException {
}
}
For Spring framework to recognize this class as a web filter, we define it as a bean with the @Component annotation.
4. MDC with Logging Frameworks
4.1. MDC with Log4J2
Log4j2 supports both the MDC and the NDC and merges them into the single class ThreadContext. The Thread Context Map is the equivalent of the MDC and the Thread Context Stack is the equivalent of the NDC.
To make newly created child threads inherit a copy of the MDC, we enable the Log4j system property isThreadContextMapInheritable.
The following example uses the Log4j2 class ThreadContext.
import org.apache.logging.log4j.ThreadContext;
//Add information in context
ThreadContext.put("id", UUID.randomUUID().toString());
ThreadContext.put("ipAddress", request.getRemoteAddr());
//To clear context
ThreadContext.clear();
To print the context values, we use the pattern layout with %X, as discussed in the section on printing MDC values.
The Log4j2 documentation describes ThreadContext and CloseableThreadContext in detail on the official website.
4.2. MDC with SLF4J, Logback and Log4j
MDC with SLF4J depends on MDC support in the underlying logging library. If the underlying library does not support MDC, SLF4J skips all MDC-related statements without side effects.
Only two logging systems, log4j and logback, offer MDC functionality. For java.util.logging, which does not support MDC, SLF4J uses BasicMDCAdapter. For other systems, SLF4J uses NOPMDCAdapter.
Log4j, Logback and SLF4J support MDC in the following classes.
- org.slf4j.MDC (SLF4J and Logback)
- org.apache.log4j.MDC (Log4j)
The following example uses the SLF4J class MDC.
import org.slf4j.MDC;
//Add information in context
MDC.put("id", UUID.randomUUID().toString());
MDC.put("ipAddress", request.getRemoteAddr());
//To clear context
MDC.clear();
To print the context values, we use the pattern layout with %X, as discussed previously.
The Logback documentation describes the MDC support of SLF4J and Logback on the official website.
5. Conclusion
Mapped Diagnostic Context (MDC) adds context information to the application logs, which improves request tracing, especially in a complex distributed application.
In a concurrent environment where threads come from a thread pool, we must clear the context information from the thread after the request has been processed.
We recommend the SLF4J-based MDC, because it is simple to use and it uses the MDC support of the underlying logging framework.
Happy Learning !!
Thank you for this article. It appears to be just what I’ve spent hours looking for!