真正可行的日志脱敏路径是在结构化前完成字段级脱敏,再经异步core写入;正则替换会破坏json、误杀合法字段、漏嵌套值且性能差,应通过实现marshallogobject接口由结构体自主控制脱敏。

直接对日志字符串做全局正则替换是危险且不可靠的——它会误杀合法字段(比如把 hostname 变成 hos****ame),也漏掉嵌套在 JSON、URL、stack trace 里的敏感值。真正可控的方式,是在日志结构化阶段就拦截并清洗字段,而不是等它变成字符串再“抢救”。
用 slog.Handler 拦截每条日志记录(Go 1.21+ 推荐)
Go 标准库 slog 的 Handler 接口允许你在日志写入前修改 record.Attrs 和 record.Message,这是目前最轻量、并发安全、且不依赖第三方库的脱敏入口。
- 必须新建 slice 处理
record.Attrs:直接修改原 slice 会导致并发 panic;用append([]slog.Attr{}, ...)构造新 slice -
record.Message也要处理:它可能是fmt.Sprintf("user %s login with token %s", u, t)拼出的字符串,需尝试json.Unmarshal解析后递归清理 - 敏感 key 判定别用全等匹配:用
strings.Contains(strings.ToLower(key), "token")覆盖X-Auth-Token、access_token、api_key等变体 - 不要只看顶层字段:
error类型可能嵌套Unwrap()链,需递归检查每个 error 的Error()返回值是否含敏感词
用 zap 的 Core + 自定义 Encoder 实现字段级过滤
如果你已在用 zap,别用 AddHook 去改 entry.Message 字符串——那只是补救。应该在 EncodeEntry 阶段,对 entry.Fields 中每个 Field 的 key 做判定,只对匹配的 value 赋值为 "***"。
- 优先使用
zap.String("password", "***")显式传脱敏值,而非拼接原始字符串 - 避免
zap.Any("req", r)这类泛型传参:它会触发反射遍历,但默认不走脱敏逻辑;应先手动剥离敏感字段再传 - 自定义
Encoder时,对map[string]interface{}、struct、[]interface{}类型做递归遍历,只清洗 key 匹配的 value - 注意
zap.Error(err):err 可能带堆栈和嵌套 error,需用zapcore.WriteError提前展开并清洗
为什么不能用 strings.ReplaceAll 或正则全局替换日志字符串
这类做法看似简单,实则埋雷:它在日志已转为字符串后才介入,此时原始结构丢失,无法区分哪个 token 是 header、哪个是 URL path、哪个是错误消息里的单词。
- 正则
regexp.MustCompile(`token=[^&\s]+`)会漏掉 JSON body 中的"auth_token":"abc" -
strings.ReplaceAll(logStr, "password", "***")会把password_reset_link变成***_reset_link - HTTP stack trace 里常含
github.com/xxx/auth.(*Token).Validate,被误删后导致调试困难 - 日志压缩或轮转后,脱敏逻辑无法复用,不同环境行为不一致
真正难的不是写一个脱敏函数,而是覆盖所有出口:HTTP handler 的 request body、error 的 Unwrap 链、slog.Message 的非结构化内容、甚至测试中 fmt.Printf 的临时调试输出。这些地方都得用同一套 key 判定逻辑,且必须在数据首次进入日志系统前就拦截——晚一步,敏感信息就已经落盘了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











