CodeGym /课程 /JAVA 25 SELF /日志格式与日志级别:best practices

日志格式与日志级别:best practices

JAVA 25 SELF
第 63 级 , 课程 1
可用

1. 日志消息的结构

把日志想象成不是程序的“意识流”,而是一本有价值的日志本。一个月或一年后,你或同事应当能在其中找到问题的答案:“这里到底发生了什么?”为此,每条日志消息都应当结构化。通常(大多数库的标准)每条消息包含:

  • 事件时间 — 发生的时间。
  • 级别 — 重要程度(INFOERROR 等)。
  • logger 名称 — 通常是类或组件名。
  • 消息文本 — 具体发生了什么。
  • 堆栈跟踪(如有错误)— 便于定位何处、为何出错。

下面是一个格式良好的日志行示例(以 Log4j/SLF4J 为例):

2024-06-16 18:42:07,123 INFO  com.example.MainApp - 用户已登录:username=user123

如果发生了错误:

2024-06-16 18:42:10,456 ERROR com.example.LoginService - 用户认证错误:user123
java.lang.IllegalArgumentException: 密码无效
    at com.example.LoginService.checkPassword(LoginService.java:42)
    ...

为什么这很重要?
当应用长时间运行时,日志可能占据数 GB。如果消息不结构化,定位问题就会变成“在风扇噪声里猜旋律”的难题。

2. 消息格式化

为什么不应该这样做:

logger.info("用户 " + username + " 登录系统");

看起来很简单,但有个陷阱:即使当前日志级别设置为 ERROR,括号里的字符串仍然会被拼接,这是额外的资源开销。在每秒产生日志成千上万行的大型系统里,这会带来实际的延迟。

正确做法:模板与参数

现代库(例如 SLF4J 和 Log4j 2)支持带参数的模板:

logger.info("用户 {} 登录系统", username);

只有当日志级别允许输出该消息时,字符串才会被组装。比如当前是 WARN,那么连字符串的计算都不会发生——节省资源和心力。

加分项:如果传入多个参数,它们会按顺序替换:

logger.info("用户 {} 在对象 {} 上执行了操作 {}", username, action, objectId);

记录异常(stacktrace)

如果你捕获了异常,不要手动把堆栈跟踪拼到消息里:

// 不要这样:
logger.error("错误: " + ex.getMessage() + "\n" + Arrays.toString(ex.getStackTrace()));

正确方式:

logger.error("处理请求时发生错误", ex);

SLF4J 和 Log4j 会自动、清晰地把堆栈跟踪加入日志。

示例:两种方式的对比

// 不佳(始终发生字符串拼接)
logger.debug("对象: " + expensiveToString(obj));

// 较好(延迟构建)
logger.debug("对象: {}", obj);

3. 选择日志级别

如果你把日志里的一切都标成 ERROR,那这不叫日志,而是“红色警报”。如果全是 DEBUG,你会淹没在细节里。我们来看看何时使用哪个级别。

级别 用途 示例消息
ERROR
严重故障,导致系统工作不正常或完全不可用 “数据库连接错误”
WARN
重要但不致命的警告,需要关注 “未找到用户,使用 guest”
INFO
反映应用正常运行的常规事件 “用户已注册:user123”
DEBUG
调试用的详细信息,生产环境通常不需要 “调用了方法 checkPassword,参数为 ...”
TRACE
最细致的诊断信息,通常用于深度排查 “处理循环开始:i=0”

典型消息示例

  • ERROR — 文件写入失败、捕获未处理异常、服务不可用。
  • WARN — 过时 API、可疑用户行为、超过尝试次数限制。
  • INFO — 用户登录/退出、订单处理完成、应用启动。
  • DEBUG — 请求参数、变量值、中间计算结果。
  • TRACE — 方法的进入/退出、内部循环、算法细节。

建议:
生产环境通常只开启 INFO 及以上,有时包含 WARNERRORDEBUGTRACE 仅在排查复杂问题时开启。

4. Best practices(日志最佳实践)

不要记录敏感数据

密码、令牌、信用卡号——这些都不应出现在日志里。即使你觉得“日志文件只给我看”,也请想想 GDPR 和那位可能把日志发到群里的同事。

// 不好:
logger.info("用户 {} 使用密码 {} 登录", username, password);

// 较好:
logger.info("用户 {} 登录系统", username);

不要滥用 ERROR 级别

如果你把所有东西都用 logger.error 打出来,那么真正发生灾难时没人能注意到——大家对“红灯”已经麻木了。仅在应用确实无法继续工作或业务逻辑被破坏时使用 ERROR

记录异常时包含完整堆栈

不要只写 ex.getMessage(),否则你永远不知道错误发生在何处。把异常作为第二个参数传给 logger。

logger.error("处理请求时发生错误", ex);

使用唯一标识符(事件关联)

在大型系统中,给每个请求、用户或操作分配一个唯一标识符很有用。这有助于把来自系统不同部分的事件“串联”起来。

logger.info("开始处理订单: orderId={}", orderId);
logger.info("订单处理成功: orderId={}", orderId);

不要什么都往日志里写

日志过多就等于无用。不要为每行代码都打日志,否则你将无法找到需要的信息。

把消息写清楚易懂

写得让非代码作者、甚至半年后看日志的人也能看懂。避免缩写、隐晦的简写和“内部笑话”。

5. 实践:配置日志格式与级别

Log4j2(log4j2.xml)中的格式配置示例

<Configuration>
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="info">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

这意味着什么?

  • %d{...} — 事件时间。
  • %-5level — 级别(ERRORINFO 等)。
  • %logger{36} — logger 名称(通常是类)。
  • %msg — 消息本身。

不同日志级别的代码示例(SLF4J)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogDemo {
    private static final Logger logger = LoggerFactory.getLogger(LogDemo.class);

    public static void main(String[] args) {
        logger.info("应用已启动");
        logger.debug("变量 x 的值: {}", 42);

        try {
            throw new IllegalArgumentException("哎呀!");
            // ...
        } catch (Exception ex) {
            logger.error("启动时发生错误", ex);
        }
    }
}

演示级别差异

如果在配置中将日志级别设为 INFO,那么 DEBUG 级别及以下的消息将不会输出。尝试在配置中把级别改为 debug——你会看到更多细节。

6. 常见错误

错误 #1:在日志中进行字符串拼接。 新手常常这么写:

logger.debug("用户: " + user.getName() + ", 角色: " + user.getRole());

结果即使关闭了 DEBUG 级别,这些字符串仍会被拼接,造成额外负载。请使用参数!

错误 #2:记录日志时没有异常堆栈。 只写消息:

logger.error("错误: " + ex.getMessage());

最终日志中没有错误发生位置的信息。请把异常作为第二个参数传入!

错误 #3:把所有东西都记录为 ERROR。 如果所有内容都是红色,就没有什么是红色。按用途使用各级别,否则重要错误会淹没在“小问题”中。

错误 #4:记录敏感数据。 永远不要把密码、令牌、卡号写进日志。即便你以为没人会看到,现实总爱出其不意。

错误 #5:晦涩难懂的消息。 如果日志里的消息像“ERR42: fail”,一个月后你自己也想不起来它的意思。请写得清楚且具体。

错误 #6:缺少唯一标识符。 在复杂系统中,没有 orderIduserId 等标识符,你将无法把事件“串联”起来,理解某个用户或订单到底发生了什么。

1
任务
JAVA 25 SELF, 第 63 级, 课程 1
已锁定
架构师
架构师
1
任务
JAVA 25 SELF, 第 63 级, 课程 1
已锁定
社交网络
社交网络
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION