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 的区别
特性 | TraceId | MDC |
---|---|---|
作用范围 | 分布式系统中的完整请求链路 | 当前线程的日志上下文 |
生成方式 | 由分布式追踪框架生成 | 由开发者手动设置 |
使用场景 | 分布式链路追踪 | 日志上下文的动态附加 |
依赖框架 | 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 的传递,提高代码的复用性和可维护性。