介绍

MDC(Mapped Diagnostic Context)是一种用于在日志记录过程中传递上下文信息的机制。它允许将自定义的键值对与日志记录相关联,并在日志输出时自动将这些键值对添加到日志消息中

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

MDC存储日志原理

MDC 使用 ThreadLocal 存储日志数据,所以它是线程安全的,它可以把同一个请求的日志都存储一个相同的值,每次打印日志的时候,会自动打印出当前日志的键值value,我们查询日志的时候就可以根据value来查询。

API 说明

  • clear() => 移除所有 MDC

  • get (String key) => 获取当前线程 MDC 中指定 key 的值

  • getContext() => 获取当前线程 MDC 的 MDC

  • put(String key, Object o) => 往当前线程的 MDC 中存入指定的键值对

  • remove(String key) => 删除当前线程 MDC 中指定的键值对

优点

代码简洁,日志风格统一,不需要在 log 打印中手动拼写 traceId,即

MDC封装

自定义封装,也可以使用hutool的

import org.slf4j.MDC;  
  
import java.util.Map;  
import java.util.UUID;  
  
public class MdcUtil {  
    public static final String TRACE_ID = "traceId";  
  
    public static String generateTraceId() {  
        return UUID.randomUUID().toString().replace("-", "");  
    }  
  
    public static String getTraceId() {  
        return MDC.get(TRACE_ID);  
    }  
  
    public static void setTraceId(String traceId) {  
        MDC.put(TRACE_ID, traceId);  
    }  
  
    public static void setContextMap(Map<String, String> context) {  
        MDC.setContextMap(context);  
    }  
  
    public static void removeTraceId() {  
        MDC.remove(TRACE_ID);  
    }  
  
    public static void clear() {  
        MDC.clear();  
    }  
}

MDC使用

在MVC层面判断设置

import org.slf4j.MDC;
public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(Constants.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtil.getTraceId();
        }

        MDC.put(Constants.TRACE_ID, traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        //调用结束后删除
        MDC.remove(Constants.TRACE_ID);
    }
}

修改日志格式

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

重点是 %X{traceId},traceId 和 MDC 中的键名称一致。

在Servlet层面判断过滤

public class XssFilter implements Filter {  
  
   @Override  
   public void init(FilterConfig config) {  
   }  
  
   @Override  
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)  
            throws IOException, ServletException {  
      XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(  
            (HttpServletRequest) request);  
      String traceId = xssRequest.getHeader(MdcUtil.TRACE_ID);  
      if (StringUtils.isBlank(traceId)) {  
         traceId = MdcUtil.generateTraceId();  
         xssRequest.addHeader(MdcUtil.TRACE_ID, traceId);  
      }  
      MdcUtil.setTraceId(traceId);  
      GlobalHeaders.INSTANCE.header(MdcUtil.TRACE_ID, traceId); 
      // 其他的Http工具类
      chain.doFilter(xssRequest, response);  
   }  
  
   @Override  
   public void destroy() {  
      MdcUtil.removeTraceId();  
      GlobalHeaders.INSTANCE.removeHeader(MdcUtil.TRACE_ID);  
      // 其他的Http工具类
   }  
  
}

@Bean  
public FilterRegistrationBean xssFilterRegistration() {  
    FilterRegistrationBean registration = new FilterRegistrationBean();  
    registration.setDispatcherTypes(DispatcherType.REQUEST);  
    registration.setFilter(new XssFilter());  
    registration.addUrlPatterns("/*");  
    registration.setName("xssFilter");  
    registration.setOrder(Integer.MAX_VALUE);  
    return registration;  
}

线程间传递

如果您想要线程池中的任务的TRACEID与外部一致,您可以将外部的TRACEID传递给线程池中的任务。这样,线程池中的任务就可以使用相同的TRACEID进行日志记录和跟踪。

方式一
  1. 在外部生成TRACEID:您可以在外部生成TRACEID,并将其存储在一个地方,以便在需要时访问。

  2. 将TRACEID传递给线程池中的任务:在将任务提交给线程池之前,将TRACEID传递给任务。这可以通过构造函数、方法参数或通过自定义的任务包装器(Task Wrapper)来实现。

public class MyTask implements Runnable {
    private final String traceId;

    public MyTask(String traceId) {
        this.traceId = traceId;
    }

    @Override
    public void run() {
        MDC.put(TRACE_ID, traceId);
        try {
            // 执行任务逻辑
            // 在日志语句中使用MDC上下文信息
            Logger logger = LoggerFactory.getLogger(MyTask.class);
            logger.info("Task execution with TRACEID: {}", MDC.get(TRACE_ID));
        } finally {
            MDC.remove(TRACE_ID);
        }
    }
}

public class MyTaskExecutor {
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    public void executeTask(String traceId) {
        taskExecutor.execute(new MyTask(traceId));
    }
}

外部生成TRACEID并提交任务:在外部生成TRACEID后,通过调用executeTask(traceId)将任务提交给线程池。

public class MainClass {
    @Autowired
    private MyTaskExecutor taskExecutor;

    public void mainMethod() {
        String traceId = generateTraceId(); // 外部生成TRACEID
        taskExecutor.executeTask(traceId); // 提交任务并传递TRACEID
    }
}
方式二

需要重写线程池,在线程池启动新线程之前,复制当前线程的 traceId

/**  
 * 多线程日志追踪工具类  
 *  
 * @author fengxc 
 */
public class ThreadMdcUtil {  
    public static void setTraceIdIfAbsent() {  
        if (MdcUtil.getTraceId() == null) {  
            MdcUtil.setTraceId(MdcUtil.generateTraceId());  
        }  
    }  
  
    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {  
        return () -> {  
            if (context == null) {  
                MdcUtil.clear();  
            } else {  
                MdcUtil.setContextMap(context);  
            }  
            setTraceIdIfAbsent();  
            try {  
                return callable.call();  
            } finally {  
                MdcUtil.clear();  
            }  
        };  
    }  
  
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {  
        return () -> {  
            if (context == null) {  
                MdcUtil.clear();  
            } else {  
                MdcUtil.setContextMap(context);  
            }  
            //设置traceId  
            setTraceIdIfAbsent();  
            try {  
                runnable.run();  
            } finally {  
                MdcUtil.clear();  
            }  
        };  
    }  
}

线程池复制 traceId

/**  
 * 日志追踪线程池配置  
 *  
 * @author fengxc 
 */
public class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {  
  
    @Override  
    public void execute(@NotNull Runnable task) {  
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));  
    }  
  
    @NotNull  
    @Override    public Future<?> submit(@NotNull Runnable task) {  
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));  
    }  
  
    @NotNull  
    @Override    public <T> Future<T> submit(@NotNull Callable<T> task) {  
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));  
    }  
}

配置线程池

@SpringBootConfiguration  
public class ThreadPoolConfig {  
    /**  
     * 这里定义一个日志追踪线程池,使用时直接注入ThreadPoolTaskExecutor,使用@Qualifier("getLogTraceExecutor")指定bean  
     */    @Bean  
    public CustomThreadPoolTaskExecutor getLogTraceExecutor() {  
       //创建了一个ThreadPoolTaskExecutor的子类,在每次提交线程的时候都会做一些子类配置的操作  
        CustomThreadPoolTaskExecutor executor =new CustomThreadPoolTaskExecutor();  
        // 设置线程名称前缀  
        executor.setThreadNamePrefix("LogTraceExecutor-");  
        executor.initialize();  
        return executor;  
    }  
  
    /**  
     * 重写默认线程池配置,@Async异步会使用这个线程池  
     */  
    @Bean  
    public Executor taskExecutor() {  
        //创建了一个ThreadPoolTaskExecutor的子类,在每次提交线程的时候都会做一些子类配置的操作  
        ThreadPoolTaskExecutor executor = new CustomThreadPoolTaskExecutor();  
        // 设置线程名称前缀  
        executor.setThreadNamePrefix("logTranceAsyncPool-");  
        executor.initialize();  
        return executor;  
    }  
}

配置logbook pattern

文章作者: 刘同学
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 刘同学的小站
后端 Java
喜欢就支持一下吧