audit — 审计日志与可插拔 sink
pkg/core/audit 记录谁对什么资源做了什么、结果如何——
合规审查和事故排查读的那条线索。默认一条事件变成一行结构化日志;
用 audit.SetSink 可以把它改道到一个持久 sink(审计表、
只追加的 Kafka topic、不可变文件)。
运行
完全离线,不需要 make dev。HTTP 默认监听 :8086,
配置文件是目录里的 config.yaml,-conf 默认指向它。
写端点要求请求头 X-API-Key(默认值 demo-key)。
go run ./examples/audit # 等价于 make example NAME=audit
它演示了什么
- 一个业务动作一条记录:
audit.Record记下Actor / Action / Resource / Result。 - 成功和失败都记:重复退款返回
409,同样入审计 (Result: audit.Failure,并带Detail说明原因)。 - Record 自动补时间与 trace id:
At与TraceID为空时由框架填,所以审计轨迹能和日志对齐。 - sink 可插拔:默认是结构化日志;示例用
audit.SetSink(s.store)换成内存表,从而能在端点里读回来。 - 鉴权拒绝不是业务动作:缺 / 错 API key 返回
401, 不写审计——它没走到业务逻辑。
记录一个业务动作
来自 examples/audit/main.go 的 refund。
注意记录在 return 之前写下,所以审计轨迹与调用方看到的响应一致;
失败路径带一个 Detail 说明原因。
s.store.mu.Lock()
done := s.store.refunded[id]
s.store.mu.Unlock()
if done {
audit.Record(ctx, audit.Event{
Actor: actor, Action: "order.refund", Resource: resource, Result: audit.Failure,
Detail: map[string]string{"reason": "already refunded"},
})
return nil, errs.Conflict(40901, "order already refunded")
}
s.store.mu.Lock()
s.store.refunded[id] = true
s.store.mu.Unlock()
audit.Record(ctx, audit.Event{
Actor: actor, Action: "order.refund", Resource: resource, Result: audit.Success,
})
return map[string]any{"refunded": id}, nil
装上自定义 sink
来自 Setup。示例的 orderStore 既是业务存储又是审计
sink;真实服务在这里换成数据库写入,同步写并上报错误,否则轨迹会有洞。
// A second destination beside the default log sink: the demo keeps the
// trail in memory so it can be read back. A real service writes to a
// table or an append-only topic here — synchronously, and reporting
// write errors, or the trail has holes.
s.store = &orderStore{refunded: map[int64]bool{}}
audit.SetSink(s.store)
flowchart LR
B["业务动作成功 / 被拒"] --> R["audit.Record(ctx, Event)"]
R --> S{"SetSink 装过吗?"}
S -- "否(默认)" --> L["logSink:obs.Info
audit_actor / audit_action
audit_resource / audit_result"]
S -- "是" --> D["自定义 Sink.Write(ctx, Event)
审计表 / DLQ topic / WORM 文件"]
R -. "补 At 与 TraceID" .-> R
classDef plug fill:#fdf6f0,stroke:#c8641f,color:#2b2722
class S,D plug
Record 负责把字段补全并盖时间戳,然后同步
调 Sink.Write;顺序、保留期、不可篡改与访问控制都由 sink 负责。
验证
curl -i -X POST localhost:8086/api/v1/orders/42/refund -H 'X-API-Key: demo-key' # 200
curl -i -X POST localhost:8086/api/v1/orders/42/refund -H 'X-API-Key: demo-key' # 409,同样入审计
curl -s localhost:8086/api/v1/audit # 内存里的审计轨迹
第一次返回 {"code":0,"msg":"ok","data":{"refunded":42}};
第二次是 409 {"code":40901,"msg":"order already refunded"}。
GET /api/v1/audit 返回一个数组,每项的字段与值形如
{"at":"…" "actor":"anonymous"
"action":"order.refund" "resource":"order/42"
"result":"success" "detail":null
"trace_id":"…"};
失败那条 result 为 "failure" 且 detail 含
reason。加 -H 'X-User: alice' 可把
actor 从 anonymous 改成 alice。
测试
main_test.go 完全离线:它按 verticle 的方式跑
Setup(装上 sink)再 BuildRouter,用
httptest 发请求。它钉住三件事:
每个结果各只记一条(一次成功 + 一次 409 失败,共 2 条,
成功事件字段正确、失败事件带 Detail,且事件都被盖了时间与 trace id);
鉴权拒绝不是业务动作(401 后审计表为空);
审计端点如实回放(POST 退款 order/7 后,
trail 里能查到 "resource":"order/7" 与 "actor":"bob")。
go test ./examples/audit
相关
- 开关与审计:审计的语义与约定。
- 认证与授权:actor 从哪来。
- examples/observability:审计事件落进日志后怎么读。
- Skill:
skills/matex-setup-audit/SKILL.md。 - 示例索引。