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

    文章

    338

    浏览量

    14307
  • 日志
    +关注

    关注

    0

    文章

    138

    浏览量

    10631
  • SpringBoot
    +关注

    关注

    0

    文章

    173

    浏览量

    167

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

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

收藏 人收藏

    评论

    相关推荐

    nginx日志配置方法

    access_log用来定义日志级别,日志位置。
    的头像 发表于 10-24 17:43 191次阅读

    日志篇:模组日志总体介绍

    ​今天我们学习合宙模组日志总体介绍,以下进入正文。 一、本文讨论的边界 本文是对合宙 4G 模组, 以及 4G+GNSS 模组的日志功能的总体介绍。通过日志,可以对研发过程中,以及模组运行过程中
    的头像 发表于 10-24 07:16 143次阅读
    <b class='flag-5'>日志</b>篇:模组<b class='flag-5'>日志</b>总体介绍

    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 490次阅读

    加法进位手动约束

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

    如何辨别光纤的好坏?

    辨别光纤的好坏,通常涉及一系列测试和检查步骤。以下是一些主要的方法: 光学连通性测试:检查光纤的光学连通性。当输出端测到的光功率与输入端实际输入的光功率的比值小于一定的数值时,
    的头像 发表于 04-11 11:48 935次阅读

    永久、信道测试的区别

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

    【嵌入式SD NAND】基于FATFS/Littlefs文件系统的日志框架实现

    文章目录【嵌入式】基于FATFS/Littlefs文件系统的日志框架实现1.概述2.设计概要3.设计实现3.1初始化`init`3.2日志写入`write`3.3
    的头像 发表于 03-14 18:12 1122次阅读
    【嵌入式SD NAND】基于FATFS/Littlefs文件系统的<b class='flag-5'>日志</b>框架<b class='flag-5'>实现</b>

    介绍五款好用的日志管理工具

    日志管理是现代IT环境中不可或缺的一部分,它有助于监视和维护应用程序、系统和网络的正常运行,帮助诊断问题,追踪事件以及确保安全性。
    的头像 发表于 12-21 14:24 1192次阅读
    介绍五款好用的<b class='flag-5'>日志</b>管理工具

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

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

    SpringBoot实现动态切换数据源

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

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

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

    一个注解搞定SpringBoot接口防刷

    技术要点:springboot的基本知识,redis基本操作,
    的头像 发表于 11-28 10:46 386次阅读

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

    以太网聚合Eth-Trunk简称聚合,它通过将多条以太网物理捆绑在一起成为一条逻辑
    的头像 发表于 11-28 09:24 3104次阅读
    什么是<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>聚合简介