出事后,日志要能当证据。
一、只翻到一条 DELETE 200
周五上午,一个团队客户来信:「上周三下午,谁把我的项目删了?整个看板都不见了。」
他打开应用日志,按时间筛到那条请求,只有一行:DELETE /api/projects/prj_8f21 200。没有操作人,没有工作区,也没有删除前的名字。日志能证明「删了」,不能回答「谁删的」。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
应用日志回答「发生了什么」,审计日志回答「谁做的」。 两者都叫日志,但字段、保留期和访问权限完全不同。
缺的那一列是 actor。应用日志围绕请求,审计日志围绕「人 + 动作 + 目标」;没有 actor,误删和定时任务在日志里长得一模一样。
客户要的也不只是追责。他要知道谁能删、删之前长什么样、能不能恢复;这三件事恰好对应审计记录里的 actor、before 和保留策略。
二、12 类事件:必记、保留与可见范围
先定清单,再写代码;清单之外的事件不加字段、不进审计表。
| 事件 | 是否必记 | 保留期 | 谁能看 |
|---|---|---|---|
| 成员邀请 | 必记 | 24 个月 | Owner、Admin |
| 成员接受 | 必记 | 24 个月 | Owner、Admin |
| 成员移除 | 必记 | 24 个月 | Owner、Admin |
| 角色变更 | 必记 | 24 个月 | Owner、Admin |
| 所有权转移 | 必记 | 36 个月 | Owner |
| API key 签发/撤销 | 必记 | 24 个月 | Owner、Admin(只看末四位) |
| 数据导出 | 必记 | 24 个月 | Owner、Admin |
| 数据删除 | 必记 | 36 个月 | Owner |
| 登录失败 | 必记(摘要) | 90 天 | 本人、Owner |
| SSO 配置 | 必记 | 36 个月 | Owner |
| 账单变更 | 必记 | 7 年 | Owner、财务 |
| 集成安装 | 选记 | 12 个月 | Owner、Admin |
必记与选记的界线是「能否改变权限或数据」。 能改的必须记,只读的可以抽样。
邀请和接受要分开记。邀请只证明某人发出了请求,接受才证明权限真的生效;只记一条,出事时分不清「发出去了」和「进来了」。
登录失败只记摘要:时间、账号哈希、来源 IP、失败原因。密码、验证码、完整令牌一律不落库;API key 也只留末四位,审计要回答「哪把钥匙开的门」,不需要把钥匙再抄一遍。
数据导出是最容易被忽略的一条。导出不是破坏性操作,但它常常是泄露的前一步;没有这条记录,客户问「数据出去过吗」时只能沉默。
保留期取「追责窗口」和「合规下限」中较长的一个:账单 7 年是税务口径,删除 36 个月覆盖一次年度审计,登录失败 90 天够查一次撞库。可见范围按最小必要给,Support 要看必须由客户在工单里授权。
写不进审计的事件,等于没发生。 这不是技术判断,是客户验收时的第一句话。
三、一条记录只需要 9 个字段
字段不是为了好看,是为了让「谁、何时、对什么、做了什么、变成什么」各占一格。
| 字段 | 例子 | 注意 |
|---|---|---|
| 时间 | 2026-10-21T14:03:21.418Z | UTC 存储,展示时再转时区 |
| actor | usr_7f3a / 简宁 | 存 id,同时存显示名快照 |
| 组织 | org_2c91 | 所有查询的第一道过滤 |
| 工作区 | ws_5d02 | 团队档可能有多个工作区 |
| 动作 | project.delete | 固定词表,不用自然语言 |
| 目标类型 | project | 与动作前缀保持一致 |
| 目标 id | prj_8f21 | 稳定 id,不用名称 |
| 变更前后 | {“name”:“旧看板”} | 只存差异,不存整行 |
| 来源 IP/UA | 203.0.113.7 / Chrome 141 | 用于异常登录判断 |
before/after 只存差异,不存整行。 整行会把审计表变成第二份业务数据,既贵又危险。
差异用 op / path / value 三个键表达:{"op":"replace","path":"/name","value":"旧看板"}。删除记 remove,新增记 add;读的人不需要猜测字段含义。
整行存储有三个问题:体积按行放大、敏感字段被复制、表结构一变历史记录就对不上。差异方案只碰真正变化的字段,也天然避开了「审计表里存着客户全量数据」。
审计表只允许追加。没有 UPDATE,没有 DELETE,需要更正就再写一条冲正记录;否则「不可篡改」只是一句口号。
actor 要存 id 加显示名快照。成员改名或离职后,只存 id 会让三年后的查询显示「已注销用户」,存快照才能回答「当时是谁」。
时间用 UTC,另加一个单调递增的序列号。同一毫秒内的两条记录靠序列号排序,不靠时间戳猜先后。
四、3600 条一个月,成本不在磁盘
按 20 个团队、每团队每月 180 条估算,存储小到可以忽略,真正的工作在索引与查询。
写入量 = 20 团队 × 180 条/月 = 3,600 条/月
单条 400 字节 → 3,600 × 400 = 1,440,000 字节 = 1.37 MiB/月
保留 12 个月 → 1.37 × 12 = 16.5 MiB
整行存储(均 2 KB)→ 3,600 × 2,000 = 7,200,000 字节 = 6.87 MiB/月
含索引与副本约 25% → 8.58 MiB/月,约 8.6 MiB/月
单条放大 5 倍,月体积约 6.3 倍
查询目标:谁 + 何时 + 删了什么 ≤ 5 分钟
16.5 MiB 一年的表,瓶颈只可能是索引和权限。 数据量从来不是这类系统的难点。
16.5 MiB 比一张手机照片还小。若你的审计表超过 1 GB,先查是不是把读操作也记了,而不是急着分库。
整行方案每月 8.6 MiB,一年约 103 MiB,看起来也能接受。但它的代价不是磁盘,是把客户数据复制了一份,保留期越长,泄露面越大。
查询要按「组织 + 时间」建复合索引,再按 actor 和目标 id 建两条辅助索引。没有组织前缀的查询,迟早会扫到别的租户。
导出给客户时按组织过滤,并在文件里写明导出人和导出时间;导出动作本身也要进审计。保留期到期用分区删除,按月分区删一个分区是元数据操作,用 DELETE 扫表会在最忙的时候锁住全表。
五、行动清单与复算验收
先把字段和清单补齐,再谈审计平台。
- 固定 12 类事件名,写进一份共享词表。
- 每条记录写 actor_id 与显示名快照。
- before/after 用差异表达,禁止整行复制。
- 审计表只开 INSERT 权限,禁 UPDATE 与 DELETE。
- 按「组织 + 时间」建复合索引,再补 actor 与目标 id。
- 按事件设定保留期,账单 7 年、删除 36 个月。
- 后台加「按目标查历史」入口,支持按组织导出。
- 每季度演练一次「谁在何时删了什么」。
验收标准:客户报出一个时间和一个对象,你能在五分钟内给出 actor、动作、变更前后与来源 IP;做不到,说明审计表还只是应用日志的第二份拷贝。
使用边界与实操验收
本文只讨论产品内的租户审计,不覆盖基础设施日志、法律取证与跨境数据合规。
审计日志不是应用日志的复制品。 应用日志可以采样、可以轮转、可以丢;审计日志一条都不能少。
读操作默认不记。只有涉及导出、密钥、账单的读才值得留痕;把每次列表查询都记下来,3,600 条会变成 360 万条。
审计日志本身要备份,而且备份要独立于业务库。业务库被误删时,审计记录往往是最先被一起删掉的那份。
不可篡改不等于上链。追加写、独立账号、只读副本三件事做到,已经能挡住绝大多数内部误操作。
客户导出审计记录时,要先脱敏其他租户的 id。审计表里有 actor 和 IP,跨租户泄露一次,比没有审计更糟;员工访问审计表同样要记一条,谁看了谁的记录本身就是审计的一部分。
关键参考与核验范围
核验日期:2026-10-25。以下链接用于核对日志管理与审计字段的一般做法,本文未逐条引用其内容。
- NIST SP 800-92:Guide to Computer Security Log Management :用于核对日志管理流程与保留策略。
- OWASP Logging Cheat Sheet :用于核对「记什么、不记什么」的字段边界。
引用口径。 本文只借用「事件清单 + 结构化字段 + 追加写」这一一般结构;20 团队、3,600 条、400 字节与 12 个月全部是教学假设,与上述材料无关。
核验范围。 核验的是「九个字段能否支撑五分钟回答」,不是比对某家审计服务的功能与计价。
如何自行复核。 取一条最近的删除操作,只允许看审计表,计时回答 actor、时间、目标与变更前内容;超过五分钟就补字段或补索引。
系列导航
- 上一篇:数据隔离:一行 workspace_id 与四道防线
- 下一篇:席位与权限的一致性:加人、降级与离职怎么计费
- 回到目录:一个人的多租户:组织、邀请、角色与数据隔离
- 相关阅读:日志与错误聚合:在五分钟内找到那一次失败
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。