业务与审计日志:领域事件、AOP 与 Outbox
业务日志应表达领域事实;审计日志则需要可追溯、可验证且受控地保存操作证据。本文说明两者的边界,以及 Domain Probe 与 Spring AOP 的正确落地方式。
目录
| 章节 | 说明 |
|---|---|
| 三类业务记录 | 操作审计、分析事件与变更历史的边界 |
| 事件的最小字段 | Who、When、Target、Action、Result 之外还需什么 |
| Domain Probe | 用领域语义隔离观测实现 |
| 审计日志的可靠性模型 | 事务、Outbox、失败策略与安全 |
| AOP 的适用边界 | 可以自动化什么,不能自动化什么 |
| 反模式 | 常见错误与替代方案 |
三类业务记录
| 类型 | 目标 | 典型存储 | 丢失与保留策略 |
|---|---|---|---|
| 操作审计 | 回答“谁改了什么、是否成功、依据是什么” | 独立审计库或审计事件流 | 通常不可丢;append-only、访问隔离,期限依制度决定 |
| 分析事件 | 漏斗、转化、功能使用 | 消息队列/数仓 | 可按场景采样或容忍小比例丢失 |
| 变更历史 | 重建对象某些字段的演变 | 历史表或事件流 | 记录字段级 diff,避免无界全量快照 |
不要把分析埋点写进 ERROR,也不要用普通应用日志替代合规审计记录。
事件的最小字段
| 要素 | 推荐字段 | 说明 |
|---|---|---|
| Who | actor.id、actor.type | 操作者身份和角色;姓名等 PII 默认不写 |
| When | @timestamp | 由服务端记录,必要时保留客户端时间为单独字段 |
| Target | target.type、target.id | 被操作资源 |
| Action | event.name | 稳定的领域事件名,如 product.price.updated |
| Result | event.outcome、error.code | success / failure;存错误码而非直接存敏感异常文本 |
| Correlation | trace_id、request_id | 用于关联,不承担用户身份 |
审计场景可额外记录来源 IP、终端类型、授权结果、变更原因和受控的 changed_fields。before / after 快照必须采用字段白名单、脱敏和大小上限。
Domain Probe
Domain Probe 将“发生了什么”留在业务代码,把日志、指标、事件投递等“如何观测”集中到独立实现中。
public interface OrderProbe {
void orderPlaced(String orderId, String userId, BigDecimal amount);
void paymentFailed(String orderId, String errorCode);
}
@Component
public class LoggingOrderProbe implements OrderProbe {
private static final Logger log = LoggerFactory.getLogger(LoggingOrderProbe.class);
@Override
public void orderPlaced(String orderId, String userId, BigDecimal amount) {
log.info("event=order.placed biz.id={} user.id={} amount={}", orderId, userId, amount);
}
}
它适合关键领域事件和多信号观测;测试可替换为 in-memory Probe,断言领域调用而不是脆弱的日志文本。它不应承载访问控制或事务一致性等业务职责。
审计日志的可靠性模型
审计事件必须与业务提交建立明确关系。推荐模型是:在业务事务内写入 Outbox 事件;事务提交后由可靠投递器写入审计库/事件流。这样业务失败不会留下“成功审计”,投递短暂失败也可重试。
flowchart LR
A["业务更新"] --> B["同一数据库事务<br/>更新实体 + 写 Outbox"]
B --> C["事务提交"]
C --> D["可靠投递器"]
D --> E["审计存储<br/>append-only"]
需要预先定义:
- 审计写入失败策略:高合规操作可 fail-closed;普通后台操作通常 fail-open 并告警、重试与对账。
- 幂等键:例如
event_id,防止重试产生重复记录。 - 完整性与访问:独立权限、读导出审计、备份/归档;有防篡改要求时使用不可变存储或签名链。
- 数据最小化:错误信息和快照不得绕过脱敏、加密、期限和访问审批。
AOP 的适用边界
AOP 注解可用于收集稳定的操作元数据,但不能凭空获得正确的变更快照,也不能替代鉴权和事务设计。
@OperationAudit(action = "product.price.updated", targetId = "#request.productId")
public void updatePrice(UpdatePriceRequest request) {
productService.updatePrice(request);
}
切面应只构建审计意图并交给事务内的审计服务/Outbox;不要在 finally 里直接把 pjp 参数当成“变更后实体”保存。还要注意 Spring AOP 基于代理:同类自调用、未被代理的方法和某些 final 场景不会触发切面。对关键写操作,应配套集成测试验证审计事件确实在提交后出现。
建议审计表至少包括:event_id、occurred_at、actor_id、action、target_type、target_id、outcome、trace_id、changed_fields、payload_ref。大快照应存受控对象存储,表中只留引用和哈希。
反模式
| 反模式 | 风险 | 改进 |
|---|---|---|
| 业务成功后再异步随手写审计 | 审计可丢或与业务状态不一致 | 事务内 Outbox + 可重试投递 |
finally 无条件写“after 快照” | 失败请求也可能产生错误事实 | 以提交结果和持久化后的实体构建事件 |
| 保存完整请求、异常和实体 JSON | PII/密钥泄漏、体积失控 | 字段白名单、脱敏、限长、受控快照引用 |
仅记录 e.getMessage() | 错误不稳定且可能泄密 | 记录稳定错误码;运行日志保留受控堆栈 |
| 用 AOP 替代鉴权或业务校验 | 绕过路径导致不安全 | 鉴权/校验保持在业务边界,AOP 只做横切记录 |
参考资料
- Martin Fowler — Domain-Oriented Observability
- OWASP Logging Cheat Sheet
- 日志体系总览(统一字段与安全边界)
- 分布式链路追踪与日志关联(审计事件的链路关联)
评论 (0)