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

   在分布式项目中需要引入  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.14return 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@ConditionalOnMissingBeanTracing 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@ConditionalOnMissingBeanHttpTracing 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@ConditionalOnMissingBeanpublic 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中持有traceidSpan 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.mzph.cn/diannao/25549.shtml

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

相关文章

评书下载到u盘,下载到内存卡,下载到手机或电脑的方法

评书下载的方法有很多种&#xff0c;无论是通过什么方法&#xff0c;我们都可以快速的获取喜爱的评书。下面将详细介绍常见的评书下载方法&#xff0c;帮助您快速上手。 1、搜索“十方评书网”。 2、要下载那个评书家的选择那个评书家就可以。 3、点击进去后可以一键下载单部评…

Elasticsearch中各种query的适用场景

Elasticsearch 提供了丰富的 Query 类型&#xff0c;以满足各种搜索需求。以下列举一些常见的 Query 类型&#xff0c;并分析其区别和应用场景&#xff1a; 一、 几个常用的基本Query 1. Term Query 应用场景: 查找包含特定词语的文档&#xff0c;适合精确匹配单个词语的场景…

【SpringBoot + Vue 尚庭公寓实战】标签和配套管理接口实现接口实现(六)

【SpringBoot Vue 尚庭公寓实战】标签和配套管理接口实现接口实现&#xff08;六&#xff09; 文章目录 【SpringBoot Vue 尚庭公寓实战】标签和配套管理接口实现接口实现&#xff08;六&#xff09;1、保存或更新标签信息2、根据id删除标签信息3、根据类型查询配套列表4、新…

Aptos Builder Jam 亚洲首站|见证 Aptos 公链 2024 年新突破

4 月下旬的「TinTin DESTINATION MOON」杭州站活动让我们构建下一个 Web3 巅峰的项目生态行动与未来战略。时隔三个月&#xff0c;「TinTin DESTINATION MOON」Aptos 线下活动将再次来到杭州&#xff0c;为 Aptos Builder Jam 亚洲首站火热造势&#xff0c;7 月 6 日诚邀 Web3 …

vue2中如何动态渲染组件

vue2中如何动态渲染组件 动态渲染组件代码解读通过函数调用渲染组件 封装一个函数调用的二次确认弹窗如何让外部知道用户点击了取消还是确定呢&#xff1f; 思考小结 vue2 的项目中&#xff0c;main.js 文件中有一个挂载 App.vue 组件的方法&#xff1a; new Vue({name: Root,…

工程师 - 什么是EMI测试

一、EMC EMI EMS定义&#xff1a; EMC&#xff08;ElectromagneticCompatibility&#xff09; 电磁兼容&#xff0c;是指设备或系统在电磁环境中性能不降级的状态。电磁兼容&#xff0c;一方面要求系统内没有严重的干扰源&#xff0c;一方面要求设备或系统自身有较好的抗电磁…

5G发牌五周年丨移远通信:全面发力,加快推进5G技术服务社会发展

2024年6月6日&#xff0c;正值中国5G商用牌照发牌五周年。根据移动通信“十年一代”的规律&#xff0c;5G已走过一半征程。在过去的五年时间里&#xff0c;5G技术从萌芽到成熟&#xff0c;深刻改变了工业、农业、医疗及消费端等各个领域的发展脉络。无论是无人机配送、自动驾驶…

【LeetCode】两数相加(基于单向链表)难度:中等

目录 理清题目 解题思路 题目代码 运行结果 我们来看一下题目描述&#xff1a; 理清题目 首先题目要求链表中的节点的值必须在[0,9]之间也就是说我们要处理的数字必为正整数&#xff0c;因此就不会涉及到太复杂的计算&#xff0c;题目其实就是要求对两个链表中的节点的值分…

详解 Flink 的状态管理

一、Flink 状态介绍 1. 流处理的无状态和有状态 无状态的流处理&#xff1a;根据每一次当前输入的数据直接转换输出结果的过程&#xff0c;在处理中只需要观察每个输入的独立事件。例如&#xff0c; 将一个字符串类型的数据拆分开作为元组输出或将每个输入的数值加 1 后输出。…

台积电代工!Intel新AI PC芯片Lunar Lake发布:AI算力120TOPS!

根据英特尔披露的数据显示&#xff0c;Lunar Lake的GPU性能提升50%、NPU内核的AI算力增加了四倍、SoC耗电量减少40%、GPU AI算力增加3.5倍&#xff0c;整个SoC的算力超过了120TOPS。 6月4日&#xff0c;英特尔CEO帕特基辛格在COMPUTEX 2024上发表主题演讲&#xff0c;正式公布…

如何确保redis缓存中的数据与数据库一致

一、双写模式&#xff1a; 在写入数据库时&#xff0c;也写入缓存。 二&#xff1a;失效模式&#xff1a; 在写入新数据后&#xff0c;删除缓存中数据&#xff0c;下次请求时查询数据库&#xff0c;并把查到的最新数据写入缓存。 不管是双写模式还是失效模式&#xff0c;缓…

Letcode-Top 100二叉树专题

94. 二叉树的中序遍历 方法一&#xff1a;递归法 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeN…

SpringBoot的学习要点

目录 SpringBoot 创建项目 配置文件 注解 命名规范 SpringBoot整合第三方技术 …… 中文文档&#xff1a;Spring Boot 中文文档 SpringBoot Spring Boot 是基于 Spring 框架的一种快速构建微服务应用的方式它主要提供了自动配置、简化配置、运行时应用监控等功能它…

大水文之------端午练练JS好了

最近有点不太知道要干啥了&#xff0c;昨天看了集cocos的介绍&#xff0c;下载了个DashBoard&#xff0c;看了看里面的内容&#xff0c;确实有点小震惊&#xff0c;还有些免费的源码可以学习&#xff0c;挺好的。 昨天学习ts&#xff0c;感觉自己的js水平好像不太行&#xff0c…

Functional ALV系列 (10) - 将填充FieldCatalog封装成函数

在前面的博文中&#xff0c;已经讲了封装的思路和实现&#xff0c;主要是利用 cl_salv_data_descr>read_structdescr () 方法来实现。在这里&#xff0c;贴出代码方便大家参考。 编写获取内表组件的通用方法 form frm_get_fields using pt_data type any tablechanging…

C++期末复习提纲(血小板)

目录 1.this指针 2.静态成员变量 3.面向对象程序设计第一阶段 4.面向对象程序设计第二阶段 5.面向对象程序设计第三阶段 6.简答题 &#xff08;1&#xff09;拷贝构造函数执行的三种情况&#xff1a; &#xff08;2&#xff09;虚析构函数的作用&#xff1a; &#xff…

Python基础——字符串

一、Python的字符串简介 Python中的字符串是一种计算机程序中常用的数据类型【可将字符串看作是一个由字母、数字、符号组成的序列容器】&#xff0c;字符串可以用来表示文本数据。 通常使用一对英文的单引号&#xff08;&#xff09;或者双引号&#xff08;"&#xff09;…

html接口响应断言

接口响应值除类json格式&#xff0c;还有html格式 断言步骤 第一步&#xff1a;替换空格replace 原本返回的格式和网页内容一致&#xff0c;每行前面有很多空格&#xff0c;需要去除这些空格 第二步&#xff1a;分割split 因为行与行之前有回车符&#xff0c;所以把回车符替…

Spring之SpringMVC源码

SpringMVC源码 一、SpringMVC的基本结构 1.MVC简介 以前的纯Servlet的处理方式&#xff1a; Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String type req.getParameter(Constant.REQUEST_PA…

【Java面试】十六、并发篇:线程基础

文章目录 1、进程和线程的区别2、并行和并发的区别3、创建线程的四种方式3.1 Runnable和Callable创建线程的区别3.2 线程的run和start 4、线程的所有状态与生命周期5、新建T1、T2、T3&#xff0c;如何保证线程的执行顺序6、notify和notifyAll方法有什么区别7、wait方法和sleep方…