Spring Boot 3.5 结构化日志实战:把 JSON 日志接入可观测性链路

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

人眼看没问题,但一旦进入检索和排查,就会遇到几个典型问题:

  • 想按 orderIduserIdtraceId 过滤,只能依赖正则或全文检索;
  • 不同服务的日志字段命名不一致,采集侧很难统一建索引;
  • 异常堆栈太长,可能把日志系统写爆;
  • 文本格式改一点,采集规则就可能失效;
  • 指标、调用链和日志无法稳定互相跳转。

结构化日志的核心不是"日志看起来更高级",而是把日志从一行文本变成可被机器稳定解析的字段集合。

复制代码
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 @timestamplog.levelservice.nameecs.version
Graylog Extended Log Format gelf 接入 Graylog versionshort_messagehost_level_name
Logstash JSON logstash 接入 Logstash / 通用 JSON 日志管道 @timestamplogger_namethread_namelevel

如果你不确定该选哪个,可以先按采集系统倒推:

  • 已经使用 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.namelog.levelprocess.pidecs.version 这类字段。官方文档说明:logging.structured.ecs.service.name 未指定时默认取 spring.application.nameservice.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。traceIdspanId 是否出现,取决于你的调用链方案是否把它们写进日志上下文。

常见做法有两种:

方案 做法 优点 注意点
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_idspan_id
  • 如果团队历史上已经使用 Spring / Sleuth 风格,可以继续使用 traceIdspanId,但采集侧要做映射;
  • 不要在不同服务里混用 traceIdtrace_idtidtraceID,否则跨服务检索会很痛苦。

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,或者希望每条日志都带上 clusterteamenv,就可以在应用侧先做统一。

复杂场景还可以实现 StructuredLoggingJsonMembersCustomizer,再通过 logging.structured.json.customizerMETA-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,例如 serviceenvlevel。像 orderIduserIdtraceId 这类高基数字段更适合作为日志内容字段,不适合都变成 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.xmllog4j2-spring.xml

这时只写:

复制代码
logging:
  structured:
    format:
      console: logstash

可能不会生效。

Spring Boot 官方文档给出的处理方式是:自定义 Logback 配置中替换 encoder,使用 StructuredLogEncoder,并读取 CONSOLE_LOG_STRUCTURED_FORMATFILE_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,不要每个服务自定义一套
字段命名 统一 serviceenvtraceId / trace_idspanId / span_id
MDC 生命周期 Web 请求结束必须清理,线程池异步任务要额外处理上下文传递
高基数字段 userIdorderIdtraceId 不要随便升成 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 专栏。