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.mzph.cn/diannao/70057.shtml

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

相关文章

微信小程序网络请求封装

微信小程序的网络请求为什么要封装&#xff1f;封装使用有什么好处&#xff1f; 封装的目的是为了偷懒&#xff0c;试想一下每次都要wx.request&#xff0c;巴拉巴拉传一堆参数&#xff0c;是不是很麻烦&#xff0c;有些公共的参数例如header&#xff0c;baseUrl是不是可以封装…

Element Plus table 去除行hover效果

需求&#xff1a; 给table的指定行设置高亮背景色且去除掉这些行的hover效果 思路&#xff1a; 给指定行设置css类名选择需要设置高亮的行的单元格&#xff0c;设置鼠标禁用属性让高亮行继承父元素的背景色 考虑到表格的第一列是勾选框&#xff0c;因此仅选择 tr 下除了第一…

认识vue-admin

认识vue-admin **核心交付:** 为什么要基于现成架子二次开发 什么是二次开发&#xff1a;基于已有的代码&#xff08;项目工程&#xff0c;脚手架&#xff09;开进行新功能的开发 所以看懂已有的框架中的既有代码&#xff0c;变得很重要了 1. 背景知识 后台管理系统是一种最…

无人机航迹规划:孟加拉虎优化( Savannah Bengal Tiger Optimization ,SBTO)算法求解无人机路径规划MATLAB

一、孟加拉虎优化算法 孟加拉虎优化&#xff08; Savannah Bengal Tiger Optimization &#xff0c;SBTO&#xff09;算法模拟了孟加拉虎的群体狩猎行为&#xff0c;采用了猎物搜索、隐身接近和攻击狩猎三种策略。 参考文献&#xff1a; [1]Yujing Sun, Xingguo Xu. Savann…

sib报错:com.*.xctrunner is not in your device!

1、问题描述 在使用sonic集成IOS设备的时候,我们需要通过sonic-agent服务去识别IOS设备。但是在识别的时候提示如下问题: 本质就是在你这个设备中找不到这个设备也就是找不到WebDriverAgentRunner,但是确实安装了,甚至appium可以正常的调用。 或执行如下命令的时候报错:…

c++中什么时候应该使用final关键字?

在C中&#xff0c;final关键字是自C11标准引入的重要特性&#xff0c;主要用于类继承和虚函数重写机制的约束。下面从技术原理、使用场景和最佳实践三个维度进行系统分析&#xff0c;并给出工业级代码示例。 目录 一、技术原理深度解析 二、关键使用场景分析 1. 类级别的fi…

【AI】Docker中快速部署Ollama并安装DeepSeek-R1模型: 一步步指南

【AI】Docker中快速部署Ollama并安装DeepSeek-R1模型: 一步步指南 一、前言 为了确保在 Docker 环境中顺利安装并高效运行 Ollama 以及 DeepSeek 离线模型&#xff0c;本文将详细介绍整个过程&#xff0c;涵盖从基础安装到优化配置等各个方面。通过对关键参数和配置的深入理解…

文件夹上传到github分支最后github上面还是没有文件和文件夹

环境&#xff1a; github 问题描述&#xff1a; 文件夹上传到github分支最后github上面还是没有文件和文件夹, 和这样一样 解决方案&#xff1a; 从 git ls-tree -r HEAD 的输出中可以看到&#xff0c;metahuman-stream 文件夹显示为如下内容&#xff1a; 160000 commi…

【JavaEE进阶】验证码案例

目 &#x1f332;实现说明 &#x1f384;Hutool介绍 &#x1f333;准备工作 &#x1f334;约定前后端交互接口 &#x1f6a9;接口定义 &#x1f6a9;实现服务器后端代码 &#x1f6a9;前端代码 &#x1f6a9;整体测试 &#x1f332;实现说明 随着安全性的要求越来越⾼…

4G模块非必要,不关机!关机建议先进飞行模式

给4G模组VBAT断电关机&#xff0c;模组关机前未能及时退出当前基站&#xff0c;会有什么影响呢&#xff1f; 基站会误以为设备还在线&#xff0c;下次开机仍会拿着上次驻网信息去连基站。基站一看&#xff0c;上次链接还在——认为你是非法设备&#xff0c;拒绝链接&#xff…

Flutter中 List列表中移除特定元素

在 Dart 语言里&#xff0c;若要从子列表中移除特定元素&#xff0c;可以使用以下几种方法&#xff0c;下面为你详细介绍&#xff1a; 方法一&#xff1a;使用 where 方法创建新列表 where 方法会根据指定的条件筛选元素&#xff0c;然后通过 toList 方法将筛选结果转换为新列…

CF 148A.Insomnia cure(Java实现)

题目分析 这道题翻译出来后比较难理解&#xff0c;但是实际情况就是从1遍历d&#xff0c;看能不能被前四个值整除 思路分析 由上所述&#xff0c;存值后判断是否能整除&#xff0c;整除就答案1 代码 import java.util.*;public class Main {public static void main(String[]…

Java:单例模式(Singleton Pattern)及实现方式

一、单例模式的概念 单例模式是一种创建型设计模式&#xff0c;确保一个类只有一个实例&#xff0c;并提供一个全局访问点来访问该实例&#xff0c;是 Java 中最简单的设计模式之一。该模式常用于需要全局唯一实例的场景&#xff0c;例如日志记录器、配置管理、线程池、数据库…

企业SSL 证书管理指南

文章从以下几个部分展开 SSL证书的用途和使用场景SSL证书的申请类型和实现方式SSL证书的管理SSL证书的续签 一、SSL 证书的用途和使用场景 1.1 为什么要使用 SSL 证书&#xff1f; 1. 数据安全 &#x1f6e1;️- 在 HTTP 传输中&#xff0c;TCP 包可以被截获&#xff0c;攻…

网络安全-攻击流程-传输层

传输层攻击主要针对OSI模型的第四层&#xff0c;涉及TCP和UDP协议的安全漏洞。以下是常见攻击类型及其流程&#xff0c;以及防御措施&#xff1a; 1. SYN洪水攻击&#xff08;TCP半连接攻击&#xff09; 攻击流程&#xff1a; 目标选择&#xff1a;确定目标服务器的IP地址和开…

朝天椒USB服务器解决前置机U盾虚拟机远程连接

本文探讨朝天椒USB服务器用Usb Over Network技术&#xff0c;解决前置机虚拟化部署后U盾的远程连接问题。 在金融、电信等关键行业&#xff0c;后台核心处理系统承担着至关重要的业务数据交互职责。为保障系统安全&#xff0c;这些单位要求企业通过前置机与他们的内网进行数据…

探索Java中的集合类_特性与使用场景

1. 引言 1.1 Java集合框架概述 Java集合框架(Java Collections Framework, JCF)是Java中用于存储和操作一组对象的类和接口的统称。它提供了多种数据结构来满足不同的需求,如列表、集合、映射等。JCF的核心接口包括Collection、List、Set、Queue和Map,以及它们的各种实现…

MySQL数据库误删恢复_mysql 数据 误删

2、BigLog日志相关 2.1、检查biglog状态是否开启 声明: 当前为mysql版本5.7 当前为mysql版本5.7****当前为mysql版本5.7 2.1.1、Navicat工具执行 SHOW VARIABLES LIKE LOG_BIN%;OFF 是未开启状态&#xff0c;如果不是ON 开启状态需要开启为ON。{默认情况下就是关闭状态} 2.…

读 DeepSeek-R1 论文笔记

DeepSeek-R1&#xff1a;通过强化学习激发大语言模型的推理能力 DeepSeek-AI 摘要 我们推出第一代推理模型DeepSeek-R1-Zero和DeepSeek-R1。DeepSeek-R1-Zero作为无需监督微调(SFT)预训练阶段、直接通过大规模强化学习(RL)训练的基础模型&#xff0c;展现出卓越的推理能力。…

Vue2/Vue3分别如何使用Watch

在 Vue 2 和 Vue 3 中&#xff0c;watch 用于监听数据的变化并执行相应的逻辑。虽然两者的核心功能相同&#xff0c;但在语法和使用方式上有一些区别。以下是 Vue 2 和 Vue 3 中使用 watch 的详细说明&#xff1a; Vue 2 中的 watch 在 Vue 2 中&#xff0c;watch 是通过选项式…