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

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

3天内不再提示

手动实现SpringBoot日志链路追踪

jf_ro2CN3Fa 来源:csdn 作者:CSDN 2022-12-15 15:04 次阅读

  • 前言
  • 正文
3abca07e-7c3d-11ed-8abf-dac502259ad0.jpg

前言

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

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

3b060c64-7c3d-11ed-8abf-dac502259ad0.png

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

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

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

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

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

3b14d7d0-7c3d-11ed-8abf-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/

正文

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

3b58db1a-7c3d-11ed-8abf-dac502259ad0.png

①pom.xml 依赖

<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>

<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.16.10version>
dependency>
dependencies>

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


<configurationdebug="false">

<propertyname="log"value="D:/test/log"/>

<appendername="console"class="ch.qos.logback.core.ConsoleAppender">
<encoderclass="ch.qos.logback.classic.encoder.PatternLayoutEncoder">

<pattern>[%X{TRACE_ID}]%d{yyyy-MM-ddHHss.SSS}[%thread]%-5level%logger{50}-%msg%npattern>
encoder>
appender>

<appendername="file"class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicyclass="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<FileNamePattern>${log}/%d{yyyy-MM-dd}.logFileNamePattern>

<MaxHistory>30MaxHistory>
rollingPolicy>
<encoderclass="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[%X{TRACE_ID}]%d{yyyy-MM-ddHHss.SSS}[%thread]%-5level%logger{50}-%msg%npattern>
encoder>

<triggeringPolicyclass="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MBMaxFileSize>
triggeringPolicy>
appender>


<rootlevel="INFO">
<appender-refref="console"/>
<appender-refref="file"/>
root>
configuration>

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的):

3b7c2340-7c3d-11ed-8abf-dac502259ad0.png

还没完。

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

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

3bb25adc-7c3d-11ed-8abf-dac502259ad0.png

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

3bc97a14-7c3d-11ed-8abf-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,重启服务,再看看效果:

3be73004-7c3d-11ed-8abf-dac502259ad0.png

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



审核编辑 :李倩


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

    关注

    0

    文章

    335

    浏览量

    14254
  • 日志
    +关注

    关注

    0

    文章

    128

    浏览量

    10591
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    145

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

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

收藏 人收藏

    评论

    相关推荐

    IR615如何实现VPN备份?

    目的:IR615的备份(WAN为主、Wi-Fi做STA为从),当VPN建好后,WAN
    发表于 07-25 08:27

    IG网关产品实现备份的方法

    的-接口备份模块。 3、选择主接口以及备份接口等数据参数;设定完成后添加即生效。(Dot11radio 1代表WLAN接口) 通过这样设定即可实现备份的基础功能设定:
    发表于 07-24 08:25

    如何识别光纤问题?

    光纤网络专为连续运行而设计。通常,光纤网络以最佳效率运行。然而,网络中有时会遇到光纤问题。由于光纤网络的复杂性,这些光纤问题很难识别。然而,为了确保光纤网络的最佳性能,识别和解
    的头像 发表于 06-11 10:12 287次阅读

    加法进位手动约束

    在激光雷达中,使用FPGA实现TDC时需要手动约束进位的位置。这里简单记录下。 在outflow下会生成一个.qplace文件 。用于指示布线的各个原语资源的分布位置 。 它的内容主是 是原语
    的头像 发表于 05-20 11:38 1103次阅读
    加法进位<b class='flag-5'>链</b>的<b class='flag-5'>手动</b>约束

    永久、信道测试的区别

    永久测试和信道测试是网络和通信领域中两个不同的概念,它们通常用于确保网络和通信系统的可靠性和性能。 永久测试(Permanent Link Testing): 永久
    的头像 发表于 03-25 10:59 1701次阅读

    使用go语言实现一个grpc拦截器

    在开发grpc服务时,我们经常会遇到一些通用的需求,比如:日志追踪、鉴权等。这些需求可以通过grpc拦截器来实现。本文使用go语言来
    的头像 发表于 12-18 10:13 520次阅读
    使用go语言<b class='flag-5'>实现</b>一个grpc拦截器

    SpringBoot实现动态切换数据源

    最近在做业务需求时,需要从不同的数据库中获取数据然后写入到当前数据库中,因此涉及到切换数据源问题。本来想着使用Mybatis-plus中提供的动态数据源SpringBoot的starter:dynamic-datasource-spring-boot-starter来实现
    的头像 发表于 12-08 10:53 829次阅读
    <b class='flag-5'>SpringBoot</b><b class='flag-5'>实现</b>动态切换数据源

    状态路由协议的基本概念和原理解析

    状态路由选择协议又被称为最短路径优先协议,它基SPF(shortest path first )算法。他比距离矢量协议复杂的多。路由器的状态的信息称为
    的头像 发表于 12-07 09:52 2204次阅读
    <b class='flag-5'>链</b><b class='flag-5'>路</b>状态路由协议的基本概念和原理解析

    什么是聚合?怎么配置聚合?聚合简介

    以太网聚合Eth-Trunk简称聚合,它通过将多条以太网物理捆绑在一起成为一条逻辑
    的头像 发表于 11-28 09:24 2799次阅读
    什么是<b class='flag-5'>链</b><b class='flag-5'>路</b>聚合?怎么配置<b class='flag-5'>链</b><b class='flag-5'>路</b>聚合?<b class='flag-5'>链</b><b class='flag-5'>路</b>聚合简介

    C语言或Matlab如何实现FF调度器仿真?

    C语言或Matlab如何实现FF调度器仿真
    发表于 10-18 06:12

    C语言或matlab如何实现ff调速器仿真?

    C语言或matlab如何实现ff调速器仿真
    发表于 10-17 07:34

    事务性日志结构文件系统的设计及实现

    本文发表于FAST 2022,探讨日志结构文件系统层面的事务支持。本文主要对F2FS进行了支持事务的修改,实现了支持ACID事务特性的文件系统层面支持。本文通过实验测试了SQLite和ROCKSDB
    的头像 发表于 10-16 16:01 545次阅读
    事务性<b class='flag-5'>日志</b>结构文件系统的设计及<b class='flag-5'>实现</b>

    SpringBoot 连接ElasticSearch的使用方式

    SpringBoot,今天我们就以 SpringBoot 整合 ElasticSearch 为例,给大家详细的介绍 ElasticSearch 的使用! SpringBoot 连接 ElasticSearch,主流
    的头像 发表于 10-09 10:35 603次阅读

    SpringBoot 如何实现热部署

    SpringBoot 如何实现热部署? 1、热部署的优点 开发周期通常包括编写代码、编译、部署和测试几个步骤。在一个快速发展的项目中,这个周期需要尽可能地缩短。热部署能让开发者在代码更改后立即看到结果,从而加速开发和测试过程。 除了加速开发,热部署也让应用
    的头像 发表于 09-30 10:16 724次阅读
    <b class='flag-5'>SpringBoot</b> 如何<b class='flag-5'>实现</b>热部署

    路由器PLC接入和多组网的区别?

    路由器PLC接入和多组网都是现代网络技术中的重要概念,但它们在实现方式和应用场景上存在明显的区别。路由器PLC接入是一种通过电力线通信(PLC)技术实现的网络接入方式。多
    的头像 发表于 09-19 13:34 1391次阅读