网关 + MDC 过滤器方案,5分钟集成 日志 traceid

网关 + MDC 过滤器方案通过在网关层统一生成 TraceId,再通过过滤器在各服务间透传,实现全链路日志追踪。以下是具体实现步骤,包含网关生成、服务接收、跨线程传递等关键细节:

一、网关层:生成并传递 TraceId(以 Spring Cloud Gateway 为例)

作用:所有请求进入系统的第一个入口生成 TraceId,确保全链路唯一。

1. 添加依赖(若未引入)
xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2. 编写全局过滤器
java 复制代码
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;

@Configuration
public class TraceIdGatewayConfig {

    @Bean
    public GlobalFilter traceIdFilter() {
        return (exchange, chain) -> {
            // 1. 生成 TraceId(去掉 UUID 中的横线,更简洁)
            String traceId = UUID.randomUUID().toString().replace("-", "");
            
            // 2. 放入 MDC,供网关自身日志使用
            MDC.put("traceId", traceId);
            
            // 3. 写入请求头,传递给下游服务
            ServerWebExchange modifiedExchange = exchange.mutate()
                    .request(exchange.getRequest()
                            .mutate()
                            .header("X-Trace-Id", traceId)  // 约定 header 键为 X-Trace-Id
                            .build())
                    .build();
            
            // 4. 执行后续过滤链,完成后清除 MDC(避免 Netty 线程复用导致的污染)
            return chain.filter(modifiedExchange)
                    .doFinally(signalType -> MDC.clear());
        };
    }

    // 设置过滤器优先级(最高,确保第一个执行)
    @Bean
    public Ordered traceIdFilterOrder() {
        return () -> Ordered.HIGHEST_PRECEDENCE;
    }
}

二、下游服务:接收并透传 TraceId(所有业务服务通用)

作用:从请求头获取 TraceId,放入当前线程的 MDC,确保日志能打印;同时在调用其他服务时传递下去。

1. 编写 Servlet 过滤器(Spring Boot Web 服务)
java 复制代码
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Component
public class TraceIdServletFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 1. 从请求头获取 TraceId,若没有则生成(应对直接访问服务的场景)
        String traceId = httpRequest.getHeader("X-Trace-Id");
        if (traceId == null || traceId.isEmpty()) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        
        // 2. 放入 MDC,供当前服务日志使用
        MDC.put("traceId", traceId);
        
        try {
            // 3. 执行后续逻辑
            chain.doFilter(request, response);
        } finally {
            // 4. 清除 MDC,避免 Tomcat 线程池复用导致的脏数据
            MDC.clear();
        }
    }
}
2. 配置日志模板(logback-spring.xml)

在日志格式中加入 [%X{traceId}],示例:

xml 复制代码
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{50} - %msg%n</pattern>
    </encoder>
</appender>

此时服务日志会自动带上 TraceId,形如:

复制代码
2025-07-31 15:00:00.123 [http-nio-8080-exec-1] INFO [a1b2c3d4e5f67890] com.example.Service - 处理订单逻辑

三、跨服务调用:确保 TraceId 透传(关键!)

当服务 A 调用服务 B 时,需手动将当前 MDC 中的 TraceId 放入请求头,否则链路会中断。

1. RestTemplate 调用场景
java 复制代码
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.Collections;

// 配置 RestTemplate 拦截器,自动添加 TraceId 头
@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(new ClientHttpRequestInterceptor() {
            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
                    throws IOException {
                // 从 MDC 获取 TraceId,添加到请求头
                String traceId = MDC.get("traceId");
                if (traceId != null) {
                    request.getHeaders().add("X-Trace-Id", traceId);
                }
                return execution.execute(request, body);
            }
        }));
        return restTemplate;
    }
}
2. OpenFeign 调用场景
java 复制代码
import feign.RequestInterceptor;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

    @Bean
    public RequestInterceptor traceIdInterceptor() {
        return template -> {
            // 从 MDC 获取 TraceId,添加到 Feign 请求头
            String traceId = MDC.get("traceId");
            if (traceId != null) {
                template.header("X-Trace-Id", traceId);
            }
        };
    }
}

四、跨线程场景:解决异步任务 TraceId 丢失

当使用线程池、CompletableFuture 等异步操作时,子线程默认无法继承父线程的 MDC 数据,需手动传递:

1. 线程池包装类(推荐)
java 复制代码
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;

public class TraceIdThreadPoolExecutor extends ThreadPoolExecutor {

    // 构造方法复用父类
    public TraceIdThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                     TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    public void execute(Runnable command) {
        // 获取父线程 MDC 上下文
        Map<String, String> context = MDC.getCopyOfContextMap();
        super.execute(() -> {
            try {
                // 子线程设置 MDC 上下文
                if (context != null) {
                    MDC.setContextMap(context);
                }
                command.run();
            } finally {
                // 清除子线程 MDC
                MDC.clear();
            }
        });
    }
}
2. 使用方式
java 复制代码
// 用包装类创建线程池,替代原生 ThreadPoolExecutor
ExecutorService executor = new TraceIdThreadPoolExecutor(
    5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
);

// 提交任务时,子线程日志会自动带上 TraceId
executor.submit(() -> {
    log.info("异步处理任务...");  // 日志中包含父线程的 TraceId
});

五、验证与效果

  1. 发起请求:通过网关访问任意接口(如 http://网关地址/order/create)。
  2. 查看日志:在网关、服务 A、服务 B 的日志中搜索同一个 TraceId,确认链路贯通。
  3. 异常排查:当出现问题时,只需 grep 某个 TraceId,即可获取全链路的日志上下文。

核心要点总结

  • 网关生成:确保入口唯一,避免分布式环境下 TraceId 冲突。
  • 过滤器透传 :所有服务必须添加过滤器,且日志模板包含 %X{traceId}
  • 跨服务调用:RestTemplate/Feign 必须加拦截器,否则调用下游时 TraceId 丢失。
  • 异步场景:线程池需包装,手动传递 MDC 上下文,否则子线程日志无 TraceId。
场景分类 具体场景 核心问题 解决方案 侵入性 适用范围
跨服务调用 HTTP调用(Feign) 服务间请求头未自动携带TraceId 配置Feign拦截器,自动从MDC获取并设置X-Trace-Id Spring Cloud微服务
RPC调用(Dubbo) 分布式调用中上下文丢失 实现Dubbo过滤器,通过Attachment传递TraceId,Provider端自动设置MDC Dubbo微服务
消息队列(Kafka/RabbitMQ) 消息生产/消费链路断裂 生产者拦截器添加TraceId到消息头,消费者拦截器读取并设置MDC 基于消息队列的异步通信
异步场景 线程池(ThreadPool) 子线程无法继承父线程MDC 自定义线程池,提交任务时捕获父线程MDC,子线程执行前恢复 所有线程池异步任务
@Async注解 Spring异步方法默认不传递MDC 使用自定义线程池(同上)作为@Async的执行器 Spring框架异步方法
CompletableFuture 链式调用中线程切换导致TraceId丢失 封装CompletableFuture工具类,自动传递MDC上下文 Java原生异步编程
基础场景 网关入口 全链路TraceId生成源头不统一 网关全局过滤器生成唯一TraceId,写入请求头并设置MDC 有网关的分布式系统
单体服务内部 单个服务内日志无统一TraceId 过滤器从请求头(或生成)TraceId,设置到MDC,日志模板包含%X{traceId} 单体服务或微服务节点
特殊场景 定时任务(Quartz) 无请求触发,无初始TraceId 任务执行时自动生成TraceId并设置MDC 定时任务、后台任务
跨语言调用(如Java→Python) 不同语言间上下文传递格式不统一 约定X-Trace-Id头,Python端通过中间件(如Flask/Django过滤器)接收并记录 多语言混合架构

通过这套方案,可在10分钟内实现轻量级全链路追踪,无需引入复杂组件,适合中小团队快速落地。

相关推荐
猷咪12 小时前
C++基础
开发语言·c++
IT·小灰灰12 小时前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧12 小时前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q12 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳012 小时前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾12 小时前
php 对接deepseek
android·开发语言·php
vx_BS8133012 小时前
【直接可用源码免费送】计算机毕业设计精选项目03574基于Python的网上商城管理系统设计与实现:Java/PHP/Python/C#小程序、单片机、成品+文档源码支持定制
java·python·课程设计
2601_9498683612 小时前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计13 小时前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
qq_1777673713 小时前
React Native鸿蒙跨平台数据使用监控应用技术,通过setInterval每5秒更新一次数据使用情况和套餐使用情况,模拟了真实应用中的数据监控场景
开发语言·前端·javascript·react native·react.js·ecmascript·harmonyos