首页 > 其他分享 >利用Spring boot+LogBack+MDC实现链路追踪

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

时间:2023-08-11 10:37:31浏览次数:52  
标签:13 traceId MDC boot 线程 Spring TraceIdUtil public

这篇文章主要介绍了利用Spring boot+LogBack+MDC实现链路追踪,MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对,下文详细介绍需要的小伙伴可以参考一下    

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.拦截器

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @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.工具类

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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("-", "");   }
  • 日志文件配置
1 <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

问题重现:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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();   }

运行结果:

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

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

解决方案:

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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重写相关的执行方法即可。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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方法

线程池配置

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @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信息进行跟踪。

2022-03-13 13:19:30.688 [Task-A1] INFO  [482929425cbc4476a4e7168615af7890] c.s.fw.controller.UserController - child thread:1
2022-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.接口调用方

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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添加拦截器即可。

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @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实现全链路跟踪内容已经讲完了

到此这篇关于利用Spring boot+LogBack+MDC实现链路追踪的文章就介绍到这了,更多相关Spring boot 链路追踪内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

 

转自:https://www.jb51.net/article/245347.htm

标签:13,traceId,MDC,boot,线程,Spring,TraceIdUtil,public
From: https://www.cnblogs.com/tiancai/p/17622377.html

相关文章

  • 在Spring Cloud中实现微服务架构的服务注册与发现
    在现代软件开发中,微服务架构已经成为一种流行的设计模式,有助于构建可伸缩、可维护的应用程序。SpringCloud作为一个强大的工具集,为开发人员提供了丰富的功能来简化微服务架构的实现。本文将深入探讨SpringCloud中的服务注册与发现,重点介绍Eureka作为服务注册中心的使用,以及如何通......
  • 【Eureka技术指南】「SpringCloud」从源码层面让你认识Eureka工作流程和运作机制(上)
    推荐超值课程:点击获取前言介绍了解到了SpringCloud,大家都应该知道注册中心,而对于我们从过去到现在,SpringCloud中用的最多的注册中心就是Eureka了,所以深入Eureka的原理和源码,接下来我们要进行讲解下eureka的源码分析,由此应运而产生的本章节的内容。基本原理EurekaServer提......
  • 【Eureka技术指南】「SpringCloud」从源码层面让你认识Eureka工作流程和运作机制(下)
    推荐超值课程:点击获取原理回顾EurekaServer提供服务注册服务,各个节点启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。EurekaClient是一个Java客户端,用于简化与EurekaServer......
  • BootstrapBlazor组件库,组件方法的表达式传参
    BootstrapBlazor组件库,组件方法的表达式传参有时候我们在循环中创建组件的时候,可能想把item的值也一并传入组件的方法事件中去处理,有很多小伙伴就不知道如何去调用事件了。下面是一段实例代码,通过循环遍历来创建图片,并且创建一个删除图片的按钮,这个按钮的OnConfirm方法需要把图......
  • Spring 架构
    Spring可能成为您的所有企业应用程序的一站式商店。但是,Spring是模块化的,允许您挑选适用于您的模块,而无需引入其他模块。下面的部分提供了SpringFramework中所有可用模块的详细信息。SpringFramework提供了大约20个模块,可以根据应用程序要求使用。核心容器核心容器由Core、Be......
  • Drools 7.67.0 + DMN 1.2 + SpringBoot3 构建规则引擎
    背景:基于项目工作的需要,要建立一个规则引擎的应用集中式的管理业务中的规则流程等,所以先探索一个MVP1.什么是规则引擎,同类竟品?规则引擎的主要思想是将应用程序中的业务决策部分分离出来,并使用预定义的语义模板编写业务决策(业务规则),由用户或开发者在需要时进行配置、管理。需......
  • SpringBoot系列---【SpringBoot在多个profiles环境中自由切换】
    SpringBoot在多个profiles环境中自由切换1.在resource目录下新建dev,prod两个目录,并分别把dev环境的配置文件和prod环境的配置文件放到对应目录下,可以在配置文件中指定激活的配置文件,也可以默认不指定。2.在pom.xml中最后位置,新增下面两部分配置<project><build>......
  • hibernate,spring 控制并发访问
    数据库(和其他的事务系统)试图确保事务隔离性(transactionisolation),这意味着,从每个并发事务的观点来看,似乎没有其他的事务在运行。传统上而言,这已经通过锁(locking)实现了。事务可以在数据库中一个特定的数据项目上放置一把锁,暂时防止通过其他事务访问这个项目。一些现代的数据库(如Or......
  • 在Java中操作Redis_Spring Data Redis使用方式_其他类型的数据操作
        ......
  • 在Java中操作Redis_Spring Data Redis使用方式_操作字符串类型的数据
        ......