一、背景介绍
1、日常开发需要追踪请求链路,方便定位问题,快速识别问题,如RequestId、TraceId等。
二、技术实现
1、通过自定义过滤器和MDC实现。
java
public class TraceIdUtil {
private static final String TRACE_ID="traceId";
private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static String getTraceId(){
String traceId=threadLocal.get();
if(StringUtils.isBlank(traceId)){
traceId=UUID.randomUUID().toString().replace("-", "");
threadLocal.set(traceId);
MDC.put(TRACE_ID,traceId);
}
return threadLocal.get();
}
public static void setTraceId(String traceId){
threadLocal.set(traceId);
MDC.put(TRACE_ID,traceId);
}
public static void removeTraceId(){
threadLocal.remove();
MDC.remove(TRACE_ID);
}
}
Trace过滤器实现
java
package com.boot.skywalk.filter;
import com.boot.skywalk.util.TraceIdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*",filterName = "TraceFilter")
public class TraceFilter extends OncePerRequestFilter {
private static final String TRACE_ID_HEADER = "X-Trace-ID";
private static final String TRACE_ID_KEY = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long start=System.currentTimeMillis();
try {
String traceId = request.getHeader(TRACE_ID_HEADER);
if (traceId == null || traceId.isEmpty()) {
traceId = TraceIdUtil.getTraceId();
response.setHeader(TRACE_ID_HEADER, traceId);
}
filterChain.doFilter(request,response);
} finally {
long end=System.currentTimeMillis();
log.info("接口地址:{},接口耗时:{}",request.getRequestURL().toString(),(end-start));
// 移除TraceId
TraceIdUtil.removeTraceId();
}
}
}
日志文件配置:log4j2
XML
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="WARN" monitorInterval="60">
<!--变量配置-->
<Properties>
<!--应用名称-->
<property name="APP_NAME">walk-boot</property>
<!--日志存放路径,当前路径还是绝对路径-->
<property name="LOG_PATH">/var/logs/${APP_NAME}</property>
<!--日志输出格式-控制台-->
<property name="PATTERN_CONSOLE">[%style{%d{yyyy-MM-dd HH:mm:ss.SSS}}{bright,green}]%notEmpty{[%X{requestId}]}%notEmpty{[%X{traceId}]}[%highlight{%p}][%style{%t}{bright,blue}][%style{%C}{bright,yellow} %L]: %msg%n%style{%throwable}{red}</property>
<!--日志输出格式-文件-->
<property name="PATTERN_FILE">[%d{yyyy-MM-dd HH:mm:ss.SSS}][%X{requestId}] [%X{userId}][%thread]%-5level-[%C-%M:%L]-%msg%n </property>
</Properties>
<!--定义日志输出目的地,内容和格式等-->
<Appenders>
<!--可归档文件
1. fileName: 日志存储路径
2. filePattern: 历史日志封存路径。其中%d{yyyy-MM-dd}表示了日志的时间单位是天,log4j2自动识别zip等后缀,表示历史日志需要压缩
-->
<!--控制台-->
<Console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式:
1.不设置默认为: %m%n
2.disableAnsi="false" noConsoleNoAnsi="false" 配置开启支持%highlight彩色日志
-->
<PatternLayout pattern="${PATTERN_CONSOLE}" disableAnsi="false" noConsoleNoAnsi="false"/>
<!--只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
</Console>
<!--业务定义-->
<RollingFile name="Service" fileName="${LOG_PATH}/${APP_NAME}.log" filePattern="${LOG_PATH}/$${date:yyyy-MM}/${APP_NAME}-%d{yyyy-MM-dd}_%i.log.gz">
<!--输出日志的格式, 不设置默认为:%m%n-->
<PatternLayout pattern="${PATTERN_FILE}"/>
<!--只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<!--归档设置-->
<Policies>
<!--按时间间隔归档:
1. interval=时间间隔, 单位由filePattern的%d日期格式指定, 此处配置代表每一天归档一次
2. modulate="true" 是否对interval取模,决定了下一次触发的时间点
-->
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<!-- 按照日志文件的大小: size表示当前日志文件的最大size,支持单位:KB/MB/GB-->
<SizeBasedTriggeringPolicy size="50MB"/>
</Policies>
<!-- 历史日志配置: 该属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="30"/>
</RollingFile>
</Appenders>
<!--Loggers配置-->
<Loggers>
<!--
注意点:
1. logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等:
(1). name: 用来指定该logger所适用的类或者类所在的包全路径,继承自Root节点.
(2). AppenderRef:关联的Appender, 只有定义了logger并引入的appender,appender才会生效
(3). additivity: logEvent的传递性。true LogEvent处理后传递给父Logger打印。false LogEvent处理后不再向上传递给父Logger(解决日志重复输出问题)
(4). logger配置的level必须高于或等于Appenders中ThresholdFilter配置的过滤level, 否则会造成信息丢失
2. root配置日志的根节点
-->
<!-- 按照模块划分日志 -->
<!-- <logger name="com.boot.skywalk" level="info" additivity="false">-->
<!-- <AppenderRef ref="Service"/>-->
<!-- </logger>-->
<root level="DEBUG">
<AppenderRef ref="Console"/>
</root>
</Loggers>
</Configuration>
注意点
1、PATTERN_CONSOLE和PATTERN_FILE都要配置使用
2、%notEmpty{[%X{requestId}]}%notEmpty{[%X{traceId}]},notEmpty为空时候不展示占位符
3、用户请求链路包括内部框架执行链路请求
对于线程池异步应用,子线程要能获取到MDC的TraceId
java
package com.boot.skywalk.config;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import java.util.Map;
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 获取主线程的MDC
Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// 将主线程的MDC设置到子线程中
if (copyOfContextMap != null) {
MDC.setContextMap(copyOfContextMap);
}
runnable.run();
} finally {
// 清除MDC
MDC.clear();
}
};
}
}
SpringBoot自定义线程池设置:threadPoolTaskExecutor.setTaskDecorator(new MdcTaskDecorator());
java
package com.boot.skywalk.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class ExecutorConfiguration {
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(20);
threadPoolTaskExecutor.setQueueCapacity(500);
threadPoolTaskExecutor.setTaskDecorator(new MdcTaskDecorator());
threadPoolTaskExecutor.setThreadNamePrefix("Data-Job");
return threadPoolTaskExecutor;
}
@Bean
public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(20);
threadPoolTaskExecutor.setQueueCapacity(500);
threadPoolTaskExecutor.setTaskDecorator(new MdcTaskDecorator());
threadPoolTaskExecutor.setThreadNamePrefix("async-service");
return threadPoolTaskExecutor;
}
@Bean
public ThreadPoolTaskExecutor testThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setMaxPoolSize(10);
threadPoolTaskExecutor.setQueueCapacity(10);
threadPoolTaskExecutor.setTaskDecorator(new MdcTaskDecorator());
threadPoolTaskExecutor.setThreadNamePrefix("async-service-test");
return threadPoolTaskExecutor;
}
}
自定义线程池扩展实现
java
package com.boot.skywalk.config;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {
public MdcThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
// 提交 Runnable 任务时,传递 MDC 上下文
@Override
public void execute(Runnable task) {
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
super.execute(wrap(task, mdcContext));
}
// 提交 Callable 任务时,传递 MDC 上下文
@Override
public <T> Future<T> submit(Callable<T> task) {
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return super.submit(wrap(task, mdcContext));
}
// 包装任务,传递 MDC 上下文
private <T> Callable<T> wrap(Callable<T> callable, Map<String, String> context) {
return () -> {
try {
if (context != null) {
MDC.setContextMap(context);
}
return callable.call();
} finally {
MDC.clear();
}
};
}
private Runnable wrap(Runnable runnable, Map<String, String> context) {
return () -> {
try {
if (context != null) {
MDC.setContextMap(context);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
三、追踪测试

响应头增加X-Trace-ID,对于微服务传递调用可以在响应头中获取添加和设置流传下去.

- 线程池异步执行任务:http://localhost:8070/skywalk/async/test
java
@Async("testThreadPoolTaskExecutor")
public void testAsync(){
for(int i=0;i<10;i++) {
log.info("async-code-thread={}-index={}",Thread.currentThread().getName(),i);
}
}


四、其他适配
- 对于Dubbo 的RPC应用、Feign的应用、RestTemplate的应用基本类似实现方式。