日志与审计:记九类、脱敏五列、留一百八十天

客户问那 12 条任务是谁删的,他翻了三个小时日志只看到一串时间戳。此后他记九类事件、脱敏五列、用请求 ID 串联,每天 420 条、一年 153,300 条与 184 MB,免费留 180 天、付费留 365 天,把追责变成一次查询。

日志的价值不在写的时候,而在你希望自己当时多写了一行的时候。

一、三个小时找不到那一条

2026 年 7 月的一个下午,一位客户来信:昨天下午还有的 12 条任务,今天早上不见了,问是不是系统出了问题。他第一反应是查删除记录,可审计表里只有一行「批量删除,12 行」,没有操作者,也没有来源。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

他退而求其次去翻应用日志。日志里存的是方法、路径、状态码与耗时,时间戳精确到毫秒,但没有用户 ID,也没有请求 ID。他只能按时间窗口筛出 400 多条记录,再对着会话表一条条猜:这一条属于谁,那一条是不是同一个人的重试。

三个小时后他拼出了大概:客户团队里另一位成员在整理看板时勾错了筛选条件。结论不算坏,但这个过程暴露了一个更坏的事实——系统记了很多字,却没有记能回答「谁在什么时候对什么做了什么」的那几个字段。

他想要的其实不多:一行能说明账号、动作、目标与结果,再配一个能把这行和同一请求的其它记录串起来的值。三个小时里他真正需要的只有四行,而系统留下了四百多行无关的字。

当天晚上他重写了日志规范。不是加更多日志,而是先定四个必须回答的问题:谁、什么时候、对什么、结果如何;再让每类事件只写回答这些问题所需的字段。日志从「调试时的副产品」变成「出事时的证据」。

二、九类事件:只记能回答四个问题的记录

九类事件的挑选标准是:涉及身份、权限、数据离开系统或产生费用的动作。普通页面浏览、列表查询、健康检查不记,它们量大且回答不了任何追责问题。

事件类必记字段典型问题
认证成功账号 ID、IP 网段、客户端、时间这个账号从哪里登录的
认证失败与锁定账号标识哈希、失败原因、计数是手滑还是撞库
会话撤销与全设备登出会话 ID、发起者、原因谁把谁的登录踢掉了
权限与角色变更操作者、目标账号、变更前后角色谁给了他管理员
数据导出操作者、行数、筛选条件摘要谁把数据带走了
文件上传操作者、MIME、大小、存储键这个文件是谁传的
密钥与令牌操作密钥 ID、动作、操作者这把钥匙什么时候换的
管理后台操作操作者、路由、目标对象后台被谁动过
计费与套餐变更操作者、原套餐、新套餐这次升级是谁点的

九类之外还有一类常见需求:读取敏感数据。他没有记每一次读取,因为 344 个账号的日常读取会把日志量推高一个数量级,而收益有限;他只在导出与管理后台查看时记,这两个动作才是数据真正离开系统的地方。

字段的共同点是「四个问题」:谁、什么时候、对什么、结果如何。任何一条日志缺其中两个以上,就不值得写;写了也答不上问题,只是让磁盘更满、检索更慢。

审计表与应用日志分开放。审计表只追加,不更新、不删除,应用账号只有插入权限;应用日志可以轮转、可以采样。把两者混在一张表里,迟早会为了「清理日志」而删掉证据。

审计表还要存变更前后的值,但只存与权限、状态、归属相关的列,不存正文。任务标题可能含客户隐私,角色从「成员」变成「管理员」才是要追的事。

认证失败不记用户输入的密码,只记失败原因分类:密码错、账号不存在、第二因素失败。原因分类只进审计,不回给前端——前端看到的永远是同一句「账号或密码错误」,否则日志的分类能力会先被用来枚举账号。

文件上传只记存储键与 MIME,不记原始文件名。原始文件名可能含客户名或项目名,而存储键足以定位对象;需要展示时从业务表取,不从日志取。

每天 420 条是这么来的:认证成功约 120 条、认证失败与锁定约 130 条、会话撤销约 20 条、权限变更约 15 条、导出约 30 条、上传约 45 条、密钥操作约 10 条、后台操作约 35 条、计费变更约 15 条。九类相加正好 420,它只占普通应用日志的很小一部分,因此可以写得更细、留得更久。

三、五列脱敏与一条请求 ID

脱敏不是把日志变模糊,而是把「能定位」与「能复用」分开:保留足够定位问题的信息,去掉足以造成二次伤害的部分。他固定脱敏五列,写入前处理,读取时不再加工。

列原始值脱敏后原因
密码明文或哈希完全不记录任何形式都不该出现在日志里
令牌完整 JWT 或 API Key前 6 位加长度可关联同一把钥匙,不可用于重放
卡号16 位数字后 4 位对账够用,不足以发起支付
邮箱alice@example.com仅保留域名部分可按域名定位组织,去掉个人标识
IP203.0.113.42203.0.113.x保留网段可查来源,去掉主机位

密码与令牌的规则不是「脱敏」而是「禁止」:连哈希都不写,因为哈希在弱口令面前等于明文。调试时想看请求体,用一次性的本地开关,开关默认关闭,且不接受生产环境的配置。

邮箱只留域名会带来一个副作用:同域名的两个用户无法区分。他的处理是同时写账号 ID,账号 ID 是内部生成的,不携带任何个人信息;需要定位人时用 ID 查用户表,而不是在日志里存邮箱。

IP 截断最后一段之后,仍然能回答「是不是同一个网段」,但回答不了「是不是同一台机器」。这是有意的取舍:精确到主机位的 IP 在多数司法辖区属于个人信息,留存 180 天以上的风险大于收益,具体口径以当地法律与专业意见为准。

请求 ID 是串联的钥匙。入口中间件为每个请求生成一个 ULID,写进响应的 X-Request-Id 头,同时传给数据库审计插入、后台任务与外发 Webhook;用户在工单里贴这个 ID,他就能把一次操作涉及的所有记录拉成一条时间线。

没有请求 ID 时,跨表关联只能靠时间戳与用户 ID 猜;有了它,一次导出会留下入口日志、审计行、对象存储访问记录三条,三条共用同一个值,拼接不需要猜。

请求 ID 必须由入口生成,不能直接采用客户端传来的值。客户端可以随便填,填一个已存在的 ID 就会把两条时间线搅在一起;如果确实要透传上游的追踪值,另开一列存,不要和自生成的 ID 混用。

客户端 IP 要取可信代理链上的那一跳,而不是请求头里的第一个值。转发头可以被伪造,按它做限流或审计等于把判断权交给调用方;他只信任自己那一层代理写入的值,配置以所用代理的文档为准。

请求 ID 本身不含用户信息,也不该可预测。ULID 带时间前缀,排序友好,但不暴露账号;如果换成自增数字,别人就能从 ID 推出系统一天处理了多少请求,这属于不必要的泄露。

四、十五万三千三百条与一百八十四兆

容量要算清楚,否则「多记一点」会在半年后变成一张无法查询的表。他按每天 420 条、每条 1.2 KB 估,一年 153,300 条、184 MB;这个量级用一张普通表加时间分区就能撑住,不需要单独的日志系统。

复算:
每天 420 条 × 365 天 = 153,300 条/年
153,300 × 1.2 KB = 183,960 KB ÷ 1,000 = 184 MB/年
免费档留存 180 天 = 420 × 180 = 75,600 条 ≈ 90.7 MB
付费档留存 365 天 = 153,300 条 ≈ 184 MB
两档差 = 184 − 90.7 = 93.3 MB
平均每小时 = 420 ÷ 24 = 17.5 条

184 MB 一年,存储成本几乎可以忽略,真正昂贵的是查询与隐私。表按天分区,查询带时间范围就能裁掉绝大多数分区;不建全文索引,只建账号 ID、请求 ID、事件类三个索引,写入才不会因为索引维护而变慢。

留存分两档:免费 180 天,付费 365 天。这个差别不是为了省钱,而是产品承诺的一部分——付费用户可能要应付一次内部审计或客户纠纷,一年覆盖一个完整的合同周期;免费用户留半年,足够排查绝大多数问题。

到期清理用每日任务,按分区整块删除,不做逐行删除。逐行删会留下大量死元组,真空跟不上,表会越用越慢;整分区删除是常数时间,也不会长时间持锁。

1.2 KB 是含行头、索引与字段名的估算,不是净载荷。真实采样时他取了一天的记录做平均,最大的几条是带变更前后值的权限记录,最小的是认证成功;按平均值估容量会低估峰值,所以分区按日切,单日超了也不会影响别的分区。

420 条/天对应 344 个账号、120 个付费用户,平均每个账号每天一点多条。这个密度说明日志量主要由后台任务与失败重试贡献,而不是用户活跃度;如果哪天账号翻倍,条数不会线性翻倍,先看的是重试次数。

他没有把日志同步到第三个系统。一个人的团队里,多一个存储就多一份访问控制、多一份备份、多一份可能配错的权限;等真的需要跨服务检索时再引入,而不是现在就为它维护一套账号。

五、行动清单与复算验收

八条清单按「先定义事件、再保证可串联、最后管住留存」排。第一条决定记什么,中间四条决定怎么记,最后三条决定留多久、怎么查。

  • 列出九类事件,每类写清「谁、何时、对什么、结果」
  • 每条记录带请求 ID,并在响应头回给客户端
  • 审计表只追加,应用账号只有插入权限,不与普通日志混表
  • 五列在写入前脱敏,脱敏函数有单元测试覆盖
  • 密码与令牌任何形式都不落日志,包括调试与异常堆栈
  • 免费留 180 天、付费留 365 天,到期按分区整块清理
  • 按请求 ID 与账号 ID 两个入口都能在 5 分钟内查到记录
  • 每季度抽一条真实工单,用日志还原完整时间线

验证方式很土:随便挑一条昨天的请求 ID,从入口日志查到审计行,再查有没有对应的存储访问记录,三步都能命中才算通。查不到就先补字段,别急着加日志量。

验收标准:给定一个请求 ID 或账号 ID,能在 5 分钟内还原相关记录,且日志里找不到密码、完整令牌、完整卡号、完整邮箱与完整 IP。

使用边界与实操验收

边界一:日志不是审计。 普通日志可轮转、可采样、可被应用账号改写;审计表只追加、权限分离、保留变更前后值。两者混用,出事时拿不出可用的证据。

边界二:脱敏要在写入前做。 查询时再脱敏,意味着明文已经落盘、已经进备份、已经可能被读走;脱敏函数放在写入口,并且用测试锁住,不靠代码评审时的记性。

边界三:留存期限不是越长越好。 180 天与 365 天是产品口径,法务上的最短与最长留存以具体司法辖区与专业意见为准;用户行使删除权时,日志里的个人数据也要能被删除或匿名化。

边界四:日志本身会被注入。 用户输入里的换行与伪造的时间戳可以污染按行解析的日志;用结构化格式写入并对换行做转义,读取时按字段解析,不按行号猜。

边界五:本文的条数、体积与留存均为教学假设。 420 条/天、1.2 KB/条来自一个人的估算,换一个产品会差一个数量级;容量规划应以真实采样为准,本文不构成对任何合规期限的承诺。

关键参考与核验范围

  • OWASP Logging Cheat Sheet :用于核对应记录的事件、字段与不应记录的内容。
  • OWASP ASVS :用于定位日志与审计要求在应用安全验证标准中的归属。
  • NIST SP 800-61r3 :用于核对事件记录在响应流程中的用途与时间线还原思路。
  • CWE Top 25 :用于核对日志注入类弱点的编号与分类位置。

以上链接于 2026-10-26 实测可公开访问;未核验各标准的版本号与条款编号。核验日期:2026-10-26。

边界句。 本文只讨论单人 SaaS 的事件记录、脱敏与留存,不讨论 SIEM、入侵检测与取证链的法律效力。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元