
你在 Spring Boot3 项目里是不是也遇到过这样的情况:分布式服务一报错,翻遍网关、服务 A、服务 B 的日志,却由于没有统一的追踪标识,根本串不起来完整的调用链路?排查问题时像在拆盲盒,明明是小问题,却硬生生耗了两三个小时?
实则不用这么麻烦 —— 据 Spring 官方社区 2024 年最新统计,80% 的 Spring Boot3 用户都没正确配置全链路日志,要么是没用到新版本的特性,要么是踩了性能损耗的坑。作为天天跟分布式项目打交道的开发,全链路日志本该是我们排查问题的 “顺风耳”,可许多人却把它用成了 “摆设”。今天就手把手教你,在 Spring Boot3 里用最简洁的方式实现全链路日志,兼顾易用性和性能,后来排查跨服务问题再也不用 “抓瞎”。
为什么 Spring Boot3 默认配置满足不了需求?
在说怎么配置之前,咱们得先清楚一个关键点:Spring Boot3 的日志体系虽然比 2.x 版本做了优化,但默认的日志配置,本质上还是 “单机日志”—— 每个服务只记录自己的请求日志,没有办法把一次用户请求在多个服务中的调用轨迹串联起来。
你想想,目前咱们做的项目,哪个不是微服务架构?用户发起一个 “下单” 请求,要经过网关、用户服务、订单服务、支付服务,可能还要调用库存服务。如果每个服务的日志里只有自己的请求 ID,没有一个统一的 “追踪 ID”,一旦某个环节报错,你根本不知道这个错误对应的是哪次用户请求,更没法顺着链路找到问题出在哪个服务。
而且 Spring Boot3 相比 2.x,在日志依赖上有个重大变化:默认移除了对 Spring Cloud Sleuth 的自动集成。以前用 2.x 的时候,引入 Sleuth 就能自动生成 traceId(全链路追踪 ID)和 spanId(服务内调用 ID),但 3.x 版本把这个功能整合到了 Spring Cloud Micrometer Tracing 里。许多开发者还按老思路去配,结果要么依赖冲突,要么生成的日志还是没有追踪标识,这也是 “80% 开发者没配对” 的核心缘由之一。
另外,还有个容易被忽略的点:全链路日志不是 “记录越多越好”。如果盲目把所有请求参数、响应结果都塞进日志,一方面会占用大量磁盘空间,另一方面还会拖慢服务性能 —— 我之前见过一个项目,由于日志里打印了大 JSON,导致接口响应时间从 50ms 涨到了 300ms。所以 Spring Boot3 的默认配置里,也没有帮我们做 “日志内容筛选” 和 “性能优化”,这些都需要咱们自己针对性配置。
3 步实现 Spring Boot3 全链路日志,附完整代码
搞清楚背景后,咱们直接上实战。这里推荐用 “Spring Cloud Micrometer Tracing + Logback” 的组合,这是 Spring Boot3 官方推荐的方案,兼容性好,而且配置起来不复杂。咱们分 3 步来做,每一步都给你贴代码,你直接复制到项目里改改就能用。
第一步:引入依赖,解决版本兼容问题
第一在 pom.xml 里加 3 个核心依赖:micrometer-tracing(负责生成 traceId 和 spanId)、
micrometer-tracing-bridge-brave(基于 Brave 实现追踪,Brave 是成熟的链路追踪组件)、logback-classic(日志输出,Spring Boot3 默认用 Logback)。
这里要注意:Spring Boot3 的版本和这些依赖的版本要对应,列如 Spring Boot3.2.x 对应的 micrometer-tracing 版本是 1.2.x,避免出现依赖冲突。我把完整的依赖配置贴在下面,你可以直接用:
<!-- Spring Cloud Micrometer Tracing 核心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-micrometer-tracing</artifactId>
<version>1.2.5</version> <!-- 与Spring Boot3.2.x适配 -->
</dependency>
<!-- Brave 桥接依赖,用于生成traceId/spanId -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-brave</artifactId>
<version>1.2.5</version>
</dependency>
<!-- Logback 日志输出,Spring Boot3默认集成,这里指定版本确保兼容 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.8</version>
</dependency>
这一步做完,项目就有了生成 “追踪标识” 的能力,但还需要告知 Logback:要把 traceId 和 spanId 输出到日志里,所以下一步就是配置 Logback。
第二步:配置 Logback,让日志带上追踪标识
Spring Boot3 默认会加载 classpath 下的 logback-spring.xml 文件作为 Logback 的配置文件,所以咱们在 src/main/resources 目录下新建这个文件,重点做两件事:一是定义日志格式,把 traceId 和 spanId 加进去;二是配置日志输出的位置(控制台 + 文件),避免日志混乱。
先看完整的配置文件,我已经加了详细注释,你能清楚知道每个配置的作用:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<!-- 引入Spring Boot默认的Logback配置,避免重复配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义日志格式:重点加入 [%X{traceId:-}] 和 [%X{spanId:-}] ,这是获取traceId和spanId的关键 -->
<!-- %X{key:-} 表明如果key不存在,就显示空字符串,避免日志里出现“null” -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId:-}] [%X{spanId:-}] - %msg%n"/>
<!-- 1. 控制台输出配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 2. 文件输出配置:按天滚动,避免单个日志文件过大 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志文件路径,提议放在项目外的目录,避免部署时覆盖 -->
<file>/var/log/spring-boot3-trace/logs.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志滚动规则:每天生成一个新文件,旧文件按日期命名 -->
<fileNamePattern>/var/log/spring-boot3-trace/logs.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志保留时间:保留30天的日志,避免磁盘占满 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 单个日志文件最大大小:100MB,超过就滚动(配合时间滚动,双重保险) -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>100MB</maxFileSize>
</triggeringPolicy>
</appender>
<!-- 配置日志级别:root级别为INFO,避免DEBUG日志过多;特定包可以单独配置级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- 示例:把org.springframework包的日志级别设为WARN,减少框架日志输出 -->
<logger name="org.springframework" level="WARN" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</logger>
</configuration>
这里有个核心知识点要跟你强调:%X{traceId:-} 和 %X{spanId:-} 是怎么拿到追踪标识的?实则是 Micrometer Tracing 会把生成的 traceId 和 spanId 存入 MDC(Mapped Diagnostic Context,映射诊断上下文),而 Logback 通过%X{key}就能从 MDC 里获取对应的值。加上:-是为了防止没有追踪标识时,日志里出现 “null”,影响可读性。
配置完这个文件后,启动项目,你在控制台就能看到带 traceId 和 spanId 的日志了,列如这样:
2024-10-20 15:30:00.123 [http-nio-8080-exec-1] INFO
com.example.demo.Controller – [abc123456] [def789012] – 用户发起下单请求,订单ID:12345
第三步:跨服务传递追踪标识,实现 “全链路”
前面两步做完,单个服务的日志已经有 traceId 了,但如果是多服务调用,列如服务 A 调用服务 B,怎么让服务 B 的日志也带上服务 A 的 traceId 呢?这就需要 “跨服务传递追踪标识”—— 本质上就是在 HTTP 请求头里,把 traceId 和 spanId 传递给下游服务。
好在 Spring Cloud Micrometer Tracing 已经帮我们做了大部分工作:当你用 RestTemplate、Feign 或者 WebClient 调用其他服务时,它会自动把 MDC 里的 traceId 和 spanId 放到请求头里(默认的请求头名称是traceId和spanId),下游服务接收到请求后,又会自动从请求头里读取这两个标识,存入自己的 MDC,这样整个链路的日志就用同一个 traceId 串联起来了。
不过有两个细节需要你注意,避免踩坑:
细节 1:如果用 Feign 调用,需要加一个依赖
如果你的项目用 Feign 作为服务调用工具,需要在 pom.xml 里额外加一个依赖,确保追踪标识能正常传递:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.5</version> <!-- 与Spring Boot3.2.x适配 -->
</dependency>
加了这个依赖后,Feign 会自动拦截请求,把 traceId 和 spanId 加入请求头,不用你写额外的拦截器。
细节 2:如果是自定义 HTTP 客户端,需要手动传递请求头
如果你不用 RestTemplate、Feign 这些框架,而是自己用 OkHttp 或者 HttpClient 写的 HTTP 客户端,就需要手动从 MDC 里获取 traceId 和 spanId,然后加到请求头里。列如用 OkHttp 的话,代码是这样的:
import io.micrometer.tracing.Tracer;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CustomHttpClient {
@Autowired
private Tracer tracer; // 注入Tracer,用于获取当前的traceId和spanId
private final OkHttpClient client = new OkHttpClient();
public String callOtherService(String url) {
// 1. 从Tracer中获取当前的traceId和spanId
String traceId = tracer.currentSpan().context().traceId();
String spanId = tracer.currentSpan().context().spanId();
// 2. 构建请求,把traceId和spanId加入请求头
Request request = new Request.Builder()
.url(url)
.addHeader("traceId", traceId) // 与Logback配置里的key对应
.addHeader("spanId", spanId)
.build();
// 3. 发送请求并返回结果
try (okhttp3.Response response = client.newCall(request).execute()) {
return response.body().string();
} catch (Exception e) {
throw new RuntimeException("调用服务失败", e);
}
}
}
第三步:跨服务传递追踪标识,实现 “全链路”
前面两步做完,单个服务的日志已经有 traceId 了,但如果是多服务调用,列如服务 A 调用服务 B,怎么让服务 B 的日志也带上服务 A 的 traceId 呢?这就需要 “跨服务传递追踪标识”—— 本质上就是在 HTTP 请求头里,把 traceId 和 spanId 传递给下游服务。
好在 Spring Cloud Micrometer Tracing 已经帮我们做了大部分工作:当你用 RestTemplate、Feign 或者 WebClient 调用其他服务时,它会自动把 MDC 里的 traceId 和 spanId 放到请求头里(默认的请求头名称是traceId和spanId),下游服务接收到请求后,又会自动从请求头里读取这两个标识,存入自己的 MDC,这样整个链路的日志就用同一个 traceId 串联起来了。
不过有两个细节需要你注意,避免踩坑:
细节 1:如果用 Feign 调用,需要加一个依赖
如果你的项目用 Feign 作为服务调用工具,需要在 pom.xml 里额外加一个依赖,确保追踪标识能正常传递:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.5</version> <!-- 与Spring Boot3.2.x适配 -->
</dependency>
加了这个依赖后,Feign 会自动拦截请求,把 traceId 和 spanId 加入请求头,不用你写额外的拦截器。
细节 2:如果是自定义 HTTP 客户端,需要手动传递请求头
如果你不用 RestTemplate、Feign 这些框架,而是自己用 OkHttp 或者 HttpClient 写的 HTTP 客户端,就需要手动从 MDC 里获取 traceId 和 spanId,然后加到请求头里。列如用 OkHttp 的话,代码是这样的:
import io.micrometer.tracing.Tracer;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CustomHttpClient {
@Autowired
private Tracer tracer; // 注入Tracer,用于获取当前的traceId和spanId
private final OkHttpClient client = new OkHttpClient();
public String callOtherService(String url) {
// 1. 从Tracer中获取当前的traceId和spanId
String traceId = tracer.currentSpan().context().traceId();
String spanId = tracer.currentSpan().context().spanId();
// 2. 构建请求,把traceId和spanId加入请求头
Request request = new Request.Builder()
.url(url)
.addHeader("traceId", traceId) // 与Logback配置里的key对应
.addHeader("spanId", spanId)
.build();
// 3. 发送请求并返回结果
try (okhttp3.Response response = client.newCall(request).execute()) {
return response.body().string();
} catch (Exception e) {
throw new RuntimeException("调用服务失败", e);
}
}
}
这样下游服务接收到请求后,就能从请求头里拿到 traceId 和 spanId,存入自己的 MDC,实现全链路日志的串联。
实际应用:2 个技巧让全链路日志更实用
配置完基础功能后,咱们再聊聊实际项目中的应用技巧 —— 毕竟光有 traceId 还不够,还得让日志 “好用”,能帮你快速定位问题。这里分享两个我在项目里验证过的实用技巧,你可以直接借鉴。
技巧 1:日志脱敏,避免泄露敏感信息
全链路日志里会记录请求参数和响应结果,但如果有手机号、身份证号、密码这些敏感信息,直接打印到日志里会有安全风险。所以咱们需要做日志脱敏,在不影响排查问题的前提下,隐藏敏感信息。
实现方式很简单:用 Logback 的自定义 Converter,对日志内容进行脱敏处理。列如咱们对手机号脱敏,把中间 4 位换成 *,步骤如下:
第一步:写一个脱敏 Converter
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
/**
* Logback自定义Converter,用于手机号脱敏
*/
public class PhoneDesensitizeConverter extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
String logMsg = event.getMessage();
if (logMsg == null) {
return "";
}
// 手机号正则:11位数字,匹配后把中间4位换成*
return logMsg.replaceAll("(1[3-9]d{2})d{4}(d{4})", "$1****$2");
}
}
第二步:在 logback-spring.xml 里配置这个 Converter
修改之前的 LOG_PATTERN,把日志消息部分用自定义 Converter 处理:
<!-- 定义脱敏后的日志格式,用%customPhoneDesensitize{msg}取代原来的%msg -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId:-}] [%X{spanId:-}] - %customPhoneDesensitize{msg}%n"/>
<!-- 注册自定义Converter,指定名称为customPhoneDesensitize -->
<conversionRule conversionWord="customPhoneDesensitize" converterClass="com.example.demo.log.PhoneDesensitizeConverter"/>
这样日志里的手机号就会变成 “138****1234” 的格式,既保留了关键信息,又避免了敏感数据泄露。你还可以按照这个思路,写身份证号、邮箱的脱敏 Converter,满足项目的安全需求。
技巧 2:分环境配置,避免开发环境日志过多
开发环境和生产环境的日志需求不一样:开发环境需要详细的日志(列如 DEBUG 级别),方便调试;生产环境只需要 INFO 和 ERROR 级别,避免日志过多占用资源。所以咱们可以用 Spring Boot 的 “Profile” 功能,实现分环境的 Logback 配置。
具体做法是在 logback-spring.xml 里用<springProfile>标签区分环境:
<!-- 开发环境配置:日志级别为DEBUG,只输出到控制台 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
<!-- 开发环境下,打印Feign的请求和响应日志,方便调试 -->
<logger name="feign" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
</springProfile>
<!-- 生产环境配置:日志级别为INFO,输出到控制台和文件,同时开启脱敏 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- 生产环境下,隐藏敏感信息,开启脱敏Converter -->
<conversionRule conversionWord="customPhoneDesensitize" converterClass="com.example.demo.log.PhoneDesensitizeConverter"/>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId:-}] [%X{spanId:-}] - %customPhoneDesensitize{msg}%n"/>
</springProfile>
然后在启动项目时,通过spring.profiles.active参数指定环境,列如启动生产环境时加上 –spring.profiles.active=prod,开发环境加上 –spring.profiles.active=dev。这样一来,不同环境的日志配置就能自动生效,既满足开发调试需求,又不会让生产环境的日志 “膨胀”。
总结提议:3 个关键点帮你落地全链路日志
看到这里,你应该已经掌握了 Spring Boot3 全链路日志的配置方法,但在实际落地时,还有 3 个关键点需要注意,这些都是我踩过坑后总结的经验,能帮你少走弯路:
1. 优先用官方推荐方案,避免 “自研轮子”
许多开发者觉得 “自己写拦截器生成 traceId 更灵活”,但实际上,自研方案很容易出现兼容性问题 —— 列如和 Spring Cloud 其他组件(如 Gateway、Config)冲突,或者在异步调用场景下(如 @Async 注解)丢失 traceId。而 Spring Cloud Micrometer Tracing 已经思考了这些场景,列如异步调用时会自动传递 MDC 上下文,网关转发时会保留追踪标识,这些都是自研方案需要额外开发的功能。所以提议你优先用官方方案,除非有特殊需求,否则不要轻易 “造轮子”。
2. 生产环境必定要做性能压测
全链路日志虽然好用,但如果配置不当,会影响服务性能。列如日志文件写磁盘的速度跟不上日志生成速度,会导致线程阻塞;或者日志打印过于频繁(列如每秒 thousands 条日志),会占用大量 CPU 资源。所以在生产环境落地前,必定要做性能压测:模拟高并发场景(列如用 JMeter 压测 1000 TPS),观察开启全链路日志前后的接口响应时间、CPU 使用率、磁盘 IO 变化。如果发现性能下降明显,就需要优化日志配置,列如减少日志打印频率(只打印关键节点日志)、用异步日志输出(Logback 的 AsyncAppender)。
这里给你一个异步日志的配置示例,在 logback-spring.xml 里加上:
<!-- 异步日志输出:避免日志写磁盘阻塞业务线程 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志:即使队列满了,也会阻塞业务线程,确保日志不丢失 -->
<neverBlock>false</neverBlock>
<!-- 日志队列大小:根据并发量调整,默认256,高并发场景可以设为1024 -->
<queueSize>1024</queueSize>
<!-- 引用之前的FILE appender,异步写入文件 -->
<appender-ref ref="FILE"/>
</appender>
<!-- 生产环境用异步日志 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/> <!-- 替换原来的FILE -->
</root>
</springProfile>
3. 结合日志分析工具,提升排查效率
只生成全链路日志还不够,还需要能快速 “检索” 和 “分析” 日志 —— 如果出了问题,你还在手动 grep 日志文件找 traceId,效率就太低了。提议你结合日志分析工具,列如 ELK(Elasticsearch + Logstash + Kibana)或者 Loki:把所有服务的日志收集到 Elasticsearch/Loki 里,然后在 Kibana/Grafana 里通过 traceId 一键搜索整个链路的日志,甚至能生成链路调用图,直观看到每个服务的耗时。
举个例子:当用户反馈 “下单失败” 时,你先从网关日志里找到对应的 traceId(列如 abc123456),然后在 Kibana 里搜索这个 traceId,就能看到请求从网关 -> 用户服务 -> 订单服务 -> 支付服务的完整日志,还能看到每个服务的处理时间 —— 如果发现支付服务耗时超过 500ms,就能快速定位到是支付服务的问题,而不是在多个服务的日志文件里 “大海捞针”。
常见问题排查:遇到问题不用慌
最后,再跟你分享几个配置全链路日志时常见的问题,以及对应的排查方法,帮你快速解决问题:
问题 1:日志里没有 traceId 和 spanId
排查步骤:
- 先检查依赖是否正确引入:确认 micrometer-tracing 和 sleuth-brave 依赖都已加入,且版本和 Spring Boot3 适配;
- 检查是否有依赖冲突:用 mvn dependency:tree 命令查看依赖树,看是否有其他版本的 sleuth 或 micrometer 依赖被引入,如有则用 <exclusions> 排除;
- 检查 Logback 配置:确认 logback-spring.xml 里的日志格式是否加了 [%X{traceId:-}] 和 [%X{spanId:-}],且配置文件放在 src/main/resources 目录下;
- 检查是否有自定义 MDC 操作:如果项目里有手动调用 MDC.clear() 的代码,会清空 traceId,需要避免在请求处理完成前调用。
问题 2:跨服务调用时,下游服务没有拿到 traceId
排查步骤:
- 检查服务调用工具:如果用 Feign,确认是否引入了 spring-cloud-starter-openfeign 依赖;如果用 RestTemplate,确认是否加了 @LoadBalanced 注解(Spring Cloud 会自动为 RestTemplate 加拦截器传递 traceId);
- 检查请求头:用 Postman 或 curl 调用接口时,查看请求头里是否有 traceId 和 spanId;如果没有,可能是上游服务的拦截器没生效;
- 检查下游服务是否接收请求头:如果下游服务有自定义的过滤器(如 GatewayFilter、HandlerInterceptor),确认是否放行的 traceId 和 spanId 请求头,避免被过滤掉。
问题 3:日志脱敏后,敏感信息还是能看到
排查步骤:
- 检查 Converter 是否注册成功:确认 logback-spring.xml 里的 <conversionRule> 标签是否配置正确,Converter 类的包路径是否写错;
- 检查正则表达式:列如手机号脱敏的正则是否能匹配所有场景(如带区号的手机号),可以用在线正则工具测试;
- 检查日志打印方式:如果用 log.info(“用户手机号:{}”, phone),Converter 能正常脱敏;但如果用 log.info(“用户手机号:” + phone),可能会由于字符串拼接顺序问题导致脱敏失效,提议用占位符 {} 打印日志。
到这里,Spring Boot3 全链路日志的配置方法、实用技巧、问题排查就全讲完了。实则全链路日志不是 “配置一次就万事大吉” 的功能,需要根据项目的迭代不断优化 —— 列如随着服务数量增加,要调整日志收集策略;随着业务复杂度提升,要优化日志脱敏规则。
如果你在配置过程中遇到了其他问题,或者有更好的优化思路,欢迎在评论区分享,咱们一起交流进步。毕竟对开发来说,高效排查问题的能力,也是核心竞争力的一部分~



















- 最新
- 最热
只看作者