springboot整合MDC实现链路日志的应用
链路日志
在日常项目中我们大量使用日志来进行记录服务运行情况, 但是当数据量变大, 接口频次变高,就会出现线程号重复的情况,在排查问题是不能及时定位到相关点上。基于此场景下,MDC就应运而生了, 我也是最近才在项目中引入的;之前项目一直没有使用,引入后,查询日志什么的也都方便很多。
新项目使用
涉及以下几个方面进行改造
1.正常http请求产生的线程号
2.线程池产生的线程号
3.异步产生的线程号
4.消费者相关产生的线程号
直接上代码(会解释相关部分):
拦截器部分
public class LogInterceptor implements HandlerInterceptor {
private static final String TRACE_ID = "TRACE_ID";
/**
* 前置拦截器
* @author freedom
* @date 2022/12/28 17:43
* @param request 请求
* @param response 响应
* @param handler 处理器
* @return {@link boolean}
*/
@Override
public boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {
String tid = RandomStringUtils.randomAlphanumeric(8);
if(!StringUtils.isEmpty(request.getHeader(TRACE_ID))){
tid=request.getHeader(TRACE_ID);
}
//利用MDC将请求存储
MDC.put(TRACE_ID,tid);
return true;
}
/**
* 后置处理器
* @author freedom
* @date 2022/12/28 17:43
* @param request 请求
* @param response 响应
* @param handler 处理器
* @param ex 异常
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
MDC.remove(TRACE_ID);
}
}
注册拦截器
@Configuration
public class WebConfigurerAdapter implements WebMvcConfigurer {
@Bean
public LogInterceptor logInterceptor() {
return new LogInterceptor();
}
/**
* 注册拦截器
* @author freedom
* @date 2022/12/28 17:46
* @param registry registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor());
}
}
MDC工具类(用于线程池相关)
public final class ThreadMdcUtil {
private static final String TRACE_ID = "TRACE_ID";
// 获取唯一性标识
public static String generateTraceId() {
return RandomStringUtils.randomAlphanumeric(8);
}
public static void setTraceIdIfAbsent() {
if (MDC.get(TRACE_ID) == null) {
MDC.put(TRACE_ID, generateTraceId());
}
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
* @author freedom
* @date 2022/12/28 17:52
* @param callable callable
* @param context context
* @return {@link Callable<T>}
*/
public static <T> Callable<T> wrap(final Callable<T> callable,final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
* @author freedom
* @date 2022/12/28 17:52
* @param runnable runnable
* @param context context
* @return {@link Runnable}
*/
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
本地线程池配置管理
public class LocalThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
public LocalThreadPoolTaskExecutor () {
super();
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
异步使用的池
@Configuration
@EnableAsync
public class ThreadPoolConfig {
/**
* 异步方法线程池
* @author freedom
* @date 2022/12/28 17:36
* @return {@link Executor}
*/
@Bean("asyncExecutor")
public Executor asyncExecutor() {
LocalThreadPoolTaskExecutor executor = new LocalThreadPoolTaskExecutor();
executor.setCorePoolSize(6);
executor.setMaxPoolSize(12);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(0);
executor.setThreadNamePrefix("asyncExecutor");
executor.initialize();
return executor;
}
}
本地自定义线程工厂
public class ThreadPoolExecutorFactory{
public static Executor get() {
LocalThreadPoolTaskExecutor executor = new LocalThreadPoolTaskExecutor();
executor.setCorePoolSize(6);
executor.setMaxPoolSize(12);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(0);
executor.initialize();
return executor;
}
public static Executor get(Integer queueSize) {
LocalThreadPoolTaskExecutor executor = new LocalThreadPoolTaskExecutor();
executor.setCorePoolSize(6);
executor.setMaxPoolSize(12);
executor.setQueueCapacity(queueSize);
executor.setKeepAliveSeconds(0);
executor.initialize();
return executor;
}
public static Executor getCallerRunsPolicy(Integer queueSize){
LocalThreadPoolTaskExecutor executor = new LocalThreadPoolTaskExecutor();
executor.setCorePoolSize(6);
executor.setMaxPoolSize(12);
executor.setQueueCapacity(queueSize);
executor.setKeepAliveSeconds(0);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
日志测试异步方法
@SneakyThrows
@Async(value="asyncExecutor")
public void getAsyncLog () {
Thread.sleep(3000);
log.info("异步方法日志打印....");
}
http请求
@RestController
@Slf4j
@RequestMapping("/log")
public class LogDemoController {
@Resource
private CrmDataService crmDataService ;
// 线程池使用中的日志打印
public static final Executor threadPoolExecutor = ThreadPoolExecutorFactory.get(100);
@GetMapping(value = "/getLog")
public String getLog() {
log.info("链路日志测试开始!");
crmDataService.getAsyncLog();
threadPoolExecutor.execute(()-> log.info("链路日志改造test"));
return "hello world!";
}
}
日志配置 [TRACE_ID]
<property name="LOG_PATTERN"
value="[%d{yyyy-MM-dd HH:mm:ss.SSS,GMT+8}] [%-5level] [%class{0}:%method:%line] -[%X{TRACE_ID}]
[%t] [-[%msg]-] %n"/>
kafka消费者这种
//获取生成的traceId
String traceId = ThreadMdcUtil.generateTraceId();
//使用MDC存入
MDC.put("TRACE_ID",traceId);
//使用完, 记住要remove掉, 建议放到finally里
MDC.remove("TRACE_ID");
针对于老项目改造
可将生成的工具类提取到公共模块, 其他模块进行引用使用就可以了(日志配置也需要修改)
我也是初次使用,有什么不足还请指出,大家共同探讨!