我一直认为,打日志远不止是“把变量打印出来”那么简单。它是一项需要深刻理解业务、具备前瞻性思维、并兼顾性能与可维护性的工程实践。一个经验丰富的开发者打出的日志,往往能在系统上线数月甚至数年后,成为排查问题、配置监控、分析报表的关键依据。
换句话说:好的日志,是写给未来的自己和团队看的。
本文将从日志级别、内容设计、性能考量、结构规范等多个维度,系统性地探讨如何在 Java 项目中打出高质量、高价值的日志。
一、日志级别:边界虽模糊,但原则要清晰
SLF4J 等日志框架提供了 ERROR、WARN、INFO、DEBUG 等级别,但实际使用中,很多人对它们的划分并不一致。团队应制定统一规范,并通过 Code Review 对齐。 我的理解如下:
ERROR
- 场景:执行结果不符合预期,且后果不可接受,或必须人工介入。
- 典型例子:
-
- 调用核心 RPC 接口失败,且无降级方案;
- 数据库写入失败导致业务中断;
- 所有
catch (Exception e)块中,若异常未被完全处理,应记录为error(并带上异常堆栈!)。
WARN
- 场景:执行偏离预期,但系统能自动恢复或降级,无需人工干预。
- 典型例子:
-
- 参数非法,但使用了默认值兜底;
- 主 RPC 调用失败,但成功切换到备用接口;
- 缓存穿透,但已走 DB 回源。
💡 判断标准:是否需要值班人员半夜被叫醒? 如果不需要,大概率是
WARN。
INFO
- 用途:记录关键业务流程,用于监控、审计、离线分析。
- 建议记录:
-
- 所有写入持久化存储的操作(如创建订单、修改用户状态);
- 核心服务的入口/出口(如 HTTP 请求开始与结束);
- 重要状态变更(如支付成功、审核通过)。
DEBUG
- 仅用于开发/测试环境,记录详细执行路径、中间变量等。
- 生产环境应关闭,避免性能损耗和磁盘爆炸。
二、日志内容:上下文比堆栈更重要
一条有价值的日志,必须包含足够的上下文信息,否则就是“噪音”。建议包含以下三类内容:
- 输入:当前逻辑的输入参数(如订单 ID、用户 ID);
- 输出结果:执行结果(成功/失败)、错误码、错误信息;
- 元数据:耗时(ms)、traceId、spanId、机器 IP、线程名等。
❌ 避免直接 toString() 或 JSON 序列化整个对象
log.info("user={}", user); // user.toString() 可能很大,或者包含敏感字段或冗余信息
推荐:显式提取关键字段
log.info("createOrder, orderId={}, userId={}, cost={}ms, status={}",
orderId, userId, cost, status);
这样既清晰,又便于后续通过正则或分隔符解析(用于监控告警或日志分析)。
三、高并发系统:日志也是性能瓶颈
在 TPS > 1000 的系统中,日志可能成为性能杀手:
- 频繁 I/O 会拖慢主线程;
- 日志量过大可能撑爆磁盘,导致服务宕机;
- 大量字符串拼接消耗 CPU 和内存。
优化策略:
- 正常路径尽量不打日志,只在异常或关键写操作时记录;
- 采样打印:对查询类请求,按
traceId或业务 ID 哈希采样(如 1%),将日志 TPS 从 1000 降至 10; - 异步日志:使用 Logback 的
AsyncAppender,避免阻塞业务线程; - 动态开关:通过配置中心控制日志开关(见下文)。
四、动态控制:让日志“按需开启”
新功能上线初期,可能需要详细日志验证逻辑;稳定后,这些日志就变成负担。此时可借助动态配置实现灵活控制:
if (LogConfig.isEnabled("orderCreateDetailLog")) {
log.info("order detail: ...");
}
上线时开启开关,观察几天后关闭。这种方式:
- 避免频繁发版调整日志;
- 支持按业务模块、用户 ID、流量比例等维度精细化控制;
- 可复用于多个日志点。
🔧 推荐工具:Apollo、Nacos、Spring Cloud Config + 自定义日志开关管理器。
五、统一日志格式:提升可读性与可分析性
日志不仅是给人看的,更是给机器分析的。结构化、字段顺序固定的日志更容易被 ELK、Loki、Splunk 等工具解析。
推荐格式原则:
- 固定字段放前面(如方法名、耗时、ID);
- 不确定内容(如 message)放后面,避免干扰解析;
- 避免在值中出现分隔符(如
=、,、空格)。
反例(message 可能含逗号,破坏结构):
log.error("queryTrackings@failed, trackingNumber={}, code={}, message={}, cost={}ms", tn, code, msg, cost);
正例(cost 在前,结构稳定):
log.error("queryTrackings@failed, cost={}ms, trackingNumber={}, code={}, message={}", cost, tn, code, msg);
我推荐一个日志格式是这样的:
log.error("【方法名】@【执行结果code】,【参数列表,逗号和等于号分隔】,【异常】");
六、安全与合规:别让日志变成“数据泄露源”
在涉及用户隐私的系统中,日志可能成为 GDPR、网络安全法的合规风险点。
必须做到:
- 敏感字段脱敏:手机号
138****1234,身份证110***********123X; - 禁止记录密码、银行卡、身份证原文;
- 使用 MDC + 自定义 Converter 自动脱敏;
- 在日志采集层增加敏感词过滤(作为兜底)。
七、日志与可观测性:不止于“查问题”
现代系统强调 可观测性(Observability) ,其三大支柱为:
- Logs(日志) :原始事件记录;
- Metrics(指标) :聚合统计(如 QPS、错误率);
- Traces(链路追踪) :请求全链路跟踪。
因此,日志应与其他可观测性手段协同工作:
- 日志中必须包含
traceId,便于在 Jaeger/Zipkin 中关联; - 关键业务日志可同时触发 Micrometer 指标埋点;
- 错误日志可自动触发告警(如通过 Prometheus + Alertmanager)。
总结:打好日志,是一种工程素养
- 不要盲目打印:不是越多越好,而是“恰到好处”;
- 不要丢失上下文:没有 traceId、没有关键 ID 的日志等于废纸;
- 不要吞掉异常:任何 catch 块,要么抛出,要么完整记录异常;
- 不要忽视性能:高并发下,日志也是资源消耗大户;
- 不要忽略安全:日志可能是最隐蔽的数据泄露渠道。
真正优秀的日志,是在系统上线前,就为未来可能的问题、监控、分析做好了准备。
它不是代码的附属品,而是系统健壮性的重要组成部分。