当前位置: 首页 > 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;会存…...

<6>-MySQL表的增删查改

目录 一&#xff0c;create&#xff08;创建表&#xff09; 二&#xff0c;retrieve&#xff08;查询表&#xff09; 1&#xff0c;select列 2&#xff0c;where条件 三&#xff0c;update&#xff08;更新表&#xff09; 四&#xff0c;delete&#xff08;删除表&#xf…...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

【项目实战】通过多模态+LangGraph实现PPT生成助手

PPT自动生成系统 基于LangGraph的PPT自动生成系统&#xff0c;可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析&#xff1a;自动解析Markdown文档结构PPT模板分析&#xff1a;分析PPT模板的布局和风格智能布局决策&#xff1a;匹配内容与合适的PPT布局自动…...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

dify打造数据可视化图表

一、概述 在日常工作和学习中&#xff0c;我们经常需要和数据打交道。无论是分析报告、项目展示&#xff0c;还是简单的数据洞察&#xff0c;一个清晰直观的图表&#xff0c;往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server&#xff0c;由蚂蚁集团 AntV 团队…...