直接用 log 包记录操作日志不可靠,因其无上下文、不结构化、易漏记或重复记;审计日志需绑定请求上下文、中间件统一拦截、字段级脱敏、失败降级且可观测。

为什么直接用 log 包记录操作日志不可靠
Go 标准库的 log 包只负责输出,不区分操作主体、上下文或业务语义。比如用户删除订单,仅写 "order deleted" 无法回溯是谁、何时、从哪台设备、带什么参数删的——审计日志的核心字段全丢了。
更关键的是,它不支持结构化输出(如 JSON),也缺乏请求生命周期钩子,导致日志容易漏记(比如 panic 时没 flush)或重复记(中间件和 handler 都写一遍)。
- 必须绑定 HTTP 请求上下文(
context.Context)才能提取userID、requestID等元信息 - 日志需在请求结束前统一落盘,不能依赖 handler 内部零散调用
- 敏感字段(如密码、token)必须默认脱敏,不能靠开发者手动过滤
用 middleware 拦截 HTTP 请求并注入审计字段
审计日志不是业务逻辑,是横切关注点,适合放在中间件里做。不要在每个 handler 里手写 log.Printf,而应统一拦截 http.ResponseWriter 和 *http.Request。
示例关键逻辑:
func AuditLogMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 从 token 或 cookie 提取 userID
userID := extractUserID(r)
reqID := r.Header.Get("X-Request-ID")
// 包装 responseWriter 以捕获状态码
wr := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wr, r)
// 统一记录审计日志
auditLog := map[string]interface{}{
"timestamp": time.Now().UTC().Format(time.RFC3339),
"method": r.Method,
"path": r.URL.Path,
"status_code": wr.statusCode,
"duration_ms": float64(time.Since(start).Microseconds()) / 1000,
"user_id": userID,
"req_id": reqID,
"ip": getClientIP(r),
"ua": r.UserAgent(),
}
json.NewEncoder(os.Stdout).Encode(auditLog) // 实际应发往日志服务或文件
})
}
- 务必用包装型
responseWriter获取真实statusCode,原生WriteHeader可能被跳过 -
extractUserID应从可信信道(如 JWT payload 或内部认证 header)读取,不依赖 query 或 body - 避免在中间件里解析 request body(会消耗 body stream),需提前在前置中间件完成解析并缓存
如何防止审计日志泄露敏感数据
审计日志一旦写入,就可能被导出、检索、展示。若包含明文密码、身份证号、银行卡号,等于主动埋雷。
最稳妥的做法是在日志序列化前做字段级脱敏,而不是靠日志系统后期过滤:
func sanitizeLogFields(logMap map[string]interface{}) map[string]interface{} {
if body, ok := logMap["body"]; ok {
if b, ok := body.(map[string]interface{}); ok {
delete(b, "password") // 明确删除
delete(b, "card_number")
if token, ok := b["token"]; ok {
b["token"] = "[REDACTED]" // 替换为占位符
}
}
}
return logMap
}
- 脱敏逻辑必须在日志写入前执行,且不可绕过(比如不能只在 debug 模式下启用)
- 不要用正则模糊匹配“password”字段名——前端可能传
pwd、auth_key,应由业务层明确声明哪些字段需脱敏 - 若使用结构体打日志(如
json.Marshal(&AuditEvent{...})),可为敏感字段加json:"-,redact"tag 并配合自定义MarshalJSON
审计日志写入失败时要不要重试或降级
要。审计日志虽不参与业务主流程,但属于合规刚性要求。如果日志服务宕机或磁盘满,直接丢弃会导致审计链路断裂,后续无法定责。
简单有效的降级策略:
- 优先写本地文件(带轮转,如
lumberjack.Logger),再异步同步到中心日志系统 - 写文件失败时,降级为内存 buffer(限制大小,如 1MB),并在健康检查接口暴露 buffer 状态
- 绝不 panic 或阻塞请求:日志写入超时应设为
100ms,超时即放弃,避免拖慢整个请求 - 对写失败事件本身也要记录(如
"audit_log_write_failed: no space left on device"),否则问题会静默恶化
真正难的不是记日志,而是确保每条操作都留下可验证、不可篡改、不丢失的痕迹。字段是否完整、脱敏是否彻底、失败是否可观测——这些细节才是审计落地的分水岭。











