0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Spring Boot如何实现日志链路追踪

jf_ro2CN3Fa 来源:芋道源码 2023-05-16 11:33 次阅读

前言

正文

前言

从文章标题就知道,这篇文章是介绍些什么。

这是我一位朋友的问题反馈:

aa554736-f395-11ed-90ce-dac502259ad0.png

好像是的,确实这种现象是普遍存在的。

有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大。

模糊匹配搜索日志能解决吗? 能解决一点点。 但是不能完全呈现出整个链路相关的日志。

那要做到方便,很显然,我们需要的是把同一次的业务调用链上的日志串起来。

什么效果? 先看一个实现后的效果图:

aa5ba7a2-f395-11ed-90ce-dac502259ad0.png

这样下来,我们再配合模糊匹配查找日志,效果不就刚刚的了。

cat-ninfo.log|grep"a415ad50dbf84e99b1b56a31aacd209c"

或者

grep-10'a415ad50dbf84e99b1b56a31aacd209c'info.log(10是指上下10行)

不多说,开整。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

视频教程:https://doc.iocoder.cn/video/

正文

惯例,先看一眼这次实战最终工程的结构:

aa615652-f395-11ed-90ce-dac502259ad0.png

①pom.xml 依赖



org.springframework.boot
spring-boot-starter-web


org.springframework.boot
spring-boot-starter-test
test


org.springframework.boot
spring-boot-starter-logging

 

org.projectlombok
lombok
1.16.10


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



 

 


 
[%X{TRACE_ID}]%d{yyyy-MM-ddHHss.SSS}[%thread]%-5level%logger{50}-%msg%n


 


 
${log}/%d{yyyy-MM-dd}.log
 
30


[%X{TRACE_ID}]%d{yyyy-MM-ddHHss.SSS}[%thread]%-5level%logger{50}-%msg%n

 

10MB



 





application.yml

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

③自定义日志拦截器 LogInterceptor.java

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

importorg.slf4j.MDC;
importorg.springframework.lang.Nullable;
importorg.springframework.util.StringUtils;
importorg.springframework.web.servlet.HandlerInterceptor;

importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
importjava.util.UUID;

/**
*@Author:JCccc
*@Date:2022-5-3010:45
*@Description:
*/
publicclassLogInterceptorimplementsHandlerInterceptor{

privatestaticfinalStringTRACE_ID="TRACE_ID";

@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){
Stringtid=UUID.randomUUID().toString().replace("-","");
//可以考虑让客户端传入链路ID,但需保证一定的复杂度唯一性;如果没使用默认UUID自动生成
if(!StringUtils.isEmpty(request.getHeader("TRACE_ID"))){
tid=request.getHeader("TRACE_ID");
}
MDC.put(TRACE_ID,tid);
returntrue;
}

@Override
publicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,
@NullableExceptionex){
MDC.remove(TRACE_ID);
}

}

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

WebConfigurerAdapter.java 添加拦截器

importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;
importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
*@Author:JCccc
*@Date:2022-5-3010:47
*@Description:
*/
@Configuration
publicclassWebConfigurerAdapterimplementsWebMvcConfigurer{
@Bean
publicLogInterceptorlogInterceptor(){
returnnewLogInterceptor();
}

@Override
publicvoidaddInterceptors(InterceptorRegistryregistry){
registry.addInterceptor(logInterceptor());
//可以具体制定哪些需要拦截,哪些不拦截,其实也可以使用自定义注解更灵活完成
//.addPathPatterns("/**")
//.excludePathPatterns("/testxx.html");
}
}

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

到这时候,其实已经完成,就是这么简单。

我们写个测试接口,看下效果:

@PostMapping("doTest")
publicStringdoTest(@RequestParam("name")Stringname)throwsInterruptedException{
log.info("入参name={}",name);
testTrace();
log.info("调用结束name={}",name);
return"Hello,"+name;
}
privatevoidtestTrace(){
log.info("这是一行info日志");
log.error("这是一行error日志");
testTrace2();
}
privatevoidtestTrace2(){
log.info("这也是一行info日志");

}

效果(OK的):

aa6b1296-f395-11ed-90ce-dac502259ad0.png

还没完。

接下来看一个场景, 使用子线程的场景:

故意写一个异步线程,加入这个调用里面:

aa70c1fa-f395-11ed-90ce-dac502259ad0.png

再次执行看开效果,显然子线程丢失了trackId:

aa7805aa-f395-11ed-90ce-dac502259ad0.png

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

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

importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.scheduling.annotation.EnableAsync;
importjava.util.concurrent.Executor;

/**
*@Author:JCccc
*@Date:2022-5-3011:07
*@Description:
*/
@Configuration
@EnableAsync
publicclassThreadPoolConfig{
/**
*声明一个线程池
*
*@return执行器
*/
@Bean("MyExecutor")
publicExecutorasyncExecutor(){
MyThreadPoolTaskExecutorexecutor=newMyThreadPoolTaskExecutor();
//核心线程数5:线程池创建时候初始化的线程数
executor.setCorePoolSize(5);
//最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(5);
//缓冲队列500:用来缓冲执行任务的队列
executor.setQueueCapacity(500);
//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("asyncJCccc");
executor.initialize();
returnexecutor;
}
}

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

importorg.slf4j.MDC;
importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

importjava.util.concurrent.Callable;
importjava.util.concurrent.Future;

/**
*@Author:JCccc
*@Date:2022-5-3011:13
*@Description:
*/
publicfinalclassMyThreadPoolTaskExecutorextendsThreadPoolTaskExecutor{
publicMyThreadPoolTaskExecutor(){
super();
}

@Override
publicvoidexecute(Runnabletask){
super.execute(ThreadMdcUtil.wrap(task,MDC.getCopyOfContextMap()));
}


@Override
publicFuturesubmit(Callabletask){
returnsuper.submit(ThreadMdcUtil.wrap(task,MDC.getCopyOfContextMap()));
}

@Override
publicFuturesubmit(Runnabletask){
returnsuper.submit(ThreadMdcUtil.wrap(task,MDC.getCopyOfContextMap()));
}
}

③ThreadMdcUtil.java

importorg.slf4j.MDC;

importjava.util.Map;
importjava.util.UUID;
importjava.util.concurrent.Callable;

/**
*@Author:JCccc
*@Date:2022-5-3011:14
*@Description:
*/
publicfinalclassThreadMdcUtil{
privatestaticfinalStringTRACE_ID="TRACE_ID";

//获取唯一性标识
publicstaticStringgenerateTraceId(){
returnUUID.randomUUID().toString();
}

publicstaticvoidsetTraceIdIfAbsent(){
if(MDC.get(TRACE_ID)==null){
MDC.put(TRACE_ID,generateTraceId());
}
}

/**
*用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
*@paramcallable
*@paramcontext
*@param
*@return
*/
publicstaticCallablewrap(finalCallablecallable,finalMapcontext){
return()->{
if(context==null){
MDC.clear();
}else{
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try{
returncallable.call();
}finally{
MDC.clear();
}
};
}

/**
*用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
*@paramrunnable
*@paramcontext
*@return
*/
publicstaticRunnablewrap(finalRunnablerunnable,finalMapcontext){
return()->{
if(context==null){
MDC.clear();
}else{
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try{
runnable.run();
}finally{
MDC.clear();
}
};
}
}

OK,重启服务,再看看效果:

aa7e694a-f395-11ed-90ce-dac502259ad0.png

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

审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    7648

    浏览量

    148521
  • spring
    +关注

    关注

    0

    文章

    333

    浏览量

    14161

原文标题:Spring Boot 实现日志链路追踪,无需引入组件,让日志定位更方便!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Spring Boot如何实现异步任务

    Spring Boot 提供了多种方式来实现异步任务,这里介绍三种主要实现方式。 1、基于注解 @Async @Async 注解是 Spring
    的头像 发表于 09-30 10:32 490次阅读

    Spring boot中Redis的使用

    【本人秃顶程序员】springboot专辑:Spring boot中Redis的使用
    发表于 03-27 11:42

    启动Spring Boot项目应用的三种方法

    的读取,开启注解4)配置日志文件...配置完成之后部署tomcat 调试。但是如果使用spring boot呢?很简单,我仅仅只需要非常少的几个配置就可以迅速方便的搭建起来一套web项目或者是构建一个
    发表于 01-14 17:33

    Spring Boot嵌入式Web容器原理是什么

    Spring Boot嵌入式Web容器原理Spring Boot的目标是构建“非常容易创建、独立、产品级别的基于Spring的应用”。这些应
    发表于 12-16 07:57

    java 日志框架Spring Boot分析

    引言:我们需要在已有的微服务代码中添加日志功能,用于输出需要关注的内容,这是最平常的技术需求了。由于我们的微服务代码是基于SpringBoot开发的,那么问题就转换为如何在Spring Boot
    发表于 09-28 14:58 0次下载

    Spring Boot从零入门1 详述

    在开始学习Spring Boot之前,我之前从未接触过Spring相关的项目,Java基础还是几年前自学的,现在估计也忘得差不多了吧,写Spring
    的头像 发表于 12-10 22:18 419次阅读

    Spring Boot特有的实践

    Spring Boot是最流行的用于开发微服务的Java框架。在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践。这些内容是基于我的个人经验
    的头像 发表于 09-29 10:24 647次阅读

    强大的Spring Boot 3.0要来了

    来源:OSC开源社区(ID:oschina2013) Spring Boot 3.0 首个 RC 已发布,此外还为两个分支发布了更新:2.7.5 2.6.13。 3.0.0-RC1: https
    的头像 发表于 10-31 11:17 1176次阅读

    手动实现SpringBoot日志链路追踪

    基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
    的头像 发表于 12-15 15:04 862次阅读

    Spring Boot Web相关的基础知识

    Boot的第一个接口。接下来将会将会介绍使用Spring Boot开发Web应用的相关内容,其主要包括使用spring-boot-starter-web组件来
    的头像 发表于 03-17 15:03 462次阅读

    Spring Boot日志框架使用

    目前市面上常见的日志框架有:slf4j(Simple Logging Facade for Java)、logback、log4j、log4j2、commons-logging(Spring默认日志框架)、JUL(java.ut
    的头像 发表于 06-02 10:59 693次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>的<b class='flag-5'>日志</b>框架使用

    Spring Boot Actuator快速入门

    不知道大家在写 Spring Boot 项目的过程中,使用过 Spring Boot Actuator 吗?知道 Spring
    的头像 发表于 10-09 17:11 344次阅读

    Spring Boot启动 Eureka流程

    在上篇中已经说过了 Eureka-Server 本质上是一个 web 应用的项目,今天就来看看 Spring Boot 是怎么启动 Eureka 的。 Spring Boot 启动 E
    的头像 发表于 10-10 11:40 422次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>启动 Eureka流程

    Spring Boot的启动原理

    可能很多初学者会比较困惑,Spring Boot 是如何做到将应用代码和所有的依赖打包成一个独立的 Jar 包,因为传统的 Java 项目打包成 Jar 包之后,需要通过 -classpath 属性
    的头像 发表于 10-13 11:44 373次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>的启动原理

    Spring Boot 的设计目标

    什么是Spring Boot Spring BootSpring 开源组织下的一个子项目,也是 S
    的头像 发表于 10-13 14:56 313次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b> 的设计目标