手写链路追踪

143 阅读4分钟

一、问题

说明: 我们已经实现了记录操作和入参出参的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占位符。

image.png

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来区分哪些日志属于同一条请求链路了,也就实现了链路追踪功能!