信息发布→ 登录 注册 退出

利用Spring boot+LogBack+MDC实现链路追踪

发布时间:2026-01-11

点击量:
目录
  • MDC介绍
  • API说明
  • MDC使用
    • 1.拦截器
    • 2.工具类
  • MDC 存在的问题
    • 子线程日志打印丢失traceId
      • HTTP调用丢失traceId
        • 1.接口调用方
        • 2.第三方服务需要添加拦截器

      MDC介绍

      MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。

      API说明

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

      MDC使用

      1.拦截器

      @Component
      public class LogInterceptor implements HandlerInterceptor
      {
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              //如果有上层调用就用上层的ID
              String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
              if (StringUtil.isEmpty(traceId)) 
              {
                  TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
              }
              else
              {
                  TraceIdUtil.setTraceId(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 {
              //调用结束后删除
              TraceIdUtil.remove();
          }
      }

      2.工具类

      public class TraceIdUtil
      {
        public static final String TRACE_ID = "requestId";
        public static String getTraceId()
        {
           String traceId =(String) MDC.get(TRACE_ID);
           return traceId == null ? "" : traceId;
        }
        public static void setTraceId(String traceId)
        {
            MDC.put(TRACE_ID,traceId);
        }
        public static void remove()
        {
            MDC.remove(TRACE_ID);
        }
        public static void clear()
        {
            MDC.clear();
        }
        public static String generateTraceId() {
           return UUID.randomUUID().toString().replace("-", "");
        }
      • 日志文件配置
      <property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n" />

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

      MDC 存在的问题

      至此基本的功能已经实现,但是存在一下几个问题

      • 多线程情况下,子线程中打印日志会丢失traceId.
      • HTTP跨服务之间的调用丢失traceId.

      子线程日志打印丢失traceId

      问题重现:

        @LogAnnotation(model="用户管理",func="查询用户信息",desc="根据用户名称")
          @GetMapping("getUserByName")
          public Result getUserByName(@RequestParam String name)
          {
              //主线程日志
              logger.info("getUserByName paramter name:"+name);
              for(int i=0;i<5;i++)
              {
                 //子线程日志
                  threadPoolTaskExecutor.execute(()->{
                      logger.info("child thread:{}",name);
                      userService.getUserByName(name); 
                  });
              }
              return Result.success();
          }

      运行结果:

      2025-03-13 12:45:44.156 [http-nio-8089-exec-1] INFO  [ec05a600ed1a4556934a3afa4883766a] c.s.fw.controller.UserController - getUserByName paramter name:1
      2025-03-13 12:45:44.173 [Pool-A1] INFO  [] c.s.fw.controller.UserController - child thread:1

      从运行的结果来看,子线程打印日志,日志中的traceId信息已经丢失。

      解决方案:

      子线程在打印日志的过程中traceId将丢失,解决方案为重写线程池(对于直接new Thread 创建线程的情况不考略),实际开发中也需要禁止这种情况。

      public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor
      {
          private static final long serialVersionUID = 3940722618853093830L;
          @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()));
          }
      }

      因为Spring Boot ThreadPoolTaskExecutor 已经对ThreadPoolExecutor进行封装,只需要继承ThreadPoolTaskExecutor重写相关的执行方法即可。

      public class ThreadMdcUtil
      {
          public static void setTraceIdIfAbsent() {
              if (MDC.get(TraceIdUtil.TRACE_ID) == null) 
              {
                  MDC.put(TraceIdUtil.TRACE_ID, TraceIdUtil.generateTraceId());
              }
          }
          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();
                  }
              };
          }
          public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
              return () -> {
                  if (context == null) {
                      MDC.clear();
                  } else {
                      MDC.setContextMap(context);
                  }
                  //设置traceId
                  setTraceIdIfAbsent();
                  try {
                      runnable.run();
                  } finally {
                      MDC.clear();
                  }
              };
          }
      }

      代码说明:

      • 判断当前线程对应MDC的Map是否存在,如果存在则设置
      • 设置MDC中的traceId值,不存在则新生成,如果是子线程,MDC中traceId不为null
      • 执行run方法

      线程池配置

      @Configuration
      public class ThreadPoolTaskExecutorConfig
      {
          //最大可用的CPU核数
          public static final int PROCESSORS = Runtime.getRuntime().availableProcessors();
          @Bean
          public ThreadPoolExecutorMdcWrapper getExecutor()
          {
              ThreadPoolExecutorMdcWrapper executor =new ThreadPoolExecutorMdcWrapper();
              executor.setCorePoolSize(PROCESSORS *2);
              executor.setMaxPoolSize(PROCESSORS * 4);
              executor.setQueueCapacity(50);
              executor.setKeepAliveSeconds(60);
              executor.setThreadNamePrefix("Task-A");
              executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
              executor.initialize();
              return executor;
          }
      }

      重新运行结果发现子线程能够正常获取traceid信息进行跟踪。

      2025-03-13 13:19:30.688 [Task-A1] INFO  [482929425cbc4476a4e7168615af7890] c.s.fw.controller.UserController - child thread:1
      2025-03-13 13:19:31.003 [Task-A1] INFO  [482929425cbc4476a4e7168615af7890] c.s.fw.service.impl.UserServiceImpl - name:1

      HTTP调用丢失traceId

      HTTP调用第三方服务接口时traceId丢失,需要在发送请求时在Request Header中添加traceId,在被调用方添加拦截器获取header中的traceId添加到MDC中。

      HTTP调用有多种方式,比较常见的有HttpClient、OKHttp、RestTemplate,以RestTemplate调用为例。

      1.接口调用方

      public class RestTemplateTraceIdInterceptor implements
              ClientHttpRequestInterceptor
      {
          @Override
          public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                  ClientHttpRequestExecution clientHttpRequestExecution) throws IOException
          {
              String traceId=MDC.get("requestId");
              if(traceId!=null)
              {
                  request.getHeaders().set("requestId", traceId);
              }
              else
              {
                  request.getHeaders().set("requestId", UUID.randomUUID().toString().replace("-", ""));
              }
              return clientHttpRequestExecution.execute(request, body);
          }
      }

      RestTemplate添加拦截器即可。

      restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));
      复制代码

      2.第三方服务需要添加拦截器

      @Component
      public class LogInterceptor implements HandlerInterceptor
      {
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              //如果有上层调用就用上层的ID
              String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
              if (StringUtil.isEmpty(traceId)) 
              {
                  TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
              }
              else
              {
                  TraceIdUtil.setTraceId(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 {
              //调用结束后删除
              TraceIdUtil.remove();
          }
      }

      其他HttpClient、OKHttp的实现方式与RestTemplate基本相同,这里就不一一列举。 Spring boot +logback+MDC实现全链路跟踪内容已经讲完了

      在线客服
      服务热线

      服务热线

      4008888355

      微信咨询
      二维码
      返回顶部
      ×二维码

      截屏,微信识别二维码

      打开微信

      微信号已复制,请打开微信添加咨询详情!