一、问题
说明: 我们已经实现了记录操作和入参出参的Aop切面了,但是我们在查看日志的时候,可能会发现很多操作日志混乱的混在一起(被两个服务同时调用或者被多个服务调用,日志层面查看无法区分是谁调用的),导致我们无从排查相关信息或错误;如果我们实现了链路追踪,那么每个请求有个独立的id,那么我们在日志中便能够明确知道该日志属于哪条请求!
现状问题: 方法调用日志
问题: 可以看到,这两次的接口请求,因为是手动调用,但是我们也有点辨别不出哪块的日志属于哪次调用了,一旦在线上,可能被多个服务调用,或者并发调用,那么日志可能会出现相互穿插的情况,那么就更难排错了!
思考: 要是同一次的调用链路上的日志都加上同一个UUID或者唯一ID即可,这样那几条日志属于同一次调用就一目了然了!
二、具体实现
2.1 修改日志配置文件
ape-common-log 模块:
说明: 在日志中加入 MDC 的变量,我们这里取为 PFTID(profile trace id)
MDC允许我们在一个线程的执行上下文中设置和获取键值对,这些键值对在日志输出中可以以占位符的形式被引用,从而在日志信息中输出这些上下文信息
<configuration status="INFO" monitorInterval="5">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--变量配置-->
<Properties>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<!-- %logger{36} 表示 Logger 名字最长36个字符 -->
<!--%x 为MDC的占位符-->
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} %X{PFTID} [%thread] %-5level %logger{36} - %msg%n" />
<!-- 定义日志存储的路径 -->
<property name="FILE_PATH" value="../log" />
<property name="FILE_NAME" value="frame.log" />
</Properties>
......
</configuration>
注意: MDC变量为: %X{PFTID},其中 %x 为MDC占位符。
2.2 自定义链路追踪过滤器
2.2.1 添加相关依赖
<!-- 为了引入MDC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- 相关工具类 字符串判空 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
2.2.1 过滤器核心配置:FilterConfig
过滤器属于web模块,所以在ape-common-web下创建config.FilterConfig类来编写
package com.ssm.config;
import com.ssm.trace.TraceIdFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* 自定义链路追踪过滤器
*/
@Configuration
@ConditionalOnProperty(name = {"traceId.filter.enable"}, havingValue = "true")
public class FilterConfig {
@Resource
private TraceIdFilter traceIdFilter;
@Bean
public FilterRegistrationBean registrationFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(traceIdFilter);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setName("traceIdFilter");
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
}
重点解读
-
trace为追踪、跟踪意思
-
FilterRegistrationBean是Spring Framework 中的一个类,它用于在 Spring 容器中注册 Servlet 过滤器(Filter)。
-
setFilter 设置过滤器实例
-
addUrlPatterns 指定哪些 URL 应该被过滤器拦截
-
setName 设置名称(这个名称在 Servlet 容器中必须是唯一的)
-
setOrder 当多个过滤器被注册时,这个顺序决定了它们被调用的顺序。数字越小,优先级越高,即越先被调用
2.2.2 TraceId常量类:TraceIdConstant
优雅地将MDC变量表示出来
package com.ssm.trace;
/**
* TraceId常量类
*/
public class TraceIdConstant {
// 和日志中的PFTID参数映射
public static final String TRACE_ID = "PFTID";
}
2.2.3 存取TraceID的上下文:TraceIdContext
package com.ssm.trace;
import org.slf4j.MDC;
import java.util.UUID;
/**
* 存取TraceID的上下文
*/
public class TraceIdContext {
// 由于ThreadLocal不能在父子线程中传递,所以此处使用InheritableThreadLocal
public static final ThreadLocal<String> CURRENT_TRACE_ID = new InheritableThreadLocal<>();
//生成traceId
public static String generateTraceId() {
return UUID.randomUUID().toString();
}
//获取traceId
public static String getTraceId() {
return MDC.get(TraceIdConstant.TRACE_ID);
}
//添加traceId
public static void setTraceId(String traceId) {
//MDC允许我们在一个线程的执行上下文中设置和获取键值对,这些键值对在日志输出中可以以占位符的形式被引用,从而在日志信息中输出这些上下文信息
//%X{PFTID} %x为MDC占位符
MDC.put(TraceIdConstant.TRACE_ID, traceId);
}
//清空traceId
public static void clearTraceId() {
CURRENT_TRACE_ID.set(null);
CURRENT_TRACE_ID.remove();
}
}
2.2.4 过滤器:TraceIdFilter
package com.ssm.trace;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
@Slf4j
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String traceId = request.getHeader(TraceIdConstant.TRACE_ID);
// 如果TraceId为空,则赋值(可能多微服务调用,会从上游Http中获取)
if(StringUtils.isBlank(traceId)) {
// 生成TraceId
traceId = TraceIdContext.generateTraceId();
}
// 将TraceId设置进MDC
TraceIdContext.setTraceId(traceId);
//继续执行其他请求
filterChain.doFilter(servletRequest, servletResponse);
//请求处理完成后清空当前线程的TraceId
TraceIdContext.clearTraceId();
}
}
三、运行结果
结论: 可以看到,在一条请求链路上的日志,其日志前面都附带有自己的uuid,这样我们就能根据这个uuid来区分哪些日志属于同一条请求链路了,也就实现了链路追踪功能!