Spring Boot 3.5 结构化日志实战:把 JSON 日志接入可观测性链路
摘要
Spring Boot 3.5 已经在官方 logging 参考文档中提供开箱即用的结构化日志能力,可以直接输出 ECS、GELF 或 Logstash 风格的 JSON 日志。对 Java 后端来说,这比继续堆 %d %level %logger %msg 文本格式更适合接入 ELK、Loki、Graylog 或 OpenTelemetry Collector 这类可观测性链路。
本文用一个 Spring Boot 3.5 示例说明:如何开启 JSON 日志、如何把 MDC / SLF4J key-value 写进日志字段、如何规划 traceId / spanId 关联、以及接入日志采集系统时最容易踩的字段和堆栈边界。
验证说明:本文配置依据 Spring Boot 3.5 官方 Logging 参考文档整理。当前执行环境没有
java/mvn,未进行本机编译运行;代码和日志为工程骨架与官方文档格式示例,读者落地时请以自己项目依赖版本和实际启动结果为准验证。
1. 为什么要把文本日志换成 JSON 日志?
很多线上 Java 服务的日志还是类似下面这种格式:
2026-06-21 10:15:30.123 INFO 12345 --- [nio-8080-exec-1] c.e.demo.OrderController : create order success, orderId=1001, userId=42
人眼看没问题,但一旦进入检索和排查,就会遇到几个典型问题:
- 想按
orderId、userId、traceId过滤,只能依赖正则或全文检索; - 不同服务的日志字段命名不一致,采集侧很难统一建索引;
- 异常堆栈太长,可能把日志系统写爆;
- 文本格式改一点,采集规则就可能失效;
- 指标、调用链和日志无法稳定互相跳转。
结构化日志的核心不是"日志看起来更高级",而是把日志从一行文本变成可被机器稳定解析的字段集合。
HTTP 请求
-> Spring Boot 应用
-> JSON 结构化日志
-> Collector / Log Agent
-> Elasticsearch / Loki / Graylog / APM 平台
-> 按 service、level、traceId、orderId 等字段检索
2. Spring Boot 3.5 支持哪些结构化格式?
Spring Boot 3.5 的官方 logging 文档中,结构化日志内置支持 3 类 JSON 格式:
| 格式 | 配置值 | 更适合的场景 | 典型字段特点 |
|---|---|---|---|
| Elastic Common Schema | ecs |
接入 Elasticsearch / Elastic Stack | @timestamp、log.level、service.name、ecs.version |
| Graylog Extended Log Format | gelf |
接入 Graylog | version、short_message、host、_level_name |
| Logstash JSON | logstash |
接入 Logstash / 通用 JSON 日志管道 | @timestamp、logger_name、thread_name、level |
如果你不确定该选哪个,可以先按采集系统倒推:
- 已经使用 Elastic Stack:优先考虑
ecs; - 已经使用 Graylog:优先考虑
gelf; - 只是希望日志变成通用 JSON,被 Filebeat、Fluent Bit、Vector、Loki agent 或自研采集器读取:可以先用
logstash,再在采集侧做字段映射。
3. 最小依赖:先准备一个 Spring Boot 3.5 服务
示例以 Maven 项目为例,核心依赖不复杂:
<properties>
<java.version>17</java.version>
<spring-boot.version>3.5.0</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
说明:
spring-boot-starter-web用于提供示例接口;spring-boot-starter-actuator不是结构化日志的必需依赖,但通常可观测性建设会一起接入健康检查、指标和管理端点;- 如果项目已经有自定义 Logback / Log4j2 配置,不能只改
application.yml,还要让自定义配置尊重 Spring Boot 提供的结构化日志系统属性,后面会单独说。
4. application.yml:开启 JSON 控制台日志和文件日志
最小配置如下:
spring:
application:
name: order-service
logging:
structured:
format:
console: logstash
file: logstash
file:
name: logs/order-service.log
对应 properties 写法是:
spring.application.name=order-service
logging.structured.format.console=logstash
logging.structured.format.file=logstash
logging.file.name=logs/order-service.log
启动后,控制台和文件日志会按 Logstash JSON 格式输出。官方文档给出的 Logstash 格式示例大致如下:
{"@timestamp":"2024-01-01T10:15:00.111037681+02:00","@version":"1","message":"No active profile set, falling back to 1 default profile: \"default\"","logger_name":"org.example.Application","thread_name":"main","level":"INFO","level_value":20000}
如果你希望接 Elastic ECS,可以改成:
logging:
structured:
format:
console: ecs
file: ecs
ecs:
service:
name: order-service
version: 1.0.0
environment: prod
node-name: node-a
ECS 格式会更强调 service.name、log.level、process.pid、ecs.version 这类字段。官方文档说明:logging.structured.ecs.service.name 未指定时默认取 spring.application.name,service.version 未指定时默认取 spring.application.version。
5. 写入业务字段:MDC 和 SLF4J fluent API
结构化日志真正有价值的地方,是把业务字段写进去。
5.1 用 MDC 写入请求级上下文
java
`package com.example.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
@PostMapping("/orders")
public String createOrder(@RequestParam String userId) {
MDC.put("userId", userId);
try {
String orderId = "ORD-1001";
MDC.put("orderId", orderId);
log.info("create order success");
return orderId;
} finally {
MDC.clear();
}
}
}
`
Spring Boot 官方文档说明,结构化日志会把 MDC 中的 key-value 加入 JSON 对象。输出可能类似:
{
"@timestamp": "2026-06-21T10:15:30.123+08:00",
"@version": "1",
"message": "create order success",
"logger_name": "com.example.demo.OrderController",
"thread_name": "http-nio-8080-exec-1",
"level": "INFO",
"level_value": 20000,
"userId": "42",
"orderId": "ORD-1001"
}
5.2 用 SLF4J fluent API 写入临时字段
如果字段只属于某一次日志,不适合放到 MDC,也可以用 SLF4J fluent logging API:
java
`log.atInfo()
.addKeyValue("orderId", orderId)
.addKeyValue("amount", 19900)
.addKeyValue("currency", "CNY")
.log("order payment created");
`
这种写法的好处是字段和日志语句绑定,不会因为线程复用或 MDC.clear() 遗漏造成串字段。
6. traceId / spanId 怎么处理?
结构化日志本身只负责把日志写成 JSON。traceId、spanId 是否出现,取决于你的调用链方案是否把它们写进日志上下文。
常见做法有两种:
| 方案 | 做法 | 优点 | 注意点 |
|---|---|---|---|
| Micrometer Tracing / Spring Boot Observability | 通过 tracing 依赖和日志上下文自动关联 | Spring 生态集成度高 | 需要按项目版本确认 bridge 和 exporter 依赖 |
| OpenTelemetry Java Agent | 通过 -javaagent 自动插桩,并配置日志关联 |
侵入低,适合老项目 | 字段名可能是 trace_id / span_id,要和日志平台字段统一 |
如果你已经在项目里把 trace 信息放进 MDC,JSON 日志会携带这些字段。例如:
{
"message": "create order success",
"level": "INFO",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"orderId": "ORD-1001"
}
落地时建议先统一字段名:
- 如果日志平台使用 OpenTelemetry 习惯,考虑统一为
trace_id、span_id; - 如果团队历史上已经使用 Spring / Sleuth 风格,可以继续使用
traceId、spanId,但采集侧要做映射; - 不要在不同服务里混用
traceId、trace_id、tid、traceID,否则跨服务检索会很痛苦。
7. 自定义字段:include / exclude / rename / add
Spring Boot 3.5 文档提供了几类 JSON 字段调整配置:
| 配置 | 作用 | 示例用途 |
|---|---|---|
logging.structured.json.include |
只包含指定路径 | 控制日志字段白名单 |
logging.structured.json.exclude |
排除指定路径 | 去掉不需要索引的字段 |
logging.structured.json.rename |
重命名字段 | 适配已有采集字段规范 |
logging.structured.json.add |
增加固定字段 | 增加公司、集群、环境等静态字段 |
示例:
logging:
structured:
json:
exclude: log.level
rename:
process.id: procid
add:
cluster: prod-a
team: payment
这类配置非常适合做字段治理。比如你们平台要求进来的字段必须叫 service 而不是 service.name,或者希望每条日志都带上 cluster、team、env,就可以在应用侧先做统一。
复杂场景还可以实现 StructuredLoggingJsonMembersCustomizer,再通过 logging.structured.json.customizer 或 META-INF/spring.factories 注册。不过这一步要谨慎:日志字段一旦被多个自定义器修改,排查成本会上升,最好配套单元测试或启动日志样例校验。
8. 异常堆栈:不要让 JSON 日志拖垮采集链路
结构化日志会把异常堆栈也写进 JSON。问题是,堆栈往往很长:一次连接超时、一次反序列化异常、一次循环重试,可能就把单条日志撑到几十 KB。
Spring Boot 文档提供了结构化堆栈相关配置,例如:
logging:
structured:
json:
stacktrace:
root: first
max-length: 8192
max-throwable-depth: 30
include-common-frames: false
include-hashes: true
建议:
- 普通业务异常:限制堆栈长度,避免日志系统写入压力过大;
- 核心链路异常:保留足够堆栈,但结合采样、告警和 trace 排查;
- 重试循环异常:不要每次重试都打印完整堆栈,可以首尾打印完整,中间打印摘要;
- 采集系统有单条日志大小限制时,应用侧必须先限制。
9. 接入采集链路:从本地日志到 ELK / Loki / Collector
结构化日志通常有两种采集方式。
9.1 文件采集
应用写文件:
logging:
structured:
format:
file: logstash
file:
name: /var/log/apps/order-service/app.log
采集器读取文件并按 JSON 解析。以 Fluent Bit 思路为例:
[INPUT]
Name tail
Path /var/log/apps/order-service/app.log
Parser json
Tag order-service
[OUTPUT]
Name stdout
Match *
如果接入 Loki,常见做法是只把少量字段变成 label,例如 service、env、level。像 orderId、userId、traceId 这类高基数字段更适合作为日志内容字段,不适合都变成 label。
9.2 控制台采集
容器化环境里,很多团队让应用只写标准输出:
logging:
structured:
format:
console: ecs
然后由容器运行时、DaemonSet 或日志 Agent 统一采集标准输出。这种方式运维简单,但要确认:
- 平台不会把 JSON 再包一层导致字段变成字符串;
- 多行异常堆栈不会破坏 JSON 解析;
- stdout 采集延迟和丢失策略符合业务要求;
- 日志级别、采样和保留周期有明确规则。
10. 本地验证命令
如果你在自己的机器上有 JDK 和 Maven,可以按下面步骤验证。
10.1 启动应用
mvn spring-boot:run
或者打包后运行:
mvn -DskipTests package
java -jar target/demo-0.0.1-SNAPSHOT.jar
10.2 调用接口
curl -X POST 'http://localhost:8080/orders?userId=42'
10.3 查看 JSON 是否可解析
如果日志写到文件:
tail -n 1 logs/order-service.log | jq .
重点检查:
tail -n 20 logs/order-service.log \
| jq -r 'select(.level == "INFO") | {time: ."@timestamp", msg: .message, orderId: .orderId, userId: .userId}'
预期至少能看到:
{
"time": "2026-06-21T10:15:30.123+08:00",
"msg": "create order success",
"orderId": "ORD-1001",
"userId": "42"
}
如果 jq 报 JSON 解析错误,优先检查是否仍在输出旧文本日志,或者自定义 Logback / Log4j2 配置没有接入 Spring Boot 的结构化日志 encoder / layout。
11. 自定义 Logback / Log4j2 的坑
很多生产项目不会完全使用 Spring Boot 默认日志配置,而是有自己的 logback-spring.xml 或 log4j2-spring.xml。
这时只写:
logging:
structured:
format:
console: logstash
可能不会生效。
Spring Boot 官方文档给出的处理方式是:自定义 Logback 配置中替换 encoder,使用 StructuredLogEncoder,并读取 CONSOLE_LOG_STRUCTURED_FORMAT 或 FILE_LOG_STRUCTURED_FORMAT 系统属性。
Logback 示例骨架:
<encoder class="org.springframework.boot.logging.logback.StructuredLogEncoder">
<format>${CONSOLE_LOG_STRUCTURED_FORMAT}</format>
<charset>${CONSOLE_LOG_CHARSET}</charset>
</encoder>
Log4j2 则需要使用结构化 layout,并读取对应系统属性。这里不要照搬旧的 PatternLayout,否则应用配置里写了 logging.structured.*,最终仍然输出文本。
12. 生产落地清单
| 检查项 | 建议 |
|---|---|
| 日志格式 | 先按采集平台选 ecs / gelf / logstash,不要每个服务自定义一套 |
| 字段命名 | 统一 service、env、traceId / trace_id、spanId / span_id |
| MDC 生命周期 | Web 请求结束必须清理,线程池异步任务要额外处理上下文传递 |
| 高基数字段 | userId、orderId、traceId 不要随便升成 Loki label 或 ES 高成本索引 |
| 异常堆栈 | 限制长度和深度,避免单条日志过大 |
| 敏感字段 | 手机号、身份证、Token、Cookie、地址等字段不要直接入日志 |
| 采集解析 | 上线前用 jq、Logstash pipeline 或 Collector 测试 JSON 是否被正确解析 |
| 版本差异 | Spring Boot、Logback、Log4j2、采集器版本要一起记录,避免升级后字段变化 |
13. 常见问题
13.1 配了 structured logging,为什么还是文本日志?
优先看是否存在自定义 logback-spring.xml / log4j2-spring.xml。如果有,自定义配置可能覆盖了 Spring Boot 默认 appender,需要改成结构化 encoder / layout。
13.2 JSON 日志里没有 traceId 怎么办?
先确认项目是否真的接入了 tracing,并且 trace 信息是否写入 MDC 或日志上下文。结构化日志不会凭空生成 traceId,它只会把已有上下文字段结构化输出。
13.3 所有业务字段都应该放进日志吗?
不应该。日志字段越多,索引、存储和合规成本越高。建议只保留排查必需字段:请求 ID、trace 信息、核心业务 ID、错误码、关键状态和耗时。敏感字段要脱敏或不写。
13.4 结构化日志能替代指标和 Trace 吗?
不能。日志适合解释"发生了什么";指标适合看趋势和告警;Trace 适合看一次请求经过了哪些环节。结构化日志的价值是让日志更容易和指标、Trace 串起来,而不是替代它们。
14. 总结
Spring Boot 3.5 的结构化日志能力,让 Java 后端不必额外引入一堆日志 JSON encoder,就能把控制台或文件日志输出成 ECS、GELF 或 Logstash 格式。真正落地时,重点不只是把日志改成 JSON,而是统一字段、控制堆栈、处理 MDC 生命周期,并把日志接入现有可观测性链路。
如果你的项目已经在做 Spring Boot 可观测性建设,建议按这个顺序推进:先统一 JSON 日志格式,再规范 traceId / 业务 ID 字段,最后接入采集、检索和告警。这样日志、指标和调用链才能真正服务线上排查,而不是各自孤立。
如果你关注 Java 后端、Spring Boot 升级、可观测性和线上问题排查,可以关注我的 CSDN 专栏。