SpringBoot使用TraceId日志链路追踪

项目场景:

??有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大。为了解决这个痛点,就使用了TraceId,根据TraceId关键字进入服务器查询日志中是否有这个TraceId,这样就把同一次的业务调用链上的日志串起来了。


实现步骤

1、pom.xml 依赖

<dependencies>
????<dependency>
????????<groupId>org.springframework.boot</groupId>
????????<artifactId>spring-boot-starter-web</artifactId>
????</dependency>
????<dependency>
????????<groupId>org.springframework.boot</groupId>
????????<artifactId>spring-boot-starter-test</artifactId>
????????<scope>test</scope>
????</dependency>
????<dependency>
????????<groupId>org.springframework.boot</groupId>
????????<artifactId>spring-boot-starter-logging</artifactId>
????</dependency>
????<!--lombok配置-->
????<dependency>
????????<groupId>org.projectlombok</groupId>
????????<artifactId>lombok</artifactId>
????????<version>1.16.10</version>
????</dependency>
</dependencies>

2、整合logback,打印日志,logback-spring.xml (简单配置下)

关键代码:[traceId:%X{traceId}],traceId是通过拦截器里MDC.put(traceId, tid)添加

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false"><!--日志存储路径--><property name="log" value="D:/test/log" /><!-- 控制台输出 --><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--输出格式化--><pattern>[traceId:%X{traceId}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern></encoder></appender><!-- 按天生成日志文件 --><appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--日志文件名--><FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern><!--保留天数--><MaxHistory>30</MaxHistory></rollingPolicy><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><pattern>[traceId:%X{traceId}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern></encoder><!--日志文件最大的大小--><triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><MaxFileSize>10MB</MaxFileSize></triggeringPolicy></appender><!-- 日志输出级别 --><root level="INFO"><appender-ref ref="console" /><appender-ref ref="file" /></root>
</configuration>

3、application.yml

server:port: 8826
logging:config: classpath:logback-spring.xml

4、自定义日志拦截器 LogInterceptor.java

用途:每一次链路,线程维度,添加最终的链路ID traceId。

MDC(Mapped Diagnostic Context)诊断上下文映射,是@Slf4j提供的一个支持动态打印日志信息的工具。

import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;/*** 日志拦截器*/
public class LogInterceptor implements HandlerInterceptor {private static final String traceId = "traceId";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String tid = UUID.randomUUID().toString().replace("-", "");//可以考虑让客户端传入链路ID,但需保证一定的复杂度唯一性;如果没使用默认UUID自动生成if (!StringUtils.isEmpty(request.getHeader("traceId"))){tid=request.getHeader("traceId");}MDC.put(traceId, tid);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,@Nullable Exception ex) {// 请求处理完成后,清除MDC中的traceId,以免造成内存泄漏MDC.remove(traceId);}}

5、WebConfigurerAdapter.java 添加拦截器

ps: 其实这个拦截的部分改为使用自定义注解+aop也是很灵活的。

import javax.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;@Configuration
public class WebConfigurerAdapter extends WebMvcConfigurationSupport {@Resourceprivate LogInterceptor logInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(logInterceptor);//可以具体制定哪些需要拦截,哪些不拦截,其实也可以使用自定义注解更灵活完成
//                .addPathPatterns("/**")
//                .excludePathPatterns("/testxx.html");}
}

6、测试接口

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController
@Api(tags = "测试接口")
@RequestMapping("/test")
@Slf4j
public class TestController {@RequestMapping(value = "/log", method = RequestMethod.GET)@ApiOperation(value = "测试日志")public String sign() {log.info("这是一行info日志");log.error("这是一行error日志");return "success";}
}

结果:


异步场景:

使用线程的场景,写一个异步线程,加入这个调用里面。再次执行看开效果,我们会发现显然子线程丢失了trackId。

所以我们需要针对子线程使用情形,做调整,思路:将父线程的trackId传递下去给子线程即可。

1、ThreadMdcUtil.java

import org.slf4j.MDC;import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;/*** @Author: JCccc* @Date: 2022-5-30 11:14* @Description:*/
public final class ThreadMdcUtil {private static final String traceId = "traceId";// 获取唯一性标识public static String generateTraceId() {return UUID.randomUUID().toString().replace("-", "");}public static void setTraceIdIfAbsent() {if (MDC.get(traceId) == null) {MDC.put(traceId, generateTraceId());}}/*** 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程** @param callable* @param context* @param <T>* @return*/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();}};}/*** 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程** @param runnable* @param context* @return*/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();}};}
}

2、MyThreadPoolTaskExecutor.java 是我们自己写的,重写了一些方法

import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Callable;
import java.util.concurrent.Future;public final class MyThreadPoolTaskExecutor  extends ThreadPoolTaskExecutor  {public MyThreadPoolTaskExecutor() {super();}@Overridepublic void execute(Runnable task) {super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}@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()));}
}

3、ThreadPoolConfig.java 定义线程池,交给spring管理

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;import java.util.concurrent.Executor;@EnableAsync
@Configuration
public class ThreadPoolConfig {/*** 声明一个线程池*/@Bean("taskExecutor")public Executor taskExecutor() {MyThreadPoolTaskExecutor executor = new MyThreadPoolTaskExecutor();//核心线程数5:线程池创建时候初始化的线程数executor.setCorePoolSize(5);//最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程executor.setMaxPoolSize(5);//缓冲队列500:用来缓冲执行任务的队列executor.setQueueCapacity(500);//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁executor.setKeepAliveSeconds(60);//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池executor.setThreadNamePrefix("taskExecutor-");executor.initialize();return executor;}
}

4、Service

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;/*** 测试Service*/
@Service("testService")
@Slf4j
public class TestService {/*** 异步操作测试*/@Async("taskExecutor")public void asyncTest() {try {log.info("模拟异步开始......");Thread.sleep(3000);log.info("模拟异步结束......");} catch (InterruptedException e) {log.error("异步操作出错:"+e);}}}

5、测试接口

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController
@Api(tags = "测试接口")
@RequestMapping("/test")
@Slf4j
public class TestController {@Resourceprivate TestService testService;@RequestMapping(value = "/log", method = RequestMethod.GET)@ApiOperation(value = "测试日志")public String sign() {log.info("这是一行info日志");log.error("这是一行error日志");//异步操作测试testService.asyncTest();return "success";}
}

结果:

我们可以看到,子线程的日志也被串起来了。


定时任务:

如果使用了定时任务@Scheduled,这时候执行定时任务,不会走上面的拦截器逻辑,所以定时任务需要单独创建个AOP切面。

1、创建个定时任务线程池

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;import java.util.concurrent.Executors;/*** 定时任务线程池*/
@EnableScheduling
@Configuration
public class SeheduleConfig implements SchedulingConfigurer{@Overridepublic void configureTasks(ScheduledTaskRegistrar taskRegistrar) {taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));}
}

2、创建个AOP切面

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;import java.util.UUID;@Aspect   //定义一个切面
@Configuration
public class SeheduleTaskAspect {// 定义定时任务切点Pointcut@Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")public void seheduleTask() {}@Around("seheduleTask()")public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {try {String traceId = UUID.randomUUID().toString().replace("-", "");//用于日志链路追踪,logback配置:%X{traceId}MDC.put("traceId", traceId);//执行定时任务方法joinPoint.proceed();} finally {//请求处理完成后,清除MDC中的traceId,以免造成内存泄漏MDC.remove("traceId");}}}

3、创建定时任务测试

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;import java.util.Date;@Service
public class SeheduleTasks {private Logger logger = LoggerFactory.getLogger(SeheduleTasks.class);/*** 1分钟执行一次*/@Scheduled(cron = "0 0/1 * * * ?")public void testTask() {logger.info("执行定时任务>"+new Date());}}

总结:

服务启动的时候traceId是空的,这是正常的,因为还没到拦截器这一层。

源码:https://download.csdn.net/download/u011974797/89981672

API 说明
  • clear()=> 移除所有 MDC
  • get (String key)=> 获取当前线程 MDC 中指定 key 的值
  • getContext()=> 获取当前线程 MDC 的 MDC
  • put(String key, Object o)=> 往当前线程的 MDC 中存入指定的键值对
  • remove(String key)=> 删除当前线程 MDC 中指定的键值对

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

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

相关文章

【网络编程】网络编程基础:TCP/UDP 协议

一、什么是网络&#xff1f; 网络是信息传输&#xff0c;接收和共享的虚拟世界&#xff0c;通过把网络上的信息汇聚在一起&#xff0c;将这些资源进行共享。 初衷&#xff1a;知识共享。这里不得不提到Internet 的历史&#xff0d;它其实是“冷战”的产物&#xff1a; 1957年…

开关电源实战(一)宽范围DC降压模块MP4560

系列文章目录 文章目录 系列文章目录MP4560MP4560 3.8V 至 55V 的宽输入范围可满足各种降压应用 MOSFET只有250mΩ 输出可调0.8V-52V SW:需要低VF肖特基二极管接地,而且要靠近引脚,高压侧开关的输出。 EN:输入使能,拉低到阈值以下关闭芯片,拉高或浮空启动 COMP:Compens…

Java 内存区域详解

1 常见面试题 1.1 基本问题 介绍下Java内存区域&#xff08;运行时数据区&#xff09;Java对象的创建过程&#xff08;五步&#xff0c;建议能够默写出来并且要知道每一步虚拟机做了什么&#xff09;对象的访问定位的两种方式&#xff08;句柄和直接指针两种方式&#xff09;…

C++多项式Lasso回归(多变量函数拟合)

多项式回归和Lasso多项式回归都是用于建模数据关系的方法&#xff0c;但它们在实现方式和目标上有一些重要的区别。以下是它们的主要区别&#xff1a; 1. 基本概念 多项式回归&#xff1a; 多项式回归是一种线性回归的扩展&#xff0c;它通过引入多项式特征&#xff08;如 ,,……

2025年股指期货和股指期权合约交割的通知!

锦鲤三三每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 2025年股指期货和股指期权合约交割的通知&#xff01; 根据中国金融期货交易所规则及相关规定&#xff0c;以下股指期货和股指期权合约于指定日期进行交割&#xff0c;现将各合…

通俗易懂的DOM事件模型指南

前言 在前端开发中&#xff0c;DOM事件是我们与用户交互的核心。无论是点击按钮、滚动页面&#xff0c;还是输入文字&#xff0c;背后都离不开DOM事件的支持。今天&#xff0c;我们就来聊聊DOM事件模型&#xff0c;用最简单的方式带你理解它的工作原理。 一、什么是DOM事件&a…

【YOLOv8】损失函数

学习视频&#xff1a; yolov8 | 损失函数 之 5、类别损失_哔哩哔哩_bilibili yolov8 | 损失函数 之 6、定位损失 CIoU DFL_哔哩哔哩_bilibili 2.13、yolov8损失函数_哔哩哔哩_bilibili YOLOv8 的损失函数由类别损失和定位损失构成 类别损失&#xff1a;BCE Loss 定位损失…

1.14作业

1 if($x[scheme]http||$x[scheme]https){ $ip gethostbyname($x[host]); echo </br>.$ip.</br>; if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {die(ip!); }echo file_get_contents($_POST[url]);可以DNS重…

【工具篇】【深度解析 DeepAI 工具:开启 AI 应用新体验】

一、DeepAI 基本信息 嘿,咱先来说说 DeepAI 这工具到底是啥。DeepAI 是一个综合性的人工智能平台,就像是一个装满各种 AI 魔法的百宝箱。它把好多先进的人工智能技术整合到一起,让咱们普通人也能轻松用上这些高大上的 AI 功能。 这个平台背后有一群超厉害的技术人员,他们…

Java八股文(下)

Java八股文下篇 八、JVM高级篇1、JVM的内存模型以及分区介绍一下&#xff1f;2、四种引用方式有什么&#xff1f;3、判断是否为垃圾算法&#xff1f;4、垃圾回收算法介绍一下&#xff1f;5、类的生命周期以及类加载过程6、加载器种类有什么&#xff1f;7、什么是双亲委派模型以…

OpenGL开发杂谈

Renderdoc 调试 OpenGL 前言 在做 OpenGL 练习时常常苦恼于无法调试shader&#xff0c;故在网上查询方法。得知 renderdoc 这一东西&#xff0c;所以在这里做一下记录&#xff0c;如何使用 renderdoc 来调试 OpenGL 程序。 配置 下载比较简单&#xff0c;不必多说&#xff…

C++,设计模式,【工厂方法模式】

文章目录 如何用汽车生产线理解工厂方法模式?一、传统生产方式的困境二、工厂方法模式解决方案三、模式应用场景四、模式优势分析五、现实应用启示✅C++,设计模式,【目录篇】 如何用汽车生产线理解工厂方法模式? 某个早晨,某车企CEO看着会议室里堆积如面的新车订单皱起眉…

我的AI工具箱Tauri版-CustomCardLayoutforH2Panel自定义描述Q版卡通大头照

本教程基于自研的AI工具箱Tauri版进行ComfyUI工作流CustomCardLayoutforH2Panel自定义描述Q版卡通大头照。 CustomCardLayoutforH2Panel 自定义描述Q版卡通大头照 特别面向需要生成个性化卡通大头照的用户需求。基于先进的Stable Diffusion&#xff08;SD&#xff09;模型技术…

Three.js 快速入门教程【六】相机控件 OrbitControls

系列文章目录 Three.js 快速入门教程【一】开启你的 3D Web 开发之旅 Three.js 快速入门教程【二】透视投影相机 Three.js 快速入门教程【三】渲染器 Three.js 快速入门教程【四】三维坐标系 Three.js 快速入门教程【五】动画渲染循环 Three.js 快速入门教程【六】相机控件 Or…

MySQL数据库(7)—— 内置函数

目录 一&#xff0c;时间函数 二&#xff0c;字符串函数 三&#xff0c;数学函数 四&#xff0c;其它函数 一&#xff0c;时间函数 函数名功能current_date()获取当前日期current_time()获取当前时间current_timestamp()获取当前时间戳now()获取当前日期时间date(datetime…

uni-app(位置1)

文章目录 一、获取当前的地理位置、速度 uni.getLocation(OBJECT)二、打开地图选择位置 uni.chooseLocation(OBJECT)三、使用应用内置地图查看位置。uni.openLocation(OBJECT) 一、获取当前的地理位置、速度 uni.getLocation(OBJECT) App平台 manifest中配置好自己的地图厂商k…

【数据库系统概论】第第12章 并发控制

12.1 并发控制概述 并发控制是指数据库管理系统&#xff08;DBMS&#xff09;通过控制多个事务同时执行&#xff0c;保证数据的一致性和隔离性&#xff0c;避免事务间的相互干扰。 事务串行执行不能充分利用系统资源 并发执行的优点&#xff1a;能够减少处理机的空闲 时间&a…

滚珠花键在使用时需注意什么?

滚珠花键是一种直线运动系统&#xff0c;当花键套利用其中的钢球在经过精密磨削的花键轴上直线运动时&#xff0c;可以传递扭矩。在使用滚珠花键时&#xff0c;需要注意以下几个重要的事项&#xff1a; 1、不要擅自拆卸滚珠花键的各部分&#xff0c;因为这样可能会导致异物进入…

Jenkins 视图(View)

Jenkins 视图(View) 一、视图是什么 Jenkins 视图(View) 如下图中 All、Apps 都是 Jenkisn 中的 View 左侧如果有 New View 或者 点击 All 这一行最右侧的 号&#xff0c;都可以创建视图 二、视图(View)的作用 点击最左侧的 All 可以看到所有的任务 随着项目不断发展&am…

蓝桥杯刷题2.21|笔记

参考的是蓝桥云课十四天的那个题单&#xff0c;不知道我发这个有没有问题&#xff0c;如果有问题找我我立马删文。&#xff08;参考蓝桥云课里边的题单&#xff0c;跟着大佬走&#xff0c;应该是没错滴&#xff0c;加油加油&#xff09; 一、握手问题 #include <iostream&g…