SpringBoot项目注入 traceId 来追踪整个请求的日志链路

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 :

# 我这里用的是 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中,你可以这样做:

<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:

<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 了。

 log.info("请求参数: {}", arg);

输出示例:

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 返回给前端。
我的代码如下:

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"));
    }
}

返回给前端的失败示例:

{
    "code": 405,
    "flag": false,
    "desc": "请求方式不支持.",
    "cause": "Request method 'POST' is not supported",
    "traceId": "ebd77e99-361c-47b3-8569-2d321d011418",
    "data": null
}

返回给前端的成功示例:

{
    "code": 0,
    "flag": true,
    "desc": "成功",
    "cause": null,
    "traceId": "07a3b99c-8cf8-45dc-a758-6c7636472cab",
    "data": {
        "count": 100,
        "name": "张三"
    }
}

扩展

打印日志内容,你很可能会涉及到 Spring 的 AOP 切面打印日志。可以参考这篇文章《Spring AOP 切面打印日志完整版


更新:

我们其实还可以通过配置文件参数来作为全局变量控制是否输出日志,一个小小的优化。

要实现这个功能,需要有下面3步:

  1. 添加Janino依赖到项目。
  2. 在logback-spring.xml 中引入logging.enable 属性。
  3. 使用条件判断包裹整个配置,根据属性值启用或禁用日志。

需要添加Janino的条件支持,通常在Logback配置文件中,条件处理需要引入Janino,并在configuration标签中声明

一、新增依赖(需支持条件判断)

Logback 的 条件判断需依赖 Janino 库。若项目中未引入,请在 pom.xml 中添加:

<dependency>
    <groupId>org.codehaus.janino</groupId> 
    <artifactId>janino</artifactId>
    <version>3.1.9</version>
</dependency>

二、修改 logback-spring.xml

  1. 在 application.yml 中配置控制参数:
logging:
  # 支持环境变量覆盖
  outputEnable: ${OUTPUT_ENABLE:true}

2. 读取 logging.outputEnable 属性
在 标签内新增 springProperty,读取配置文件中的自定义属性:

<springProperty scope="context" name="loggingEnable" source="logging.outputEnable"  defaultValue="false"/>

2. 通过条件判断控制日志输出
用 标签包裹所有日志配置,当 logging.outputEnable=true 时启用日志,否则关闭:

<configuration debug="true">

    <!-- 新增属性读取 -->
    <springProperty scope="context" name="loggingEnable" source="logging.outputEnable"  defaultValue="true"/>
    <!-- 条件判断 -->
    <if condition='property("loggingEnable").equals("true")'>
    <then>
        <!-- 引用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.tyelrzhong" 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>
    </then>
        <else>
            <!-- 当 logging.enable=false  时,关闭所有日志 -->
            <root level="OFF"/>
        </else>
    </if>
</configuration>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值