4. 核心数据结构:审计事件 Event、事件类型、元数据字段
好,我们开始啃这块硬骨头。
上一章我们把 SDK 的架子搭起来了,能初始化、能注册插件。但真正干活的时候,数据长什么样?你想想看,用户登录、文件下载、权限变更……这些行为千奇百怪,怎么统一成一个结构体,让下游的存储、告警、分析模块都能认?
答案就是 审计事件(Event)。这个东西是整条数据流水线的基石。我刚开始做审计 SDK 的时候,觉得 Event 嘛,不就是塞一堆字段进去就行了?结果项目一跑,发现日志没法查、告警总误报、存储爆炸——全都是因为事件结构设计得太随意了。
这节课,我们就把 Event 的里里外外讲透。说白了,就是搞清楚三个事:事件长什么样、事件有哪些种类、元数据该怎么塞。
4.1 审计事件的骨架:Event 结构体
先看核心定义,这是我个人习惯使用的简化版结构,但精髓都在里面了。
@dataclass
class AuditEvent:
# 1. 事件唯一标识
event_id: str
timestamp: datetime
# 2. 事件类型(核心分类)
event_type: str
# 3. 主体(谁干的)
principal_id: str
principal_type: str # 'user', 'service_account', 'system'
# 4. 客体(对谁干的)
resource_id: str
resource_type: str # 'file', 'database', 'api_endpoint'
resource_zone: str # 资源所在区域/命名空间
# 5. 行为(干了什么)
action: str # 'read', 'write', 'delete', 'grant'
action_result: str # 'success', 'denied', 'error'
# 6. 上下文(怎么干的)
source_ip: Optional[str] = None
session_id: Optional[str] = None
user_agent: Optional[str] = None
# 7. 自定义元数据(扩展载荷)
metadata: Dict[str, Any] = field(default_factory=dict)
这个结构看着字段不少,其实可以分成三组:
- 身份组:谁 + 对谁。这是审计的核心,你不能只记录「有人删了文件」,你得知道是哪个倒霉蛋删了哪个重要文件。
- 行为组:干了什么 + 结果如何。成功和失败要分开记,不然排查问题时你根本看不出来是权限不够还是代码写错了。
- 环境组:IP、请求 ID、时间戳。这些是辅助线索,但往往能救命。我记得有一次排查安全事件,全靠 session_id 把一串异常操作串了起来。
核心原则:不要为了「差不多够用」就删字段。审计数据一旦漏了关键信息,事后是补不回来的。宁可多塞一个可选的 metadata,也别在设计阶段拍脑袋砍字段。
4.2 事件类型的分类体系
你可能会问:event_type 不是有个 action 字段了吗?为什么还要单独拆一个 event_type 出来?
嗯,这是个好问题。action 描述的是「动作」,而 event_type 描述的是「事件的归属类别」。这两者有本质区别。
| 分层 | 名称 | 示例值 | 作用 |
|---|---|---|---|
| L1 | 领域分类 | iam、storage、compute |
快速过滤,知道是哪个子系统干的 |
| L2 | 行为分类 | access、modify、admin |
区分读写和特权操作,方便设置告警优先级 |
| L3 | 详细动作 | object.get、policy.attach |
精确描述,用于查询和报表 |
我个人习惯把 event_type 存成字符串,比如 storage.modify.object.put。这样在 ES 或 ClickHouse 里做前缀匹配查询特别快,还能按层级做聚合。你想想看,如果只存一个「put」,想查所有「modify」操作时就只能写一堆 OR 条件,多累啊。
事件类型还有一个隐藏维度:操作型 vs 状态型。操作型事件是「瞬间行为」,比如读文件、改密码。状态型事件是「持续性结果」,比如用户角色过期、资源被锁定。我建议把这两类分开存储,因为状态型事件往往需要额外的聚合运算,混在一起会让查询变得很慢。
4.3 元数据字段的选型与扩展
metadata 那个 Dict,是整个 Event 里最有弹性、也最容易翻车的地方。为什么?
- 太放开:什么乱七八糟的数据都往里塞,最后 JSON 膨胀到几 KB,存储和带宽都扛不住。
- 太保守:业务方想多记一个字段,你告诉他「没地方存」,然后他就在备注里手写……这种审计记录等于废纸。
那我们怎么折中? 我的做法是制定一个「元数据规范层级」:
- 核心字段:上述 dataclass 里的字段。这些必须填,是硬性要求。不填就不生成事件。
- 推荐字段:比如请求耗时、API 版本号、客户端 SDK 版本。审计 SDK 内部会帮你自动填充,但你可以选择不存。
- 扩展字段:业务方自定义的,全塞进 metadata 里。但我们强制要求 key 使用命名空间前缀,比如
order.payment_id、sso.original_ip。
这样做的好处是:核心数据保证可读性和查询效率,扩展数据保留了业务的灵活性。我曾经在一个项目中接过客户的需求,他要在审计日志里记录「采购单的审批链」。如果当时没留 metadata 这个口子,整个 Event 结构都得重新设计。
一个小技巧:在 metadata 里加上 _version 字段,标识元数据的 schema 版本号。这样后续结构升级时,你能知道哪些旧数据是哪个版本的,不至于出现数据兼容性灾难。
4.4 我踩过的坑:审计事件设计三大雷区
讲完了理论,我来聊聊实战中容易掉进去的坑。
- 雷区一:用 `int` 存事件类型。
你以为用枚举值压缩空间很聪明?错。查日志的时候,你盯着一个event_type=2的字段,还得去翻文档看 2 是啥意思。我建议直接用可读字符串,大不了加个索引压缩一下存储。 - 雷区二:忘记记录操作结果。
只记「谁干了什么」,不记「干成没干成」。你想想看,如果你想统计某个 API 的失败率,没有 action_result 字段,你就得去解析响应码,累死个人。 - 雷区三:metadata 变成无底洞。
不加限制地往里塞数据。曾经有个团队把整个 HTTP Request Body 都存进去了,结果单个事件超过 10KB。审计 SDK 的序列化性能直接崩了。记住:metadata 里只存「元数据」,不是「全量数据」。
一个重要提醒:不要试图在审计事件里记录密码、 或任何敏感数据。即使你把它加密了,只要它出现在审计流里,就增加了泄露风险。合规审计要求「最小必要原则」,别给自己惹麻烦。
4.5 本章核心概念一览
说了这么多,我画了一张图帮你把知识点串起来。说白了,整个核心数据结构的精髓都在这里了。
好,这张图把 Event 的三大支柱——元数据、事件类型、载荷——以及它们内部的子层级都画清楚了。你做个审计 SDK,只要把这张图上的东西在代码里落地,基础就稳了。