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 仓库


原始标题:Structured Logging in Java