Java日志规范与实践

Java 日志规范与实践

作者: shura | 日期: 2025-02-23

说明

本文讲 Java 日志的正确用法,重点解决三个问题:

  1. 日志框架怎么选?
  2. 日志该怎么打?
  3. 部署时日志该输出到哪?(标准输出 vs 文件)

最后一个问题很多人搞不清楚,本文会详细讲解。


一、日志框架选择

1.1 框架关系图

应用代码
   ↓
SLF4J (门面)          ← 你应该使用的 API
   ↓
实现层:
├── Logback           ← Spring Boot 默认
├── Log4j2            ← 高性能选择
└── JUL/Log4j/...     ← 其他实现

核心原则: 代码只依赖 SLF4J,运行时选择具体实现。

1.2 Logback vs Log4j2

对比项LogbackLog4j2说明
性能普通优秀Log4j2 异步性能高 10 倍+
集成Spring Boot 默认需替换Logback 开箱即用
配置简单较复杂Logback 上手快
新特性Log4j2 支持更多特性
推荐场景中小项目高并发项目-

1.3 如何选择

选 Logback:

  • Spring Boot 项目,无特殊性能要求
  • 团队不想折腾,开箱即用

选 Log4j2:

  • 高并发场景(日志量大)
  • 追求极致性能
  • 新项目,愿意花时间配置

依赖配置:

<!-- Logback (Spring Boot 默认,无需额外配置) -->

<!-- Log4j2 (需替换) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

二、日志规范

2.1 日志级别

级别用途示例
ERROR系统错误,影响功能数据库连接失败、外部接口超时
WARN警告,不影响主流程配置缺失用默认值、余额不足
INFO关键业务节点用户登录、订单创建、支付完成
DEBUG调试信息方法入参、查询结果

生产环境:ERROR/WARN/INFO 开启,DEBUG 关闭。

2.2 什么该打日志

必须记录:

// 1. 关键业务操作
log.info("创建订单, userId: {}, amount: {}", userId, amount);

// 2. 外部调用
log.info("调用支付接口, orderId: {}", orderId);

// 3. 异常(必须包含堆栈)
try {
    paymentService.pay(orderId);
} catch (Exception e) {
    log.error("支付失败, orderId: {}", orderId, e);  // 注意:e 放最后,不用 {}
    throw e;
}

// ❌ 错误:不会打印堆栈
log.error("支付失败, orderId: {}, error: {}", orderId, e.getMessage());

// ✅ 正确:异常对象放最后,自动打印堆栈
log.error("支付失败, orderId: {}", orderId, e);

// 4. 状态变更
log.info("订单状态变更, orderId: {}, {} -> {}", orderId, oldStatus, newStatus);

不要记录:

// ❌ 敏感信息
log.info("用户登录, password: {}", password);  // 密码
log.info("银行卡号: {}", cardNo);              // 卡号

// ❌ 大对象
log.info("查询结果: {}", largeList);            // 可能几千条数据

// ❌ 循环内
for (User user : users) {
    log.info("处理用户: {}", user);             // 性能问题
}

2.3 性能优化

1. 使用占位符避免字符串拼接

// ✅ 正确:占位符延迟计算
log.debug("用户信息: {}", user);

// ❌ 错误:即使 DEBUG 关闭,字符串拼接也会执行
log.debug("用户信息: " + user.toString());

2. 复杂对象构建需要判断

// ❌ 问题:buildComplexObject() 方法会执行,即使日志不打印
log.debug("详细信息: {}", buildComplexObject());

// ✅ 正确:先判断日志级别
if (log.isDebugEnabled()) {
    log.debug("详细信息: {}", buildComplexObject());
}

为什么要判断?

占位符只能避免字符串拼接,但不能避免方法调用

// 场景:构建复杂对象的方法
private String buildComplexObject() {
    // 耗时操作:查询数据库、计算、遍历集合等
    List<User> users = userRepository.findAll();  // 查数据库
    return users.stream()
        .map(User::toString)
        .collect(Collectors.joining(","));        // 遍历拼接
}

// 即使生产环境 DEBUG 关闭,这个方法依然会执行!
log.debug("用户列表: {}", buildComplexObject());  // ❌ 性能浪费

// 先判断,DEBUG 关闭时方法不会执行
if (log.isDebugEnabled()) {
    log.debug("用户列表: {}", buildComplexObject());  // ✅ 性能优化
}

总结:

  • 简单对象(已存在的变量):直接用占位符
  • 复杂对象(需要方法构建):先判断日志级别

三、日志输出:标准输出 vs 文件

3.1 两种输出方式

标准输出(stdout/stderr):

日志 → 控制台/终端

文件输出:

日志 → 文件(/var/log/app.log)

3.2 传统部署方式(物理机/虚拟机)

推荐:文件输出

<!-- logback-spring.xml -->
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/myapp/application.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/var/log/myapp/application.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="FILE"/>
    </root>
</configuration>

为什么用文件输出:

  • 持久化,重启不丢失
  • 可按日期滚动,方便查找
  • 支持日志收集工具(Filebeat)

3.3 容器化部署(Docker/K8s)

推荐:标准输出

<!-- logback-spring.xml -->
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

为什么用标准输出:

1. 容器是短暂的

  • 容器销毁,文件就没了
  • 写文件需要挂载 Volume,麻烦

2. 日志收集更简单

  • Docker:docker logs <container>
  • K8s:kubectl logs <pod>
  • 日志收集工具直接读取 stdout

3. 符合 12-Factor 原则

  • 应用不关心日志存储
  • 日志当作事件流处理

3.4 实际场景决策

场景 1:容器 + ELK 日志集成

推荐:标准输出 + JSON 格式

应用 stdout → Docker → Fluentd/Filebeat → Elasticsearch → Kibana

配置:

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <!-- JSON 格式,直接适配 ELK -->
            <includeMdcKeyName>traceId</includeMdcKeyName>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

优点:

  • 日志收集工具直接读取 stdout
  • JSON 格式方便 Elasticsearch 解析
  • 不需要挂载 Volume

场景 2:容器 + NFS 共享存储

推荐:文件输出 + 挂载 NFS

应用日志 → 文件 → NFS 共享目录 → 多个容器可访问

配置:

<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/app/logs/application.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/app/logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>
    <root level="INFO">
        <appender-ref ref="FILE"/>
    </root>
</configuration>

Docker 运行:

docker run -v /nfs/logs:/app/logs myapp:latest

优点:

  • 多个容器共享日志
  • 日志持久化,容器重启不丢失
  • 可以直接在 NFS 服务器上查看

场景 3:容器 + 传统运维工具(Filebeat 读文件)

推荐:文件输出 + 挂载宿主机

应用日志 → 文件 → 宿主机目录 → Filebeat 读取 → Elasticsearch

这种场景适用于:

  • 公司已有 Filebeat 基础设施
  • 运维习惯于读取文件
  • 需要日志文件分级管理

场景 4:本地开发/调试

推荐:双输出(控制台 + 文件)

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
    </appender>
    
    <root level="DEBUG">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

优点:

  • 控制台实时查看
  • 文件保留历史日志

3.5 常见误区

误区 1:容器里写文件不挂载 Volume

# ❌ 错误:日志在容器内,容器删除就没了
docker run myapp:latest

# ✅ 正确:挂载出来或用标准输出
docker run -v /var/log:/app/logs myapp:latest

误区 2:生产环境标准输出和文件同时开

<!-- ❌ 不推荐:生产环境双写浪费性能 -->
<root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
</root>

生产环境应根据部署方式选一个,本地开发可以双开。

误区 3:标准输出没配置日志驱动

Docker 默认用 json-file 驱动,日志会堆积:

# 查看日志文件大小
du -sh /var/lib/docker/containers/*/

解决方法:

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

四、多环境配置

4.1 Spring Boot Profile

# application.yml
spring:
  profiles:
    active: ${ENV:dev}

# 开发环境
---
spring:
  config:
    activate:
      on-profile: dev
logging:
  level:
    root: INFO
    com.example: DEBUG

# 生产环境
---
spring:
  config:
    activate:
      on-profile: prod
logging:
  level:
    root: INFO
    com.example: INFO

4.2 Logback 按环境切换

<configuration>
    <!-- 开发环境:控制台 + 彩色 -->
    <springProfile name="dev">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%-5level) %clr(${PID:- }){magenta} %clr([%thread]){faint} %clr(%logger{36}){cyan} %msg%n</pattern>
            </encoder>
        </appender>
        <root level="DEBUG">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
    
    <!-- 生产环境:标准输出(容器)或文件(物理机) -->
    <springProfile name="prod">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
</configuration>

五、实践建议

5.1 日志分级输出

<!-- ERROR 日志单独文件(方便告警) -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/myapp/error.log</file>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>/var/log/myapp/error.%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
</appender>

5.2 异步日志

<!-- 异步 Appender(提升性能) -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE"/>
    <queueSize>512</queueSize>
    <discardingThreshold>0</discardingThreshold>  <!-- 0 = 不丢弃任何级别日志 -->
    <neverBlock>false</neverBlock>                <!-- false = 队列满时阻塞,保证不丢失 -->
</appender>

<root level="INFO">
    <appender-ref ref="ASYNC"/>
</root>

注意:

  • discardingThreshold=0 保证不丢弃日志
  • neverBlock=false 队列满时阻塞,防止丢失
  • 高吞吐场景可设置 neverBlock=true 允许丢失非 ERROR 日志

5.3 日志 JSON 格式

<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.4</version>
</dependency>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeMdcKeyName>traceId</includeMdcKeyName>
    </encoder>
</appender>

输出:

{
  "timestamp": "2025-02-23T10:30:45.123+08:00",
  "level": "INFO",
  "thread": "http-nio-8080-exec-1",
  "logger": "com.example.UserService",
  "message": "创建用户, userId: 123",
  "traceId": "abc123"
}

六、检查清单

代码层面:

  • 使用 SLF4J,不直接使用 Logback/Log4j2
  • 日志级别正确(ERROR/WARN/INFO/DEBUG)
  • 异常日志包含堆栈(第三个参数传异常对象)
  • 使用占位符 {},不用字符串拼接
  • 没有记录敏感信息(密码、卡号)

配置层面:

  • 生产环境关闭 DEBUG 日志
  • 配置日志滚动策略(防止磁盘爆满)
  • 容器化部署用标准输出
  • 物理机部署用文件输出
  • ERROR 日志有监控告警

部署层面:

  • 容器配置日志驱动限制大小
  • 日志收集工具正常运行(ELK/EFK)
  • 定期清理过期日志

七、总结

场景推荐方案
传统部署文件输出 + 日志滚动
容器部署标准输出 + 日志收集
开发环境控制台输出 + DEBUG 级别
生产环境看部署方式 + INFO 级别

核心原则:

  1. 代码只依赖 SLF4J
  2. 日志框架根据场景选(Logback 够用,Log4j2 更快)
  3. 容器化优先用标准输出
  4. 传统部署用文件输出

本文基于 Spring Boot 3.x

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值