MDC日志追踪
介绍
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进行日志记录和跟踪。
方式一
在外部生成TRACEID:您可以在外部生成TRACEID,并将其存储在一个地方,以便在需要时访问。
将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