51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Boot 中的结构化日志

1、概览 {#1概览}

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

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

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

2、Maven 依赖 {#2maven-依赖}

首先,在 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 默认的日志 {#3spring-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、配置 {#4配置}

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

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

4.1、Elastic Common Schema {#41elastic-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 扩展日志格式 {#42graylog-扩展日志格式}

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 格式 {#43logstash-格式}

开箱即用的 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、其他信息 {#44其他信息}

我们可以使用 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、自定义日志格式 {#45自定义日志格式}

此外,我们还可以定义自己的自定义结构日志格式,并在 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、记录日志到文件 {#46记录日志到文件}

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

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

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

5、总结 {#5总结}

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


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

赞(1)
未经允许不得转载:工具盒子 » Spring Boot 中的结构化日志