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,存储和带宽都扛不住。
  • 太保守:业务方想多记一个字段,你告诉他「没地方存」,然后他就在备注里手写……这种审计记录等于废纸。

那我们怎么折中? 我的做法是制定一个「元数据规范层级」:

  1. 核心字段:上述 dataclass 里的字段。这些必须填,是硬性要求。不填就不生成事件。
  2. 推荐字段:比如请求耗时、API 版本号、客户端 SDK 版本。审计 SDK 内部会帮你自动填充,但你可以选择不存。
  3. 扩展字段:业务方自定义的,全塞进 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 本章核心概念一览

说了这么多,我画了一张图帮你把知识点串起来。说白了,整个核心数据结构的精髓都在这里了。

审计事件核心结构图 审计事件(AuditEvent)核心组成 AuditEvent 事件元数据 ID / Timestamp / 来源 事件类型 领域·行为·动作三层 事件载荷 主体·客体·上下文 字段规范层级 核心字段 推荐字段 扩展字段 操作型 vs 状态型 瞬间行为 持续性状态 身份·行为·环境 谁 + 对谁 动作 + 结果 IP · Session · UA ⚠ metadata 使用命名空间前缀 + 加 _version 禁止记录密码 / 等敏感信息

好,这张图把 Event 的三大支柱——元数据、事件类型、载荷——以及它们内部的子层级都画清楚了。你做个审计 SDK,只要把这张图上的东西在代码里落地,基础就稳了。