SpringBoot项目注入 traceId 来追踪整个请求的日志链路,有了 traceId, 我们在排查问题的时候,可以迅速根据 traceId 查找到相关请求的日志,特别是在生产环境的时候,用户可能只提供一个错误截图,我们作为开发人员想要更详细的日志,这时候我们就可以直接根据用户提供的 traceId 来查找用户这次请求的整个日志,会方便很多。
springboot 单体项目没办法做到有 traceId 一样的效果吗?
即使是在单体应用中,为请求添加一个唯一的traceId以追踪整个请求链路的日志也是完全可行的。虽然Spring Cloud Sleuth等工具主要用于分布式系统中的链路追踪,但在单体Spring Boot应用中你仍然可以通过一些方法来实现类似的功能。下面是一个基本的方法来手动实现这样的功能:
手动实现 traceId 注入
1. 创建一个过滤器或拦截器:你可以创建一个Servlet过滤器或者Spring的拦截器,用于在每个请求到达时生成一个唯一的traceId(如果不存在的话),并将其存储到当前线程的上下文中。
2. 使用MDC(Mapped Diagnostic Context):SLF4J提供了MDC工具,可以用来保存日志的上下文信息。你可以在过滤器或拦截器中将traceId放入MDC中,这样所有的日志记录器都可以自动包含这个ID。 示例代码:
TraceIdFilter :
bash
# 我这里用的是 jdk17, 所以 servlet 工具类被移动到了 jakarta 包下,如果你用的是比较老的 jdk, 可以删除后重新导入 servlet 的类。
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Compone
nt
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String traceId = request.getHeader(TRACE_ID);
if (traceId == null) {
traceId = UUID.randomUUID().toString();
}
MDC.put(TRACE_ID, traceId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
}
}
3. 配置日志格式:确保你的日志配置文件(如logback.xml、log4j2.xml等)中包含了输出traceId的配置。例如,在logback中,你可以这样做:
xml
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - [%thread] %-5level %logger{36} - [%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
我的项目使用的是 logback-spring.xml ,我的文件完整内容如下:
logback-spring.xml:
xml
<configuration debug="true">
<!-- 引用Spring Boot全局配置文件中的日期格式配置项 -->
<springProperty scope="context" name="dateformat" source="logging.pattern.dateformat" defaultValue="yyyy-MM-dd HH:mm:ss.SSS"/>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<springProperty scope="context" name="LOG_FILE" source="logging.file.path"/>
<!-- 定义颜色编码 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<!-- 定义日志输出到控制台的appender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%clr(%d{${dateformat}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([%15.15t]){faint} [%-40.40logger{39}] [%X{traceId}] %clr(%msg%n){faint}%wEx</pattern>
</encoder>
</appender>
<!-- 开发环境配置 -->
<springProfile name="local | test">
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
<!-- 开发环境专属的详细日志 -->
<logger name="com.tylerzhong" level="TRACE" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
</springProfile>
<springProfile name="dev | prod">
<!-- 文件日志:输出全部日志到文件 -->
<appender name="FILE_INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}/${APP_NAME}.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>180</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{${dateformat}} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 错误日志:用于将错误日志输出到独立文件 -->
<appender name="FILE_ERROR_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}/error-${APP_NAME}.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}/error-${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>180</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{${dateformat}} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE_INFO_LOG" />
<appender-ref ref="FILE_ERROR_LOG" />
</root>
</springProfile>
</configuration>
关于更具体的日志输出配置,可以参考我的这篇文章《SpringBoot 项目配置日志输出》。
通过这种方式,你就可以在单体Spring Boot项目中为每个请求生成一个唯一的traceId,并通过日志输出它,从而实现在处理请求期间的所有日志条目都能关联起来的效果。这种方法不仅适用于单体应用,也可以作为理解更复杂的分布式追踪概念的基础。
这样配置之后,你在代码中打印日志的时候就会携带上 traceId 了。
java
log.info("请求参数: {}", arg);
输出示例:
java
2025-02-26 19:25:02.187 INFO [nio-8070-exec-2] [com.tylerzhong.web.config.LoggingAspect ] [ab85097a-6d31-44c3-bcfb-2bcc97b87ab9] 方法: TransferController.xxx(..)
当然,我们不止要将 traceId 输出到控制台,还需要将 traceId 返回给前端用户,这样用户找我们排查问题的时候,只需要给一个 traceId 给我们即可。
所以在返回的包装类中注入 traceId 返回给前端。
我的代码如下:
java
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.slf4j.MDC;
@Getter
@NoArgsConstructor
public class Result<T> {
private int code;
private boolean flag;
private String desc;
private String cause;
private String traceId;
private T data;
public Result(int code, boolean flag, String desc) {
this.code = code;
this.flag = flag;
this.desc = desc;
}
public Result(int code, boolean flag, String desc, String traceId) {
this.code = code;
this.flag = flag;
this.desc = desc;
this.traceId = traceId;
}
public Result(int code, boolean flag, String desc, String cause, String traceId) {
this.code = code;
this.flag = flag;
this.desc = desc;
this.cause = cause;
this.traceId = traceId;
}
public Result(int code, boolean flag, String desc, T data, String traceId) {
this.code = code;
this.flag = flag;
this.desc = desc;
this.data = data;
this.traceId = traceId;
}
public static <T> Result<T> success() {
return new Result<>(0, true, "成功", MDC.get("traceId"));
}
public static <T> Result<T> success(T data) {
return new Result<>(0, true,"成功", data, MDC.get("traceId"));
}
public static <T> Result<T> fail(int code, String desc, String cause) {
return new Result<>(code, false, desc, cause, MDC.get("traceId"));
}
}
返回给前端的失败示例:
json
{
"code": 405,
"flag": false,
"desc": "请求方式不支持.",
"cause": "Request method 'POST' is not supported",
"traceId": "ebd77e99-361c-47b3-8569-2d321d011418",
"data": null
}
返回给前端的成功示例:
json
{
"code": 0,
"flag": true,
"desc": "成功",
"cause": null,
"traceId": "07a3b99c-8cf8-45dc-a758-6c7636472cab",
"data": {
"count": 100,
"name": "张三"
}
}