审计日志必须带上下文、结构化、强持久化且字段统一:用runtime.Caller(2)捕获调用位置,context传audit元数据,JSON结构化输出,高危操作同步落库,固定字段名如event_type、target_id、source_ip、user_id。

审计日志必须带上下文,否则 log.Printf 写出来的全是“谁在什么时候干了啥”但找不到“在哪干的”
Go 原生 log 包不带调用栈追踪,log.Printf 打印的行号、文件名全指向日志封装层,不是业务代码位置。审计日志要定位到具体 handler 或 service 方法,得主动捕获上下文。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用
runtime.Caller(2)(跳过封装函数和当前包装层)获取调用方的file:line,拼进日志字段;不要用1,容易错位 - 把
http.Request中的RemoteAddr、Header["User-Agent"]、Context.Value("user_id")等关键字段统一注入日志map[string]interface{},别只打字符串 - 避免在中间件里直接
log.Println—— 审计日志要结构化,优先走json.Marshal后写入io.Writer(如文件或lumberjack.Logger)
敏感操作必须同步落库,不能只写文件或异步发消息
用户删库、改权限、导出数据这类操作,如果只丢进 chan 异步处理,进程崩溃或队列积压会导致日志丢失。审计日志是事后追责依据,必须强持久化。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对高危操作(如
DeleteUser、GrantAdmin),先用tx, err := db.Begin()开事务,写完审计记录再执行业务逻辑,任一失败都回滚 - 审计表字段至少包含:
op_type(字符串枚举)、op_target(被操作资源 ID)、op_data(JSON 字符串,记录修改前/后值)、created_at(用time.Now().UTC()) - 别用
fmt.Sprintf拼 SQL 插入语句 ——op_data里可能含单引号或 JSON 转义字符,必须用db.Exec("INSERT ...", opType, opTarget, jsonData)参数化
context.Context 是传审计元数据的唯一可靠通道
HTTP handler 到 service 再到 repository,跨多层传递 user_id、req_id、ip 时,如果靠函数参数硬塞,每加一层都要改签名;用全局变量或 goroutine 局部变量又无法保证生命周期。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在入口处(如 Gin 的
gin.Context)把审计字段塞进context.WithValue,键用自定义type auditKey string避免字符串冲突 - 下游函数统一从
ctx.Value(auditKey("user_id"))取值,不依赖外部传参;注意ctx.Value返回interface{},务必做类型断言并检查ok - 别把整个
http.Request塞进 context —— 它不是线程安全的,且含大量无用字段,内存开销大
日志字段命名要一致,否则 ELK 或 Loki 查起来全是猜谜
一个项目里 user_id、uid、operator_id、account_id 混着用,查“谁删了订单”时得翻遍所有字段名。审计日志是给机器读的,不是给人看的。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 定一套最小字段集:固定用
event_type(不是action)、target_id(不是resource_id)、source_ip(不是ip)、user_id(不是uid) - 所有日志输出前,用 map 做字段标准化映射,例如把
req.Header.Get("X-Real-IP")统一赋给source_ip键,而不是直接打原始 header - 上线前用
grep -r "log\.Print" ./ | grep -E "(uid|ip|user_id)"扫一遍,确保没有漏网之鱼

















