0


SpringBoot使用TraceId日志链路追踪

项目场景:

  1. 有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大。为了解决这个痛点,就使用了TraceId,根据TraceId关键字进入服务器查询日志中是否有这个TraceId,这样就把同一次的业务调用链上的日志串起来了。

实现步骤

1、pom.xml 依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-test</artifactId>
  9. <scope>test</scope>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-logging</artifactId>
  14. </dependency>
  15. <!--lombok配置-->
  16. <dependency>
  17. <groupId>org.projectlombok</groupId>
  18. <artifactId>lombok</artifactId>
  19. <version>1.16.10</version>
  20. </dependency>
  21. </dependencies>

2、整合logback,打印日志,logback-spring.xml (简单配置下)

关键代码:[traceId:%X{traceId}],traceId是通过拦截器里MDC.put(traceId, tid)添加

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration debug="false">
  3. <!--日志存储路径-->
  4. <property name="log" value="D:/test/log" />
  5. <!-- 控制台输出 -->
  6. <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  7. <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  8. <!--输出格式化-->
  9. <pattern>[traceId:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
  10. </encoder>
  11. </appender>
  12. <!-- 按天生成日志文件 -->
  13. <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
  14. <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
  15. <!--日志文件名-->
  16. <FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern>
  17. <!--保留天数-->
  18. <MaxHistory>30</MaxHistory>
  19. </rollingPolicy>
  20. <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  21. <pattern>[traceId:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
  22. </encoder>
  23. <!--日志文件最大的大小-->
  24. <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
  25. <MaxFileSize>10MB</MaxFileSize>
  26. </triggeringPolicy>
  27. </appender>
  28. <!-- 日志输出级别 -->
  29. <root level="INFO">
  30. <appender-ref ref="console" />
  31. <appender-ref ref="file" />
  32. </root>
  33. </configuration>

3、application.yml

  1. server:
  2. port: 8826
  3. logging:
  4. config: classpath:logback-spring.xml

4、自定义日志拦截器 LogInterceptor.java

用途:每一次链路,线程维度,添加最终的链路ID traceId。

MDC(Mapped Diagnostic Context)诊断上下文映射,是@Slf4j提供的一个支持动态打印日志信息的工具。

  1. import org.slf4j.MDC;
  2. import org.springframework.lang.Nullable;
  3. import org.springframework.util.StringUtils;
  4. import org.springframework.web.servlet.HandlerInterceptor;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. import java.util.UUID;
  8. /**
  9. * 日志拦截器
  10. */
  11. public class LogInterceptor implements HandlerInterceptor {
  12. private static final String traceId = "traceId";
  13. @Override
  14. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
  15. String tid = UUID.randomUUID().toString().replace("-", "");
  16. //可以考虑让客户端传入链路ID,但需保证一定的复杂度唯一性;如果没使用默认UUID自动生成
  17. if (!StringUtils.isEmpty(request.getHeader("traceId"))){
  18. tid=request.getHeader("traceId");
  19. }
  20. MDC.put(traceId, tid);
  21. return true;
  22. }
  23. @Override
  24. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
  25. @Nullable Exception ex) {
  26. // 请求处理完成后,清除MDC中的traceId,以免造成内存泄漏
  27. MDC.remove(traceId);
  28. }
  29. }

5、WebConfigurerAdapter.java 添加拦截器

ps: 其实这个拦截的部分改为使用自定义注解+aop也是很灵活的。

  1. import javax.annotation.Resource;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  4. import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
  5. @Configuration
  6. public class WebConfigurerAdapter extends WebMvcConfigurationSupport {
  7. @Resource
  8. private LogInterceptor logInterceptor;
  9. @Override
  10. public void addInterceptors(InterceptorRegistry registry) {
  11. registry.addInterceptor(logInterceptor);
  12. //可以具体制定哪些需要拦截,哪些不拦截,其实也可以使用自定义注解更灵活完成
  13. // .addPathPatterns("/**")
  14. // .excludePathPatterns("/testxx.html");
  15. }
  16. }

6、测试接口

  1. import io.swagger.annotations.Api;
  2. import io.swagger.annotations.ApiOperation;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RequestMethod;
  6. import org.springframework.web.bind.annotation.RestController;
  7. import javax.annotation.Resource;
  8. @RestController
  9. @Api(tags = "测试接口")
  10. @RequestMapping("/test")
  11. @Slf4j
  12. public class TestController {
  13. @RequestMapping(value = "/log", method = RequestMethod.GET)
  14. @ApiOperation(value = "测试日志")
  15. public String sign() {
  16. log.info("这是一行info日志");
  17. log.error("这是一行error日志");
  18. return "success";
  19. }
  20. }

结果:


异步场景:

  1. 使用线程的场景,写一个异步线程,加入这个调用里面。再次执行看开效果,我们会发现显然子线程丢失了trackId
  2. 所以我们需要针对子线程使用情形,做调整,思路:将父线程的trackId传递下去给子线程即可。

1、 ThreadMdcUtil.java

  1. import org.slf4j.MDC;
  2. import java.util.Map;
  3. import java.util.UUID;
  4. import java.util.concurrent.Callable;
  5. /**
  6. * @Author: JCccc
  7. * @Date: 2022-5-30 11:14
  8. * @Description:
  9. */
  10. public final class ThreadMdcUtil {
  11. private static final String traceId = "traceId";
  12. // 获取唯一性标识
  13. public static String generateTraceId() {
  14. return UUID.randomUUID().toString().replace("-", "");
  15. }
  16. public static void setTraceIdIfAbsent() {
  17. if (MDC.get(traceId) == null) {
  18. MDC.put(traceId, generateTraceId());
  19. }
  20. }
  21. /**
  22. * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
  23. *
  24. * @param callable
  25. * @param context
  26. * @param <T>
  27. * @return
  28. */
  29. public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
  30. return () -> {
  31. if (context == null) {
  32. MDC.clear();
  33. } else {
  34. MDC.setContextMap(context);
  35. }
  36. setTraceIdIfAbsent();
  37. try {
  38. return callable.call();
  39. } finally {
  40. MDC.clear();
  41. }
  42. };
  43. }
  44. /**
  45. * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
  46. *
  47. * @param runnable
  48. * @param context
  49. * @return
  50. */
  51. public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
  52. return () -> {
  53. if (context == null) {
  54. MDC.clear();
  55. } else {
  56. MDC.setContextMap(context);
  57. }
  58. setTraceIdIfAbsent();
  59. try {
  60. runnable.run();
  61. } finally {
  62. MDC.clear();
  63. }
  64. };
  65. }
  66. }

2、 MyThreadPoolTaskExecutor.java 是我们自己写的,重写了一些方法

  1. import org.slf4j.MDC;
  2. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  3. import java.util.concurrent.Callable;
  4. import java.util.concurrent.Future;
  5. public final class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
  6. public MyThreadPoolTaskExecutor() {
  7. super();
  8. }
  9. @Override
  10. public void execute(Runnable task) {
  11. super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
  12. }
  13. @Override
  14. public <T> Future<T> submit(Callable<T> task) {
  15. return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
  16. }
  17. @Override
  18. public Future<?> submit(Runnable task) {
  19. return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
  20. }
  21. }

3、 ThreadPoolConfig.java 定义线程池,交给spring管理

  1. import org.springframework.context.annotation.Bean;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.scheduling.annotation.EnableAsync;
  4. import java.util.concurrent.Executor;
  5. @EnableAsync
  6. @Configuration
  7. public class ThreadPoolConfig {
  8. /**
  9. * 声明一个线程池
  10. */
  11. @Bean("taskExecutor")
  12. public Executor taskExecutor() {
  13. MyThreadPoolTaskExecutor executor = new MyThreadPoolTaskExecutor();
  14. //核心线程数5:线程池创建时候初始化的线程数
  15. executor.setCorePoolSize(5);
  16. //最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
  17. executor.setMaxPoolSize(5);
  18. //缓冲队列500:用来缓冲执行任务的队列
  19. executor.setQueueCapacity(500);
  20. //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
  21. executor.setKeepAliveSeconds(60);
  22. //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
  23. executor.setThreadNamePrefix("taskExecutor-");
  24. executor.initialize();
  25. return executor;
  26. }
  27. }

4、 Service

  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.scheduling.annotation.Async;
  3. import org.springframework.stereotype.Service;
  4. /**
  5. * 测试Service
  6. */
  7. @Service("testService")
  8. @Slf4j
  9. public class TestService {
  10. /**
  11. * 异步操作测试
  12. */
  13. @Async("taskExecutor")
  14. public void asyncTest() {
  15. try {
  16. log.info("模拟异步开始......");
  17. Thread.sleep(3000);
  18. log.info("模拟异步结束......");
  19. } catch (InterruptedException e) {
  20. log.error("异步操作出错:"+e);
  21. }
  22. }
  23. }

5、测试接口

  1. import io.swagger.annotations.Api;
  2. import io.swagger.annotations.ApiOperation;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RequestMethod;
  6. import org.springframework.web.bind.annotation.RestController;
  7. import javax.annotation.Resource;
  8. @RestController
  9. @Api(tags = "测试接口")
  10. @RequestMapping("/test")
  11. @Slf4j
  12. public class TestController {
  13. @Resource
  14. private TestService testService;
  15. @RequestMapping(value = "/log", method = RequestMethod.GET)
  16. @ApiOperation(value = "测试日志")
  17. public String sign() {
  18. log.info("这是一行info日志");
  19. log.error("这是一行error日志");
  20. //异步操作测试
  21. testService.asyncTest();
  22. return "success";
  23. }
  24. }

结果:

我们可以看到,子线程的日志也被串起来了。


定时任务:

如果使用了定时任务@Scheduled,这时候执行定时任务,不会走上面的拦截器逻辑,所以定时任务需要单独创建个AOP切面。

1、创建个定时任务线程池

  1. import org.springframework.context.annotation.Configuration;
  2. import org.springframework.scheduling.annotation.EnableScheduling;
  3. import org.springframework.scheduling.annotation.SchedulingConfigurer;
  4. import org.springframework.scheduling.config.ScheduledTaskRegistrar;
  5. import java.util.concurrent.Executors;
  6. /**
  7. * 定时任务线程池
  8. */
  9. @EnableScheduling
  10. @Configuration
  11. public class SeheduleConfig implements SchedulingConfigurer{
  12. @Override
  13. public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
  14. taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
  15. }
  16. }

2、创建个AOP切面

  1. import org.aspectj.lang.ProceedingJoinPoint;
  2. import org.aspectj.lang.annotation.Around;
  3. import org.aspectj.lang.annotation.Aspect;
  4. import org.aspectj.lang.annotation.Pointcut;
  5. import org.slf4j.MDC;
  6. import org.springframework.context.annotation.Configuration;
  7. import java.util.UUID;
  8. @Aspect //定义一个切面
  9. @Configuration
  10. public class SeheduleTaskAspect {
  11. // 定义定时任务切点Pointcut
  12. @Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
  13. public void seheduleTask() {
  14. }
  15. @Around("seheduleTask()")
  16. public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
  17. try {
  18. String traceId = UUID.randomUUID().toString().replace("-", "");
  19. //用于日志链路追踪,logback配置:%X{traceId}
  20. MDC.put("traceId", traceId);
  21. //执行定时任务方法
  22. joinPoint.proceed();
  23. } finally {
  24. //请求处理完成后,清除MDC中的traceId,以免造成内存泄漏
  25. MDC.remove("traceId");
  26. }
  27. }
  28. }

3、创建定时任务测试

  1. import org.slf4j.Logger;
  2. import org.slf4j.LoggerFactory;
  3. import org.springframework.scheduling.annotation.Scheduled;
  4. import org.springframework.stereotype.Service;
  5. import java.util.Date;
  6. @Service
  7. public class SeheduleTasks {
  8. private Logger logger = LoggerFactory.getLogger(SeheduleTasks.class);
  9. /**
  10. * 1分钟执行一次
  11. */
  12. @Scheduled(cron = "0 0/1 * * * ?")
  13. public void testTask() {
  14. logger.info("执行定时任务>"+new Date());
  15. }
  16. }

总结:

服务启动的时候traceId是空的,这是正常的,因为还没到拦截器这一层。

源码:https://download.csdn.net/download/u011974797/89981672

API 说明
  • clear() => 移除所有 MDC
  • get (String key) => 获取当前线程 MDC 中指定 key 的值
  • getContext() => 获取当前线程 MDC 的 MDC
  • put(String key, Object o) => 往当前线程的 MDC 中存入指定的键值对
  • remove(String key) => 删除当前线程 MDC 中指定的键值对

本文转载自: https://blog.csdn.net/u011974797/article/details/143682368
版权归原作者 涛哥是个大帅比 所有, 如有侵权,请联系我们删除。

“SpringBoot使用TraceId日志链路追踪”的评论:

还没有评论