使用MDC标注日志上下文

最近在研究ELK,想通过ELK来统一管理日志,并简单分析系统的一些功能,比如:机构下的交易量,交易成功/失败的比例,单位时间内某种交易的笔数,访问系统前50IP……,但是苦于无法建立统一的分析标准,无法实施,想法是把一些业务参数打印到日志中,进行分析统计。

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的 一种方便在多线程条件下记录日志的功能 。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。

一种解决的办法是 采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中 。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC 的作用是解决这个问题。

MDC 可以看成是一个与当前线程绑定的哈希表 ,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

  • 如果你的系统已经上线,突然有一天老板说我们增加一些用户数据到日志里分析一下。如果没有MDC我猜此时此刻你应该处于雪崩状态。MDC恰到好处的让你能够实现在日志上突如其来的一些需求
  • 如果你是个代码洁癖,封装了公司LOG的操作,并且将处理线程跟踪日志号也封装了进去,但只有使用了你封装日志工具的部分才能打印跟踪日志号,其他部分(比如hibernate、mybatis、httpclient等等)日志都不会体现跟踪号。当然我们可以通过linux命令来绕过这些困扰。
  • 使代码简洁、日志风格统一
  • spirng: import org.slf4j.MDC;
  • springboot: import org.apache.log4j.MDC;
  • 过滤器(Filter)

  • 遇到问题:如何在过滤器中读取controller的返回值( 因为直接Response没有提供直接拿到返回值的方法。所以要通过代理来取得返回值 )

@WebFilter(urlPatterns = "/*")
@Order(1)
public class MyFilter implements Filter {
    private static Logger logger = LoggerFactory.getLogger(MyFilter.class);
    private static ObjectMapper mapper = new ObjectMapper();
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    @SuppressWarnings("unchecked")
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        String body = IOUtils.toString(request.getInputStream());
        Map<String, Object> reqMap = mapper.readValue(body, Map.class);
        // 可以在此处解析请求报文,然后将业务参数放入日志打印
        MDC.put("service_name", "my_service1");
        MDC.put("trcode", "123456");
        MDC.put("chid", "999999");
        ResponseWrapper wrapperResponse = new ResponseWrapper((HttpServletResponse) response);// 转换成代理类
        ParameterRequestWrapper wrapRequest = new ParameterRequestWrapper(req, reqMap);
        chain.doFilter(wrapRequest, wrapperResponse);
        byte[] content = wrapperResponse.getContent();// 获取返回值
        String str = new String(content, "UTF-8");
        Map<String, Object> rspMap = JSONUtil.jsonToObj(str, Map.class);
        Map<String, String> rspHeadMap = (Map<String, String>) rspMap.get("rspHead");
        MDC.put("rspcode", rspHeadMap.get("rspcode"));
        MDC.clear();// 清除数据
     * (non-Javadoc)
     * @see javax.servlet.Filter#destroy()
    @Override
    public void destroy() {
 * 返回值输出代理类
public class ResponseWrapper extends HttpServletResponseWrapper {
    private ByteArrayOutputStream buffer;
    private ServletOutputStream out;
    public ResponseWrapper(HttpServletResponse httpServletResponse) {
        super(httpServletResponse);
        buffer = new ByteArrayOutputStream();
        out = new WrapperOutputStream(buffer);
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return out;
    @Override
    public void flushBuffer() throws IOException {
        if (out != null) {
            out.flush();
    public byte[] getContent() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    class WrapperOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos;
        public WrapperOutputStream(ByteArrayOutputStream bos) {
            this.bos = bos;
        @Override
        public void write(int b) throws IOException {
            bos.write(b);
        @Override
        public boolean isReady() {
            return false;
        @Override
        public void setWriteListener(WriteListener arg0) {
  • 拦截器(暂时未实现获取controller中的返回值)
* 拦截器 * @author Fan.W * @since 1.8 public class MDCInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(MDCInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { MDC.put("user_name", "fan wei"); MDC.put("user_id", "123456"); return true; @Override public void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { *这个方法的主要作用是用于清理资源的,当然这个方法也只能在当前这个Interceptor的preHandle方法的返回值为true时才会执行。 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { MDC.clear();// 清除
  • 约束及注册
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx.xsd 
        http://www.springframework.org/schema/mvc
         http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd"
    default-lazy-init="false">
    <!--拦截器 -->  
    <mvc:interceptors> 
        <bean class="com.seeker.interceptor.MDCInterceptor"/> 
    </mvc:interceptors>  
</beans>
  • spring aop实现(建议)

异常通知要先于controller中ExceptionHandler捕获异常!!!!!

@Aspect
@Component
public class MyInterceptor {
    private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
     * 拦截类的入口--拦截所有controller类
    @Pointcut("execution(public * com.seeker.controller..*.*(..)) ")
    public void pointCut() {
     * 方法调用之前调用
     * @param joinPoint
    @Before(value = "pointCut()")
    public void doBefore(JoinPoint joinPoint) {
     * 环绕通知
     * @param pjp
     * @return
     * @throws Throwable
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Around("pointCut()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        String className = pjp.getTarget().getClass().getName(); // 拦截类
        String methodName = pjp.getSignature().getName() + "()";
        Object[] args = pjp.getArgs();// 获取请求参数,可以校验属性
        // 解析请求报文,使用MDC,打印日志,也可在前置通知中获取
        MDC.put("service_name", "my_service1");
        MDC.put("trcode", "123456");
        MDC.put("chid", "999999");
        // 此处返回的是拦截的方法的返回值,如果不执行此方法,则不会执行被拦截的方法,利用环绕通知可以很好的做权限管理
        Object obj = pjp.proceed();
        MDC.put("rspcode", "0000");
        return obj;
     * 异常通知:pjp.proceed();跑出异常即捕获,先于@ExceptionHandler中捕获到
     * @param joinPoint
     * @param e
    @AfterThrowing(pointcut = "pointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        if (e instanceof BizException) {
            MDC.put("rspcode", ((BizException) e).messageCode);
        } else {
            MDC.put("rspcode", "9999");
  • 约束及开启aop
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:aop="http://www.springframework.org/schema/aop" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd  
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop 
                        http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--通知spring使用cglib而不是jdk的来生成代理方法 AOP可以拦截到Controller  -->
    <aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
</beans>
  • logback.xml配置
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>[%X{service_name}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{chid}] [%X{trcode}] [%X{rspcode}] - %m%n</pattern> </encoder>
使用MDC标注日志上下文背景最近在研究ELK,想通过ELK来统一管理日志,并简单分析系统的一些功能,比如:机构下的交易量,交易成功/失败的比例,单位时间内某种交易的笔数,访问系统前50IP……,但是苦于无法建立统一的分析标准,无法实施,想法是把一些业务参数打印到日志中,进行分析统计。简介  MDC(Mapped Diagnostic Context,映射调试上下文)是 lo...
一、为什么会有MDC(MappedDiagnosticContext)? 审计和调试分布式应用是logback的设计目标之一,在多线程环境中,不同的线程会处理不同的客户端,为了区分不同客户端日志输出,一种轻量级但不可取的做法是为每个实例化一个新的、完全分离日志记录器,这种方法会产生很多的日志记录器而且难以管理。 更轻量级的做法是唯一标记每个来自客户端的日志请求,为了唯一标记每个请求,用户把上下...
可以通过线程 ID 来区分,但线程是可复用的。 解决方法是 MDC。 通过添加 filter,请求到达时,把 traceId 放到 MDC,这样每次请求生命周期,打出的日志就可以带上唯一标识。 1. 实现 TraceInterceptor import java.util.UUI...
我们在调试代码时会打很多日志,这些错综复杂的日志往往混杂在一起,很难筛选出某个请求链路的日志。我们就希望在每个请求到来时生成一个唯一的traceid,可以set到请求头信息中,打日志时带上就可以快速筛选请求链路的日志了。slf4j提供了这样的功能(MDC),slf4j用ThreadLocal来存储traceid。 首先注册一个拦截器,在preHandle方法中调用MDC.put()方法,保存随机生成的id。在afterCompletion方法中删除。 public class TraceIntercep
如今,在 Java 开发中,日志的打印输出是必不可少的,Slf4j + LogBack 的组合是最通用的方式。 关于 Slf4j 的介绍,请参考本博客http://ketao1989.github.io/posts/Java-slf4j-Introduce.html 有了日志之后,我们就可以追踪各种线上问题。但是,在分布式系统中,各种无关日志穿行其中,导致我们可能无法直接定
文章目录目标诊断上下文映射 (MDC)引入MDC 的目的MDC 的作用用法示例高级用法代码示例一些问题MDC 与线程管理代码示例MDCInsertingServletFilter注意点 了解MDC基本概念和用法 参考:MDC官方文档 推荐参考 阅读前需要先了解logback的基本内容。以下内容,都是从官方文档引用,只是增加了一些示例情况。 诊断上下文映射 (MDC) 引入MDC 的目的 在一个多线程程序中,不同线程处理不同客户端的请求,如果对每个客户端都实例化一个新的且独立的 logger对象
MDC(Mapped Diagnostic Context)是log4j提供的一种机制,用于在日志输出中添加自定义的上下文信息。MDC的使用方式比较简单,只需要在代码中设置MDC的键值对,然后在log4j的输出模板中使用对应的键名即可。例如: ```java import org.apache.log4j.Logger; import org.apache.log4j.MDC; public class TestLog { private static final Logger logger = Logger.getLogger(TestLog.class); public static void main(String[] args) { MDC.put("user", "Tom"); logger.info("Hello, world!"); MDC.remove("user"); 在上面的例子中,我们使用MDC添加了一个名为"user"的键值对,然后在log4j的输出模板中使用"%X{user}"来输出该键的值。 如果您在使用MDC时出现了报错,可能是由于以下原因: 1. MDC没有正确导入。请确保您的项目中已经正确导入了log4j的依赖,并且在代码中正确导入了MDC的类。 2. MDC的键名或键值不合法。请确保您使用的键名和键值都是合法的字符串,并且没有使用特殊字符或空格。 3. MDC的键名或键值包含了log4j的占位符。请注意,log4j的输出模板中使用的占位符是以"%xxx"的形式表示的,如果您的键名或键值中包含了这种形式的字符串,可能会导致log4j解析出错。 4. MDC的键名或键值超过了log4j的限制。请注意,log4j默认限制MDC的键名和键值的长度分别为100和1000,如果您的键名或键值超过了这个限制,可能会导致log4j出现异常。 如果您仍然无法解决报错问题,请提供具体的报错信息和代码片段,我可以帮您进一步分析。