> "你的日志里除了error就是info,出了事让我猜猜猜?"
开篇:一个让人崩溃的夜晚
那天凌晨,我正睡着觉,突然被一阵急促的报警声吵醒。生产环境报警,说某个接口超时。
我连滚带爬打开电脑,一通排查。最后发现——尼玛,数据库连接池满了。
我赶紧去看日志,结果日志里显示:
INFO - Starting application INFO - Started application in 2.345 seconds ERROR - Connection pool exhausted
没了。
我请问呢?!连接池什么时候满的?哪个接口干的?每个接口耗时多少?一概没有。
从那以后我就明白了一个道理:日志不是打了就行的,打得不对等于没打。
坑一:日志级别乱用 = 没打日志
见过最离谱的日志是这样的:
logger.info("用户登录");
logger.info("用户登出");
logger.info("查询订单");
logger.info("更新库存");
logger.info("发货");
清一色的info,跟记流水账似的。
出了事你想查?行啊,一条一条往上翻,看完上万条info,找到那一条error。
正确的做法:
// DEBUG - 调试信息,生产一般不开
logger.debug("SQL: {}", sql);
logger.debug("参数: {}", params);
// INFO - 正常业务流程
logger.info("用户 {} 登录成功", userId);
logger.info("订单 {} 已发货", orderId);
// WARN - 需要注意,但不影响流程
logger.warn("库存不足,当前库存: {}", stock);
logger.warn("缓存命中率为 0,可能是缓存挂了");
// ERROR - 错误日志
logger.error("订单 {} 发货失败", orderId, e); // 记得带异常!
关键点:
- error必须带异常堆栈,否则查啥?
- warn是有意义的,不是info的备用选项
- 没必要日志:循环内的普通日志、重复的info
坑二:日志里带敏感信息 = 给公司挖坑
这个太常见了:
logger.info("用户登录: username={}, password={}", username, password);
logger.info("下单参数: {}", request); // 里面可能有手机号、地址
logger.error("支付失败: {}", paymentRequest); // 可能有银行卡号
打日志一时爽,泄露火葬场。
正确姿势:
// 打印日志前脱敏
logger.info("用户登录, userId={}, ip={}", userId, ip);
// 或者用专门的脱敏工具
logger.info("下单参数: {}", SensitiveUtil.mask(request));
// 错误日志可以打,但不要打完整的request对象
logger.error("支付失败, orderId={}, error={}", orderId, e.getMessage());
坑三:日志格式太随意 = 无法解析
有些日志是这样的:
logger.info("订单创建成功");
logger.info("创建订单成功");
logger.info("order created");
logger.info("OK");
请问这都是什么玩意儿?
正确姿势:统一格式
// 推荐格式:时间 | 级别 | 模块 | 操作 | 结果 | 关键信息
// 2024-01-15 10:30:25 | INFO | OrderService | CreateOrder | Success | orderId=12345
// 2024-01-15 10:30:26 | ERROR | OrderService | CreateOrder | Failed | orderId=12346 | reason=库存不足
// 或者用JSON格式,方便日志收集系统解析
logger.info("{\"module\":\"OrderService\",\"action\":\"CreateOrder\",\"orderId\":{},\"status\":\"success\"}", orderId);
坑四:日志性能问题 = 系统更慢
同步日志会阻塞主线程!高并发下这就是灾难。
// 同步日志 - 慢!
logger.info("用户 {} 订单列表: {}", userId, orders);
// 异步日志 - 快!
// 配置异步logger,让日志写入不影响主业务
性能建议:
- 能用异步别用同步
- 避免日志中做复杂计算
- 日志写入别用StringBuilder,用占位符
// 不好 - 先拼接字符串,再打日志
logger.info("用户" + userId + "的订单:" + orderList.size() + "条");
// 好 - 占位符
logger.info("用户的订单: {} 条", orderList.size());
坑五:关键操作没日志 = 出了事找不到锅
这些场景必须打日志:
// 1. 外部调用
logger.info("调用支付网关, orderId={}, amount={}", orderId, amount);
logger.info("支付结果: {}", result);
// 2. 状态变更
logger.info("订单状态变更: {} -> {}", oldStatus, newStatus);
// 3. 异常处理
logger.error("处理异常, orderId={}", orderId, e); // 堆栈一定要带!
// 4. 关键分支
if (stock <= 0) {
logger.warn("库存不足, productId={}, stock={}", productId, stock);
}
记住:业务关键路径必须有日志覆盖。
实战技巧
1. 日志追踪TraceId
分布式系统必备:
// 请求入口生成traceId
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
// 后续所有日志都带这个 traceId
logger.info("查询订单");
// 日志格式加traceId
// %d{yyyy-MM-dd HH:mm:ss} %p %X{traceId} %c - %m%n
2. 关键日志打两份
重要日志既打业务日志,又打审计日志:
// 业务日志
logger.info("用户 {} 提现 {} 元", userId, amount);
// 审计日志 - 单独的文件,保留时间长
auditLogger.info("WITHDRAW|{}|{}|{}|{}", timestamp, userId, amount, status);
3. 错误日志的标准写法
// ❌ 错误示范
logger.error("出错了");
logger.error(e.getMessage());
logger.error("error", e); // 消息呢?
// ✅ 正确示范
logger.error("订单发货失败, orderId={}, 原因: {}", orderId, e.getMessage(), e);
总结
日志这事儿,说简单也简单,说复杂也复杂。
核心原则:
- 级别分明 - error/warn/info/debug各司其职
- 关键路径全覆盖 - 业务流程、异常处理、外部调用必须有日志
- 格式统一 - 方便检索和分析
- 敏感信息脱敏 - 密码、手机号、银行卡别往日志里打
- 异步优先 - 高并发系统用异步日志
最后送大家一句话:
日志是给自己看的,不是给面试官看的。
写得再多,不如写得有用。
本文作者:小小龙虾
一个被日志坑过的后端工程师