映射诊断上下文(Mapped Diagnostic Context),简称MDC,是一种用于区分来自不同来源的交错日志输出的工具。当一个服务器几乎同时处理多个客户时, 日志输出通常是交错的。
1.用MDC标记请求
MDC 被用来标记每个请求。它是通过将关于请求的上下文信息放入 MDC 来完成的。
MDC类包含以下静态方法。
void put(String key, String val):将一个由key标识的上下文值放入当前线程的上下文映射中。我们可以根据需要在MDC中放置尽可能多的值/键关联。String get(String key):获取由key参数确定的上下文值。void remove(String key): 删除key参数所标识的上下文值。void clear():清除 MDC 中的所有条目。
使用 MDC API 对请求进行标记的示例是。
MDC.put("sessionId", "abcd");
MDC.put("userId", "1234");
2.在日志中打印 MDC 值
为了在日志中打印上下文信息,我们可以在日志模式字符串中使用 MDC 键。
为了引用MDC上下文键,我们使用%X指定符,用于打印当前线程的嵌套诊断上下文(NDC)和/或映射诊断上下文(MDC)。
- 使用
%X,包括地图的全部内容。 - 使用
%X{key},包括指定的键。 - 使用
%x,以包括堆栈的全部内容。
例如,我们可以引用第一节中创建的userId和sessionId键,如下所示。在应用程序运行期间,MDC信息将使用给定的appended附加到每个日志消息中。
<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.使用Servet过滤器添加MDC
将MDC 上下文信息任意地放在应用程序的几个地方并不是一个好主意。维护这样的代码会很困难。
由于MDC在本质上是线程本地的,我们可以使用Servlet过滤器作为添加MDC日志的好地方,因为Servlet使用单个线程来处理整个请求。通过这种方式,我们可以确保MDC信息不会与同一处理程序/控制器所处理的其他请求混在一起。
使用框架提供的Servlet过滤器来处理它,也使我们摆脱了线程安全或同步问题,因为框架会安全透明地处理这些问题。
Servlet容器是每个请求的线程。当一个HTTP请求被提出时,就会有一个线程被创建或从一个池子中检索出来来服务它。
因此,不要忘记在请求处理完成后清除MDC上下文,否则当同一个线程在下一次提供另一个请求时,它们的MDC信息将是陈旧的,并可能创建错误的日志条目。
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 {
}
}
注意,为了让Spring框架识别这个类是一个Web过滤器,我们需要用*@Component*注解将其定义为一个Bean。
4.带有日志框架的 MDC
4.1.使用 Log4J2 的 MDC
Log4j2同时支持MDC和NDC,但将它们合并为一个单一的ThreadContext类。Thread Context Map等同于MDC,Thread Context Stack等同于NDC。
要使MDC的副本自动继承到新创建的子线程,请启用 "isThreadContextMapInheritable" Log4j系统属性。
Log4j2 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();
为了打印上下文的值,我们可以使用基于%X 的模式布局,正如在打印MDC值一节中讨论的那样。
我们可以在官方网站上阅读更多关于ThreadContext和CloseableThreadContext的信息。
4.2.带有 SLF4J、Logback 和 Log4j 的 MDC
使用 SLF4J 的 MDC 取决于底层日志库对 MDC 的支持。如果底层库不支持 MDC,那么所有与 MDC 相关的语句都会被跳过,不会有任何副作用。
请注意,目前只有两个日志系统,即log4j和logback,提供MDC功能。对于不支持 MDC 的java.util.logging ,将使用BasicMDCAdapter。对于其他系统,将使用NOPMDCAdapter。
上述框架在以下类中支持 MDC。
org.slf4j.MDC(SLF4J 和 Logback)org.apache.log4j.MDC(Log4j)
一个SLF4J 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();
打印上下文值,我们需要使用前面讨论的基于%X 的模式布局。
我们可以在官方网站上阅读更多关于SLF4J和Logback中的MDC支持。
5.总结
映射诊断上下文(MDC)是一种很好的方式,可以在应用日志中添加更多的上下文信息,以达到改进请求跟踪的目的,特别是当应用是一个复杂的分布式应用时。
但是我们在并发环境中使用MDC时需要非常小心,因为线程是来自线程池的。在这种情况下,在处理完请求后,清除线程的上下文信息是非常重要的。
为了保持简单,建议使用基于SLF4J的MDC,因为它使用起来很简单,而且它使用底层日志框架对MDC日志的支持。
学习愉快!!