spring+MDC+TrackId实现日志链路追踪

spring+MDC+TrackId实现日志链路追踪

 
 

MDC

MDC(Mapped Diagnostic Context,映射调试上下文)是Java中用于日志跟踪的一个工具,它主要被集成在日志框架中,如Log4j、Logback等。MDC提供了一种机制,允许开发者在多线程环境下关联和传递特定的上下文信息,这些信息对于日志的追踪、诊断和调试非常有用。

MDC 是日志框架(如 Logback、Log4j)提供的一种机制,用于在日志中动态添加上下文信息

  • 允许开发者在日志中附加与当前线程相关的上下文信息(如用户 ID、会话 ID、TraceId 等)。
  • 这些信息可以在日志输出时自动插入,无需手动在每条日志中重复添加。

 
 
实现:

  • 通过 MDC.put(key, value) 方法将上下文信息存储到当前线程的上下文中。
  • 在日志配置中使用 %X{key} 占位符输出 MDC 中的值。
  • 通常在请求开始时设置 MDC,在请求结束时清除 MDC。
     
     

优点:

MDC是基于线程本地变量(ThreadLocal)实现的,这意味着每个线程都有自己独立的MDC存储空间,线程之间不会相互干扰。

可维护:MDC中的信息可以被设置、获取和清除,这为日志的上下文管理提供了极大的灵活性。
可传递:在分布式系统中,MDC信息可以随着请求在多个服务或节点之间传递,帮助开发者追踪请求的完整处理流程。

 
 
 

TraceId

TraceId 是分布式系统中用于标识一个完整请求链路的唯一标识符。它贯穿整个调用链,帮助开发者追踪请求在不同服务之间的流转过程。

  • 在分布式系统中,每个请求会生成一个唯一的 TraceId。
  • 通过 TraceId,可以将一个请求在不同服务、不同模块中的日志串联起来,便于排查问题。

 
 
 

区别与结合

 

TraceId 和 MDC 的区别

特性TraceIdMDC
作用范围分布式系统中的完整请求链路当前线程的日志上下文
生成方式由分布式追踪框架生成由开发者手动设置
使用场景分布式链路追踪日志上下文的动态附加
依赖框架Zipkin、SkyWalking、Jaeger 等Logback、Log4j 等日志框架
输出方式显式输出到日志中通过占位符 %X{key} 输出
生命周期贯穿整个请求链路绑定到当前线程,请求结束时清除

 

结合使用

在实际项目中,TraceId 和 MDC 通常会结合使用:

  • 在请求开始时,生成 TraceId 并将其放入 MDC 中。
  • 在日志输出时,通过 MDC 自动附加 TraceId,方便日志聚合和追踪。
  • 在请求结束时,清除 MDC 中的 TraceId,避免内存泄漏。
// 请求开始时
String traceId = generateTraceId();
MDC.put("traceId", traceId);

// 日志输出
logger.info("Processing request...");

// 请求结束时
MDC.remove("traceId");

 

实现

添加依赖

确保项目中引入了日志框架(如 Logback 或 Log4j)和分布式追踪工具(如 Spring Cloud Sleuth 或自定义实现)。

如果使用 Spring Cloud Sleuth,可以直接引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

如果手动实现, 确保引入了日志框架依赖,例如 Logback:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>

配置日志格式

logback.xml 中配置日志输出格式,使用 %X{traceId} 占位符输出 MDC 中的 TraceId。

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

解析代码:

  • <configuration> 标签

    <configuration> 是 Logback 配置文件的根标签,用于包裹所有的配置内容

  • <appender> 标签

    <appender> 标签用于定义日志的输出方式。这里定义了一个名为 STDOUT 的 appender,它的实现类是 ch.qos.logback.core.ConsoleAppender,表示日志将输出到控制台。

  • <encoder> 标签

    <encoder> 标签用于定义日志的输出格式。这里使用了 <pattern> 标签来指定日志的格式。

  • <pattern> 的内容:

    • %d{yyyy-MM-dd HH:mm:ss}: 表示日期和时间,格式为 年-月-日 时:分:秒
    • [%thread]: 表示当前线程的名称,用方括号 [] 包裹。
    • [%X{traceId}]: 表示从 MDC(Mapped Diagnostic Context)中获取 traceId 的值,用方括号 [] 包裹。traceId 通常用于分布式系统中的请求跟踪。
    • %-5level: 表示日志级别(如 INFO, DEBUG, ERROR 等)。-5 表示左对齐并占用 5 个字符宽度,不足部分用空格填充。
    • %logger{36}: 表示日志记录器的名称。{36} 表示日志记录器的名称最多显示 36 个字符,如果超过则从左侧开始截断。
    • %msg: 表示日志消息内容。
    • %n: 表示换行符。
    eg:  2023-10-05 14:30:45 [main] [12345] INFO  com.example.MyClass - This is a log message
    
  • <root> 标签

<root> 标签用于配置根日志记录器,所有日志记录器都会继承它的配置。

  • <appender-ref ref="STDOUT" />: 引用之前定义的 STDOUT appender,表示日志将输出到控制台。

生成和传递 TraceId

方式 1:使用 Spring Cloud Sleuth

Spring Cloud Sleuth 会自动生成 TraceId 并将其放入 MDC 中。你只需要引入依赖,无需额外配置。

方式 2:手动实现

如果不想使用 Sleuth,可以手动生成 TraceId 并将其放入 MDC 中。可以通过以下方式实现:

  • 使用过滤器(Filter)
    在请求开始时生成 TraceId,并在请求结束时清除 MDC。
import org.slf4j.MDC;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

public class TraceIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 生成 TraceId
        String traceId = UUID.randomUUID().toString();
        // 将 TraceId 放入 MDC
        MDC.put("traceId", traceId);

        try {
            // 继续处理请求。将请求放行,将请求传递给下一个过滤器或目标资源。
            chain.doFilter(request, response);
        } finally {
            // 请求结束后清除 MDC
            MDC.remove("traceId");
        }
    }

    //过滤器的初始化方法,在过滤器启动时调用
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    //过滤器的销毁方法,在过滤器被销毁时调用
    @Override
    public void destroy() {
    }
}
  • 注册过滤器
    在 Spring Boot 中注册过滤器:
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<TraceIdFilter> traceIdFilter() {
        //FilterRegistrationBean 是 Spring Boot 提供的一个类,用于注册 Servlet 过滤器。
        FilterRegistrationBean<TraceIdFilter> registrationBean = new FilterRegistrationBean<>();
        //设置要注册的过滤器实例,这里使用的是 TraceIdFilter
        registrationBean.setFilter(new TraceIdFilter());
        //设置过滤器的 URL 匹配规则。/* 表示匹配所有请求
        //拦截所有 符合路径的 请求
        registrationBean.addUrlPatterns("/*");
        
        return registrationBean;
    }
}

 
 
 

在日志中使用 TraceId

在业务代码中,直接使用日志框架输出日志,MDC 中的 TraceId 会自动附加到日志中。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private static final Logger logger = LoggerFactory.getLogger(UserService.class);

    public void getUserById(String userId) {
        logger.info("Fetching user by ID: {}", userId);
        // 业务逻辑
    }
}

输出:

2023-10-10 12:34:56 [http-nio-8080-exec-1] [abc123] INFO  com.example.UserService - Fetching user by ID: 123

 
 
 

跨服务(分布式系统)传递 TraceId

在分布式系统中,需要将 TraceId 传递给下游服务。可以通过以下方式实现:

  • HTTP 请求头
    在发起 HTTP 请求时,将 TraceId 放入请求头中。
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.stereotype.Component;

import java.io.IOException;

@Component
public class TraceIdInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        // 从 MDC 中获取 TraceId
        String traceId = MDC.get("traceId");
        if (traceId != null) {
            // 将 TraceId 放入请求头
            request.getHeaders().add("X-Trace-Id", traceId);
        }
        //继续执行请求,并返回响应
        return execution.execute(request, body);
    }
}
  • 注册拦截器
    在 RestTemplate 中注册拦截器:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(TraceIdInterceptor traceIdInterceptor) {
        //创建一个 RestTemplate 实例,RestTemplate 是 Spring 提供的用于发起 HTTP 请求的工具类。
        RestTemplate restTemplate = new RestTemplate();
       
        //setInterceptors 方法用于设置 RestTemplate 的拦截器列表
       //Collections.singletonList(traceIdInterceptor) 将 TraceIdInterceptor 包装成一个单元素列表。 
           restTemplate.setInterceptors(Collections.singletonList(traceIdInterceptor));
        return restTemplate;
    }
}

为什么需要在 RestTemplate 中注册拦截器?

 

1.1 分布式系统中的链路追踪

在分布式系统中,一个请求可能经过多个服务。为了追踪完整的调用链路,需要将 TraceId 传递给下游服务。RestTemplate 是 Spring 中用于发起 HTTP 请求的工具,因此需要在 RestTemplate 中实现 TraceId 的传递。

 

1.2 拦截器的作用

拦截器(ClientHttpRequestInterceptor)可以在 RestTemplate 发起请求之前和之后执行自定义逻辑。通过拦截器,可以在请求发起前将 TraceId 添加到请求头中,确保下游服务能够获取到 TraceId。

 

1.3 避免手动重复代码

如果没有拦截器,每次发起 HTTP 请求时,都需要手动将 TraceId 添加到请求头中。这样会导致代码重复,且容易遗漏。通过拦截器,可以统一处理 TraceId 的传递,提高代码的复用性和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值