SpringBoot如何使用TraceId日志链路追踪

2025-01-20 04:50

本文主要是介绍SpringBoot如何使用TraceId日志链路追踪,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《SpringBoot如何使用TraceId日志链路追踪》文章介绍了如何使用TraceId进行日志链路追踪,通过在日志中添加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.qoshttp://www.chinasem.cn.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";
	}
}

结果:

SpringBoot如何使用TraceId日志链路追踪

异步场景:

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

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

SpringBoot如何使用TraceId日志链路追踪

1、ThreadMdcUtil.java

import org.slf4j.MDC;
import java.util.Map;
import androidjava.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 是我们China编程自己写的,重写了一些方法

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&gphpt; 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";
	}
}

结果:

SpringBoot如何使用TraceId日志链路追踪

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

定时任务:

如果使用了定时任务@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{
	@Override
	public 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是空的,这是正常的,因为还没到拦截器这一层。

源码点击此处下载:

http://xiazai.jb51.net/202501/yuanma/springbootlog_jb51.rar

API 说明

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

到此这篇关于SpringBoot如何使用TraceId日志链路追踪的文章就介绍到这了,更多相关SpringBoot TraceId日志链路追踪内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!

这篇关于SpringBoot如何使用TraceId日志链路追踪的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

Java如何从Redis中批量读取数据

《Java如何从Redis中批量读取数据》:本文主要介绍Java如何从Redis中批量读取数据的情况,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一.背景概述二.分析与实现三.发现问题与屡次改进3.1.QPS过高而且波动很大3.2.程序中断,抛异常3.3.内存消

Python使用FFmpeg实现高效音频格式转换工具

《Python使用FFmpeg实现高效音频格式转换工具》在数字音频处理领域,音频格式转换是一项基础但至关重要的功能,本文主要为大家介绍了Python如何使用FFmpeg实现强大功能的图形化音频转换工具... 目录概述功能详解软件效果展示主界面布局转换过程截图完成提示开发步骤详解1. 环境准备2. 项目功能结

SpringBoot使用ffmpeg实现视频压缩

《SpringBoot使用ffmpeg实现视频压缩》FFmpeg是一个开源的跨平台多媒体处理工具集,用于录制,转换,编辑和流式传输音频和视频,本文将使用ffmpeg实现视频压缩功能,有需要的可以参考... 目录核心功能1.格式转换2.编解码3.音视频处理4.流媒体支持5.滤镜(Filter)安装配置linu

Redis中的Lettuce使用详解

《Redis中的Lettuce使用详解》Lettuce是一个高级的、线程安全的Redis客户端,用于与Redis数据库交互,Lettuce是一个功能强大、使用方便的Redis客户端,适用于各种规模的J... 目录简介特点连接池连接池特点连接池管理连接池优势连接池配置参数监控常用监控工具通过JMX监控通过Pr

Apache 高级配置实战之从连接保持到日志分析的完整指南

《Apache高级配置实战之从连接保持到日志分析的完整指南》本文带你从连接保持优化开始,一路走到访问控制和日志管理,最后用AWStats来分析网站数据,对Apache配置日志分析相关知识感兴趣的朋友... 目录Apache 高级配置实战:从连接保持到日志分析的完整指南前言 一、Apache 连接保持 - 性

apache的commons-pool2原理与使用实践记录

《apache的commons-pool2原理与使用实践记录》ApacheCommonsPool2是一个高效的对象池化框架,通过复用昂贵资源(如数据库连接、线程、网络连接)优化系统性能,这篇文章主... 目录一、核心原理与组件二、使用步骤详解(以数据库连接池为例)三、高级配置与优化四、典型应用场景五、注意事

在Spring Boot中实现HTTPS加密通信及常见问题排查

《在SpringBoot中实现HTTPS加密通信及常见问题排查》HTTPS是HTTP的安全版本,通过SSL/TLS协议为通讯提供加密、身份验证和数据完整性保护,下面通过本文给大家介绍在SpringB... 目录一、HTTPS核心原理1.加密流程概述2.加密技术组合二、证书体系详解1、证书类型对比2. 证书获

使用Python实现Windows系统垃圾清理

《使用Python实现Windows系统垃圾清理》Windows自带的磁盘清理工具功能有限,无法深度清理各类垃圾文件,所以本文为大家介绍了如何使用Python+PyQt5开发一个Windows系统垃圾... 目录一、开发背景与工具概述1.1 为什么需要专业清理工具1.2 工具设计理念二、工具核心功能解析2.

Linux系统之stress-ng测压工具的使用

《Linux系统之stress-ng测压工具的使用》:本文主要介绍Linux系统之stress-ng测压工具的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、理论1.stress工具简介与安装2.语法及参数3.具体安装二、实验1.运行8 cpu, 4 fo

Java使用MethodHandle来替代反射,提高性能问题

《Java使用MethodHandle来替代反射,提高性能问题》:本文主要介绍Java使用MethodHandle来替代反射,提高性能问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录一、认识MethodHandle1、简介2、使用方式3、与反射的区别二、示例1、基本使用2、(重要)