springboot在feign和线程池中使用TraceId日志链路追踪(最终版)-2

基于OpenFeign扩展机制实现的微服务traceId链路追踪 1.通过Filter过滤器读取和设置traceId,并把traceId设置到response中,前端也可以查看2.对OpenFeign扩展,通过RequestInterceptor接口扩展请求头,通过请求头传递traceId3.将全局的traceId放入MDC中,MDC为slf4j-api包中提供的日志上下文工具全程MappedDiagnosticContext,详细信息(Chapter8),本质是ThreadLocal。放入MDC中的key可以在日志中输出}}}.................. 阅读详情
AI摘要

本文围绕Spring Boot中Feign和线程池使用TraceId日志链路追踪展开。指出Spring默认日志框架存在子线程和下游服务MDC值丢失问题,介绍了feign调用时给head加入traceId的方法,包括拦截器配置等。还阐述了使用TTL自定义线程池解决异步请求上下文传递问题,总结了实现线程池全局链路追踪的要点。

简述

书接前文:SpringBoot使用TraceId日志链路追踪-1

在上文中我们使用了springboot+MDC+Log4j2支持链路ID打印,但是如果我们使用feigin调用其他服务,线程池如何获取链路id呢;这里我们要用到阿里的TTL,在多线程面试中也会常用问到一个问题那就是线程传递问题。刚好这里就实践以下具体用法,并且这也是一个很好的案例。

问题

Spring 默认的日志框架 Logback 中提供的 LogbackMDCAdapter 内部使用的是ThreadLocal,只有本线程才有效,子线程和下游的服务 MDC 里的值会丢失。

主要的难点是解决值传递问题,主要包括以下几个部分:
异步情况下(线程池)如何传递 MDC 中的 TraceId 到子线程
API Gateway 网关中如何传递 MDC 中的 TraceId
微服务之间互相远程调用时如何传递 MDC 中的 TraceId

阿里的TTL组件解决了线程池场景下的上下文传递问题。通过装饰线程池,TTL在任务提交时自动拷贝父线程的上下文到子线程,并在任务结束后清理副本,确保多级线程池调用链路完整。

feign调用时给head加入traceId

1.需要拦截feign的requst请求,并加入自定义的报文头信息
2.需要把这个拦截器配置到feign的配置项中

在application.yml配置文件里面,可以添加feign相关的配置信息,常见的配置信息有如下这些:

loggerLevel:日志级别,四个取值:

  • NONE 不打印日志;
  • BASIC:只打印请求方法、URL、响应状态码、执行时间。
  • HEADERS:打印请求头、响应头的日志信息。
  • FULL:打印所有日志。

连接配置参数

  • connectTimeout:连接超时时间,单位毫秒:ms。
  • readTimeout:读取超时时间,单位毫秒:ms。
  • retryer:重试策略。
  • requestInterceptors:自定义的拦截器,可以多个,是一个List集合。
  • defaultRequestHeaders:默认的请求头信息。
  • defaultQueryParameters:默认的查询参数信息。
  • followRedirects: 是否允许重定向。

这里我们开启HEADERS级别的日志,方便本地服务调用时打印报文头我们可以查看是否TraceId信息;


import feign.RequestInterceptor;
import feign.RequestTemplate;
import gyqx.spd.common.constants.GlobalConstants;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.jboss.logging.MDC;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Optional;

/**
 * @version 1.0.0
 * @Date: 2025/3/15 8:49
 * @Description: 自定义feigin请求拦截器加入traceId,甚至我们可以把前端携带的token信息和用户id等报文头也放进来,方便转发给下一个被调用的服务
 */
@Slf4j
public class CustomFeignInterceptor implements RequestInterceptor {


    @Override
    public void apply(RequestTemplate template) {
        // TODO 在这里可以实现一些自定义的逻辑,例如:用户认证
        log.info("Feign执行拦截器....");
        Optional.ofNullable(RequestContextHolder.getRequestAttributes()).map(it -> ((ServletRequestAttributes) it).getRequest())
                .ifPresent(it -> {
                            String traceId = it.getHeader(GlobalConstants.GLOBAL_X_TRACE_ID);

                            if (StringUtil.isEmpty(traceId)) {
                                traceId = MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID) == null ? null : MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID).toString();
                            }

                            if (StringUtil.isNotBlank(traceId)) {
                                MDC.put(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);
                                template.header(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);
                            }
                            log.info("Feign执行拦截器traceId={}", traceId);
                        }
                );

    }
}

FeignConfig配置

一般都是全局配置,每个服务引入依赖自动配置生效,当然如果只测试可以在某个feigin上自己加上


import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @version 1.0.0
 * @Date: 2024/3/15 8:53
 * @Description: OpenFeign 配置类全局生效(直接放入核心配置,其他服务引入该依赖项即可)
 */
@Configuration
public class FeignConfig {
    /**
     * 注入自定义的拦截器
     */
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new CustomFeignInterceptor();
    }

}

FeignConfig 局部生效

以下方式皆可

  • 把上面的配置文件FeignConfig只放入某个服务中生效(本文就是这种)
  • yam中配置只对某个服务生效时配置
# feign 配置
feign:
  client:
    config:
      # 这里写微服务的服务名称,例如:我这里写的是 service-provider 服务名称
      # 针对 service-provider 微服务的请求,都将执行这些配置信息
      service-provider:
        loggerlevel: full
        # 配置请求拦截器,可以多个
        requestInterceptors:
          - com.gitee.code.interceptor.CustomFeignInterceptor

feign拦截器和配置合并为一个文件(最终版)

上面的写法是拦截器和配置文件分开了,当然我们可以缩写直接合并为一个文件,全局生效即可;


import feign.RequestInterceptor;
import feign.RequestTemplate;
import gyqx.spd.common.constants.GlobalConstants;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.jboss.logging.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Optional;

/**
 * @version 1.0.0
 * @Date: 2024/3/15 8:53
 * @Description: OpenFeign 配置类+拦截器
 */
@Slf4j
@Configuration
public class FeignConfig {
    /**
     * 注入自定义的拦截器
     */
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor(){

            @Override
            public void apply(RequestTemplate template) {
                // TODO 在这里可以实现一些自定义的逻辑,例如:用户认证
                log.info("Feign执行拦截器....");
                Optional.ofNullable(RequestContextHolder.getRequestAttributes()).map(it -> ((ServletRequestAttributes) it).getRequest())
                        .ifPresent(it -> {
                                    String traceId = it.getHeader(GlobalConstants.GLOBAL_X_TRACE_ID);

                                    if (StringUtil.isEmpty(traceId)) {
                                        traceId = MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID) == null ? null : MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID).toString();
                                    }
                                    log.info("Feign执行拦截器token:traceId={}", traceId);
                                    if (StringUtil.isNotBlank(traceId)) {
                                        MDC.put(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);
                                        traceId= traceId.substring(traceId.indexOf(":") + 1);//我这里去掉了用户的token信息,不影响使用
                                        template.header(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);
                                    }
                                    log.info("Feign执行拦截器traceId={}", traceId);
                                }
                        );

            }
        };
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;//日志级别全打印-方便调试,后续可以修改级别
    }
}

开启报文打印后,可以看到报文头里面已经有了traceId
在这里插入图片描述

在服务提供方的日志中也可以看到拦截到了
在这里插入图片描述

feign异步调用拦截器配置[不常用]

对于上面的配置对异步阻塞调用,异步非阻塞调用的情况就不再适用,需要单独把请求头获取后放入本地ThreadLocal变量中,也就是有个备份,然后放入异步线程的请求头里面。

详细请参考下面的blog,这种异步的异步项目中很少用到,但是放入本地ThreadLocal变量中的方式倒是很常用,比如我们把当前登录用户的session信息放入当前线程的上下文环境中,方便在业务中获取当前用户的ID,部门,权限等数据。

而这里把请求头的数据用拦截器拿到后放入本地ThreadLocal变量中只是为了方便调用其他feign异步接口时获取到当前线程携带过来的数据防止被覆盖和清空,为给另一个异步操作参数做准备工作。
https://blog.youkuaiyun.com/LatiaoCanCode/article/details/144358227

原理比较简单,上文中的TraceId我们也是在拦截器中获取并存入了MDC的ThreadLocal变量,这里其实也是自定义了一个ThreadLocal变量用户存放请求头信息,甚至可以拓展这个ThreadLocal变量为当前线程上下文环境变量CurrentThreadContex专门存放前端调用携带过来的上文数据作为缓存在当前线程中,方便后续操作使用。

对于线程部分的配置则无需参考,没有什么实际意义直接参考下面的自定义线程池使用TraceId

使用TTL自定义线程池

异步请求丢失上文的问题,这些问题追根究底都是ThreadLocal惹得祸。
由于ThreadLocal只能保存当前线程的信息,不能实现父子线程的继承。
说到这,很多人想到了InheritableThreadLocal,确实InheritableThreadLocal能够实现父子线程间传递本地变量,但是你的程序如果采用线程池,则存在着线程复用的情况,这时就不一定能够实现父子线程间传递了,因为在线程在线程池中的存在不是每次使用都会进行创建,InheritableThreadlocal是在线程初始化时intertableThreadLocals=true才会进行拷贝传递。

失败样例

@Test
public void test() throws Exception {
    //单一线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    //InheritableThreadLocal存储
    InheritableThreadLocal<String> username = new InheritableThreadLocal<>();
    for (int i = 0; i < 10; i++) {
    username.set("公众号:牧竹子—"+i);
    Thread.sleep(3000);
    CompletableFuture.runAsync(()-> System.out.println(username.get()),executorService);
   }
}

//打印如下
-----------------------
公众号:牧竹子—0
公众号:牧竹子—0
公众号:牧竹子—0
公众号:牧竹子—0

所以若使用的子线程是已经被池化的线程,从线程池中取出线下进行使用,是没有经过初始化的过程,也就不会进行父子线程的本地变量拷贝。
由于在日常应用场景中,绝大多数都是会采用线程池的方式进行资源的有效管理。

为什么需要TransmittableThreadLocal?

在Spring框架中,默认情况下,线程池的任务执行是通过java.util.concurrent包中的ThreadPoolExecutor实现的。然而,如果你想要在使用Spring框架的同时,利用阿里巴巴的TransmittableThreadLocal(TTL)来传递线程局部变量(ThreadLocal)的值到异步任务中,你可以通过自定义线程池配置来实现这一点。

线程中JUC默认父子线程传递的InheritableThreadlocal是在线程初始化时intertableThreadLocals=true才会进行拷贝传递。但是在线程中已经是初始化之后的线程无法获取新的拷贝所以总是初始值。

TransmittableThreadLocal是阿里巴巴开源的库,主要用于解决在使用线程池时,线程局部变量(ThreadLocal)不能正确地传递到异步任务中的问题。这在微服务架构或者使用Spring Boot进行异步处理时尤其重要

样例

@Test
public void test() throws Exception {
    //单一线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    //需要使用TtlExecutors对线程池包装一下
    executorService=TtlExecutors.getTtlExecutorService(executorService);
    //TransmittableThreadLocal创建
    TransmittableThreadLocal<String> username = new TransmittableThreadLocal<>();
    for (int i = 0; i < 10; i++) {
    username.set("公众号:牧竹子—"+i);
    Thread.sleep(3000);
    CompletableFuture.runAsync(()-> System.out.println(username.get()),executorService);
  }
}

//打印如下
-----------------------
公众号:牧竹子—0
公众号:牧竹子—1
公众号:牧竹子—2

可以看到已经能够实现了线程池中的父子线程的数据传递。
在每次调用任务的时,都会将当前的主线程的TTL数据copy到子线程里面,执行完成后,再清除掉。同时子线程里面的修改回到主线程时其实并没有生效。这样可以保证每次任务执行的时候都是互不干涉。

替换 Spring 默认线程池 使用 alibaba 的 TtlRunnable进行替换。

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.14.4</version>
        </dependency>

自定义线程池


import com.alibaba.ttl.TtlRunnable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class TaskThreadPoolConfig {


    @Autowired
    private TaskThreadPoolProperties config;

    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程池大小
        executor.setCorePoolSize(config.getCorePoolSize());
        //最大线程数
        executor.setMaxPoolSize(config.getMaxPoolSize());
        //队列容量
        executor.setQueueCapacity(config.getQueueCapacity());
        //活跃时间
        executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
        //线程名字前缀
        executor.setThreadNamePrefix("MyExecutor-");

        // setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(120); //等待任务执行时间,如果超过这个时间还没有销毁就 强制销


        executor.setTaskDecorator(getTraceContextDecorator());//使用TTL包装
        executor.initialize();
        return executor;
    }

private TaskDecorator getTraceContextDecorator() {
        return runnable -> TtlRunnable.get(() -> {
            try {
                //把父级值放入当前线程的MDC本地ThreadLocal中,log4j打印时使用
                MDC.put("traceId", GlobTraceContext.getTraceId());
                runnable.run();
            } finally {
                MDC.clear();
            }
        });
    }

}

因为MDC默认使用的ThreadLocal缓存当前线程的值,因此这里不能在用MDC默认的方式,得使用TTL改写为TransmittableThreadLocal类型放线程变量;

GlobTraceContext


import cn.hutool.core.util.IdUtil;
import com.alibaba.ttl.TransmittableThreadLocal;

/**
 * 基于TransmittableThreadLocal实现线程池安全的TraceID传递
 *
 * @author wnhyang
 * @date 2025/3/3
 **/
public class GlobTraceContext {


    private static final TransmittableThreadLocal<String> TRACE_ID = new TransmittableThreadLocal<>();

    /**
     * 设置TraceID,并同步到Log4j2的MDC
     */
    public static void setTraceId(String traceId) {
        TRACE_ID.set(traceId);
    }

    public static String getTraceId() {
        return TRACE_ID.get();
    }

    public static void clear() {
        TRACE_ID.remove();
    }

    public static String generateTraceId() {
        return IdUtil.simpleUUID();
    }
}

AsyncService


import gyqx.spd.outside.conf.GlobTraceContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * 异步调用service
 * @author summer
 */
@Service
@Slf4j
public class AsyncService {
    /**
     * 使用 @Async 注解 实现异步调用
     * taskExecutor为自定义线程池,指定自定义线程池
     * @return
     * @throws InterruptedException
     */

    @Async("taskExecutor")
    public void async(){
        log.info("async异步任务开始: " + Thread.currentThread().getName());

        try {
            log.info("子线程traceId:: " + GlobTraceContext.getTraceId());
            // 模拟耗时操作(实际工作中,此处写业务逻辑处理)
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("async异步任务完成");
    }
}

AsyncService

@RequestMapping("test")
public class TestController {
    @Autowired
    private AsyncService asyncService;
    @Resource
    private ThreadPoolTaskExecutor taskExecutor;//使用我们自定义的线程池

    @GetMapping("/async")
    public AjaxResult<String> async() {
        String traceId = UUID.randomUUID().toString();
        System.out.println("父亲线程traceId:" +traceId);
        GlobTraceContext.setTraceId(traceId);
        //异步执行
        CompletableFuture.runAsync(() -> asyncService.async(), taskExecutor);

//        asyncService.async();
        log.info("async异步任务调用成功");
        return AjaxResult.ok("async异步任务调用成功");
    }

}

打印结果如下

父亲线程traceId:db9a27e4-549a-4a41-ba7d-25297d875dca
controller.HrpStockController  : async异步任务调用成功
controller.AsyncService     : async异步任务开始: MyExecutor-1
controller.AsyncService     : 子线程traceId:: db9a27e4-549a-4a41-ba7d-25297d875dca
controller.AsyncService     : async异步任务完成

总结

要实现线程池支持的全局链路追踪需要上一章节和本章节feign拦截器中都要使用TransmittableThreadLocal作为替换ThreadLocal类型MDC作为存放链路ID,同样在父子线程中想存放其他值也需要使用TransmittableThreadLocal类型。

这里我们需要把上一章,和本章的LogInterceptor,FeignConfig中的MDC.put前加上GlobTraceContext.setTraceId即可,在线程池配置中我们在包装TTL的时候执行任务前把获取到的,getTraceId()放入当前线程的MDC变量中,这样log4j打印日志即可拿到当前线程的traceId值

在这里插入图片描述

参考和拓展阅读

SpringBoot使用TraceId日志链路追踪-1
https://blog.youkuaiyun.com/zjcjava/article/details/146237248

微服务中使用阿里开源的TTL,优雅的实现身份信息的线程间复用
https://developer.aliyun.com/article/12012001056715.html

阿里开源支持缓存线程池的ThreadLocal Transmittable ThreadLocal(TTL)
https://www.cnblogs.com/xiaopotian/p/11056715.html

配置spring/springboot默认的异步线程池
https://www.cnblogs.com/duanxz/p/6084494.html?ivk_sa=1024320u

ThreadLocal父子线程数据传递
https://blog.youkuaiyun.com/zjcjava/article/details/125601123

异步线程traceId如何传递 上面两种方式其实本质都是通过Mdc来进行异步线程间的traceId同步,可以看下Mdc的源码,最终还是通过InheritableThreadLocal来实现子线程获取父线程信息//省略若干...... 阅读详情

相关推荐

SpringBoot项目注入 traceId 来追踪整个请求的日志链路

SpringBoot项目注入 traceId 来追踪整个请求的日志链路,有了 traceId, 我们在排查问题的时候,可以迅速根据 traceId 查找到相关请求的日志,特别是在生产环境的时候,用户可能只提供一个错误截图,我们作为开发人员想要更详细的日志,这时候我们就可以直接根据用户提供的 traceId 来查找用户这次请求的整个日志,会方便很多。即使是在单体应用中,为请求添加一个唯一的traceId以追踪整个请求链路的日志也是完全可行的。关于更具体的日志输出配置,可以参考我的这篇文章《

z284747的博客 2663

基于trace_id的链路追踪,含Feign、Hystrix、线程池等场景

目前公司的微服务架构是基于Spring Cloud来实现的,而在实现服务间trace_id链路追踪的时候,发现服务提供方和服务调用方的trace_id不一致,所以在此记录该问题的解决方案,并针对Java体系中常见的场景进行了分析和给出了具体的实现方案。 一、概述 在微服务的体系架构中,都存在一个服务与服务之间的调用链路追踪问题。虽然在生产环境中会采用第三方的组件或服务来实现链路追踪,比如SkyWalking、阿里云arms等,但一旦脱离开这些第三方的功能,微服务体系中的问题排查将会变得异常艰辛,所以如果.

白云 8132

保姆级教程:SpringBoot日志链路追踪终极方案 - Feign拦截器+TTL线程池配置详解

本文详细介绍了SpringBoot中实现全链路日志追踪的终极方案,通过Feign拦截器自动传递TraceId和TTL线程池配置,解决微服务架构下跨服务、跨线程的日志追踪难题。文章包含完整的代码示例和配置指南,帮助开发者快速构建生产级的日志链路追踪系统。

weixin_30772105的博客 382

springboot + feign 下的全链路traceID 的实现

全链路traceid的实现

V_Carolin的博客 3440

SpringBoot Feign调用丢失TraceId?3种解决方案实测对比(含TTL线程池配置)

本文深入探讨了SpringBoot Feign调用中TraceId丢失的问题,提供了三种解决方案的实测对比,包括MDC+自定义线程装饰器、InheritableThreadLocal和TransmittableThreadLocal(TTL)线程池配置。重点介绍了TTL线程池的最佳实践,帮助开发者实现高效的日志链路追踪,提升分布式系统的故障排查效率。

weixin_33728268的博客 465

SpringBoot TraceId日志链路追踪:从设计到落地

在微服务架构中,几十个服务各自打印日志,线上问题时靠人工翻查日志往往难以串联完整调用链。链路追踪(Tracing)通过为每次请求分配全局唯一的TraceId,并结合MDC(Mapped Diagnostic Context)机制,让日志自动携带该标识,从而实现跨服务的日志串联。TraceId不仅是分布式系统可观测性的基础,也是日志排查、性能分析、告警联动的关键支撑。无论是网关入口、HTTP/RPC调用还是线程池异步场景,都需要正确传递与清理TraceId,才能确保链路不中断。从Filter设计、MDC映射、

weixin_32518317的博客 213

SpringBoot集成TTL实现Feign与线程池的TraceId无缝传递(实战优化版)

本文详细介绍了如何在SpringBoot中集成TTL实现Feign与线程池的TraceId无缝传递,解决分布式系统中的日志链路追踪难题。通过Feign拦截器设计、线程池深度改造及全链路解决方案,确保TraceId在跨服务调用和异步处理中的一致性,大幅提升问题排查效率。

weixin_29238147的博客 356

SpringBoot日志链路追踪实战:TraceId+MDC+ELK全链路排查

微服务架构下,一次请求往往横跨多个服务与线程,日志碎片化问题让故障排查耗时数小时甚至数天。日志链路追踪技术通过为每个请求生成全局唯一的TraceId,并借助SLF4J的MDC(Mapped Diagnostic Context)实现线程内日志自动携带标识,形成可串联的日志上下文。在SpringBoot应用中,基于Filter完成TraceId的生成与透传,结合Feign、RestTemplate等拦截器实现跨服务传递,配合线程池TaskDecorator解决异步场景下的上下文丢失,最终将日志接入ELK(El

weixin_31292729的博客 142

SpringBoot 接入 MDC 实现日志链路追踪,轻松排查接口超时

在软件开发和运维中,日志是定位线上问题的第一手资料。当并发请求增多时,多行日志由不同线程交替写入,单靠时间戳与 IP 很难还原一次完整调用链路,导致“有日志却排查不动”的困境。MDC 作为一种基于 ThreadLocal 的映射诊断上下文机制,可以将 traceId 自动注入同一条请求的所有日志输出,让日志按请求维度天然归组,显著提升排障效率。在 SpringBoot 工程中,通过过滤器统一生成与继承 traceId,借助线程池装饰器完成异步传递,再通过 HTTP Header 实现跨服务透传,即可覆盖同步

weixin_33508411的博客 314

从MDC到TTL:SpringBoot日志链路追踪的进阶指南(含线程池避坑技巧)

本文深入探讨了SpringBoot日志链路追踪的进阶方案,从传统MDC到TTL的技术演进,特别针对线程池环境下的TraceId传递问题提供解决方案。通过TransmittableThreadLocal实现线程池上下文传递,结合Feign客户端和异步任务处理,构建完整的分布式追踪体系,并分享生产环境中的避坑技巧和性能优化建议。

weixin_30300225的博客 408

SpringBoot日志追踪:从TraceId到MDC的完整落地指南

在分布式系统和微服务架构中,日志是排查线上问题的重要依据。然而,并发请求交织在一起,传统日志难以还原单次请求的完整调用链路。TraceId作为一种全局唯一标识符,能够贯穿请求的整个生命周期,将分散的日志串成有序链路。结合MDC(Mapped Diagnostic Context)这一日志上下文机制,SpringBoot项目可以低成本实现日志追踪:由过滤器生成或透传TraceId,存入MDC后通过Logback的%X占位符输出,并在请求结束时安全清理。异步线程、跨服务调用及消息队列场景还需额外处理上下文传递的

weixin_31309419的博客 160

SpringBoot日志链路追踪:基于MDC实现TraceId的完整方案

在分布式系统与微服务架构中,排查一次跨服务的异常调用,往往需要从海量日志中手工拼凑上下文,耗时且容易出错。TraceId作为一种全局唯一的请求标识,能够将一次业务调用在多个服务、多个线程、多个异步任务中产生的日志串联起来,是实现日志链路追踪的核心基础。而MDC(Mapped Diagnostic Context)基于ThreadLocal提供了一套轻量级的上下文传递机制,配合SpringBoot的过滤器和线程池装饰器,可以低成本地在现有项目落地TraceId能力。无论是RestTemplate、OpenFe

weixin_33391446的博客 234

SpringBoot日志追踪:用TraceId构建全链路日志排查体系

在分布式系统与微服务架构中,日志串联是排查线上问题的关键能力。TraceId作为一次请求的全链路唯一标识,通过MDC日志上下文在服务间传递,能将分散在多个服务的日志按同一ID快速聚合。借助SpringBoot的Filter过滤器、Logback的%X占位符以及TaskDecorator线程池装饰器,可以实现从入口生成、异步传递到跨服务HTTP透传的完整链路。该方法适用于网关、订单、库存等多服务调用场景,可显著提升故障定位与耗时分析效率。本文梳理TraceId日志追踪在SpringBoot中的落地思路,帮助后

weixin_32932149的博客 213

SpringBoot MDC链路追踪实战:过滤器、线程池与traceId排坑

MDC(Mapped Diagnostic Context)是 SLF4J 提供的线程级日志上下文机制,通过 ThreadLocal 保存 traceId,并借助日志框架的 %X 占位符自动输出,让同一次请求的所有日志都携带唯一标识。理解这个原理后,日志治理就能从“grep 报错”升级为按链路聚合:无论是同步调用、异步线程池还是跨服务 Feign 调用,都能用同一条 traceId 串起完整过程。在 SpringBoot 中落地,需要结合 Filter 注入 traceId、Logback pattern

weixin_30768881的博客 166

深入浅出:使用MDC构建SpringBoot全链路请求追踪系统(实战避坑版)

做微服务开发的,大概率都有过这样的崩溃时刻:用户反馈“下单接口偶尔失败,但重试又能成功”,你去查日志,发现网关日志、订单服务日志、支付服务日志、库存服务日志、异步通知日志杂乱无章,没有任何统一标识——你分不清哪条日志属于同一个请求,只能凭着时间戳、用户ID一点点筛选,翻了几十分钟甚至几小时,才勉强串联起链路,定位到问题根源。这就是微服务开发中“日志碎片化”的痛点,尤其是在高并发场景下,一个请求可能经过网关、多个微服务、多个异步线程,没有统一的追踪标识,排查问题的效率极低。

2601_94871597的博客 736

GIS坐标系转换以及空间校正和地理配准

许多小伙伴都对坐标系,投影这类问题迷迷糊糊,我之前也一样,现在总结一下。 坐标系的转换1.为什么要进行坐标系的转换?2. 不同坐标系的介绍1.地理坐标系我国常见的地理坐标(1).北京54坐标系(2).西安80坐标系(3).WGS-84坐标系(4).大地2000坐标系2.投影坐标系我国常用的投影坐标系(1) UTM投影(2) 高斯-克吕格投影(3) 兰伯特投影(4) 阿尔伯斯投影投影坐标系的选择3. 坐标系的转换1.已知坐标系之间的转换1.1 两坐标系的参考椭球相同1.2 两坐标系的参考椭球不一致,但是GIS

m0_47258611的博客 8912

SpringBoot自定义链路追踪

SpringBoot自定义链路追踪

今天又是充满希望的一天 463

UE4项目迁移指南:如何无缝导入外部.uasset文件并保持材质关联

本文详细解析了将外部.uasset文件无缝导入UE4项目的专业方法。针对手动复制文件时常见的材质关联丢失问题,提供了使用引用查看器进行诊断和修复的实用技巧,并推荐使用引擎内置的“迁移”功能确保资源依赖关系的完整性,帮助开发者高效整合项目资源。

t8u9v0的博客 1045

SpringBoot分布式系统日志链路追踪实践指南

分布式系统架构中,日志链路追踪是实现系统可观测性的关键技术。通过为每个请求分配唯一的TraceId,可以串联跨服务的调用日志,解决传统日志分散难追踪的痛点。其核心原理是通过上下文传播机制,在服务间透传追踪标识。这项技术不仅能快速定位异常链路,还能进行性能分析和系统监控。在微服务场景下,结合Spring Cloud Sleuth等工具可以自动实现TraceId生成和传递。典型应用包括电商交易链路追踪、支付系统异常排查等场景。本文详细介绍如何在SpringBoot项目中集成Sleuth+Logback方案,实现

weixin_34297704的博客 366

Android Activity面试题最全解析(5万字长文)

Intent是Android中用于启动活动、服务或广播的一种消息对象。它包含了一系列键值对数据,用于描述操作的目的以及需要传递的信息。IntentString:字符串intfloatlong:基本数据类型Parcelable:实现了Parcelable接口的对象:实现了接口的对象示例代码如下:Java深色版本1// 创建Intent并传递数据");67// 启动新的Activity是一个XML标签,用于定义ActivityService或可以响应的Intent类型。它包含了。

大模型大数据攻城狮的专栏 801

反光柱定位算法实战03:VEnus代码解析和纯反光柱定位中的可改进方向

本文深入解析了开源框架VEnus的反光柱定位与建图实现流程,包括强度提取、聚类、数据关联、位姿优化等核心模块。同时指出其三大局限:匹配质量脆弱性、单一传感器依赖和地图扩展逻辑缺失,并提出改进方向:融合里程计/IMU数据、引入图优化后端、建立动态地图维护机制。该分析为基于反光柱的定位系统优化提供了实用参考,有助于提升工业场景下的定位鲁棒性和精确性。

Studying…… 520

fegin调用接口传递traceId,拦截器

通过拦截器在HttpServerletRequest里获取header里的trace_id。1、consumer消费者(服务调用方)2、provider服务者(服务提供方)

qq_34004088的博客 945

word多级列表为什么会变成黑块解决思路

今天在写文档时,发现WORD5级的标题都变黑块了,找了度娘的资料,终于解决了。 第一种 1、光标bai放在黑快的右边,按向左的箭头,然后ctrl+shift+s,点击重新应用。 2、点击“定义新的多级列表”(不知道从哪儿找这个按钮的百度下),直接点确定。 3、这里很重要!!!不要直接保存,而是直接关掉word,会提示保存还是不保存,这个地方再点击保存。 第二种 Word2016 使用宏代码修复方法 1.找到视图-宏,打开 查看宏 2.创建宏 3.贴...

霍力强的专栏 8580
上一篇: SpringBoot使用TraceId日志链路追踪-1
下一篇: 生成数据库表结构markdown描述文档
牧竹子
博客等级 码龄17年 572粉丝 233原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值