当前位置: 首页 > news >正文

【SpringBoot】分布式日志跟踪—通过MDC实现全链路调用日志跟踪

一.MDC

1.MDC介绍

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程场景下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

  • 简而言之,MDC就是日志框架提供的一个InheritableThreadLocal,所以它是线程安全的,在项目代码中可以将键值对放入其中,然后使用指定方式取出打印即可。

  • 优点:代码简洁,日志风格统一,不需要在log打印中手动拼写traceId,即log.info("traceId:{} ", traceId)

    • 在 log4j 和 logback 的取值方式为:
    %X{traceid}
    

2.API说明

在这里插入图片描述

  • clear():移除所有MDC

  • get (String key):获取当前线程MDC中指定key的值

  • getContext() : 获取当前线程MDC的MDC

  • put(String key, Object o) :往当前线程的MDC中存入指定的键值对

  • remove(String key) : 删除当前线程MDC中指定的键值对

  • getPropertyMap():返回当前线程的context map的直接引用!不是拷贝副本

  • getCopyOfContextMap():返回当前线程的context map的一个副本,对这个map的修改不会影响原来copyOnInheritThreadLocal中的内容。

二.MDC使用

1.使用方式

public class Constants {/*** 日志跟踪id名。*/public static final String TRACE_ID= "trace_id";/*** 请求头跟踪id名。*/public static final String HTTP_HEADER_TRACE_ID = "app_trace_id";
}
public class TraceIdUtil {public static String getTraceId(){return UUID.randomUUID().toString().replace("-","");}
}

HTTP调用第三方服务接口全流程traceId需要第三方服务配合,第三方服务需要添加拦截器拿到request header中的traceId并添加到MDC中

public class LogInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//如果有上层调用就用上层的IDString traceId = request.getHeader(Constants.TRACE_ID);if (traceId == null) {traceId = TraceIdUtil.getTraceId();}MDC.put(Constants.TRACE_ID, traceId);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {//调用结束后删除MDC.remove(Constants.TRACE_ID);}
}

修改日志格式

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>
  • 重点是%X{traceId},traceId和MDC中的键名称一致

2.存在问题

2.1.子线程日志打印丢失traceId

  • 子线程在打印日志的过程中traceId将丢失,当前线程创建的子线程获取不到ThreadLocal存储的键值
    • 解决方式为重写线程池,对于直接new创建线程的情况不考略【实际应用中应该避免这种用法】,重写线程池无非是对任务进行一次封装

问题重现:

    @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封装工具类

public class ThreadMdcUtil {public static void setTraceIdIfAbsent() {if (MDC.get(Constants.TRACE_ID) == null) {MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());}}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);}setTraceIdIfAbsent();try {runnable.run();} finally {MDC.clear();}};}
}

说明【以封装Runnable为例】:

  • 判断当前线程对应MDC的Map是否存在,存在则设置
  • 设置MDC中的traceId值,不存在则新生成,针对不是子线程的情况,如果是子线程,MDC中traceId不为null
  • 执行run方法
  • 重新返回的是包装后的Runnable,在该任务执行之前【runnable.run()】先将主线程的Map设置到当前线程中【 即MDC.setContextMap(context)】,这样子线程和主线程MDC对应的Map就是一样的了

因为Spring Boot ThreadPoolTaskExecutor 已经对ThreadPoolExecutor进行封装,只需要继承ThreadPoolTaskExecutor重写相关的执行方法即可。

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);}public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);}public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);}@Overridepublic void execute(Runnable task) {super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}@Overridepublic <T> Future<T> submit(Runnable task, T result) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);}@Overridepublic <T> Future<T> submit(Callable<T> task) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}@Overridepublic Future<?> submit(Runnable task) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}
}

线程池配置

@Configuration
public class ThreadPoolTaskExecutorConfig{//最大可用的CPU核数public static final int PROCESSORS = Runtime.getRuntime().availableProcessors();@Beanpublic 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

2.2.HTTP调用丢失traceId

在使用HTTP调用第三方服务接口时traceId将丢失,需要对HTTP调用工具进行改造,在发送时在request header中添加traceId,在下层被调用方添加拦截器获取header中的traceId添加到MDC中

  • HTTP调用有多种方式,比较常见的有HttpClient、OKHttp、RestTemplate,所以只给出这几种HTTP调用的解决方式

1.HttpClient

实现HttpRequestInterceptor接口并重写process方法

  • 如果调用线程中含有traceId,则需要将获取到的traceId通过request中的header向下透传下去

public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {@Overridepublic void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {String traceId = MDC.get(Constants.TRACE_ID);//当前线程调用中有traceId,则将该traceId进行透传if (traceId != null) {//添加请求体httpRequest.addHeader(Constants.TRACE_ID, traceId);}}
}

为HttpClient添加拦截器

private static CloseableHttpClient httpClient = HttpClientBuilder.create().addInterceptorFirst(new HttpClientTraceIdInterceptor()).build();

2.OKHttp

– 实现Interceptor拦截器,重写interceptor方法,实现逻辑和HttpClient差不多,如果能够获取到当前线程的traceId则向下透传

public class OkHttpTraceIdInterceptor implements Interceptor {@Overridepublic Response intercept(Chain chain) throws IOException {String traceId = MDC.get(Constants.TRACE_ID);Request request = null;if (traceId != null) {//添加请求体request = chain.request().newBuilder().addHeader(Constants.TRACE_ID, traceId).build();}Response originResponse = chain.proceed(request);return originResponse;}
}

为OkHttp添加拦截器

  private static OkHttpClient client = new OkHttpClient.Builder().addNetworkInterceptor(new OkHttpTraceIdInterceptor()).build();

3. RestTemplate

实现ClientHttpRequestInterceptor接口,并重写intercept方法,其余逻辑都是一样的不重复说明

public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {@Overridepublic ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {String traceId = MDC.get(Constants.TRACE_ID);if (traceId != null) {httpRequest.getHeaders().add(Constants.TRACE_ID, traceId);}return clientHttpRequestExecution.execute(httpRequest, bytes);}
}

为RestTemplate添加拦截器

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

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

需要第三方服务配合,添加拦截器拿到request header中的traceId并添加到MDC中

public class LogInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//如果有上层调用就用上层的IDString traceId = request.getHeader(Constants.TRACE_ID);if (traceId == null) {traceId = TraceIdUtil.getTraceId();}MDC.put(Constants.TRACE_ID, traceId);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {//调用结束后删除MDC.remove(Constants.TRACE_ID);}
}
  • 先从request header中获取traceId
  • 从request header中获取不到traceId则说明不是第三方调用,直接生成一个新的traceId
  • 将生成的traceId存入MDC中

相关文章:

【SpringBoot】分布式日志跟踪—通过MDC实现全链路调用日志跟踪

一.MDC 1.MDC介绍 MDC&#xff08;Mapped Diagnostic Context&#xff0c;映射调试上下文&#xff09;是 log4j 和 logback 提供的一种方便在多线程场景下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map&#xff0c;可以往其中添加键值对。MDC 中包含的内容可以被同…...

【设计模式】创建型模式

简单工厂模式 系列综述&#xff1a; xxxxxxxxx 文章目录对象创建型模式简单&#xff08;静态&#xff09;工厂模式工厂方法模式参考博客&#x1f60a;点此到文末惊喜↩︎ 对象创建型模式 简单&#xff08;静态&#xff09;工厂模式 抽象原理 抽象产品基类 &#xff1a;定义了…...

Spark Catalyst 查询优化器原理

这里我们讲解一下SparkSQL的优化器系统Catalyst&#xff0c;Catalyst本质就是一个SQL查询的优化器&#xff0c;而且和 大多数当前的大数据SQL处理引擎设计基本相同&#xff08;Impala、Presto、Hive&#xff08;Calcite&#xff09;等&#xff09;。了解Catalyst的SQL优化流程&…...

贝叶斯分析法在市场调研中的应用

一、市场调研的需求场景 在营销活动的用研调研时,我们经常会去问用户在不同平台的品类付费情况,以对比大促期间本品和竞品分别在哪些品类上具有市场优势,他们之间的差距具体在哪里、差距有多大。假如根据调研问卷结果,我们知道拼多多用户有30%的人在大促购买生鲜类,而淘宝…...

JavaEE——MyBatis将查询结果集封装进POJO实体类

简单介绍 在之前的我们比较详细的介绍过MyBatis的配置信息的时候&#xff0c;在SQL映射文件中说过我们可以直接将结果集映射到我们的POJO实体类中&#xff0c;省去了我们自己处理查询结果集的时间和代码&#xff0c;接下来我们就来演示将单条数据和多条数据映射到我们POJO实体…...

C++11 包装器function

文章首发公众号&#xff1a;iDoitnow C提供了多个包装器&#xff0c;它们主要是为了给其他编程接口提供更一致或更合适的接口。C11提供了多个包装器&#xff0c;这里我们重点了解一下包装器function。 对于function, C 参考手册给出的定义为&#xff1a; 类模板 std::function…...

XCP实战系列介绍14-基于Vector_Davinci工具的XCP配置介绍(三)

本文框架 1.概述2. 其他模块配置2.1 XCP初始化3. 手工代码部分3.1 周期函数添加3.2 DAQ Event调用3.3 XCP模块本身代码3.4 标定量的添加1.概述 在对XCP的配置部分介绍中我们计划分别对通讯部分配置、XCP模块本身配置及其他相关模块配置三篇进行介绍,在前两篇我们介绍了XCP配置…...

计算机图形学:中点BH算法对任意斜率的直线扫描转换方法

作者&#xff1a;非妃是公主 专栏&#xff1a;《计算机图形学》 博客地址&#xff1a;https://blog.csdn.net/myf_666 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录专栏推荐专栏系列文章序一、问题提出二、…...

(十一)、用户中心页面【uniapp+uinicloud多用户社区博客实战项目(完整开发文档-从零到完整项目)】

1,个人中心页面 1.1 新建个人中心页面 1.2 纯净版个人中心页面代码&#xff1a; <template><view class"user"><view class"top"><view class"group"><view class"userinfo"><!-- 顶部 左侧 头像 …...

LA@复数和复矩阵@实对称阵相关定理

文章目录复数&#x1f388;复矩阵和复向量共轭矩阵性质定理实对称阵的相关定理复数&#x1f388; 复数 (数学) (wikipedia.org) 加法&#xff1a;(abi)(cdi)(ac)(bd)i)减法&#xff1a;(abi)−(cdi)(a−c)(b−d)i)乘法&#xff1a;(abi)(cdi)acbciadibdi2(ac−bd)(bcad)i除法&…...

cmd set命令笔记

使用 set是cmd最基础的命令&#xff0c;每个人都会用&#xff0c;但其实它还是有些知识的。 set 用来接收入参 set /p var请选择&#xff08;1或2或3&#xff09;: echo %var%可以接收输入的参数。 set /p var请选择&#xff08;1或2或3&#xff09;: echo %var% 语法 he…...

IB学校获得IBO授权究竟有多难?

IB 学校认证之路&#xff0c;道阻且长 The road to IB school accreditation is long and difficult一所学校能获得IB授权必须经过IBO非常严格的审核&#xff0c;在办学使命&教育理念、组织架构、师资力量&授课技能、学校硬件设施和课程体系上完全符合标准才可获得授权…...

火山引擎 DataTester:A/B 测试,让企业摆脱广告投放“乱烧钱”

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 在广告投放的场景下&#xff0c;一线广告优化师通常会创建多个计划&#xff0c;去测试不同的广告素材效果。这套方法看似科学&#xff0c;实际上却存在诸多问题&…...

黑马redis学习记录:缓存

一、介绍 什么是缓存&#xff1f; 缓存(Cache)&#xff0c;就是数据交换的缓冲区&#xff0c;俗称的缓存就是缓冲区内的数据&#xff0c;一般从数据库中获取&#xff0c;存储于本地代码 缓存无处不在 为什么要使用缓存&#xff1f; 因为速度快,好用缓存数据存储于代码中,而…...

CD20靶向药物|适应症|市场销售-上市药品前景分析

CD20是靶向治疗的第一个靶点&#xff0c;是B细胞淋巴瘤的现代治疗药物。CD20作为治疗剂的使用被认为是方便的&#xff0c;原因有二。首先&#xff0c;在 CD20 阳性肿瘤的情况下&#xff0c;这种受体大量存在于 B 淋巴细胞表面——每个细胞大约有十万个分子。其次&#xff0c;干…...

多源 复制

使复制从属服务器能够同时从多个主服务器接收事务至少需要两个主服务器和一个从属服务器设备从属服务器为每个主服务器创建一个 复制通道从属服务器必须使用基于表的资料档案库多源复制与基于文件的资料档案库不兼容不尝试检测或解决冲突如果需要此功能&#xff0c;则由应用程序…...

微服务项目【消息推送(RabbitMQ)】

创建消费者 第1步&#xff1a;基于Spring Initialzr方式创建zmall-rabbitmq消费者模块 第2步&#xff1a;在公共模块中添加rabbitmq相关依赖 <!--rabbitmq--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-bo…...

vr电力刀闸事故应急演练实训系统开发

电力事故是在电力生产和输电过程中可能发生的意外事件&#xff0c;它们可能会对人们的生命财产安全造成严重的威胁。因此&#xff0c;电力事故应急演练显得尤为重要。而VR技术则可以为电力事故应急演练提供一种全新的解决方案。 在虚拟环境中&#xff0c;元宇宙VR会模拟各种触电…...

C++类和对象补充

目录 前言&#xff1a; 1. 构造函数->初始化列表 1.1 初始化列表出现原因 1.2 初始化列表写法 2. explicit关键字 2.1 explict的出现 2.2 explict的写法 3. static成员 4. 友元 4.1 友元函数 4.2 友元类 5. 内部类和匿名对象 5.1 内部类 5.2 匿名对象 前言&a…...

08 SpringCloud 微服务网关Gateway组件

网关简介 大家都都知道在微服务架构中&#xff0c;一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢&#xff1f; 如果没有网关的存在&#xff0c;我们只能在客户端记录每个微服务的地址&#xff0c;然后分别去用。 这样的架构&#xff0c;会存…...

绩效考核的量化迷思:如何衡量不可直接测量的技术贡献

一、量化绩效考核的困境&#xff1a;软件测试的“隐形”价值在软件行业的绩效考核体系中&#xff0c;量化指标似乎成了“公平”与“高效”的代名词。代码行数、Bug数量、测试用例覆盖率……这些清晰可统计的数字&#xff0c;被当作衡量技术人员贡献的核心标尺。然而&#xff0c…...

LSLib:让《神界原罪》和《博德之门3》MOD制作变得高效完整的实用指南

LSLib&#xff1a;让《神界原罪》和《博德之门3》MOD制作变得高效完整的实用指南 【免费下载链接】lslib Tools for manipulating Divinity Original Sin and Baldurs Gate 3 files 项目地址: https://gitcode.com/gh_mirrors/ls/lslib 你是否曾想为《神界原罪》或《博德…...

应急通信无人机中继部署与覆盖率优化【附仿真】

✨ 长期致力于应急通信、无人机、中继部署、通信覆盖率、无人机部署数目研究工作&#xff0c;擅长数据搜集与处理、建模仿真、程序编写、仿真设计。 ✅ 专业定制毕设、代码 ✅如需沟通交流&#xff0c;点击《获取方式》 &#xff08;1&#xff09;视距概率信道建模与高度部署&a…...

Midjourney蓝莓印相技术白皮书(2024V2.3权威修订版):基于1726张A/B测试图谱验证的色阶偏移阈值与CMYK映射规则

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;Midjourney Blueberry印相技术的演进脉络与核心定义 Midjourney Blueberry印相技术并非官方术语&#xff0c;而是社区对Midjourney V6中基于蓝光敏感通道&#xff08;Blue Channel Emulation&#xff0…...

Flutter Provider 状态管理完全指南

Flutter Provider 状态管理完全指南 引言 Provider 是 Flutter 中最流行的状态管理方案之一&#xff0c;它基于 InheritedWidget 实现&#xff0c;提供了简单而强大的状态管理方式。本文将深入探讨 Provider 的各种用法和高级技巧。 基础概念回顾 Provider 类型 Provider - 最基…...

大模型长文档理解新拐点已至(2026年Claude专项能力解密):支持128K上下文+动态摘要锚点+引用溯源追踪

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;大模型长文档理解新拐点已至&#xff1a;Claude 2026能力演进全景图 随着长上下文窗口突破200万token、原生支持跨页语义锚定与结构化元数据感知&#xff0c;Claude 2026标志着大模型对长文档的理解正式…...

排查华为USG防火墙上不了网?先检查这5个配置点(附真实配置案例)

华为USG防火墙上网故障排查实战指南 当内网用户突然无法访问互联网时&#xff0c;作为运维人员往往会面临巨大的压力。华为USG防火墙作为企业网络的核心安全设备&#xff0c;其配置的每一个细节都可能成为网络连通性的关键。本文将从一个真实的故障排查案例出发&#xff0c;带您…...

05 - rocrtst 功能测试详解

本文档深入介绍 rocrtst 功能测试套件&#xff08;suites/functional/&#xff09;中的各个测试模块&#xff0c;帮助你理解每个测试验证的 HSA API 功能。 1. 功能测试概览 功能测试注册在 rocrtstFunc 测试套件下&#xff0c;共 26 个源码模块&#xff0c;涵盖 ROCr Runtim…...

微信好友关系终极检测:WechatRealFriends帮你一键识别单向好友

微信好友关系终极检测&#xff1a;WechatRealFriends帮你一键识别单向好友 【免费下载链接】WechatRealFriends 微信好友关系一键检测&#xff0c;基于微信ipad协议&#xff0c;看看有没有朋友偷偷删掉或者拉黑你 项目地址: https://gitcode.com/gh_mirrors/we/WechatRealFri…...

终极指南:如何5分钟搞定B站字幕提取与格式转换

终极指南&#xff1a;如何5分钟搞定B站字幕提取与格式转换 【免费下载链接】BiliBiliCCSubtitle 一个用于下载B站(哔哩哔哩)CC字幕及转换的工具; 项目地址: https://gitcode.com/gh_mirrors/bi/BiliBiliCCSubtitle 你是否曾为保存B站视频中的精彩内容而烦恼&#xff1f;…...