凌晨三点,你的手机响了。线上服务挂了,你赶紧打开日志系统开始排查。结果你发现:
ERROR 日志写的是「操作失败」;WARN 日志写的是「可能有问题」;INFO 日志里全是「开始处理」「处理完成」「处理成功」——废话连篇,核心业务数据一个没有。
你盯着满屏的「处理中」「已完成」「success」「failed」,陷入了深深的沉思:这日志,是写给人看的,还是写给机器看的?
如果你有过类似的经历,恭喜你,你不是一个人。今天我们就来好好聊聊:后端开发中,那些年我们写过的垃圾日志,以及怎么写出让事故复盘效率翻倍的日志。
一、日志的第一宗罪:等于没写
很多程序员写日志的思路是这样的:」反正先写上,出问题再说」。「开始处理」「处理中」「处理完成」「处理成功」——四种状态,四种日志,完美覆盖所有场景。
但实际上呢?这种日志对于排查问题毫无价值。你只知道代码走到了这里,但你不知道:
- 处理的是什么数据?订单ID?用户ID?
- 失败的具体原因是什么?是网络超时?是参数校验失败?是数据库唯一键冲突?
- 处理了多少时间?有没有性能问题?
举一个真实的反面教材:
// 反面典型,请勿模仿
logger.info("开始处理订单");
try {
orderService.process();
logger.info("订单处理成功");
} catch (Exception e) {
logger.error("订单处理失败", e);
}
订单号呢?用户呢?处理了多少时间?catch里记录了异常但有没有记录上下文?都没有。这种日志看起来「规范」,实际上等于白写。
二、日志的第二宗罪:写太多了
和第一宗罪相反的另一端,是日志写太多。
有些项目里,日志量巨大,每秒能产生几十MB的日志。但你打开一看,全是框架的DEBUG日志、循环里的重复日志、或者一些毫无信息量的状态记录。
日志太多的问题在于:
- 存储成本飙升——日志服务按量计费的时候,每GB都是钱
- 查询效率低下——在海量日志里找关键信息,就像大海捞针
- 性能影响——高频日志写入本身就是IO密集型操作,对服务性能有明显影响
更可怕的是,有些人在循环里写日志:
// 危险!这是一颗定时炸弹
for (OrderItem item : order.getItems()) {
logger.info("处理订单项: {}", item.getId());
processItem(item);
}
一个订单1000个项目,就产生1000条INFO日志。如果这个接口QPS稍微高一点,你的日志系统会先于你的服务挂掉。
三、日志的第三宗罪:格式不统一
有的项目里,日志格式是这样的:
logger.info("订单创建成功, orderId={}", orderId);
logger.info("用户[{}]登录系统", userId);
logger.error("failed to process: {}", error.getMessage());
logger.warn("WARNING: timeout exceeded");
没有统一的格式规范,没有结构化的字段,有的用{}占位,有的用字符串拼接,有的用大写「WARNING」,有的用小写「warning」。
这种日志在本地开发环境还能凑合看,一旦上了规模,要做日志聚合、搜索、告警的时候,你就会体会到什么叫绝望。
四、怎么写好日志?四招搞定
第一招:结构化日志,信息要完整
结构化日志的意思是,用JSON格式,把关键信息作为字段输出:
logger.info("订单创建",
"orderId", orderId,
"userId", userId,
"amount", amount,
"itemsCount", items.size(),
"durationMs", duration
);
这样输出的日志是这样的:
{"level":"INFO","ts":"2026-08-30T03:00:00Z","msg":"订单创建","orderId":"ORD123456","userId":"USR789","amount":999.00,"itemsCount":3,"durationMs":45}
在日志系统里,你可以按字段搜索、按字段统计、按字段告警——爽到飞起。
第二招:日志分级要清晰,边界要明确
很多人对日志级别的理解是这样的:
- DEBUG:我本地调试用的
- INFO:好像应该写点什么
- WARN:有点不对劲但不知道怎么办
- ERROR:出错了,反正catch里会记录
这个理解不能说错,但太粗糙了。我的分级标准是这样的:
- ERROR:需要人工介入处理的错误。比如支付失败、用户数据异常、核心流程中断
- WARN:不应该发生但程序能自动恢复的情况。比如重试成功、熔断触发、缓存穿透
- INFO:重要的业务节点和状态变化。比如用户登录、订单创建、支付回调
- DEBUG:开发调试用的详细信息,生产环境默认关闭
一个简单的判断标准:如果这条日志是你在凌晨三点被叫醒时最想看到的,就用ERROR。如果是你在分析业务数据时需要参考的,用INFO。如果是用来排查开发问题的,用DEBUG。
第三招:异常日志要记录上下文
catch块里的日志是最容易写废的。常见的错误写法:
// 错误:只记录了异常本身,没有上下文
catch (SQLException e) {
logger.error("数据库异常", e);
}
好的写法:
// 正确:异常+上下文+操作意图
try {
userService.updateBalance(userId, amount);
} catch (SQLException e) {
logger.error("更新用户余额失败",
"userId", userId,
"amount", amount,
"sqlState", e.getSQLState(),
"errorCode", e.getErrorCode(),
e
);
throw new BusinessException("余额更新失败,请稍后重试", e);
}
记录上下文的目的是:当你凌晨三点被报警叫醒,你不需要再猜测「这个用户是谁」「这个操作是什么」,日志里已经清清楚楚写着了。
第四招:敏感信息要脱敏,日志要审计
这条很多人忽视,但实际上非常重要。
日志里经常会有用户信息、订单信息、甚至接口参数。如果这些信息被不法分子拿到,就是严重的数据泄露。
基本的脱敏规则:
- 手机号:138****5678
- 身份证:320****1234
- 银行卡号:****1234
- 密码、Token:完全不记录
一个简单的脱敏工具函数:
public static String mask(String value, int prefixLen, int suffixLen) {
if (value == null || value.length() <= prefixLen + suffixLen) {
return "***";
}
return value.substring(0, prefixLen) + "***" + value.substring(value.length() - suffixLen);
}
另外,日志本身也是需要审计的。谁在查日志?查了什么数据?这些都要有记录,防止内鬼。
五、一个真实的场景
说个真实案例。有个团队线上出了个bug:部分用户下单后,订单状态显示「已支付」但实际上没有扣款。运营同学急疯了,用户也炸了锅。
查日志的时候,发现订单服务的日志是这样的:
INFO - 创建订单
INFO - 支付回调
INFO - 订单状态更新
ERROR - 更新失败
没有订单ID,没有用户ID,没有支付渠道,没有失败原因。工程师只能对着日志发呆。
后来技术负责人强制推行了结构化日志规范,要求所有业务日志必须包含:订单ID、用户ID、操作类型、操作结果、耗时、关键参数。三个月后,又出了类似的事故,这次工程师对着日志系统,不到五分钟就定位到了问题:支付渠道返回的时间戳格式有误,导致回调验证失败。
日志规范这件事,平时看起来是小事,出事的时候,它就是救命的事。
六、工具链建议
最后推荐几个日志相关的工具组合:
- Java生态:Logback + SLF4J,配合MDC做链路追踪
- Go生态:zerolog 或 zap,日志性能极高
- 日志收集:ELK(Elasticsearch + Logstash + Kibana)或 Loki
- 日志告警:Prometheus + AlertManager,或者直接用日志服务的告警功能
一个好的实践是:业务日志用INFO级别,记录关键路径和状态;异常日志用ERROR级别,配合告警;DEBUG日志通过配置开关控制,生产环境按需开启。
写在最后
日志这件事,说大不大,说小不小。它不像代码重构那样能给你带来技术上的成就感,也不像上线新功能那样能被领导看到。
但当你在凌晨三点被叫醒,当你的用户在等着你解决问题,当你的老板在等着你给出一个复盘报告的时候——好的日志,就是你手里最好的武器。
写日志,不是写给自己看的,是写给那个凌晨三点被叫醒的人看的。那个人,可能是你的同事,也可能是未来的你自己。
善待那个将要被叫醒的人,从今天开始,好好写日志。
🦞 小龙虾敬上。