SpringBoot使用TraceId日志链路追踪

news2024/11/24 17:54:07

项目场景:

  有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大。为了解决这个痛点,就使用了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";
 
    @Override
    public 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;
    }
 
    @Override
    public 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 {
    @Resource
	private LogInterceptor logInterceptor;

	@Override
	public 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 TRACE_ID = "TRACE_ID";
 
    // 获取唯一性标识
    public static String generateTraceId() {
        return UUID.randomUUID().toString();
    }
 
    public static void setTraceIdIfAbsent() {
        if (MDC.get(TRACE_ID) == null) {
            MDC.put(TRACE_ID, 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();
    }
    
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
 
 
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
 
    @Override
    public 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 {

	@Resource
	private TestService testService;

	@RequestMapping(value = "/log", method = RequestMethod.GET)
	@ApiOperation(value = "测试日志")
	public String sign() {
		log.info("这是一行info日志");
		log.error("这是一行error日志");

		//异步操作测试
		testService.asyncTest();
		return "success";
	}
}

 结果:

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


总结:

服务启动的时候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.coloradmin.cn/o/2239215.html

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

相关文章

SSE (Server-Sent Events) 服务器实时推送详解

Server-Sent Events 一、什么是 SSE ?二、SSE 的工作原理三、SSE 的基本配置1.HTTP 请求和响应头设置2.SSE 字段介绍3.SSE 事件数据流示例 四、SseEmitter 的基本配置1.SseEmitter 介绍及用法2.使用 SseEmitter 示例11)编写核心 SSE Client2)编写 Controller3)前端接收与处理 …

【C++】踏上C++的学习之旅(六):深入“类和对象“世界,掌握编程的黄金法则(一)

文章目录 前言1. "面向过程"和"面向对象"的碰撞1.1 面向过程1.2 面向对象 2. "类"的引入3. "类"的定义3.1 &#x1f349;语法展示&#xff1a;3.2 "类"的两种定义方式3.3 "类"的命名规则 4. 类的访问限定符以及封…

机器学习:梯度提升树(GBDT)——基于决策树的树形模型

梯度提升树&#xff08;Gradient Boosting Decision Trees&#xff0c;GBDT&#xff09;是一种强大的机器学习方法&#xff0c;广泛用于回归和分类任务。它通过构建一系列决策树来优化模型的预测能力&#xff0c;基于梯度提升框架&#xff0c;使得每一棵树都试图纠正前一棵树的…

Spark SQL大数据分析快速上手-伪分布模式安装

【图书介绍】《Spark SQL大数据分析快速上手》-CSDN博客 《Spark SQL大数据分析快速上手》【摘要 书评 试读】- 京东图书 大数据与数据分析_夏天又到了的博客-CSDN博客 Hadoop完全分布式环境搭建步骤-CSDN博客,前置环境安装参看此博文 伪分布模式也是在一台主机上运行&…

github使用基础

要通过终端绑定GitHub账号并进行文件传输&#xff0c;你需要使用Git和SSH密钥来实现安全连接和操作。以下是一个基本流程&#xff1a; 设置GitHub和SSH 检查Git安装 通过终端输入以下命令查看是否安装Git&#xff1a; bash 复制代码 git --version配置Git用户名和邮箱 bash …

Python OpenCV孤立点检测

孤立点检测 在Python中使用OpenCV进行孤立点&#xff08;异常点&#xff09;检测&#xff0c;可以通过应用统计分析或者使用OpenCV的findContours和convexHull函数来识别。以下是一个简单的例子&#xff0c;使用OpenCV的findContours和convexHull来识别并绘制孤立点。 孤立点…

Vue自定义指令详解——以若依框架中封装指令为例分析

自定义指令 在Vue.js中&#xff0c;自定义指令提供了一种非常灵活的方式来扩展Vue的功能。以下是对Vue中自定义指令的详细解释&#xff1a; 一、自定义指令的基本概念 自定义指令允许开发者直接对DOM元素进行低层次操作&#xff0c;而无需编写大量的模板或者JavaScript代码。…

云渲染:服务器机房与物理机房两者有什么区别

云渲染选择服务器机房与物理机房两者主要区别在哪里呢&#xff1f; 服务器机房和物理机房作为云渲染的基础设施&#xff0c;各自扮演着不同的角色。 服务器机房的特点 服务器机房&#xff0c;通常指的是那些专门用于托管服务器的设施&#xff0c;它们可能位于云端&#xff0c…

即插即用篇 | YOLOv8 引入 代理注意力 AgentAttention

Transformer模型中的注意力模块是其核心组成部分。虽然全局注意力机制具有很强的表达能力,但其高昂的计算成本限制了在各种场景中的应用。本文提出了一种新的注意力范式,称为“代理注意力”(Agent Attention),以在计算效率和表示能力之间取得平衡。代理注意力使用四元组(Q…

机器学习基础02

目录 1.特征工程 1.1特征工程概念 1.2特征工程的步骤 1.3特征工程-特征提取 1.3.1字典特征提取 1.3.2文本特征提取 英文文本提取 中文文本提取 1.3.3TF-IDF文本特征词的稀有程度特征提取 2.无量纲化 2.1归一化 2.2标准化 2.3fit、fit_transform、transform 3.特征…

vue-h5:在h5中实现相机拍照加上身份证人相框和国徽框

1.基础功能 参考&#xff1a; https://blog.csdn.net/weixin_45148022/article/details/135696629 https://juejin.cn/post/7327353533618978842?searchId20241101133433B2BB37A081FD6A02DA60 https://www.freesion.com/article/67641324321/ https://github.com/AlexKrat…

【Elasticsearch入门到落地】1、初识Elasticsearch

一、什么是Elasticsearch Elasticsearch&#xff08;简称ES&#xff09;是一款非常强大的开源搜索引擎&#xff0c;可以帮助我们从海量数据中快速找到需要的内容。它使用Java编写&#xff0c;基于Apache Lucene来构建索引和提供搜索功能&#xff0c;是一个分布式、可扩展、近实…

Rust开发一个命令行工具(一,简单版持续更新)

依赖的包 cargo add clap --features derive clap命令行参数解析 项目目录 代码 main.rs mod utils;use clap::Parser; use utils::{editor::open_in_vscode,fs_tools::{file_exists, get_file, is_dir, list_dir, read_file}, }; /// 在文件中搜索模式并显示包含它的行。…

Xshell,Shell的相关介绍与Linux中的权限问题

目录 XShell的介绍 Shell的运行原理 Linux当中的权限问题 Linux权限的概念 Linux权限管理 文件访问者的分类&#xff08;人&#xff09; 文件类型和访问权限&#xff08;事物属性&#xff09; 文件权限值的表示方法 文件访问权限的相关设置方法 如何改变文件的访问权…

golang 实现比特币内核:公钥的 SEC 编码格式详解

比特币作为区块链的一个应用,它建立在分布式系统之上,‘节点’遍布全球。为了使所有节点协同工作并作为一个整体系统运行,需要保持所有节点同步在相同的状态中,也就是说节点之间需要频繁通信,并且相互交换大量数据消息。这要求在网络上传输的消息或数据要使用某种格式编码…

【JAVA】使用IDEA创建maven聚合项目

【JAVA】使用IDEA创建maven聚合项目 1.效果图 2.创建父模块项目 2.1删除父模块下面的src目录以及不需要的maven依赖 3创建子模块项目 3.1右击父模块项目选择Module… 3.2创建子模块 3.3删除子模块下不需要的maven依赖 4.子模块创建完成后引入SpringBoot依赖启动项目

《Django 5 By Example》阅读笔记:p17-p53

《Django 5 By Example》学习第2天&#xff0c;p17-p53总结&#xff0c;总计37页。 一、技术总结 1.数据库迁移 python manage.py makemigrations blog python manage.py sqlmigrate blog 0001 python manage.py migrate 2.ORM Django自带ORM。 3.view (1)定义 p42, …

基于物联网的智能超市快速结算系统

摘 要 当今社会的商品层出不穷&#xff0c;人们因为越来越多大型仓储超市的出现使得生活更加便利&#xff0c;但许多随之而来的新问题也给人们带来了许多的不便&#xff0c;例如商家一直被更换标签不及时、货物丢失、超市内物品更换处理不及时、超市内人流高峰期人流控制不得…

阿里云Linux安装Docker服务报错问题

今天使用了阿里云99计划的服务器&#xff0c;之前用惯了 CentOS&#xff0c;这次想体验下阿里云调教的 Alibaba Cloud Linux 3 系统性能&#xff0c;但是在安装 docker 的时候遇到了问题&#xff01; 传统安装方式 之前习惯安装docker方式&#xff1a; #查看是否已经安装的D…

数据结构《链表》

文章目录 前言一、什么是链表&#xff1f;二、单向链表2.1 单向链表的个人实现2.2 单向链表的例题 三、双向链表3.1 双向链表的个人实现3.2 关于真正的java中提供的链表的使用 总结 前言 提示&#xff1a;概念来源于&#xff1a;>>LinkedList<< 一、什么是链表&am…