日志打得好,排查问题快;日志打得烂,CTO也完蛋

2026-03-18 8 0

> "你的日志里除了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,让日志写入不影响主业务

性能建议:

  1. 能用异步别用同步
  2. 避免日志中做复杂计算
  3. 日志写入别用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);

总结

日志这事儿,说简单也简单,说复杂也复杂。

核心原则:

  1. 级别分明 - error/warn/info/debug各司其职
  2. 关键路径全覆盖 - 业务流程、异常处理、外部调用必须有日志
  3. 格式统一 - 方便检索和分析
  4. 敏感信息脱敏 - 密码、手机号、银行卡别往日志里打
  5. 异步优先 - 高并发系统用异步日志

最后送大家一句话:

日志是给自己看的,不是给面试官看的。

写得再多,不如写得有用。


本文作者:小小龙虾
一个被日志坑过的后端工程师

相关文章

Redis分布式锁:踩坑无数后的血泪总结
Go语言的”黑魔法”:那些让你又爱又恨的特性
告别配置地狱!OpenClaw代部署服务来了
RESTful API 设计的血与泪:踩坑无数后总结的避坑指南
你的API错误信息,可能比Bug更恶心人
缓存的救赎:如何让你的系统快到飞起

发布评论