Spring Boot 3.4 中的结构化日志

日志记录是应用故障排除的重要组成部分,也是可观测性的三大支柱之一,另外两个是指标和追踪(Trace)。没有人喜欢在生产环境中瞎操作,当事故发生时,开发者会很乐意看到日志文件。日志通常以人类可读的格式输出。

结构化日志是一种技术,其中日志输出以定义良好的格式编写,通常是机器可读的。这种格式可以输入到日志管理系统中,从而实现强大的搜索和分析功能。结构化日志最常用的格式之一是 JSON。

Spring Boot 3.4 开箱即支持结构化日志。它支持 Elastic Common Schema(ECS)Logstash 格式,也可以使用自己的格式进行扩展。

结构化日志的 Hello World

start.springboot.io 上创建一个新项目,不需要添加任何依赖,但至少要选择 Spring Boot 3.4.0-M2

要在控制台上启用结构化日志记录,请将如下配置添加到 application.properties 中:

logging.structured.format.console=ecs

这将指示 Spring Boot 以 Elastic Common Schema(ECS) 格式输出日志。

启动应用,你就会看到日志是 JSON 格式的:

{"@timestamp":"2024-07-30T08:41:10.561295200Z","log.level":"INFO","process.pid":67455,"process.thread.name":"main","service.name":"structured-logging-demo","log.logger":"com.example.structured_logging_demo.StructuredLoggingDemoApplication","message":"Started StructuredLoggingDemoApplication in 0.329 seconds (process running for 0.486)","ecs.version":"8.11"}

将结构化日志记录到文件

你还可以将结构化日志写入文件。例如,这可以用于在控制台上打印人类可读的日志,同时将结构化日志写入文件以供机器读取。

要启用此功能,请将如下配置添加到 application.properties 中,并确保删除 logging.structured.format.console=ecs 设置:

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

现在启动应用,你会看到控制台上有人类可读的日志,而 log.json 文件则包含机器可读的 JSON 内容。

添加额外字段

结构化日志的一个强大功能是,开发人员可以以结构化的方式在日志事件中添加信息。例如,你可以在每个日志事件中添加用户 ID,然后根据该 ID 进行过滤,查看这个特定用户做了什么。

Elastic Common SchemaLogstash 都在 JSON 中包含了 Mapped Diagnostic Context 的内容。

创建自己的日志消息:

@Component
class MyLogger implements CommandLineRunner {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyLogger.class);

    @Override
    public void run(String... args) {
        MDC.put("userId", "1");
        LOGGER.info("Hello structured logging!");
        MDC.remove("userId");
    }
}

在记录日志信息之前,这段代码会在 MDC 中设置用户 ID。Spring Boot 会自动在 JSON 中包含用户 ID

{ ... ,"message":"Hello structured logging!","userId":"1" ... }

你还可以使用 Fluent 日志 API 添加其他字段,而无需依赖 MDC

@Component
class MyLogger implements CommandLineRunner {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyLogger.class);

    @Override
    public void run(String... args) {
        LOGGER.atInfo().setMessage("Hello structured logging!").addKeyValue("userId", "1").log();
    }

}

Elastic Common Schema 定义了很多字段名称,Spring Boot 内置支持服务名称、服务版本、服务环境和节点名称。要为这些字段设置值,可以在 application.properties 中使用以下内容:

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

查看 JSON 输出,你可以看到日志有了 service.nameservice.versionservice.environmentservice.node.name 字段。有了这些字段,你就可以在日志系统中根据节点名称、服务版本等进行过滤。

自定义日志格式

如上所述,Spring Boot 开箱即支持 Elastic Common SchemaLogstash 格式。要添加自己的格式,必须执行以下步骤:

  1. 创建 StructuredLogFormatter 接口的自定义实现
  2. application.properties 中引用自定义实现

首先,创建自定义实现:

class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {

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

}

如你所见,结构化日志支持并不局限于 JSON,你以返回任何想要的字符串。在本例中,我们使用 key=value 对。

现在,需要让 Spring Boot 加载到我们的自定义实现。为此,需要在 application.properties 中添加以下内容:

logging.structured.format.console=com.example.structured_logging_demo.MyStructuredLoggingFormatter

现在,启动应用,查看日志输出!

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

如上,这正是我们想要的。

如果要输出 JSON,可以使用 Spring Boot 3.4 中新增的 JsonWriter

class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {

    private final JsonWriter<ILoggingEvent> writer = JsonWriter.<ILoggingEvent>of((members) -> {
        members.add("time", (event) -> event.getInstant());
        members.add("level", (event) -> event.getLevel());
        members.add("thread", (event) -> event.getThreadName());
        members.add("message", (event) -> event.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();

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

}

当然,你也可以使用任何其他 JSON 库(如 Jackson)来创建 JSON,而不必使用 JsonWriter

生成的日志信息如下所示:

{"time":"2024-07-30T09:14:49.377308361Z","level":"INFO","thread":"main","message":"Hello structured logging!","application":{"name":"StructuredLoggingDemo","version":"1.0.0-SNAPSHOT"},"node":{"hostname":"node-1","ip":"10.0.0.7"}}

最后

你可以查看 Spring Boot 3.4 的 文档 来了解更多详细的信息。


Ref:https://spring.io/blog/2024/08/23/structured-logging-in-spring-boot-3-4