MDC

Posted by Static on April 25, 2021

1. What?

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能,也可以说是一种轻量级的日志跟踪工具。

2. Why?

MDC是应用内的线程级别,不是分布式的应用层级别,所以仅靠它无法做到分布式应用调用链路跟踪的需求。它要解决的问题主要是让我们可以在海量日志数据中快速捞到可用的日志信息。

3. How?

其实很简单,在请求的入口处,添加traceId跟踪,我们就要利用MDC了。

import org.slf4j.MDC;

...

MDC.put("traceId", "uuid");
log.info("test");

...

// 打印结果
[DEBUG][2021-04-28 04:00:01.451][com.xxx.xxx.xxx.xxx][traceid=uuid]||msg=test

在spring项目中,可以通过添加过滤器的方式在入口处统一处理

public class AccessFilter implements Filter {
    private final static String TRACE_ID_KEY = "traceId";

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String traceId = request.getHeader(TRACE_ID_KEY);
        if (StringUtils.isBlank(traceId)) {
            // 此处traceId可按自定义的算法生成,保证唯一性
            MDC.put(TRACE_ID_KEY, TraceId.gen());
        }
        filterChain.doFilter(servletRequest, servletResponse);
        MDC.clear();
    }
}

相关的日志配置需要将traceId打印出来,添加 [traceid=%X{traceId}]

MDC原理就是在本地线程map中添加traceId参数,在使用时 MDC.get("traceId"),从本地线程中获取,我们知道本地线程存储的map事实是在当前线程中,那么在多线程中如何实现traceId继承呢,其实也很简单,我们需要在下一个线程开始时将当前线程的traceId传递过去。使用多线程一般使用线程池,所以就需要包装下我们的线程池了,如下示例:

public class CommonExecutorPool {
    // 线程数
    private static int threadCount = Runtime.getRuntime().availableProcessors();
    // 线程相关参数可由自己的机器资源来调整
    private static ExecutorService threadPoolExecutor =
            new ThreadPoolExecutor(threadCount, threadCount,
                    1L,
                    TimeUnit.MINUTES,
                    new LinkedBlockingQueue<>(256),
                    new CustomizableThreadFactory("common-task-"),
                    new ThreadPoolExecutor.CallerRunsPolicy()) {

                // 对ThreadPoolExecutor任务进行包装,其它线程池类似
                @Override
                public void execute(Runnable task) {
                    super.execute(MDCUtil.wrap(task));
                }

                @Override
                public <T> Future<T> submit(Runnable task, T result) {
                    return super.submit(MDCUtil.wrap(task), result);
                }

                @Override
                public <T> Future<T> submit(Callable<T> task) {
                    return super.submit(MDCUtil.wrap(task));
                }

                @Override
                public Future<?> submit(Runnable task) {
                    return super.submit(MDCUtil.wrap(task));
                }
            };

    public static ExecutorService getThreadPoolExecutor() {
        return threadPoolExecutor;
    }
}

public class MDCUtil {
    public static final String traceIdKey="traceId";

    public static <T> Callable<T> wrap(final Callable<T> callable) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            String traceId = MDC.get(traceIdKey);
            if(StringUtils.isNotBlank(traceId)){
                MDC.put(traceIdKey, UUID.randomUUID().toString());
            }
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    public static Runnable wrap(final Runnable runnable) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            String traceId = MDC.get(traceIdKey);
            if(StringUtils.isNotBlank(traceId)){
                MDC.put(traceIdKey, UUID.randomUUID().toString());
            }
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}