审计日志必须用 runtime.Caller(2) 定位真实调用位置,通过 context.Context 透传 audit 元数据,高危操作需同步落库并事务强一致,日志须物理隔离且脱敏。

审计日志必须带 runtime.Caller(2) 才能定位到业务代码
原生 log.Printf 打出来的文件名和行号永远指向封装层,不是 handler 或 service 里触发操作的真实位置。不解决这个问题,审计日志就失去“谁在哪儿干了啥”的追溯能力。
- 用
runtime.Caller(2):跳过日志封装函数本身 + 当前包装函数,拿到调用方的file:line - 别用
1—— 中间加一层 wrapper 就错位;也别用3—— 过深容易越界返回 nil - 捕获后存进结构体字段,例如
caller: "user_handler.go:42",不要拼进 message 字符串里
context.Context 是传 audit 元数据的唯一可靠通道
HTTP 请求进来后,user_id、req_id、source_ip、role 这些必须从 request 解析出来,塞进 context.Context,再一路透传到 DB 层。靠函数参数硬传或全局变量,要么改签名爆炸,要么 goroutine 生命周期失控。
- 中间件里做注入:
ctx = context.WithValue(ctx, audit.UserIDKey, userID) - 所有日志写入前校验关键字段是否非空,缺则打
warning并补"system"或"unknown",避免日志残缺 - 异步任务(如定时清理)必须显式携带上下文:
go func(ctx context.Context) { ... }(req.Context()),否则ctx.Value全为空
高危操作必须同步落库,不能走 chan 或消息队列
删账号、改权限、导出敏感数据这类操作,如果只丢进 chan 异步处理,进程 crash 或队列积压会导致日志彻底丢失。审计日志是追责依据,不是普通业务日志。
- 对高危操作,先开事务:
tx, err := db.Begin() - 写完审计记录(
INSERT INTO audit_log (...) VALUES (?, ?, ?, ?))再执行业务逻辑 - 任一失败都
tx.Rollback(),保证“操作成功 ⇔ 审计记录存在”强一致 -
op_data字段存 JSON 字符串,用参数化方式插入,别拼 SQL —— 防止单引号或转义字符破坏语句
审计日志要物理隔离,不能和应用日志混用同一个 logger
混在一起会直接导致安全合规失效:运维没法单独归档加密,安全团队 grep 效率极低,等保要求的 180 天保留策略也无法落地。
立即学习“go语言免费学习笔记(深入)”;
- 用独立的
*log.Logger实例,输出到专用路径,例如/var/log/app/audit.log - 配
lumberjack.Logger时设MaxAge: 180,且禁止写入 stdout 或通用 JSON 日志流 - 写入前做最小脱敏:
token存sha256(token)[:8],手机号存138****1234,但额外记sha256(id)供溯源


















