JAVA 如何用 Spring Boot 实现全链路请求日志追踪?MDC 的正确用法

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() 方法将 userIdrequestId 添加到 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:ServiceAControllerServiceBController

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,查看控制台输出的日志。你会发现 ServiceAServiceB 的日志都包含了相同的 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 的操作,提高代码的可维护性。

希望今天的分享能够帮助大家更好地理解和应用全链路追踪技术。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注