
前年冬天的一个凌晨我被值班同事的电话从被窝里拽出来。对方的声音有点抖你赶紧看下群用户手机号在日志平台里明文暴露了法务那边已经收到了投诉。我猛地坐起来打开电脑看到那行日志的瞬间后背一凉log.info(用户信息{}, JSON.toJSONString(user))而那个user对象里除了昵称、年龄还有一个没被JsonIgnore修饰的phone字段。这条日志每天打印几万次在ELK里安安稳稳躺了三个月直到某天被一位较真的用户发现截图发到了社交平台。那一次事故没有导致数据泄露却让公司公关部忙了整整一周。最后的技术复盘结论简单到荒唐打印日志的时候没有人想过手机号该不该出现在这里。日志是给未来自己的遗书很多后端开发者对日志的认知停留在方便排查问题这个层面于是代码里到处都是log.info(进入了xx方法)和log.info(参数是 param)。这种习惯在开发阶段确实爽——一眼就能看到程序跑到了哪里。但生产环境不是游乐场每一行日志都在消耗磁盘、网络和未来那个熬夜排查问题的你的精力。日志规范的第一条也是最容易被忽视的一条区分日志级别不是给你看的是给监控系统看的。ERROR是系统真出了岔子需要立刻有人介入WARN是虽然能跑但埋了隐患值得关注INFO是业务关键节点的里程碑比如订单支付成功、用户注册完成DEBUG是给开发环境准备的碎碎念生产环境千万别开。把所有东西都打成INFO等于把所有东西都打成了噪音。那行不该出现的手机号回头说那次事故。如果团队有一套明确的日志脱敏规范那行JSON根本不会打印出来。所谓规范就是在刀砍下来之前就把刀刃包好。我们后来定了两条铁律第一任何包含phone、idCard、email、password字段的对象打印之前必须经过脱敏工具类处理手机号只显示前三位和后四位第二禁止直接用JSON.toJSONString()打印整个请求体或响应体必须按需选取字段。这两条规则写进文档没人看我们就写进了Code Review的Checklist里——每次MR必须确认新增的日志没有敏感字段。三个月后有同事在Review时拦住了一段log.info(用户信息{}, user)原因就是代码里没有调用脱敏方法。规范的落地不是靠自觉是靠流程把侥幸心理堵死。上下文比日志本身更值钱另一个隐蔽的痛点是上下文丢失。一个用户请求从网关到服务A再到服务B中间经历了好几次异步线程和RPC调用。如果日志里没有统一的traceId排查问题时就只能靠时间戳和IP拼凑像是把一本撕碎的书重新拼起来。没有上下文的日志就是一堆带时间戳的废话。我们在日志规范里强制要求每个请求入口生成一个traceId通过MDC放进SLF4J的上下文中所有日志格式统一加上[%X{traceId}]。这样一条完整的调用链用grep就能把所有相关日志捞出来按时间排序就是一篇完整的事故现场还原。第一次用这个方式排查线上问题时我对着终端屏幕愣了三秒——以前要花半小时串联的日志现在十秒钟就看懂了。异常日志的黄金法则还有一句教训是用停电换来的。有一次线上故障排查时同事发现catch块里只写了一行e.printStackTrace()控制台里什么都有但日志平台里一片空白。因为标准错误流没有被接入日志收集器。异常日志的三不原则不吞、不裸、不重复。不吞——catch到异常后至少要log.error(业务描述关键参数, e)把堆栈和业务上下文一起留下不裸——不要只写log.error(e.getMessage())异常信息往往不含业务参数你根本不知道是哪条数据出的问题不重复——高层和低层不要在同一场异常里各打一遍否则排查时满屏都是同一堆栈的复制粘贴。规范的价值在事故之前看不见在事故之后追不回那次手机号事件之后我们花了两周把全项目的日志打印点过了一遍。删掉了几百条毫无意义的info(执行完毕)给几十处敏感字段加了脱敏统一了所有服务的日志格式。做完这些事的第二周线上又出了一次故障但这次因为有完整的traceId和清晰的异常上下文定位时间从过去的平均四十分钟缩短到了八分钟。日志规范不是写给机器看的是写给三个月的自己看的。当你凌晨三点被叫起来处理故障时最怕的不是问题有多复杂而是翻遍日志只看到一堆进来了和出去了却找不到那条真正告诉你这里为什么炸了的关键信息。规矩这东西写在文档里是累赘写进事故里就是学费。愿你交过一次学费之后就不必再交第二次。