项目场景:
??有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大。为了解决这个痛点,就使用了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 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 是我们自己写的,重写了一些方法
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";
}
}结果:

我们可以看到,子线程的日志也被串起来了。
定时任务:
如果使用了定时任务@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是空的,这是正常的,因为还没到拦截器这一层。
源码点击此处下载:
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日志链路追踪内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论