Spring Boot 全链路请求日志追踪:MDC 的正确用法
大家好!今天我们来聊聊如何在 Spring Boot 项目中实现全链路请求日志追踪,并深入探讨 MDC(Mapped Diagnostic Context)的正确用法。在微服务架构日益流行的今天,请求往往会跨越多个服务,传统的日志方式很难追踪一次请求的完整路径。全链路追踪可以帮助我们快速定位问题,提升开发和运维效率。
1. 全链路追踪的必要性
在单体应用时代,一个请求的处理通常在一个服务内部完成,通过日志文件可以相对容易地追踪问题。然而,在微服务架构下,一个用户请求可能需要经过多个服务的协同处理。如果出现问题,仅仅依靠单个服务的日志很难定位到问题的根源。全链路追踪可以解决以下问题:
- 跨服务追踪: 记录请求在各个服务间的流转路径。
 - 性能分析: 分析每个服务的耗时,找出性能瓶颈。
 - 故障定位: 快速定位出错的服务和具体代码。
 - 请求上下文传递: 在各个服务间传递必要的请求上下文信息。
 
2. MDC 简介
MDC(Mapped Diagnostic Context)是 SLF4J 和 Logback 提供的一种机制,它允许我们在日志上下文中添加一些键值对,这些键值对可以在整个线程的生命周期内被访问,并且可以被输出到日志中。简单来说,MDC 就像一个线程级别的 Map,我们可以向其中添加一些信息,这些信息会被自动添加到该线程产生的日志中。
3. MDC 的基本用法
MDC 的使用非常简单,主要涉及以下几个方法:
MDC.put(String key, String value): 向 MDC 中添加一个键值对。MDC.get(String key): 从 MDC 中获取指定键的值。MDC.remove(String key): 从 MDC 中移除指定键。MDC.clear(): 清空 MDC。
示例:
import org.slf4j.MDC;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyService {
    private static final Logger logger = LoggerFactory.getLogger(MyService.class);
    public void processRequest(String userId, String requestId) {
        MDC.put("userId", userId);
        MDC.put("requestId", requestId);
        try {
            logger.info("开始处理请求...");
            // ... 业务逻辑 ...
            logger.info("请求处理完成。");
        } finally {
            MDC.remove("userId");
            MDC.remove("requestId");
            //或者使用 MDC.clear();
        }
    }
    public static void main(String[] args) {
        MyService service = new MyService();
        service.processRequest("user123", "req456");
    }
}
在这个例子中,我们使用 MDC.put() 方法将 userId 和 requestId 添加到 MDC 中。在 logger.info() 方法输出日志时,这些信息会自动被包含在日志中。
Logback 配置:
为了在日志中显示 MDC 的内容,我们需要配置 Logback 的 pattern。
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X{userId} %X{requestId}%n</pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>
%X{userId} 和 %X{requestId}  表示从 MDC 中获取对应键的值。
注意:
- 一定要在 
try-finally块中使用 MDC,确保在请求处理完成后,清除 MDC 中的信息,避免线程污染。 - MDC 是线程级别的,每个线程拥有独立的 MDC。
 
4. Spring Boot 集成 MDC 实现全链路追踪
要实现全链路请求日志追踪,我们需要解决以下几个问题:
- 生成全局唯一的追踪 ID: 确保每次请求都有一个唯一的 ID,用于关联各个服务的日志。
 - 在请求入口处设置 MDC: 在请求进入服务时,将追踪 ID 放入 MDC。
 - 在服务调用时传递追踪 ID: 通过 HTTP Header 或其他方式,将追踪 ID 传递给下游服务。
 - 在下游服务中设置 MDC: 下游服务接收到请求后,将追踪 ID 放入 MDC。
 - 配置日志输出格式: 将 MDC 中的追踪 ID 输出到日志中。
 
下面我们通过一个简单的示例来演示如何在 Spring Boot 中实现全链路追踪。
4.1. 创建 Spring Boot 项目
创建一个简单的 Spring Boot 项目,包含两个 Controller:ServiceAController 和 ServiceBController。
4.2. 添加依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
添加了 Spring Web, AOP, SLF4J 和 Logback 依赖。 AOP 用于拦截请求,设置和清除 MDC。
4.3. 生成全局唯一的追踪 ID
可以使用 UUID 或其他算法生成全局唯一的追踪 ID。为了简单起见,我们使用 UUID。
4.4. 创建 MDC 拦截器
创建一个拦截器,用于在请求进入服务时设置 MDC,在请求处理完成后清除 MDC。
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.UUID;
@Component
public class MDCInterceptor implements HandlerInterceptor {
    private static final String TRACE_ID = "traceId";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = UUID.randomUUID().toString();
        MDC.put(TRACE_ID, traceId);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        MDC.remove(TRACE_ID);
    }
}
preHandle 方法在请求处理之前执行,我们在这里生成一个 UUID 作为 traceId,并将其放入 MDC 中。afterCompletion 方法在请求处理之后执行,我们在这里清除 MDC 中的 traceId。
4.5. 注册拦截器
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 WebMvcConfig implements WebMvcConfigurer {
    private final MDCInterceptor mdcInterceptor;
    public WebMvcConfig(MDCInterceptor mdcInterceptor) {
        this.mdcInterceptor = mdcInterceptor;
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(mdcInterceptor);
    }
}
将 MDCInterceptor 注册到 Spring MVC 中。
4.6. 创建 Controller
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.net.URISyntaxException;
@RestController
public class ServiceAController {
    private static final Logger logger = LoggerFactory.getLogger(ServiceAController.class);
    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/serviceA")
    public String serviceA() throws URISyntaxException {
        logger.info("进入 Service A");
        // 调用 Service B
        HttpHeaders headers = new HttpHeaders();
        // 从 MDC 中获取 traceId,并添加到 HTTP Header 中
        headers.set("traceId", org.slf4j.MDC.get("traceId"));
        RequestEntity<String> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, new URI("http://localhost:8080/serviceB"));
        String response = restTemplate.exchange(requestEntity, String.class).getBody();
        logger.info("Service A 调用 Service B 完成,返回结果:{}", response);
        return "Service A -> " + response;
    }
}
@RestController
class ServiceBController {
    private static final Logger logger = LoggerFactory.getLogger(ServiceBController.class);
    @GetMapping("/serviceB")
    public String serviceB() {
        logger.info("进入 Service B");
        return "Service B";
    }
}
ServiceAController 调用 ServiceBController。 在调用 ServiceB 之前,从 MDC 中获取 traceId,并将其添加到 HTTP Header 中。
4.7. 配置 RestTemplate
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
配置 RestTemplate,用于服务间的调用。
4.8. 配置 Logback
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X{traceId}%n</pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>
在 Logback 的 pattern 中添加 %X{traceId},以便在日志中显示 traceId。
4.9. 运行项目
启动 Spring Boot 项目,访问 http://localhost:8080/serviceA,查看控制台输出的日志。你会发现 ServiceA 和 ServiceB 的日志都包含了相同的 traceId。
5. 更进一步:使用 AOP 简化 MDC 操作
虽然使用拦截器可以实现 MDC 的设置和清除,但如果每个方法都需要手动操作 MDC,代码会变得冗余。可以使用 AOP 来简化 MDC 的操作。
5.1. 创建 MDC 注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MDCTrace {
    String traceIdKey() default "traceId";
}
创建一个自定义注解 @MDCTrace,用于标记需要进行 MDC 操作的方法。traceIdKey 用于指定 MDC 中 traceId 的 key。
5.2. 创建 AOP 切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.UUID;
@Aspect
@Component
public class MDCTraceAspect {
    @Around("@annotation(MDCTrace)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        MDCTrace annotation = method.getAnnotation(MDCTrace.class);
        String traceIdKey = annotation.traceIdKey();
        String traceId = UUID.randomUUID().toString();
        MDC.put(traceIdKey, traceId);
        try {
            return joinPoint.proceed();
        } finally {
            MDC.remove(traceIdKey);
        }
    }
}
创建一个 AOP 切面 MDCTraceAspect,用于拦截带有 @MDCTrace 注解的方法。在方法执行之前,生成 traceId 并放入 MDC;在方法执行之后,清除 MDC。
5.3. 使用 AOP
修改 ServiceBController:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class ServiceBController {
    private static final Logger logger = LoggerFactory.getLogger(ServiceBController.class);
    @GetMapping("/serviceB")
    @MDCTrace
    public String serviceB() {
        logger.info("进入 Service B");
        return "Service B";
    }
}
在 serviceB() 方法上添加 @MDCTrace 注解,这样就无需手动操作 MDC 了。
注意:
- 确保在 Spring Boot 启动类上添加 
@EnableAspectJAutoProxy注解,启用 AOP。 
6. 传递 TraceId 的其他方式
除了使用 HTTP Header 传递 traceId 之外,还可以使用其他方式:
- Message Queue: 如果服务之间通过消息队列进行通信,可以将 
traceId放入消息的 Header 中。 - gRPC Metadata: 如果使用 gRPC,可以将 
traceId放入 Metadata 中。 - 自定义上下文对象:  创建一个自定义的上下文对象,用于存储 
traceId,并在服务之间传递。 
7. 总结和要点回顾
全链路追踪对于微服务架构至关重要,它可以帮助我们快速定位问题,提升开发和运维效率。 MDC 是实现全链路追踪的关键技术之一,它可以让我们在日志中记录请求的上下文信息。 通过拦截器和 AOP,可以简化 MDC 的操作,提高代码的可维护性。
要点回顾:
- MDC 的作用: 在日志中记录请求的上下文信息,方便追踪。
 - MDC 的正确用法: 使用 
try-finally块,确保在请求处理完成后清除 MDC。 - 全链路追踪的关键: 生成全局唯一的 
traceId,并在服务之间传递。 - AOP 的优势: 简化 MDC 的操作,提高代码的可维护性。
 
希望今天的分享能够帮助大家更好地理解和应用全链路追踪技术。谢谢大家!