SpringBoot自定义链路追踪

一、背景介绍

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,对于微服务传递调用可以在响应头中获取添加和设置流传下去.

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的应用基本类似实现方式。
相关推荐
Charlie_lll1 小时前
LibreOffice 实现 Word 转 PDF
java·spring boot·pdf·word
hhzz2 小时前
Springboot项目中使用EasyPOI操作Excel(详细教程系列4/4)
java·spring boot·后端·spring·excel·poi·easypoi
星火开发设计2 小时前
表达式与语句:C++ 程序的执行逻辑基础
java·开发语言·c++·学习·知识·表达式
计算机毕设指导62 小时前
基于微信小程序求职招聘-兼职管理系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·求职招聘
小白不会Coding2 小时前
一文讲清楚JVM字节码文件的组成
java·jvm·字节码文件
深念Y2 小时前
IDEA下载JDK慢的真相:权限、DNS与CDN的解析
java·ide·intellij-idea
Remember_9932 小时前
【数据结构】二叉树:从基础到应用全面解析
java·数据结构·b树·算法·leetcode·链表
冷冷的菜哥2 小时前
springboot调用ffmpeg实现对视频的截图,截取与水印
java·spring boot·ffmpeg·音视频·水印·截图·截取
C++chaofan2 小时前
JUC并发编程:LockSupport.park() 与 unpark() 深度解析
java·开发语言·c++·性能优化·高并发·juc