数字遗迹的挽歌:长期审计、日志轮转与合规收官
What(本文讲什么)
当 agent 系统 7x24 运行后,日志会从“调试工具”变成“基础设施负担”。如果你不做轮转、分级、脱敏、审计与不可篡改保留:
- 磁盘会被打爆,进程会因 I/O 卡死(超时、资源释放)。
- 敏感信息会在日志里长期存在,变成泄露源(权限、审计)。
- 事故发生时你拿不到证据链,或者证据链被篡改(审计)。
这篇文章把日志系统拆成两条链:
- 容量链:轮转、压缩、保留期、采样,解决“存储与成本”。
- 证据链:不可篡改审计(WORM)、签名、访问控制,解决“可信与追责”。
Problem(要解决的工程问题)
agent 的日志与普通服务不同,它天然包含更多高风险信息:
- prompt 与上下文片段(可能含源代码/密钥/PII)。
- 工具输出(可能含数据库连接串、内部 IP、异常堆栈)。
- 工具调用参数(可能暴露敏感路径、系统拓扑)。
如果不治理,常见事故是:
- 轮转缺失导致磁盘打爆,触发连锁超时与重试风暴(超时、重试)。
- “调试日志”被当作“审计证据”,但日志可篡改、可删除(审计失败)。
- 脱敏缺失导致敏感信息长期可检索(权限、审计)。
Principle(日志分级:调试/审计/成本三套数据,三套生命周期)
建议最少分三类,并明确保留期与存储介质:
- 调试日志(Debug Logs):
- 用途:定位 bug、解释失败原因。
- 特点:量大、噪声大、价值短。
- 策略:短保留(例如 7-14 天)+ 轮转 + 压缩。
- 审计日志(Audit Logs):
- 用途:回答“谁在什么时候做了什么副作用”。
- 特点:量相对小,但必须可信、不可篡改。
- 策略:写入不可篡改介质(WORM)+ 明确保留期 + 严格访问控制。
- 成本与性能日志(FinOps/Perf):
- 用途:token 消耗、延迟、缓存命中、失败原因分布。
- 特点:可聚合,适合时序化。
- 策略:聚合后入 TSDB,保留更久但不存原文。
“三类日志三套策略”是为了避免两个极端:
- 全都永久保存:成本爆炸且泄露风险常驻。
- 全都短期保存:事故发生时没有证据链。
Usage(怎么用:轮转 + 结构化 + 脱敏 + WORM)
1) 轮转(logrotate):先解决容量与资源释放
logrotate 是常见的轮转工具,它支持按大小/时间轮转、压缩与保留策略。你需要的不是“会写配置”,而是把轮转当成线上必备门禁:
- 强制上限:单文件最大 size,避免单个异常输出把磁盘打爆。
- 保留策略:保留 N 份或保留 N 天,避免无限增长。
- 压缩:对冷日志压缩,降低存储与传输成本。
- 失败可观测:轮转失败也要报警,否则等于没轮转(观测)。
参考:logrotate(8) 手册。
https://linux.die.net/man/8/logrotate
2) 结构化日志:让日志可聚合,而不是只能读文本
OpenTelemetry logs 规范强调“日志是结构化事件”。你至少需要把这些字段做成结构化属性:
trace_id/span_id(与 tracing 关联)。task_id/agent_id/step_id(归因)。tool_name/timeout_ms/attempt/retry_reason(超时、重试)。idempotency_key/wal_id(幂等、审计)。severity/error_code(聚合与报警)。
参考:OpenTelemetry Logs Spec。
https://opentelemetry.io/docs/specs/otel/logs/
3) 脱敏(redaction):日志写入前的最后一道闸门
脱敏必须发生在“写入之前”,而不是“查询时再过滤”,原因很简单:写入之后你已经制造了泄露面。
常见需要脱敏的内容:
- API key / token
- 邮箱/手机号等 PII
- 数据库连接串
- 内网地址与元数据地址
示例(伪代码):
import re
class Redactor:
"""
脱敏器:
在日志落盘前替换敏感信息,避免 observability 系统变成泄露源。
"""
def __init__(self):
self._patterns = [
re.compile(r"sk-[A-Za-z0-9]{20,}"),
re.compile(r"[\\w\\.-]+@[\\w\\.-]+\\.[A-Za-z]{2,}"),
re.compile(r"postgres(ql)?://[^\\s]+"),
]
def redact(self, s: str) -> str:
out = s
for p in self._patterns:
out = p.sub("[REDACTED]", out)
return out
4) WORM 审计:不可篡改保留解决“证据链可信”
轮转解决“容量”,但解决不了“可信”。审计日志必须写入不可篡改介质(WORM)。S3 Object Lock 是这类能力的典型实现:它允许设置对象保留期与不可删除策略,用于实现 Write Once Read Many。
参考:
https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html
工程上你要抽象出能力,而不是绑定某家产品:
- 不可修改(immutability)
- 保留期(retention)
- 法务冻结(legal hold,若你的组织需要)
- 访问控制与审计(谁读过、谁导出过)
5) 自动化审计:让系统自己巡检自己
当日志量巨大时,靠人读是不现实的。你需要一个 nightly job(或 audit agent)做巡检:
- 权限偏离:尝试读取敏感路径的次数(权限)。
- 敏感泄露:是否出现 key/连接串模式命中(审计)。
- 行为退化:平均轮次上升、超时上升、重试上升(超时、重试、观测)。
- 幂等冲突:同一 idempotency_key 的重复提交(幂等)。
Design(设计取舍:为什么要把“审计日志”独立出来)
审计日志必须比调试日志更严格:
- 字段更少但更强:只记录副作用与批准链。
- 可信更重要:不可篡改比可读性更重要。
- 访问更严格:不是所有开发都应有读审计日志的权限(权限)。
如果你把审计日志混在普通调试日志里,你最终会得到两者都不合格的产物。
Pitfall(常见坑与防错)
- 只做轮转不做脱敏:你只是把泄露面切成小块而已(审计)。
- 把 trace 当 log:trace 适合因果链,log 适合细节;两者要关联而不是互代(观测)。
- 没有失败报警:轮转失败、上传失败、脱敏失败必须报警(观测、降级)。
- 审计日志可删除:这会直接摧毁追责能力(审计)。
Debug(当日志系统出问题)
排查顺序建议:
- 看磁盘:是否触发上限?轮转是否执行?
- 看超时/重试:是否因为 I/O 阻塞导致调用超时并重试?
- 看脱敏:敏感模式是否命中?是否有原文泄露?
- 看审计链:WORM 写入是否成功?是否可回放?
一份可直接落地的轮转配置(示例)
下面是一个典型 logrotate 配置骨架,重点是表达策略:按天轮转、保留 14 份、压缩、并保证应用能无感继续写。
/var/log/agent/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
dateext
# 如果应用不支持重新打开文件句柄,可以用 copytruncate(有丢日志风险,需评估)
copytruncate
}
你需要在自己的运行环境验证两件事:
- 轮转过程中是否丢日志(尤其是高并发写)。
- 压缩与上传是否会引发 I/O 峰值,导致 tool 执行超时(超时)。
审计日志的最小 schema(建议固定)
审计日志不需要长,但必须强。建议至少固定这些字段:
wal_id/commit_idtask_id/trace_idactor(哪个 agent/哪个用户触发)action(工具名/操作类型)resource_targets(写入资源集合)idempotency_key(幂等)result/error_codeapproved_by(若存在审批链)
这样你才能在事故时回答:
- 副作用有没有发生?
- 是否重复发生?
- 发生前有没有权限检查?
- 能不能回滚?
常见事故复盘(为什么“轮转 + WORM + 脱敏”缺一不可)
- 只做轮转:
- 结果:磁盘不炸了,但敏感信息仍然散落在日志里(审计风险)。
- 只做 WORM:
- 结果:证据链可信了,但成本爆炸,且把敏感信息永久固化(权限/审计风险)。
- 只做脱敏:
- 结果:泄露风险下降,但容量与证据链仍然不可控(资源释放/审计失败)。
所以这三件事必须被当作一套系统同时落地,而不是“选一个做做”。
Source(资料来源)
- logrotate man page: https://linux.die.net/man/8/logrotate
- OpenTelemetry logs spec: https://opentelemetry.io/docs/specs/otel/logs/
- S3 Object Lock (WORM): https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html
- 12-factor logs: https://12factor.net/logs