springboot中记录链路日志traceId

news2025/1/17 23:24:49

一 相关概念

在分布式服务架构下,一个 Web 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。在这个过程中每个服务之间的通信又是单独的网络请求,无论请求流经的哪个服务除了故障或者处理过慢都会对前端造成影响。
在这里插入图片描述
在分布式链路追踪中有两个重要的概念:跟踪(trace)和 跨度(span)。trace 是请求在分布式系统中的整个链路视图,span 则代表整个链路中不同服务内部的视图,span 组合在一起就是整个 trace 的视图。

  • traceId:用于标识某一次具体的请求ID。当用户的请求进入系统后,会在RPC调用网络的第一层生成一个全局唯一的traceId,并且会随着每一层的RPC调用,不断往后传递,这样的话通过traceId就可以把一次用户请求在系统中调用的路径串联起来。
  • spanId:用于标识某一次RPC调用在分布式请求中的位置。请求到达每个服务后,服务都会为请求生成spanId。
  • parent-spanId:用于标识上游RPC调用在分布式请求中的位置。请求到达每个服务后,随请求一起从上游传过来的上游服务的 spanId 会被记录成 parent-spanId,或者叫 pspanId。当前服务生成的 spanId 随着请求一起,在传到下游服务时,这个 spanId 又会被下游服务当作 parent-spanId 记录。
  • MDC:(Mapped Diagnostic Context)映射诊断环境,是 log4j 和 logback 提供的一种方便在线多线程条件下记录日志的功能,可以看成是一个与当前线程绑定的 ThreadLocal。
public class MDC {
    // 添加 key-value
    public static void put(String key, String val) {...}
    // 根据 key 获取 value
    public static String get(String key) {...}
    // 根据 key 删除映射
    public static void remove(String key) {...}
    // 清空
    public static void clear() {...}
}


二 实现方式

2.1 设置TraceId

2.1.1 使用filter过滤器设置traceId

新建一个过滤器,实现Filter,重写init,doFilter,destroy方法,设置traceId放在doFilter中,在destroy中调用MDC.clear()方法。

@Slf4j
@WebFilter(filterName = "traceIdFilter",urlPatterns = "/*")
public class traceIdFilter implements Filter {

	/**
     * 日志跟踪标识
     */
    private static final String TRACE_ID = "TRACE_ID";
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        MDC.put(TRACE_ID, UUID.randomUUID().toString());
        filterChain.doFilter(request, servletResponse);
    }

    @Override
    public void destroy() {
    	MDC.clear();
    }
}

2.1.2 使用Interceptor拦截器设置traceId

定义一个拦截器,重写preHandle方法,在方法中通过MDC设置traceId

/**
 * MDC设置traceId拦截器
 *
 * @author china
 */
@Component
public abstract class TraceIdInterceptor extends HandlerInterceptorAdapter {
    private static final String UNIQUE_ID = "TRACE_ID";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        MDC.put(UNIQUE_ID, UUID.randomUUID().toString());
        return true;
    }

	@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        MDC.clear();
    }
}

2.1.3 使用JWT token过滤器的项目

springboot项目经常使用spring security+jwt来做权限限制,在这种情况下,我们通过新建filter过滤器来设置traceId,那么在验证token这部分的日志就不会带上traceId,因此我们需要把代码放在jwtFilter中。

/**
 * token过滤器 验证token有效性
 *
 * @author china
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private TokenService tokenService;

    /**
     * 日志跟踪标识
     */
    private static final String TRACE_ID = "TRACE_ID";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        MDC.put(TRACE_ID, UUID.randomUUID().toString());
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
    
    @Override
    public void destroy() {
        MDC.clear();
    }
}

2.2 logback.xml配置traceId

与之前的相比只是添加了[%X{TRACE_ID}], [%X{***}]是一个模板,中间属性名是我们使用MDC put进去的。

#之前
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
#增加traceId后
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - [%X{TRACE_ID}] - %msg%n" />


2.3 扩展

2.3.1 补充异步方法带入上下文的traceId

异步方法会开启一个新线程,我们想要是异步方法和主线程共用同一个traceId,首先先新建一个任务适配器MdcTaskDecorator:

public class MdcTaskDecorator implements TaskDecorator 
    /**
     * 使异步线程池获得主线程的上下文
     * @param runnable
     * @return
     */
    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String,String> map = MDC.getCopyOfContextMap();
        return () -> {
            try{
                MDC.setContextMap(map);
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

然后,在线程池配置中增加executor.setTaskDecorator(new MdcTaskDecorator())的设置

 /**
 * 线程池配置
 *
 * @author china
 **/
@EnableAsync
@Configuration
public class ThreadPoolConfig {
    private int corePoolSize = 50;
    private int maxPoolSize = 200;
    private int queueCapacity = 1000;
    private int keepAliveSeconds = 300;

    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(maxPoolSize);
        executor.setCorePoolSize(corePoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setTaskDecorator(new MdcTaskDecorator());
        // 线程池对拒绝任务(无线程可用)的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

最后,在业务代码上使用@Async开启异步方法即可

    @Async("threadPoolTaskExecutor")
    void testSyncMethod();

2.3.1 在接口放回中,增加traceId返回

在笔者项目中,接口返回都使用了一个叫AjaxResult自定义类来包装,所以只需要把这个类的构造器中增加traceId返回即可,相对简单。

   /**
     * 日志跟踪标识
     */
    private static final String TRACE_ID = "TRACE_ID";
    
    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult() {
        super.put(TRACE_ID, MDC.get(TRACE_ID));
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg  返回内容
     */
    public AjaxResult(int code, String msg) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        super.put(TRACE_ID, MDC.get(TRACE_ID));
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg  返回内容
     * @param data 数据对象
     */
    public AjaxResult(int code, String msg, Object data) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        super.put(TRACE_ID, MDC.get(TRACE_ID));
        if (StringUtils.isNotNull(data)) {
            super.put(DATA_TAG, data);
        }
    }

三 MDC的实现方式

3.1 InheritableThreadLocal

在代码开发中,经常使用 ThreadLocal来保证在同一个线程中共享变量。在 ThreadLocal 中,每个线程都拥有了自己独立的一个变量,线程间不存在共享竞争发生,并且它们也能最大限度的由CPU调度,并发执行。显然这是一种以空间来换取线程安全性的策略。

但是,ThreadLocal有一个问题,就是它只保证在同一个线程间共享变量,也就是说如果这个线程起了一个新线程,那么新线程是不会得到父线程的变量信息的。因此,为了保证子线程可以拥有父线程的某些变量视图,JDK提供了一个数据结构,InheritableThreadLocal。

javadoc 文档对 InheritableThreadLocal 说明:

该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数。

当必须将变量(如用户 ID 和 事务 ID)中维护的每线程属性(per-thread-attribute)自动传送给创建的所有子线程时,应尽可能地采用可继承的线程局部变量,而不是采用普通的线程局部变量。

代码对比可以看出两者区别:

  • ThreadLocal:
public class ThreadLocal<T> {

    /**
     * Method childValue is visibly defined in subclass
     * InheritableThreadLocal, but is internally defined here for the
     * sake of providing createInheritedMap factory method without
     * needing to subclass the map class in InheritableThreadLocal.
     * This technique is preferable to the alternative of embedding
     * instanceof tests in methods.
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }
}

  • InheritableThreadLocal
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

这个是开发时一般使用的类,直接copy父线程的变量

public class CopyOnInheritThreadLocal extends
    InheritableThreadLocal<HashMap<String, String>> {

  /**
   * Child threads should get a copy of the parent's hashmap.
   */
  @Override
  protected HashMap<String, String> childValue(
      HashMap<String, String> parentValue) {
    if (parentValue == null) {
      return null;
    } else {
      return new HashMap<String, String>(parentValue);
    }
  }

}

为了支持InheritableThreadLocal的父子线程传递变量,JDK在Thread中,定义了ThreadLocal.ThreadLocalMap inheritableThreadLocals 属性。该属性变量在线程初始化的时候,如果父线程的该变量不为null,则会把其值复制到ThreadLocal。

从上面的代码实现,还可以看到,如果我们使用原生的 InheritableThreadLocal类则在子线程中修改变量,可能会影响到父线程的变量值,及其他子线程的值。因此,一般我们推荐没有特殊情况,最好使用CopyOnInheritThreadLocal类,该实现是新建一个map来保持值,而不是直接使用父线程的引用。


参考文档:
https://blog.csdn.net/qq_33204709/article/details/125358252
https://blog.csdn.net/t194978/article/details/123012783
https://blog.csdn.net/m0_37556444/article/details/100142429

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/435237.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于PCA和NSST算法联合实现红外与可见光图像融合的Matlab仿真(完整源码+35组数据集)

以下是一个使用PCA和NSST算法实现红外与可见光图像融合的Matlab仿真完整源码。源码中只需修改红外图像&#xff08;IR.bmp&#xff09;和可见光图像&#xff08;VI.bmp&#xff09;名字即可 文章目录 效果展示数据集展示步骤说明完整源码下载地址 效果展示 最终融合效果展示&am…

cifar-10数据集+ResNet50

CIFAR-10-ObjectRecognition 作为一个古老年代的数据集&#xff0c;用ResNet来练一下手也是不错的。 比赛链接&#xff1a;CIFAR-10 - Object Recognition in Images | Kaggle 1. 预设置处理 创建各类超参数&#xff0c;其中如果是在Kaggle上训练的话batch_size是可以达到40…

安全狗深度参与编写《数据安全产品与服务观察报告》发布!

4月11日&#xff0c;由中国通信标准化协会联合数据安全推进计划主办的《数据安全产品与服务观察报告》发布会在北京顺利开展。 作为国内云原生安全领导厂商&#xff0c;安全狗也参与了数据安全推进计划《数据安全产品与服务观察报告》撰写。 此次活动针对数据安全产业、技术、…

排序算法合集(1)

前言&#xff1a; 今天我们正式开始讲述算法中的排序。排序算法是我们十分重要的算法&#xff0c;为什么呢&#xff1f; 排序是在各种情况下都非常重要的&#xff0c;无论是在人类社会还是在计算机科学中。以下是一些排序的重要性&#xff1a; 数据分析&#xff1a;在数据分析…

多连接数据库管理Navicat Premium 中文

Navicat Premium 是一款强大的数据库管理工具&#xff0c;它支持多种关系型数据库&#xff0c;包括 MySQL、MariaDB、Oracle、SQL Server、PostgreSQL 等等。 以下是 Navicat Premium 的一些主要功能&#xff1a; 连接管理&#xff1a;可以在一个用户界面中同时连接到多个数据库…

HCIP-6.7BGP的路径选择

BGP的路径选择 1、BGP路径属性1.1、路由选择1.1.1、BGP路由选择过程1.1.2、BGP选路参数2、BGP的路由策略2.1、Preferred-Value相当权重weight2.2、local-preference本地优先级2.3、AS_PATH经过的AS号 不常用2.4、Origin起源属性修改2.5、MED多出口鉴别器3、BGP非策略性选路原则…

【C++】右值引用(极详细版)

在讲右值引用之前&#xff0c;我们要了解什么是右值&#xff1f;那提到右值&#xff0c;就会想到左值&#xff0c;那左值又是什么呢&#xff1f; 我们接下来一起学习&#xff01; 目录 1.左值引用和右值引用 1.左值和右值的概念 2.左值引用和右值引用的概念 2.左值引用和右…

C++linux高并发服务器项目实践 day2

Clinux高并发服务器项目实践 day2 静态库的制作静态库命名规则静态库的制作 动态库的制作命名规则制作使用动态库与静态库的区别解决动态库连接失败问题静态库和动态库的对比静态库的优缺点动态库的优缺点 Makefile什么是MakefileMakefile文件命名和规则Makefile的使用工作原理…

SpringSpringBoot常用注解总结

0.前言 可以毫不夸张地说&#xff0c;这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法&#xff0c;掌握搞懂&#xff0c;使用 SpringBoot 来开发项目基本没啥大问题了&#xff01; 为什么要写这篇文章…

【分享】Excel表格的密码忘记了怎么办?附解决办法

我们知道通过设置密码可以保护Excel表格&#xff0c;可有时候设置后很久没用就把密码忘记了&#xff0c;而Excel并没有找回密码的选项&#xff0c;那要怎么办呢&#xff1f;今天小编就来分享一下忘记Excel密码的解决方法。 Excel表格可以设置多种密码&#xff0c;不同密码对应…

短视频平台-小说推文(Lofter)推广任务详情

​Lofter日结内测中&#xff0c;可能暂只对部分优质会员开放! 注意 Lofter 关键词7天未使用&#xff0c;可能会被下线。 Lofter 不再需要回填视频链接了。 接Lofter官方通知 关于近期部分博主反馈播放量高但搜索量很低的问题尤其是快手平台&#xff0c;我们做了代码、服务器…

No.040<软考>《(高项)备考大全》【第24章】成熟度模型

【第24章】成熟度模型 1 考试相关2 第一维四个阶梯3 项目成熟度模型OPM3CMMI过程域 4 成熟度级别级别区别 5 练习题参考答案&#xff1a; 1 考试相关 选择可能考0-1分&#xff0c;案例论文不考。 2 第一维四个阶梯 3 项目成熟度模型OPM3 CMMI过程域 CMMI过程域可以分为4类&a…

智能对话机器人Rasa学习资料

文章目录 背景收集的Rasa学习资料官网B站其他 类似产品教学机器人售后咨询效果手机推荐效果 背景 最近做了一个Ros2项目&#xff0c;界面如下图&#xff1a; 客户要求能够使用语音快速执行特定动作如:打开视频窗口、显示小车1视频、无人机1返航等&#xff0c;这就涉及到了自然…

C++ : 整体工程构架设计流程

重点&#xff1a; 1.一个项目通常分为bin(存放项目生成的dll和整体工程的exe)&#xff0c;code(存每个项目的代码)&#xff0c;lib(存每个项目生成的lib),pdb(存放项目生成的pdb文件)&#xff0c;sln(解决方案) 整体创建流程&#xff1a; 一个主干项目&#xff0c;其他若干依赖…

Java接口自动化测试框架系列:提升测试效率的自动化测试框架

目录&#xff1a;导读 一、什么是自动化测试 二、自动化测试的缺点 三、自动化测试框架选型 原则 对比 四、框架构建 【自动化测试工程师学习路线】 一、什么是自动化测试 自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程。 通常&#xff0c;在设计了测试…

【UE】暂停游戏界面及功能实现

效果 步骤 1. 首先在项目设置中添加一个暂停的操作映射 2. 新建一个控件蓝图&#xff0c;命名为“PauseMenuWidget” 3. 打开“ThirdPersonCharacter”&#xff0c;添加一个布尔类型变量&#xff0c;命名为“isScreenShow”&#xff0c;用于判断当前玩家是否打开了暂停界面 在…

【Linxu网络服务】DHCP

DHCP 一、DHCP工作原理1.1背景1.2优点1.3 DHCP分配方式1.4DHCP工作原理 二、使用DHCP动态配置主机地址2.1实验一&#xff1a;动态配置主机地址2.2给Linux客户机配置动态地址**2.4设置一个外网口&#xff0c;给客户端设置一个固定的ip地址 一、DHCP工作原理 作为服务端负责集中…

uniapp 之 将marker 渲染在地图上 点击弹层文字时显示当前信息

目录 效果图 总代码 分析 1.template 页面 地图显示代码 2. onload ①经纬度 ②取值 ③注意 ④ 3.methods ① 先发送 getStationList 请求 获取 数组列表信息 ② regionChange 视野发生变化时 触发 分页逻辑 ③ callouttap 点击气泡时触发 查找 当前 marker id 等…

基于第一性原理DFT密度泛函理论的计算项目

随着计算机技术的不断发展&#xff0c;计算材料科学的方法也日益成熟。其中&#xff0c;基于第一性原理的密度泛函理论&#xff08;DFT&#xff09;计算方法&#xff0c;因其准确性、可靠性和高效性而广受欢迎。本文将介绍基于DFT的密度泛函理论的计算项目&#xff0c;包括电子…

云内基于 SRv6 的 SFC 方案

1. 基于 SRv6 的 SFC 服务链 为满足用户的业务数据安全、稳定等需求&#xff0c;提供各种基础保障或增值优化服务&#xff0c;在传统网络中&#xff0c;经常使用业务功能节点&#xff08;如负载均衡、防火墙等&#xff09;实现服务供应。但这些业务功能节点往往与网络拓扑和硬件…