1. 引言
应用日志是排查问题、性能监控和观察软件行为的重要资源。本文将介绍如何在 Java 中实现结构化日志,并对比传统非结构化日志的优势。
2. 结构化 vs 非结构化日志
在动手编码前,先理解两者的核心区别:
非结构化日志
- 本质是格式化文本块,变量通过字符串拼接嵌入
- 示例(来自 Spring 应用):
22:25:48.111 [restartedMain] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 42 ms. Found 1 JPA repository interfaces.
- ❌ 痛点:提取特定信息(如类名)需要复杂的字符串处理
结构化日志
- 以字典形式独立展示每个信息字段
- 同样信息的结构化版本:
{ "timestamp": "22:25:48.111", "logger": "restartedMain", "log_level": "INFO", "class": "o.s.d.r.c.RepositoryConfigurationDelegate", "message": "Finished Spring Data repository scanning in 42 ms. Found 1 JPA repository interfaces." }
- ✅ 优势:通过字段名直接访问数据,无需文本解析
3. 配置结构化日志
使用 logback 和 slf4j 实现结构化日志配置
3.1. 依赖配置
在 pom.xml
添加核心依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.4.14</version>
</dependency>
⚠️ Spring Boot 项目无需手动添加,这些依赖已包含在
spring-boot-starter-logging
中
添加结构化编码器:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
3.2. logback 基础配置
创建 logback.xml
:
<configuration>
<appender name="jsonConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="jsonConsoleAppender"/>
</root>
</configuration>
关键组件说明:
appender
:日志输出器,这里使用控制台输出encoder
:使用LogstashEncoder
将日志转为 JSON
示例输出:
{"@timestamp":"2023-12-20T22:16:25.2831944-03:00","@version":"1","message":"Example log message","logger_name":"info_logger","thread_name":"main","level":"INFO","level_value":20000,"custom_message":"my_message","password":"123456"}
3.3. 优化结构化日志
增强配置提升可读性和安全性:
<configuration>
<appender name="jsonConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeCallerData>true</includeCallerData>
<jsonGeneratorDecorator class="net.logstash.logback.decorate.CompositeJsonGeneratorDecorator">
<decorator class="net.logstash.logback.decorate.PrettyPrintingJsonGeneratorDecorator"/>
<decorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
<defaultMask>XXXX</defaultMask>
<path>password</path>
</decorator>
</jsonGeneratorDecorator>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="jsonConsoleAppender"/>
</root>
</configuration>
优化点解析:
includeCallerData
:添加调用者元数据PrettyPrintingJsonGeneratorDecorator
:美化 JSON 输出格式MaskingJsonGeneratorDecorator
:敏感字段脱敏(如密码)
优化后输出:
{
"@timestamp" : "2023-12-20T22:44:58.0961616-03:00",
"@version" : "1",
"message" : "Example log message",
"logger_name" : "info_logger",
"thread_name" : "main",
"level" : "INFO",
"level_value" : 20000,
"custom_message" : "my_message",
"password" : "XXXX",
"caller_class_name" : "StructuredLog4jExampleUnitTest",
"caller_method_name" : "givenStructuredLog_whenUseLog4j_thenExtractCorrectInformation",
"caller_file_name" : "StructuredLog4jExampleUnitTest.java",
"caller_line_number" : 16
}
4. 实现结构化日志
通过示例应用演示结构化日志使用
4.1. 创建示例 User 类
public class User {
private String id;
private String name;
private String password;
// getters, setters, and all-args constructor
}
4.2. 结构化日志使用场景
创建测试类:
public class StructuredLog4jExampleUnitTest {
Logger logger = LoggerFactory.getLogger("logger_name_example");
//...
}
场景1:常规信息日志
@Test
void whenInfoLoggingData_thenFormatItCorrectly() {
User user = new User("1", "John Doe", "123456");
logger.atInfo().addKeyValue("user_info", user)
.log();
}
输出:
{
"@timestamp" : "2023-12-21T23:58:03.0581889-03:00",
"@version" : "1",
"message" : "Processed user succesfully",
"logger_name" : "logger_name_example",
"thread_name" : "main",
"level" : "INFO",
"level_value" : 20000,
"user_info" : {
"id" : "1",
"name" : "John Doe",
"password" : "XXXX"
},
"caller_class_name" : "StructuredLog4jExampleUnitTest",
"caller_method_name" : "whenInfoLoggingData_thenFormatItCorrectly",
"caller_file_name" : "StructuredLog4jExampleUnitTest.java",
"caller_line_number" : 21
}
场景2:错误日志记录
@Test
void givenStructuredLog_whenUseLog4j_thenExtractCorrectInformation() {
User user = new User("1", "John Doe", "123456");
try {
throwExceptionMethod();
} catch (RuntimeException ex) {
logger.atError().addKeyValue("user_info", user)
.setMessage("Error processing given user")
.addKeyValue("exception_class", ex.getClass().getSimpleName())
.addKeyValue("error_message", ex.getMessage())
.log();
}
}
输出:
{
"@timestamp" : "2023-12-22T00:04:52.8414988-03:00",
"@version" : "1",
"message" : "Error processing given user",
"logger_name" : "logger_name_example",
"thread_name" : "main",
"level" : "ERROR",
"level_value" : 40000,
"user_info" : {
"id" : "1",
"name" : "John Doe",
"password" : "XXXX"
},
"exception_class" : "RuntimeException",
"error_message" : "Error saving user data",
"caller_class_name" : "StructuredLog4jExampleUnitTest",
"caller_method_name" : "givenStructuredLog_whenUseLog4j_thenExtractCorrectInformation",
"caller_file_name" : "StructuredLog4jExampleUnitTest.java",
"caller_line_number" : 35
}
5. 结构化日志的优势
5.1. 可读性提升
- 字典式结构便于人类快速定位字段
- 类比:查字典(结构化)vs 逐页翻书(非结构化)
5.2. 效率优势
- 查询效率:Kibana/Splunk 等工具可直接通过字段名查询
- 业务监控:便于提取业务指标(如错误率、处理时长)
- 成本优化:简化查询算法,降低云服务计算成本
6. 总结
本文展示了使用 slf4j 和 logback 实现 Java 结构化日志的完整方案。结构化日志通过标准化格式显著提升了日志的可读性和处理效率,使问题排查和性能监控更加高效。
完整代码示例请参考 GitHub 仓库