4. 日志数据结构设计:字段定义
字段设计这件事,说白了就是给你每一笔操作做一张身份证。审计日志能不能用、能不能查、能不能过合规审计,全看这张身份证信息全不全。我个人习惯,在设计字段之前先问自己三个问题:谁做了什么?什么时候做的?结果怎么样?把这三个问题回答清楚了,字段至少不会漏。
我在项目中遇到过好几回,客户上线后才发现日志字段不够用,结果要么补丁打得像狗皮膏药,要么只能阉割审计能力。嗯,所以这堂课咱们先把字段的骨架搭清楚。
4.1 字段设计原则
字段不是越多越好。多到没边界,存储扛不住;少到不够用,审计过不了。我一般遵循四个原则:
- 必要性——每个字段都得有明确用途,能推导出来的就别单独存。
- 原子性——每个字段只存一种信息。比如事件ID和时间戳就别混在一块儿。
- 可审计性——字段的值必须可还原、可验证,不能出现「未知」这种黑洞。
- 可扩展性——预留元数据字段,应付未来需求变化。
4.2 核心字段详解
核心字段一共6个。我把它们叫做审计日志的「六边形战士」——缺一个,审计逻辑就站不稳。
| 字段名 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
| event_id | UUID v4 | 是 | 全局唯一事件标识 | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
| timestamp | ISO 8601 | 是 | 操作发生的UTC时间 | 2025-04-10T14:30:00.123Z |
| user_id | string | 是 | 执行操作的用户标识 | usr_7a8b9c0d |
| action | enum | 是 | 操作类型,预定义枚举值 | KEY_GENERATE |
| result | enum | 是 | 操作结果状态 | SUCCESS |
| metadata | JSON | 否 | 上下文附加信息 | {"key_type": "RSA-2048"} |
4.3 事件ID(event_id)
事件ID是整条日志的唯一门牌号。我见过有人用自增整数,结果跨节点同步时疯狂冲突——教训深刻。
我建议用 UUID v4 或 ULID。UUID v4 随机性强,几乎不可能碰撞;ULID 还可以按时间排序,方便做分页查询。说白了,别在这里搞聪明,规规矩矩用标准方案。
4.4 时间戳(timestamp)
时间戳是审计链条里的锚点。一个原则:永远存UTC,永远存精确到毫秒。
记得有一次对接合规审计,对方要求时间戳必须能追溯出是谁先操作、谁后操作。如果是秒级精度,同一秒内的操作就彻底乱套了。所以我后来强制要求毫秒级,如果平台支持还可以上微秒级。
// 推荐的时间戳格式
// ISO 8601 带毫秒和时区标识(统一存UTC)
2025-04-10T14:30:00.123Z
// 不推荐的做法
// 1. 不带时区 —— 跨时区看不懂
// 2. 只有日期没有时间 —— 基本等于没写
// 3. 使用本地时间不记录偏移量 —— 审计必崩
4.5 用户标识(user_id)
user_id 不是随便一个字符串就行。它需要满足三个条件——
- 可溯源: 能关联到真实用户或服务账户。
- 不可抵赖: 审计日志里的 user_id 必须和认证系统打通,不能出现孤立的ID。
- 坚持用唯一ID: 不要用邮箱、手机号。这些会变,一变审计链条就断了。
我曾经在一个项目里用邮箱做 user_id,结果老员工离职后邮箱被回收,新员工注册同一个邮箱……嗯,审计报告里两个人变成同一个人了。所以现在一律用 用户系统主键ID,配合 context.client_ip、context.user_agent 一起记录,交叉验证。
4.6 操作类型(action)
操作类型必须用枚举。为什么呢?自由填字符串的话,「GenerateKey」「gen_key」「创建密钥」会同时出现,查询时生不如死。
我维护的枚举表长这样:
| 操作大类 | 枚举值 | 说明 |
|---|---|---|
| 密钥管理 | KEY_CREATE |
创建新密钥 |
KEY_DELETE |
删除密钥 | |
| 加密操作 | ENCRYPT |
加密数据 |
DECRYPT |
解密数据 | |
| 签名验证 | SIGN |
数字签名 |
VERIFY |
验证签名 | |
| 配置变更 | CONFIG_UPDATE |
更新SDK策略 |
你想想看,枚举值一旦发布就不能改了。所以我一般会在设计阶段拉上安全、运维、法务一起过一遍,把所有可能操作都列出来。漏掉的怎么办?版本迭代加,但别删旧值。
4.7 结果(result)
结果不能只写 SUCCESS 或 FAILURE。尤其是失败时,必须带上 错误码 和 错误原因。否则审计看到一行 FAILURE,根本不知道哪里出了问题。
// 合理的结果结构(放入metadata)
{
"result": "FAILURE",
"error_code": "KEY_ALREADY_EXISTS",
"error_message": "试图创建的密钥ID已存在"
}
4.8 元数据(metadata)
元数据是审计数据的弹性缓冲区。SDK版本、客户端IP、请求追踪ID、密钥ID……这些不定长但又可能重要的信息,统统放进一个JSON对象里。
我设计 metadata 时有个习惯:JSON 的 key 用 snake_case,值类型尽量用标量。为什么?因为下游日志解析工具对嵌套数组和支持有限,扁平的 JSON 才是通用语言。
{
"sdk_version": "1.3.2",
"client_ip": "192.168.1.10",
"user_agent": "MyApp/2.1 (iOS)",
"key_id": "key_abc123",
"request_id": "req_9876fedc",
"encryption_algorithm": "AES-256-GCM"
}
4.9 本章知识体系
这六个字段组成了审计日志的数据骨架。下面这张图可以帮你快速把握整体关系——
这个结构我做成了几个关键设计决策供参考:
- event_id 是根节点,其他所有字段都是它的属性,逻辑上便于索引和查询。
- timestamp 和 user_id 是审计真实性的基石,前者防篡改时间线,后者防抵赖责任链。
- metadata 是弹性扩展层,内部字段可增可减,不影响核心审计的完整性。