日志记录的重要性及结构化日志的引入
日志记录是应用程序故障排除和系统可观测性的核心部分,是支持系统稳定运行的三大支柱之一(其他两个支柱为指标和分布式追踪)。在生产环境中,日志文件作为问题诊断的重要依据,帮助开发者快速定位并解决问题。通常情况下,日志以人类可读的文本格式输出。
结构化日志是一种通过预定义的、机器可读的格式输出日志信息的技术。这种日志格式能够被轻松引入日志管理系统,实现更强大的搜索、过滤和分析功能。当前,最常用的结构化日志格式之一是JSON。
在Spring Boot 3.4中,结构化日志记录功能已开箱即用。默认支持Elastic Common Schema (ECS)和Logstash格式,并可灵活扩展以满足自定义需求。
结构化日志 Hello World
要开始使用结构化日志,请创建一个新的Spring Boot项目,并确保使用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.file=ecs
logging.file.name=log.json
启动应用程序后,控制台将显示人类可读的日志,而log.json
文件中则会记录结构化的JSON日志内容。
添加附加字段
结构化日志的一个强大功能是能够以结构化的方式添加额外信息到日志事件中。例如,您可以将用户ID添加到每个日志事件中,然后通过该ID进行过滤,查看特定用户的操作记录。
Elastic Common Schema和Logstash都支持Mapped Diagnostic Context (MDC)。示例如下:
@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");
}
}
Spring Boot会自动将用户ID包含在JSON日志中:
{ ... ,"message":"Hello structured logging!","userId":"1" ... }
也可以使用流式日志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还提供了对service.name
、service.version
、service.environment
和service.node-name
等字段的内置支持。您可以通过以下配置在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输出中,您将看到包含这些信息的字段,便于在日志系统中根据节点名称、服务版本等进行过滤。
自定义日志格式
除了Elastic Common Schema和Logstash格式,Spring Boot 3.4还支持自定义日志格式。步骤如下:
-
创建一个 StructuredLogFormatter
接口的自定义实现。 -
在 application.properties
中配置该自定义实现。
示例代码:
class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {
@Override
public String format(ILoggingEvent event) {
return "time=" + event.getTimeStamp() + " level=" + event.getLevel() + " message=" + event.getMessage() + "\n";
}
}
在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);
}
}
生成的日志消息示例如下:
{"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"}}
没有回复内容