# 第4章:事件模型与数据定义——构建通用审计事件结构

事件模型与数据定义

审计SDK的核心是什么?说白了,就是事件。你采集的每一条操作记录、每一次权限变更、每一个异常请求,最终都要落地成一条结构化的审计事件。如果这个结构定义得不够好,后面不管是存储、查询还是分析,都会让你痛不欲生。

我个人习惯在设计阶段就把事件模型定死,而不是等到开发中期再回头改。为什么呢?因为一旦数据开始流通,改模型就相当于给所有下游系统做一次“换血手术”。

通用审计事件结构

先看最基础的结构。我把它拆成三层:元数据层、业务数据层和扩展层。

层级 字段 说明 是否必填
元数据层 eventId 全局唯一标识,UUID格式 是
timestamp 事件发生时间,Unix毫秒时间戳 是
eventType 事件类型(如LOGIN、DELETE、EXPORT等) 是
eventVersion 事件结构版本号 是
业务数据层 actor 操作主体(用户ID、服务名称等) 是
target 操作对象(文件路径、API端点等) 是
action 具体操作(CREATE、UPDATE、DELETE) 是
扩展层 extensions 预定义扩展字段 否
attributes 自定义属性Map 否

你可能会问:“为什么要把扩展层单独拎出来?” 我在项目中遇到过这样一种情况:业务方突然说要在审计事件里加个“设备指纹”字段。如果当初没留扩展层,你得改核心代码、改数据库表、改下游消费逻辑。但是有了扩展层,只需要在attributes里加个key-value,什么都不用动。

扩展字段设计思路

扩展字段不是随便塞的。我总结了一套命名规范:

  • 预定义扩展:使用固定的key,比如requestId、clientIp、userAgent。这些字段出现频率高,直接定义在extensions里,序列化时走固定路径,性能更好。
  • 自定义属性:用Map<String, Object>存储。key采用命名空间格式,比如module:field。举个实际例子:payment:merchantId、auth:sessionToken,一眼就能看出属于哪个模块。

核心原则:扩展字段只增不减。一旦某个key出现在生产环境中,就永远不要删除它,最多标记为“已废弃”。否则历史数据解析时会直接炸掉。

自定义属性的灵活性设计

自定义属性这块,我踩过不少坑。最开始我用的是Map<String, String>,后来发现不够用——业务方要传一个嵌套的JSON对象进去。怎么办?我改成了Map<String, Object>,value支持基础类型、数组和嵌套Map。

但是有个前提——不能无限嵌套。我规定最多3层,防止有人把整个业务对象塞进去。你想想看,如果一条审计事件里包含一个几十MB的JSON,存储和传输都会崩掉。

避坑:我曾经遇到过业务方往attributes里塞了一个byte[]图片数据,导致整个审计管道OOM。后来我加了字段大小限制:单个自定义属性值不超过1KB,整条事件的扩展层总大小不超过10KB。

事件模型的实际落地

来看具体代码。我习惯用Java POJO定义事件结构,配合注解标注扩展字段:

@Data
@Accessors(chain = true)
public class AuditEvent {
    // 元数据层
    private String eventId;
    private Long timestamp;
    private String eventType;
    private Integer eventVersion = 1;

    // 业务数据层
    private String actor;
    private String target;
    private String action;

    // 扩展层
    private Map<String, Object> extensions;
    private Map<String, Object> attributes;

    // 辅助方法:安全写入自定义属性
    public void putAttribute(String key, Object value) {
        if (attributes == null) {
            attributes = new HashMap<>();
        }
        // 检查value大小
        String jsonValue = JsonUtils.toJsonString(value);
        if (jsonValue.length() > 1024) {
            throw new IllegalArgumentException("自定义属性value超过1KB限制");
        }
        attributes.put(key, value);
    }
}

小贴士:使用@Accessors(chain = true)可以让链式调用更优雅。比如event.setEventId(x).setActor(y).putAttribute("key", val),代码可读性好很多。

事件序列化与反序列化的考量

事件结构定义好了,怎么存?我推荐用JSON格式,因为它对自定义属性的支持最好。但是要注意两点:

  • 版本的兼容性:事件结构中一定要带eventVersion字段。反序列化时,根据版本号选择对应的解析逻辑。我见过有团队忘记加版本号,结果升级后老数据全部解析失败。
  • 未知字段的处理:反序列化时不要直接抛出异常。我习惯把未知字段存入attributes里,保证数据不丢失。就像这样:
// 反序列化时保留未知字段
@JsonIgnoreProperties(ignoreUnknown = true)
public class AuditEvent {
    // ...
    @JsonAnySetter
    public void handleUnknownField(String key, Object value) {
        this.putAttribute(key, value);
    }
}

事件模型的关系图谱

下面这张图展示了审计事件各层之间的关系。你把它当成一张蓝图,后续所有开发都围绕它展开:

审计事件模型结构图 审计事件 AuditEvent 元数据层 eventId, timestamp, eventType, eventVersion 业务数据层 actor, target, action 扩展层 extensions + attributes 预定义扩展字段 requestId, clientIp... 自定义属性Map key-value, 3层限制 设计要点 • 事件版本号必须保留,用于向后兼容 • 自定义属性key采用命名空间格式(module:field) • 扩展字段只增不减,禁止物理删除

事件模型规范总结

做审计事件模型,其实就围绕三个词转——通用性、扩展性、稳定性。

  • 通用性:基础字段所有事件都用得上,不要出现业务耦合。
  • 扩展性:extensions和attributes给了你无限可能,但要有边界(大小、层数)。
  • 稳定性:字段命名、类型、结构一旦确定,尽量不改。改就要走版本号升级。

我当年负责的一个审计系统,就因为事件结构设计得够灵活,上线三年没改过核心模型。后来接入了十几个业务模块,每个模块只需要往attributes里塞自己的专属字段就行。你想想看,如果每个模块都要求改核心事件结构,那审计SDK早就变成一团乱麻了。

嗯,到这里,事件模型和数据定义的骨架就搭完了。后面我们会基于这个结构,一步步把审计管道、存储层和分析层填起来。