Spring Boot 中的结构化日志

1、概览

日志是任何软件应用程序的基本功能。它通过记录错误、警告和其他事件,帮助跟踪应用程序在运行期间的行为。

默认情况下,Spring Boot 应用程序会生成非结构化、人类可读的日志。虽然这些日志对开发人员很有用,但它们不容易被日志聚合工具解析或分析。结构化日志解决了这一限制。

本文将带你了解如何使用 Spring Boot 3.4.0 版中引入的功能实现结构化日志。

2、Maven 依赖

首先,在 pom.xml 中添加 spring-boot-starter 来启动 Spring Boot 项目:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>

上述依赖为 Spring Boot 应用中的自动配置和日志记录提供了支持。

3、Spring Boot 默认的日志

以下是 Spring Boot 的默认日志:

INFO 22059 --- [ main] c.b.s.StructuredLoggingApp  : No active profile set, falling back to 1 default profile: "default"
INFO 22059 --- [ main] c.b.s.StructuredLoggingApp   : Started StructuredLoggingApp in 2.349 seconds (process running for 3.259)

虽然这些日志信息量很大,但却无法被 Elasticsearch 等工具轻松抓取或进行指标分析。JSON 等结构化日志格式通过标准化日志内容解决了这一问题。

4、配置

Spring Boot 3.4.0 版开始,内置了结构化日志,并支持 Elastic Common Schema (ECS)Graylog Extended Log Format (GELF)Logstash JSON 等格式。

我们可以直接在 application.properties 文件中配置结构化的日志格式。

4.1、Elastic Common Schema

Elastic Common Schema(ECS) 是一种基于 JSON 的标准化日志格式,可无缝集成 ElasticsearchKibana。要在应用程序中配置 ECS,需要在 application.properties 文件中添加它的属性:

logging.structured.format.console=ecs

下面是一个输出示例:

{
  "@timestamp": "2024-12-19T01:17:47.195098997Z",
  "log.level": "INFO",
  "process.pid": 16623,
  "process.thread.name": "main",
  "log.logger": "com.baeldung.springstructuredlogging.StructuredLoggingApp",
  "message": "Started StructuredLoggingApp in 3.15 seconds (process running for 4.526)",
  "ecs.version": "8.11"
}

输出包含键值对,可在 Elasticsearch 和 Kibana 中被轻松解析。

此外,我们还可以通过添加服务名称、环境和节点名称等字段来增强 ECS 日志的可观察性:

# 服务名称
logging.structured.ecs.service.name=MyService
# 版本号
logging.structured.ecs.service.version=1
# 环境
logging.structured.ecs.service.environment=Production
# 节点名称
logging.structured.ecs.service.node-name=Primary

新的输出如下:

{
  "@timestamp": "2024-12-19T01:25:15.123108416Z",
  "log.level": "INFO",
  "process.pid": 18763,
  "process.thread.name": "main",
  "service.name": "BaeldungService",
  "service.version": "1",
  "service.environment": "Production",
  "service.node.name": "Primary",
  "log.logger": "com.baeldung.springstructuredlogging.StructuredLoggingApp",
  "message": "Started StructuredLoggingApp in 3.378 seconds (process running for 4.376)",
  "ecs.version": "8.11"
}

输出的结果包含了我们在 application.properties 文件中定义信息。

4.2、Graylog 扩展日志格式

Graylog Extend Log Format (GELF) 是另一种受支持的基于 JSON 的结构化日志格式。

application.properties 文件中启用它:

logging.structured.format.console=gelf

GELF 格式与 ECS 格式相似,但属性名称不同:

{
  "version": "1.1",
  "short_message": "Started StructuredLoggingApp in 2.77 seconds (process running for 3.89)",
  "timestamp": 1734572549.172,
  "level": 6,
  "_level_name": "INFO",
  "_process_pid": 23929,
  "_process_thread_name": "main",
  "_log_logger": "com.baeldung.springstructuredlogging.StructuredLoggingApp"
}

与 ECS 配置一样,我们可以通过在 application.properties 文件中定义主机和服务版本来进一步增强输出:

# 主机 HOST
logging.structured.gelf.host=MyService
# 服务版本号
logging.structured.gelf.service.version=1

这将通过添加主机和服务键值对来扩展日志。

4.3、Logstash 格式

开箱即用的 Logstash 格式也受支持。要按照这种格式构建日志,需要在 application.properties 中指定它:

logging.structured.format.file=logstash

输出的示例日志如下:

{
  "@timestamp": "2024-12-19T02:49:33.017851728+01:00",
  "@version": "1",
  "message": "Started StructuredLoggingApp in 2.749 seconds (process running for 3.605)",
  "logger_name": "com.baeldung.springstructuredlogging.StructuredLoggingApp",
  "thread_name": "main",
  "level": "INFO",
  "level_value": 20000
}

使用支持 Logstash 格式的日志聚合器(Log Aggregation)可以轻松分析上述格式。

4.4、其他信息

我们可以使用 Mapped Diagnostic Context (MDC) 类为结构化日志添加更多信息。例如,我们可以在日志中添加 userId,以便根据 userId 过滤日志:

private static final Logger LOGGER = LoggerFactory.getLogger(CustomLog.class);
public void additionalDetailsWithMdc() {
    MDC.put("userId", "1");
    MDC.put("userName", "Baeldung");
    LOGGER.info("Hello structured logging!");
    MDC.remove("userId");
    MDC.remove("userName");
}

在上述代码中,我们会在日志输出后清理 MDC 上下文,以防止内存泄漏。

包含用户详细信息的日志输出如下:

{
  "@timestamp": "2024-12-19T07:52:30.556819106+01:00",
  "@version": "1",
  "message": "Hello structured logging!",
  "logger_name": "com.baeldung.springstructuredlogging.CustomLog",
  "thread_name": "main",
  "level": "INFO",
  "level_value": 20000,
  "userId": "1",
  "userName": "Baeldung"
}

如上,我们为日志添加了更多信息。我们可以轻松地根据 userId 过滤日志。我们可以使用 MDC 类为日志添加更多属性。

此外,还可以使用 Fluent 风格的日志 API 来实现类似的目的:

public void additionalDetailsUsingFluentApi() {
    LOGGER.atInfo()
      .setMessage("Hello Structure logging!")
      .addKeyValue("userId", "1")
      .addKeyValue("userName", "Baeldung")
      .log();
}

这种方法更简洁,而且能自动处理上下文清理,减少出错的可能性。

4.5、自定义日志格式

此外,我们还可以定义自己的自定义结构日志格式,并在 application.properties 中加以使用。这在支持的日志格式不符合我们的使用情况时可能会很有用。

首先,我们需要实现 StructuredLogFormatter 接口,并覆写其 format() 方法:

class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {
    @Override
    public String format(ILoggingEvent event) {
       return "time=" + event.getTimeStamp() + " level=" + event.getLevel() + " message=" + event.getMessage() + "\n";
    }
}

如上,我们的自定义格式是文本格式,而不是标准的 JSON 格式。这为我们提供了灵活性,我们可以根据任何格式(JSON、XML 等)来构建日志。

然后,在 application.properties 中定义自定义配置:

logging.structured.format.console=com.baeldung.springstructuredlogging.MyStructuredLoggingFormatter

如上,我们定义了 MyStructuredLoggingFormatter 的全路径类名。

其日志输出如下:

time=1734598194538 level=INFO message=Hello structured logging!

输出为文本格式,键和值对代表日志详细信息。

如果支持的格式不适合我们的需求,自定义格式可能会更有优势。

此外,我们还可以使用 JSONWriter 编写自定义格式的 JSON:

private final JsonWriter<ILoggingEvent> writer = JsonWriter.<ILoggingEvent>of((members) -> {
    members.add("time", ILoggingEvent::getInstant);
    members.add("level", ILoggingEvent::getLevel);
    members.add("thread", ILoggingEvent::getThreadName);
    members.add("message", ILoggingEvent::getFormattedMessage);
    members.add("application").usingMembers((application) -> {
        application.add("name", "StructuredLoggingDemo");
        application.add("version", "1.0.0-SNAPSHOT");
    });
    members.add("node").usingMembers((node) -> {
        node.add("hostname", "node-1");
        node.add("ip", "10.0.0.7");
    });
}).withNewLineAtEnd();

接下来,将 writer() 方法集成到 format() 方法中:

@Override
public String format(ILoggingEvent event) {
    return this.writer.writeToString(event);
}

输出的日志是 JSON 格式:

{
  "time": "2024-12-19T08:55:13.284101533Z",
  "level": "INFO",
  "thread": "main",
  "message": "No active profile set, falling back to 1 default profile: \"default\"",
  "application": {
    "name": "StructuredLoggingDemo",
    "version": "1.0.0-SNAPSHOT"
  },
  "node": {
    "hostname": "node-1",
    "ip": "10.0.0.7"
  }
}

如上例,编写自定义格式可提供更多灵活性,以处理 Spring Boot 默认不支持的日志聚合。

4.6、记录日志到文件

我们之前的示例直接将日志记录到控制台。不过,我们可以通过修改配置,在控制台中保持人类可读的日志格式,并将结构化日志写入文件:

logging.structured.format.file=ecs
logging.file.name=log.json

如上,我们使用 file 属性而不是 console 属性。这会在项目根目录下创建一个包含结构化日志的 log.json 文件。

5、总结

本文介绍了如何通过 application.properties 配置文件来定义 Spring Boot 中的结构化日志,以及如何实现自定义的日志格式。


Ref:https://www.baeldung.com/spring-boot-structured-logging