引入Springcloud--Sleuth-链路追踪中MDC是如何获取到traceid和何时放入traceid的

news2025/3/12 11:17:40

   在分布式项目中需要引入  spring-cloud-starter-sleuth框架来记录跟踪请求在不同服务之前流转的路径。在整个流转路径通过traceid将所有的路径给串联起来。

项目中需要保存traceid来实现日志快速搜索和定位,可以通过MDC.get("traceId")获取到traceId。

当前项目集成了

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
    <version>2.0.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-core</artifactId>
    <version>2.0.4.RELEASE</version>
</dependency>

MDC((Mapped Diagnostic Contexts))翻译过来就是映射的诊断上下文 。意思是:在日志中 (映射的) 请求ID(requestId),可以作为我们定位 (诊断) 问题的关键字 (上下文)。这并不是一个新鲜的产物,MDC类基本原理其实非常简单,其内部持有一个ThreadLocal实例,用于保存context数据,MDC提供了put/get/clear等几个核心接口,用于操作ThreadLocal中的数据。

我们在使用MDC获取traceid时要考虑traceid为什么能通过MDC获取到,按照调用顺序,分析以下的两个问题:

1.traceid从哪里取值的?

2.traceid从哪里赋值进去的?

1.traceid从哪里取值的

MDC.get("traceId")分析MDC类里调用的get方法

public static String get(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }

        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        return mdcAdapter.get(key);
    }

在该方法中mdcAdapter是通过 MDC类里的静态方法块赋值的。

    private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {
        try {
            return StaticMDCBinder.getSingleton().getMDCA();
        } catch (NoSuchMethodError nsme) {
            // binding is probably a version of SLF4J older than 1.7.14
            return StaticMDCBinder.SINGLETON.getMDCA();
        }
    }
public class StaticMDCBinder {

    /**
     * The unique instance of this class.
     */
    public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();

    private StaticMDCBinder() {
    }

    /**
     * Currently this method always returns an instance of 
     * {@link StaticMDCBinder}.
     */
    public MDCAdapter getMDCA() {
        return new LogbackMDCAdapter();
    }

    public String getMDCAdapterClassStr() {
        return LogbackMDCAdapter.class.getName();
    }
}

在上面的代码段中可以看到该属性对应的类类型为LogbackMDCAdapter

查看该类的get方法

public String get(String key) {
        final Map<String, String> map = copyOnThreadLocal.get();
        if ((map != null) && (key != null)) {
            return map.get(key);
        } else {
            return null;
        }
    }

发现上面的方法是通过copyOnThreadLocal属性取值

final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();

从上面的属性定义中我们可以看到,该属性值的来源源自线程Thread中的ThreaLocal中取值。

那么threadLocal里的值是在哪里赋值的?

2.traceid从哪里赋值进去的

spring-cloud-sleuth-core这个jar包中找到了spring.factories文件。

在该文件中找到了TraceAutoConfiguration类,该类中注入了Tracing类型的bean。该bean对currentTraceContext属性赋值了一个CurrentTraceContext类型的bean。注意该上下文后面会用上。

    @Bean
    @ConditionalOnMissingBean
    Tracing tracing(@Value("${spring.zipkin.service.name:${spring.application.name:default}}") String serviceName, Factory factory, CurrentTraceContext currentTraceContext, Reporter<Span> reporter, Sampler sampler, ErrorParser errorParser, SleuthProperties sleuthProperties) {
        return Tracing.newBuilder().sampler(sampler).errorParser(errorParser).localServiceName(serviceName).propagationFactory(factory).currentTraceContext(currentTraceContext).spanReporter(this.adjustedReporter(reporter)).traceId128Bit(sleuthProperties.isTraceId128()).supportsJoin(sleuthProperties.isSupportsJoin()).build();
    }

而CurrentTraceContext类型的bean是通过SleuthLogAutoConfiguration这个配置类注入的,可以看到以下代码,下面的代码通过后置处理器对CurrentTraceContext类型的bean进行处理,如果该CurrentTraceContext类型的bean不是Slf4jCurrentTraceContext类型的,则对该类型的bean通过代理的方式向spring容器中注入了Slf4jCurrentTraceContext类型的bean。    

@Bean
        @ConditionalOnProperty(
            value = {"spring.sleuth.log.slf4j.enabled"},
            matchIfMissing = true
        )
        @ConditionalOnBean({CurrentTraceContext.class})
        public static BeanPostProcessor slf4jSpanLoggerBPP() {
            return new SleuthLogAutoConfiguration.Slf4jConfiguration.Slf4jBeanPostProcessor();
        }

        static class Slf4jBeanPostProcessor implements BeanPostProcessor {
            Slf4jBeanPostProcessor() {
            }

            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                return bean;
            }

            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                return bean instanceof CurrentTraceContext && !(bean instanceof Slf4jCurrentTraceContext) ? Slf4jCurrentTraceContext.create((CurrentTraceContext)bean) : bean;
            }
        }

分析spring.factories文件中配置的TraceHttpAutoConfiguration类,在该类中通过@Bean的方式注入了HttpTracing 这个类型的bean对象。该对象的入参使用了Tracing类型的Bean对象。

    @Bean
    @ConditionalOnMissingBean
    HttpTracing httpTracing(Tracing tracing, SkipPatternProvider provider) {
        HttpSampler serverSampler = this.combineUserProvidedSamplerWithSkipPatternSampler(provider);
        return HttpTracing.newBuilder(tracing).clientParser(this.clientParser).serverParser(this.serverParser).clientSampler(this.clientSampler).serverSampler(serverSampler).build();
    }

分析spring.factories文件中配置的TraceWebServletAutoConfiguration类,在TraceWebServletAutoConfiguration类。在该类中通过以下代码

    @Bean
    @ConditionalOnMissingBean
    public TracingFilter tracingFilter(HttpTracing tracing) {
        return (TracingFilter)TracingFilter.create(tracing);
    }

该方法的入参是前面通过TraceHttpAutoConfiguration类注入的HttpTracing类型的对象。在调用TracingFilter.create(tracing)方法时,会通过new TracingFilter(httpTracing)返回向spring容器中注入了TracingFilter类型的对象。在创建对象的同时向currentTraceContext属性赋值,该属性源自HttpTracing中生成时设置的上下文Slf4jCurrentTraceContext。

TracingFilter实现了Filter接口,意味着该对象是一个拦截器类型的对象。当接受到请求后,拦截器会拦截请求并执行拦截器中的doFilter方法。

查看doFilter方法

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse res = this.servlet.httpServletResponse(response);
        TraceContext context = (TraceContext)request.getAttribute(TraceContext.class.getName());
        if (context != null) {
            Scope scope = this.currentTraceContext.maybeScope(context);

            try {
                chain.doFilter(request, response);
            } finally {
                scope.close();
            }

        } else {
            //该段代码生成Span,span通过TraceContext中持有traceid
            Span span = this.handler.handleReceive(new HttpServletRequestWrapper(req));
            request.setAttribute(SpanCustomizer.class.getName(), span);
            request.setAttribute(TraceContext.class.getName(), span.context());
            TracingFilter.SendHandled sendHandled = new TracingFilter.SendHandled();
            request.setAttribute(TracingFilter.SendHandled.class.getName(), sendHandled);
            Throwable error = null;
            //通过该方法向MDC赋值
            Scope scope = this.currentTraceContext.newScope(span.context());
            boolean var17 = false;

            try {
                var17 = true;
                chain.doFilter(req, res);
                var17 = false;
            } catch (Throwable var22) {
                error = var22;
                throw var22;
            } finally {
                if (var17) {
                    if (this.servlet.isAsync(req)) {
                        this.servlet.handleAsync(this.handler, req, res, span);
                    } else if (sendHandled.compareAndSet(false, true)) {
                        HttpServerResponse responseWrapper = HttpServletResponseWrapper.create(req, res, error);
                        this.handler.handleSend(responseWrapper, span);
                    }

                    scope.close();
                }
            }

            if (this.servlet.isAsync(req)) {
                this.servlet.handleAsync(this.handler, req, res, span);
            } else if (sendHandled.compareAndSet(false, true)) {
                HttpServerResponse responseWrapper = HttpServletResponseWrapper.create(req, res, error);
                this.handler.handleSend(responseWrapper, span);
            }

            scope.close();
        }
    }

 在上面的代码中重点关注this.currentTraceContext.newScope(span.context())方法,其中

此时的currentTraceContext是Slf4jCurrentTraceContext类型的。在Slf4jCurrentTraceContext类的newScope方法可以看到MDC.put("traceId", traceIdString)将traceid赋值到了MDC中。

 public Scope newScope(@Nullable TraceContext currentSpan) {
        final String previousTraceId = MDC.get("traceId");
        final String previousParentId = MDC.get("parentId");
        final String previousSpanId = MDC.get("spanId");
        final String spanExportable = MDC.get("spanExportable");
        final String legacyPreviousTraceId = MDC.get("X-B3-TraceId");
        final String legacyPreviousParentId = MDC.get("X-B3-ParentSpanId");
        final String legacyPreviousSpanId = MDC.get("X-B3-SpanId");
        final String legacySpanExportable = MDC.get("X-Span-Export");
        if (currentSpan != null) {
            String traceIdString = currentSpan.traceIdString();
            //重点关注此处
            MDC.put("traceId", traceIdString);
            MDC.put("X-B3-TraceId", traceIdString);
            String parentId = currentSpan.parentId() != null ? HexCodec.toLowerHex(currentSpan.parentId()) : null;
            replace("parentId", parentId);
            replace("X-B3-ParentSpanId", parentId);
            String spanId = HexCodec.toLowerHex(currentSpan.spanId());
            MDC.put("spanId", spanId);
            MDC.put("X-B3-SpanId", spanId);
            String sampled = String.valueOf(currentSpan.sampled());
            MDC.put("spanExportable", sampled);
            MDC.put("X-Span-Export", sampled);
            this.log("Starting scope for span: {}", currentSpan);
            if (currentSpan.parentId() != null && log.isTraceEnabled()) {
                log.trace("With parent: {}", currentSpan.parentId());
            }
        } else {
            MDC.remove("traceId");
            MDC.remove("parentId");
            MDC.remove("spanId");
            MDC.remove("spanExportable");
            MDC.remove("X-B3-TraceId");
            MDC.remove("X-B3-ParentSpanId");
            MDC.remove("X-B3-SpanId");
            MDC.remove("X-Span-Export");
        }

        final Scope scope = this.delegate.newScope(currentSpan);

        class ThreadContextCurrentTraceContextScope implements Scope {
            ThreadContextCurrentTraceContextScope() {
            }

            public void close() {
                Slf4jCurrentTraceContext.this.log("Closing scope for span: {}", currentSpan);
                scope.close();
                Slf4jCurrentTraceContext.replace("traceId", previousTraceId);
                Slf4jCurrentTraceContext.replace("parentId", previousParentId);
                Slf4jCurrentTraceContext.replace("spanId", previousSpanId);
                Slf4jCurrentTraceContext.replace("spanExportable", spanExportable);
                Slf4jCurrentTraceContext.replace("X-B3-TraceId", legacyPreviousTraceId);
                Slf4jCurrentTraceContext.replace("X-B3-ParentSpanId", legacyPreviousParentId);
                Slf4jCurrentTraceContext.replace("X-B3-SpanId", legacyPreviousSpanId);
                Slf4jCurrentTraceContext.replace("X-Span-Export", legacySpanExportable);
            }
        }

        return new ThreadContextCurrentTraceContextScope();
    }

跟踪MDC的put方法最后发现,该方法中将key和对应的value放置到了copyOnThreadLocal属性中。该属性是ThreadLocal类型的,这样在线程的任何地方都可以通过MDC.get来获取到traceid了。

    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }

        Map<String, String> oldMap = copyOnThreadLocal.get();
        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
            Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
            newMap.put(key, val);
        } else {
            oldMap.put(key, val);
        }
    }

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

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

相关文章

统计信号处理基础 习题解答10-9

题目 某质检员的工作是监控制造出来的电阻阻值。为此他从一批电阻中选取一个并用一个欧姆表来测量它。他知道欧姆表质量较差&#xff0c;它给测量带来了误差&#xff0c;这个误差可以看成是一个的随机变量。为此&#xff0c;质检员取N个独立的测量。另外&#xff0c;他知道阻值…

gRPC(狂神说)

gRPC&#xff08;狂神说&#xff09; 视频地址&#xff1a;【狂神说】gRPC最新超详细版教程通俗易懂 | Go语言全栈教程_哔哩哔哩_bilibili 1、gRPC介绍 单体架构 一旦某个服务宕机&#xff0c;会引起整个应用不可用&#xff0c;隔离性差只能整体应用进行伸缩&#xff0c;浪…

基于小波多分辨分析的一维时间序列信号趋势检测与去除(MATLAB R2018a)

小波最开始是数学上提出的概念&#xff0c;并且在纯数学的王国里存在了一个世纪之久。最开始是为了弥补傅里叶分析的缺陷&#xff0c;即傅里叶级数发散的问题&#xff0c;并寻找出能够代替傅里叶分析的方法。从最早的一些艰难的探索开始直到慢慢发展成为一套完整系统的小波分析…

Flutter Image源码分析

本文用于记录分析Imge图片加载流程源码分析学习笔记 切入点是Image.network,加载网络图片 构造方法会创建NetworkImage,加载图片的实现类,父类是ImageProvider 加载本地图片等等都是类似 下面进入_ImageState类 void resolveStreamForKey(ImageConfiguration configurat…

【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第37课-自动切换纹理

【WEB前端2024】3D智体编程&#xff1a;乔布斯3D纪念馆-第37课-自动切换纹理 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界引擎&…

【iOS】JSONModel源码阅读笔记

文章目录 前言一、JSONModel使用二、JSONModel其他方法转换属性名称 三、源码分析- (instancetype)initWithDictionary:(NSDictionary*)dict error:(NSError **)err[self init]__setup____inspectProperties - (BOOL)__doesDictionary:(NSDictionary*)dict matchModelWithKeyMa…

Day25 首页待办事项及备忘录添加功能

​ 本章节,完成首页待办事项及备忘录添加功能 一.修改待办事项和备忘录逻辑处理类,即AddMemoViewModel和AddTodoViewModel 在 AddMemoViewModel逻辑处理类中,为了支持与其关联的View视图文件的数据绑定,需要定义一个与视图文件相匹配的实体类 Model。这个Model将包含 View中…

Java开发-面试题-0005-==和String的equals()和String的intern()方法的区别

Java开发-面试题-0005-和String的equals()和String的intern()方法的区别 更多内容欢迎关注我&#xff08;持续更新中&#xff0c;欢迎Star✨&#xff09; Github&#xff1a;CodeZeng1998/Java-Developer-Work-Note 技术公众号&#xff1a;CodeZeng1998&#xff08;纯纯技术…

算法:101. 对称二叉树

对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&#xff1a; 树中节…

蓝桥杯--跑步计划

问题描述 小蓝计划在某天的日期中出现 11 时跑 55 千米&#xff0c;否则只跑 11 千米。注意日期中出现 11 不仅指年月日也指星期。 请问按照小蓝的计划&#xff0c;20232023 年小蓝总共会跑步锻炼多少千米?例如&#xff0c;55 月 11 日、11 月 1313 日、1111 月 55 日、44 月…

Dubbo 3.x源码(20)—Dubbo服务引用源码(3)

基于Dubbo 3.1&#xff0c;详细介绍了Dubbo服务的发布与引用的源码。 此前我们学习了调用createProxy方法&#xff0c;根据服务引用参数map创建服务接口代理引用对象的整体流程&#xff0c;我们知道会调用createInvokerForRemote方法创建远程引用Invoker&#xff0c;这是Dubbo …

开启数字化校园解决方案,实现教育智能化

现代社会的教育面临诸多挑战&#xff0c;如何提高教育质量&#xff0c;实现教育智能化成为了当务之急。数字化校园解决方案应运而生&#xff0c;为学校提供了全新的教学模式和管理方式。本文将介绍数字化校园解决方案的重要性&#xff0c;以及如何开启数字化校园&#xff0c;实…

客户案例|Zilliz Cloud 助力点石科技转型 AI 智能服务商

福建点石科技网络科技有限公司成立于2010年&#xff0c;是国家高新技术企业&#xff0c;阿里云、蚂蚁金服等大厂海内外生态合作伙伴ISV。在餐饮、零售、酒店、旅游、商圈的行业定制化服务化上有深厚积累&#xff0c;在境内外做了大量标杆性软件项目&#xff0c;如东南亚RWS圣淘…

《python程序语言设计》2018版第5章第46题均值和标准方差-上部(我又一次被作者的出题击倒)

第N次被作者打倒了&#xff0c;第5章46题解题上集的记录 计算均值的代码段 step_num 0num_c 0 pow_c 0 while step_num < 10:a eval(input("Enter number is: "))num_c apow_c pow(a, 2)step_num 1 t2 num_c / 10这个结果和书里的答案差一点。书里写的是…

容器中运行ping提示bash: ping: command not found【笔记】

容器中运行ping提示bash: ping: command not found 原因是容器中没有安装ping命令 在容器中安装ping命令&#xff0c;可以使用以下命令&#xff1a; 对于基于Debian/Ubuntu的容器&#xff0c;使用以下命令&#xff1a; apt-get update apt-get install -y iputils-ping对于基…

Docker run 命令常用参数详解

Docker run 命令提供了丰富的参数选项&#xff0c;用于配置容器的各种设置。以下是docker run命令的主要参数详解&#xff0c; 主要参数详解 后台运行与前台交互 -d, --detach: 在后台运行容器&#xff0c;并返回容器ID。-it: 分配一个伪终端&#xff08;pseudo-TTY&#xff0…

如何做好电子内窥镜的网络安全管理?

电子内窥镜作为一种常用的医疗器械&#xff0c;其网络安全管理对于保护患者隐私和医疗数据的安全至关重要。以下是一些基本原则和步骤&#xff0c;用于确保电子内窥镜的网络安全&#xff1a; 1. 数据加密 为了防止数据泄露&#xff0c;电子内窥镜在传输患者图像数据时应采取有…

解决!word转pdf时,怎样保持图片不失真

#今天用word写了期末设计报告&#xff0c;里面有很多过程的截图&#xff0c;要打印出来&#xff0c;想到pdf图片不会错位&#xff0c;就转成了pdf&#xff0c;发现图片都成高糊了&#xff0c;找了好多方法&#xff0c;再不下载其他软件和插件的情况下&#xff0c;导出拥有清晰的…

cf 欧几里得距离

说明&#xff1a;欧几里得距离本质就是两点间距离 distancesqrt( sum(ai-bi)2 ) Problem - F - Codeforces 代码

下载安装Thonny并烧录MicroPython固件至ESP32

Thonny介绍 一、Thonny的基本特点 面向初学者&#xff1a;Thonny的设计初衷是为了帮助Python初学者更轻松、更快速地入门编程。它提供了直观易懂的用户界面和丰富的功能&#xff0c;降低了编程的门槛。轻量级&#xff1a;作为一款轻量级的IDE&#xff0c;Thonny不会占用过多的…