事件模型与数据定义
审计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);
}
}
事件模型的关系图谱
下面这张图展示了审计事件各层之间的关系。你把它当成一张蓝图,后续所有开发都围绕它展开:
事件模型规范总结
做审计事件模型,其实就围绕三个词转——通用性、扩展性、稳定性。
- 通用性:基础字段所有事件都用得上,不要出现业务耦合。
- 扩展性:extensions和attributes给了你无限可能,但要有边界(大小、层数)。
- 稳定性:字段命名、类型、结构一旦确定,尽量不改。改就要走版本号升级。
我当年负责的一个审计系统,就因为事件结构设计得够灵活,上线三年没改过核心模型。后来接入了十几个业务模块,每个模块只需要往attributes里塞自己的专属字段就行。你想想看,如果每个模块都要求改核心事件结构,那审计SDK早就变成一团乱麻了。
嗯,到这里,事件模型和数据定义的骨架就搭完了。后面我们会基于这个结构,一步步把审计管道、存储层和分析层填起来。