1. 日志消息的结构
把日志想象成不是程序的“意识流”,而是一本有价值的日志本。一个月或一年后,你或同事应当能在其中找到问题的答案:“这里到底发生了什么?”为此,每条日志消息都应当结构化。通常(大多数库的标准)每条消息包含:
- 事件时间 — 发生的时间。
- 级别 — 重要程度(INFO、ERROR 等)。
- 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,你会淹没在细节里。我们来看看何时使用哪个级别。
| 级别 | 用途 | 示例消息 |
|---|---|---|
|
严重故障,导致系统工作不正常或完全不可用 | “数据库连接错误” |
|
重要但不致命的警告,需要关注 | “未找到用户,使用 guest” |
|
反映应用正常运行的常规事件 | “用户已注册:user123” |
|
调试用的详细信息,生产环境通常不需要 | “调用了方法 checkPassword,参数为 ...” |
|
最细致的诊断信息,通常用于深度排查 | “处理循环开始:i=0” |
典型消息示例
- ERROR — 文件写入失败、捕获未处理异常、服务不可用。
- WARN — 过时 API、可疑用户行为、超过尝试次数限制。
- INFO — 用户登录/退出、订单处理完成、应用启动。
- DEBUG — 请求参数、变量值、中间计算结果。
- TRACE — 方法的进入/退出、内部循环、算法细节。
建议:
生产环境通常只开启 INFO 及以上,有时包含 WARN 和 ERROR。DEBUG 和 TRACE 仅在排查复杂问题时开启。
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 — 级别(ERROR、INFO 等)。
- %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:缺少唯一标识符。 在复杂系统中,没有 orderId、userId 等标识符,你将无法把事件“串联”起来,理解某个用户或订单到底发生了什么。
GO TO FULL VERSION