Rust 后台系统里,审计日志不是普通日志
普通日志回答“系统发生了什么”,审计日志回答“谁在什么时候,对什么对象,做了什么决定”。
很多后台系统都有日志,但没有审计日志。
管理员改了用户权限,客服导出了订单,财务发起了退款,运营下架了商品。出了问题以后,如果只能在普通应用日志里搜索,很快就会发现信息不完整:没有操作者、没有操作对象、没有前后值、没有请求来源、没有审批单号。
审计日志不是给程序员排错的,是给业务追责和风险复盘的。
本文代码环境:Rust edition 2024,示例只使用标准库。
审计日志要有固定结构
普通日志可以自由一些,审计日志不行。
它至少要回答六个问题:
- 谁操作
- 在哪个租户
- 对什么对象
- 做了什么动作
- 结果是什么
- 请求来源是什么
用 Rust 结构表达:
#[derive(Debug)]
struct AuditLog {
actor_id: u64,
tenant_id: Option<u64>,
action: AuditAction,
resource: AuditResource,
result: AuditResult,
}动作和资源也不要只用字符串:
#[derive(Debug)]
enum AuditAction {
GrantRole,
RefundOrder,
ExportData,
}
#[derive(Debug)]
enum AuditResource {
User(u64),
Order(u64),
Report(String),
}结构化以后,后面查询、告警、报表、合规导出才有基础。
只记录成功操作是不够的
审计日志不仅要记录成功,也要记录失败。
比如有人连续尝试导出不属于自己租户的数据,最后都被拒绝了。这不是业务成功,但它是安全信号。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AuditResult {
Succeeded,
Denied,
Failed,
}
fn should_alert(result: AuditResult, action: AuditAction) -> bool {
matches!((result, action), (AuditResult::Denied, AuditAction::ExportData))
}失败审计不能只写“操作失败”。要记录失败类型:权限拒绝、参数非法、目标不存在、系统错误。
我的偏好是:高风险操作无论成功失败都要审计。
前后值要谨慎记录
权限、金额、状态变更通常需要记录前后值。
但不是所有字段都能原样记录。密码、token、身份证、手机号、密钥,都需要脱敏或禁止进入审计日志。
可以把变更值建模:
#[derive(Debug)]
struct FieldChange {
field: &'static str,
before: RedactedValue,
after: RedactedValue,
}
#[derive(Debug)]
enum RedactedValue {
Plain(String),
Masked,
}这比直接塞 JSON 更安全。
审计日志本身也可能成为敏感数据源。记录得越全,越要控制访问权限和保留周期。
审计写入失败不能悄悄吞掉
高风险操作有一个现实问题:业务操作成功了,审计日志写入失败,怎么办?
不同场景答案不同。
| 操作 | 审计失败策略 |
|---|---|
| 普通资料修改 | 业务成功,后台补写审计 |
| 权限授予 | 审计失败则操作失败 |
| 数据导出 | 审计失败则操作失败 |
| 退款 | 至少写入本地审计 Outbox |
可以把策略写清楚:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AuditPolicy {
BestEffort,
MustRecord,
}
fn policy_for(action: AuditAction) -> AuditPolicy {
match action {
AuditAction::GrantRole | AuditAction::ExportData => AuditPolicy::MustRecord,
AuditAction::RefundOrder => AuditPolicy::MustRecord,
}
}不是所有审计都要阻塞业务,但关键审计不能悄悄丢。
审计日志要防篡改
审计日志如果能被普通管理员随便修改,就失去了意义。
至少要做到:
- 业务表和审计表权限分离
- 审计日志只追加,不更新
- 删除走保留策略,不走人工随手删
- 高风险系统可以做哈希链或写入独立存储
简单的哈希链模型:
#[derive(Debug)]
struct AuditRecord {
id: u64,
payload_hash: String,
previous_hash: String,
}
fn chain_input(record: &AuditRecord) -> String {
format!("{}:{}:{}", record.id, record.previous_hash, record.payload_hash)
}这不是说每个系统都要做区块链式审计。重点是:审计日志不能和普通业务数据一样随便改。
审计查询也是产品能力
很多团队写了审计表,却没有好用的查询入口。
真正需要时,才发现只能让开发写 SQL。
审计查询至少支持:
- 按操作者查
- 按租户查
- 按资源查
- 按动作查
- 按时间范围查
- 按失败和拒绝查
#[derive(Debug, Default)]
struct AuditQuery {
actor_id: Option<u64>,
tenant_id: Option<u64>,
action: Option<AuditAction>,
result: Option<AuditResult>,
}审计不是写进去就完事。查不出来、看不懂、导不出,就很难在风险复盘里发挥作用。
结论
审计日志不是普通应用日志的另一种叫法。它服务的是追责、合规、风控和业务复盘。
审计日志要结构化;高风险操作成功失败都要记录;前后值要脱敏;关键审计写入失败不能静默忽略;审计数据要只追加、防篡改、可查询。
Rust 的类型系统很适合把审计动作、资源和策略写清楚,不要把这些关键字段埋在自由文本里。