SpringBoot 项目添加 MDC 日志链路追踪
日志链路追踪的意思就是将一个标志跨线程进行传递,在一般的小项目中也就是在你新起一个线程的时候,或者使用线程池执行任务的时候会用到,比如追踪一个用户请求的完整执行流程。
这里用到MDC
和ThreadLocal
,分别由下面的包提供:
java.lang.ThreadLocal
org.slf4j.MDC
1. 线程池配置
如果你直接通过手动新建线程来执行异步任务,想要实现标志传递的话,需要自己去实现,其实和线程池一样,也是调用MDC
的相关方法,如下所示:
//取出父线程的MDC
Map<String, String> context = MDC.getCopyOfContextMap();
//将父线程的MDC内容传给子线程
MDC.setContextMap(context);
首先提供一个常量:
package com.example.demo.common.constant;
/**
* 常量
*
* @author wangbo
* @date 2021/5/13
*/
public class Constants {
public static final String LOG_MDC_ID = "trace_id";
}
接下来需要对ThreadPoolTaskExecutor
的方法进行重写:
package com.example.demo.common.threadpool;
import com.example.demo.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
/**
* MDC线程池
* 实现内容传递
*
* @author wangbo
* @date 2021/5/13
*/
@Slf4j
public class MdcTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public <T> Future<T> submit(Callable<T> task) {
log.info("mdc thread pool task executor submit");
Map<String, String> context = MDC.getCopyOfContextMap();
return super.submit(() -> {
T result;
if (context != null) {
//将父线程的MDC内容传给子线程
MDC.setContextMap(context);
} else {
//直接给子线程设置MDC
MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));
}
try {
//执行任务
result = task.call();
} finally {
try {
MDC.clear();
} catch (Exception e) {
log.warn("MDC clear exception", e);
}
}
return result;
});
}
@Override
public void execute(Runnable task) {
log.info("mdc thread pool task executor execute");
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> {
if (context != null) {
//将父线程的MDC内容传给子线程
MDC.setContextMap(context);
} else {
//直接给子线程设置MDC
MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));
}
try {
//执行任务
task.run();
} finally {
try {
MDC.clear();
} catch (Exception e) {
log.warn("MDC clear exception", e);
}
}
});
}
}
然后使用自定义的重写子类MdcTaskExecutor
来实现线程池:
package com.example.demo.common.threadpool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
*
* @author wangbo
* @date 2021/5/13
*/
@Slf4j
@Configuration
public class ThreadPoolConfig {
/**
* 异步任务线程池
* 用于执行普通的异步请求,带有请求链路的MDC标志
*/
@Bean
public Executor commonThreadPool() {
log.info("start init common thread pool");
//ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
MdcTaskExecutor executor = new MdcTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(10);
//配置最大线程数
executor.setMaxPoolSize(20);
//配置队列大小
executor.setQueueCapacity(3000);
//配置空闲线程存活时间
executor.setKeepAliveSeconds(120);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("common-thread-pool-");
//当达到最大线程池的时候丢弃最老的任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
//执行初始化
executor.initialize();
return executor;
}
/**
* 定时任务线程池
* 用于执行自启动的任务执行,父线程不带有MDC标志,不需要传递,直接设置新的MDC
* 和上面的线程池没啥区别,只是名字不同
*/
@Bean
public Executor scheduleThreadPool() {
log.info("start init schedule thread pool");
MdcTaskExecutor executor = new MdcTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(3000);
executor.setKeepAliveSeconds(120);
executor.setThreadNamePrefix("schedule-thread-pool-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
executor.initialize();
return executor;
}
}
2. 拦截器配置
需要在拦截器中手动设置和删除 MDC 标志。
package com.example.demo.common.interceptor;
import com.example.demo.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* 日志拦截器
*
* @author wangbo
* @date 2021/5/13
*/
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//log.info("进入 LogInterceptor");
//添加MDC值
MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));
//打印接口请求信息
String method = request.getMethod();
String uri = request.getRequestURI();
log.info("[请求接口] : {} : {}", method, uri);
//打印请求参数
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//log.info("执行 LogInterceptor");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//log.info("退出 LogInterceptor");
//打印请求结果
//删除MDC值
MDC.remove(Constants.LOG_MDC_ID);
}
}
对拦截器进行注册:
package com.example.demo.common.config;
import com.example.demo.common.interceptor.LogInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* MVC配置
*
* @author wangbo
* @date 2021/5/13
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LogInterceptor logInterceptor;
/**
* 拦截器注册
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
}
3. 日志文件配置
需要在logback-spring.xml
文件中的日志打印格式里添加%X{trace_id}
,如下所示:
<!-- 控制台打印日志的相关配置 -->
<appender name="console_out" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志格式 -->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{trace_id}] [%level] [%thread] [%class:%line] - %m%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
也可以简单配置,下面是在 application.properties 文件中配置的 SpringBoot 默认日志打印格式,只是在其中添加了%clr([%X{trace_id}])
:
logging.level.root=info
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([%X{trace_id}]) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
转载自:https://juejin.cn/post/7348785955510370358