java后端实现全链路日志ID记录

背景:当前系统线上问题排查比较麻烦,因为没有全链路的日志id,导致排查日志只能搜索关键字然后根据相关日志进行排查,过于耗时,因此项目考虑引入全链路日志id

--------------------------------------------------------------话不多说,直接开干-------------------------------------------------------------------------------------------

第一步、引入日志框架和Web相关依赖(如果项目中已存在,则不需要再次引入)

复制代码
<!-- SLF4J核心 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.9</version>
</dependency>
<!-- Logback实现 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>
<!-- Spring Web(若为Spring Boot项目可直接用spring-boot-starter-web) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.0</version>
</dependency>

第二步、添加TraceID上下文工具类

复制代码
import org.slf4j.MDC;

/**
 * 链路追踪上下文工具类
 * 推荐结合MDC(Mapped Diagnostic Context)使用,日志框架可直接读取MDC中的值
 */
public class TraceIdContext {
    // MDC的Trace ID键名(日志配置中会用到)
    public static final String TRACE_ID_KEY = "traceId";
    // ThreadLocal备用(若不使用MDC时)
    private static final ThreadLocal<String> TRACE_ID_THREAD_LOCAL = new ThreadLocal<>();

    /**
     * 生成唯一Trace ID(UUID简化版,去掉横线)
     */
    public static String generateTraceId() {
        return java.util.UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 设置Trace ID到MDC和ThreadLocal
     */
    public static void setTraceId(String traceId) {
        MDC.put(TRACE_ID_KEY, traceId);
        TRACE_ID_THREAD_LOCAL.set(traceId);
    }

    /**
     * 获取当前Trace ID
     */
    public static String getTraceId() {
        String traceId = MDC.get(TRACE_ID_KEY);
        return traceId != null ? traceId : TRACE_ID_THREAD_LOCAL.get();
    }

    /**
     * 清除上下文(必须在请求结束时调用)
     */
    public static void clear() {
        MDC.remove(TRACE_ID_KEY);
        TRACE_ID_THREAD_LOCAL.remove();
    }
}

第三步、检索当前项目中的拦截器

复制代码
implements HandlerInterceptor

如果不存在,则直接新增拦截器代码类即可:

复制代码
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * Web请求拦截器:为每个请求绑定Trace ID
 */
@Component
public class TraceIdInterceptor implements HandlerInterceptor {

    /**
     * 请求处理前:生成Trace ID(优先使用请求头中的Trace ID,便于跨服务调用)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 优先从请求头获取Trace ID(如前端/网关传递),没有则生成
        String traceId = request.getHeader(TraceIdContext.TRACE_ID_KEY);
        if (traceId == null || traceId.isBlank()) {
            traceId = TraceIdContext.generateTraceId();
        }
        // 绑定到上下文
        TraceIdContext.setTraceId(traceId);
        // 响应头返回Trace ID,便于前端排查
        response.setHeader(TraceIdContext.TRACE_ID_KEY, traceId);
        return true;
    }

    /**
     * 请求处理后:清除上下文(避免ThreadLocal内存泄漏)
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        TraceIdContext.clear();
    }
}

如果存在了,那么就直接在当前拦截器中增加拦截代码(如下所示)即可--(当然也可以直接继续新增拦截器,两者不冲突):

复制代码
// 优先从请求头获取Trace ID(如前端/网关传递),没有则生成
        String traceId = request.getHeader(TraceIdContext.TRACE_ID_KEY);
        if (traceId == null || traceId.isBlank()) {
            traceId = TraceIdContext.generateTraceId();
        }
        // 绑定到上下文
        TraceIdContext.setTraceId(traceId);
        // 响应头返回Trace ID,便于前端排查
        response.setHeader(TraceIdContext.TRACE_ID_KEY, traceId);
        return true;

第四步、注册拦截器,这一步就需要注意了,尽量避免重复创建拦截器,直接查找是否有实现了WebMvcConfigurer的类,然后在addInterceptor方法中增加自己的拦截处理即可,下属图片二选一

复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final TraceIdInterceptor traceIdInterceptor;

    // 构造注入拦截器
    public WebConfig(TraceIdInterceptor traceIdInterceptor) {
        this.traceIdInterceptor = traceIdInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求(排除静态资源,可根据实际调整)
        registry.addInterceptor(traceIdInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/static/**", "/favicon.ico");
    }
}

第五步、配置日志框架,自动打印 Trace ID(同理,如果已存在对应的日志输出配置文件,直接修改如下图二)

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 控制台输出Appender -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 日志格式:时间 [线程名] [Trace ID] 日志级别 类名 - 日志内容 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 全局日志级别 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

PS:

复制代码
%clr([%X{traceId:-N/A}]){yellow}中的-N/A指的是非拦截的请求traceId可能是空的,那么就会显示如下图的日志,但是为空的情况显示的是N/A这种标识,而不是数字

至此--基本请求的全链路日志ID已经有了,下述方法是为了处理线程池和异步线程的情况同样应用上全链路日志ID,如果不考虑线程的情况,那么可以忽略下述步骤

第六步、多线程场景:传递 Trace ID

复制代码
import org.slf4j.MDC;
import java.util.concurrent.*;
import java.util.function.Consumer;

/**
 * 全局统一的Trace ID线程工具类(强制所有线程池/异步任务使用此类)
 */
public class TraceIdThreadUtil {
    // Trace ID键名(与日志配置、拦截器保持一致)
    private static final String TRACE_ID_KEY = "traceId";

    // ====================== 1. 封装线程池创建:自动传递Trace ID ======================
    /**
     * 创建带Trace ID传递的线程池(替代直接new ThreadPoolExecutor)
     */
    public static ThreadPoolExecutor createTraceIdExecutor(int corePoolSize,
                                                          int maximumPoolSize,
                                                          long keepAliveTime,
                                                          TimeUnit unit,
                                                          BlockingQueue<Runnable> workQueue,
                                                          RejectedExecutionHandler handler) {
        // 自定义ThreadFactory,包装Runnable传递Trace ID
        ThreadFactory traceIdThreadFactory = r -> new Thread(wrapRunnable(r), "TraceId-Thread-" + System.currentTimeMillis());
        
        return new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                traceIdThreadFactory,
                handler
        );
    }

    // ====================== 2. 封装异步任务执行(替代ThreadUtil.execute) ======================
    /**
     * 执行异步任务(自动传递Trace ID,替代原生ThreadUtil)
     */
    public static void execute(Runnable task) {
        // 包装任务,传递当前线程的Trace ID
        Runnable traceIdTask = wrapRunnable(task);
        // 复用通用线程池(避免频繁创建线程)
        getCommonExecutor().execute(traceIdTask);
    }

    // ====================== 3. 内部工具:包装Runnable,传递Trace ID ======================
    /**
     * 包装Runnable,绑定当前线程的Trace ID到子线程
     */
    public static Runnable wrapRunnable(Runnable task) {
        // 捕获当前线程的Trace ID
        String currentTraceId = MDC.get(TRACE_ID_KEY);
        return () -> {
            // 备份子线程原有MDC(避免覆盖)
            String oldTraceId = MDC.get(TRACE_ID_KEY);
            try {
                // 将当前线程的Trace ID绑定到子线程
                if (currentTraceId != null) {
                    MDC.put(TRACE_ID_KEY, currentTraceId);
                }
                // 执行原任务
                task.run();
            } finally {
                // 恢复子线程原有MDC(避免影响后续任务)
                if (oldTraceId != null) {
                    MDC.put(TRACE_ID_KEY, oldTraceId);
                } else {
                    MDC.remove(TRACE_ID_KEY);
                }
            }
        };
    }

    // ====================== 4. 通用线程池(复用,避免重复创建) ======================
    private static ThreadPoolExecutor commonExecutor;

    /**
     * 获取通用异步任务线程池(单例)
     */
    private static ThreadPoolExecutor getCommonExecutor() {
        if (commonExecutor == null || commonExecutor.isShutdown()) {
            synchronized (TraceIdThreadUtil.class) {
                if (commonExecutor == null || commonExecutor.isShutdown()) {
                    commonExecutor = createTraceIdExecutor(
                            5, 10, 0L, TimeUnit.MILLISECONDS,
                            new LinkedBlockingQueue<>(1000),
                            new ThreadPoolExecutor.CallerRunsPolicy()
                    );
                }
            }
        }
        return commonExecutor;
    }

    // 私有化构造器,禁止实例化
    private TraceIdThreadUtil() {}
}

使用方法如下:

线程池:

复制代码
// 替换为TraceIdThreadUtil创建,自动传递Trace ID
private final ThreadPoolExecutor executor = TraceIdThreadUtil.createTraceIdExecutor(
        5,
        10,
        0L,
        TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>(100),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

// 任务执行逻辑无需修改(ThreadFactory已自动包装Runnable)
updateRatePlanDTOList.forEach(dto -> executor.execute(() -> {
    try {
        dto.setUserId(user.getUserId());
        dto.setNickName(finalUserName);
        this.update(dto);
    } catch (Exception e) {
        log.error("批量更新价格计划条款异常", e); // 日志自动带Trace ID
    } finally {
        countDownLatch.countDown();
    }
}));

异步线程:

复制代码
// 替换为TraceIdThreadUtil.execute,自动传递Trace ID
TraceIdThreadUtil.execute(() -> {
    List<HotelChannelPricePolicyLog> logs = new ArrayList<>();
    // ... 原有业务逻辑不变 ...
    this.saveBatch(logs); // 日志自动带Trace ID
});

--------------------------------------------------------------打完收工,直接下班-------------------------------------------------------------------------------------------

相关推荐
hgz07102 小时前
企业级Web应用部署实战:Tomcat + MySQL
java
闭上眼让寒冷退却2 小时前
知识库发布按钮引发的查询版本发布状态(轮询?——>调用后端接口)以及api接口设计学习
java·前端·javascript
ZePingPingZe2 小时前
MySQL与Spring,事务与自动提交有什么关系?
mysql·spring
running up2 小时前
Spring IOC/DI 核心知识
java·spring·rpc
木头软件2 小时前
批量将 Word 文档重命名为其标题
开发语言·c#·word
fantasy5_52 小时前
C++ 智能指针深度解析:原理、实现与实战避坑
java·开发语言·c++
q_19132846952 小时前
基于SpringBoot2+Vue2的企业合作与活动管理平台
java·vue.js·经验分享·spring boot·笔记·mysql·计算机毕业设计
ERROR:992 小时前
野路子:把海量文档一次性转换成多个PPT
开发语言·人工智能·c#
凌冰_2 小时前
JAVA与MySQL实现银行管理系统
java·开发语言·mysql