Logrus Hook 不能直接过滤日志字段,因其在 Entry.Log() 格式化后触发,此时字段已固化为字符串或 JSON 字节;真正有效的过滤必须在 Format() 方法中对 entry.Data 原地清理,且需深拷贝保障并发安全。

为什么 logrus 的 Hook 不能直接过滤日志字段
很多人误以为给 logrus 加个 Hook 就能按需删掉敏感字段(比如 password、token),但实际运行时发现字段还在输出里——因为 Hook 是在日志格式化**之后**触发的,此时结构体已转成字符串或 JSON 字节,再想“删字段”只能做正则替换,既不可靠又破坏结构完整性。
真正有效的过滤必须发生在结构化数据序列化**之前**,也就是在 Entry.Data 还是 map[string]interface{} 的时候。
- 使用
logrus.WithField()或WithFields()添加的数据,会存进Entry.Data - 自定义
Formatter(如logrus.JSONFormatter)的Format()方法接收的是未序列化的*logrus.Entry - 所以过滤逻辑应放在
Format()内部,对entry.Data做原地清理
如何在 JSONFormatter 中安全删除敏感字段
直接修改 entry.Data 是可行的,但要注意:它可能被并发写入(尤其在 web handler 中),而 map 非并发安全。所以得先深拷贝,再删字段,最后交给原 Formatter 序列化。
示例代码(基于 logrus v1.9+):
立即学习“go语言免费学习笔记(深入)”;
type SafeJSONFormatter struct {
logrus.JSONFormatter
RedactKeys []string
}
func (f *SafeJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
// 浅拷贝 map(logrus 本身不保证 Data 并发安全)
data := make(logrus.Fields)
for k, v := range entry.Data {
data[k] = v
}
// 删除敏感 key
for _, key := range f.RedactKeys {
delete(data, key)
}
// 替换 entry.Data,避免影响原始 entry(某些 hook 可能还要用)
entryCopy := *entry
entryCopy.Data = data
return f.JSONFormatter.Format(&entryCopy)
}
-
RedactKeys是你要屏蔽的字段名列表,比如[]string{"password", "auth_token", "credit_card"} - 不要用
delete(entry.Data, ...)—— 会污染原始Entry,导致下游 Hook 拿到残缺数据 - 如果字段值是嵌套结构(如
user: {name: "a", password: "123"}),这个方案不处理;需要额外递归清理,但绝大多数场景只需平级过滤
在 Gin 框架中集成时,为什么中间件里打的日志还是带敏感字段
因为 Gin 默认用 gin.DefaultWriter 输出访问日志,它和你配置的 logrus 实例不是同一个 logger 实例。你改了主 logger 的 formatter,但 Gin 的 gin.LoggerWithWriter() 用的是自己的 logger,没同步你的过滤规则。
- 要么统一用一个
*logrus.Logger实例:把logrus.StandardLogger()传给gin.LoggerWithWriter() - 要么重写 Gin 的
gin.Logger中间件,手动调用你配置好的 logger,并确保传入的logrus.Fields已提前过滤 - 更稳妥的做法:所有业务日志都走同一 logger 实例,Gin 访问日志也通过该实例记录(禁用默认 logger,自己实现
gin.HandlerFunc)
用 zerolog 替代 logrus 是否更省事
是的,zerolog 的设计天然支持字段级控制:它的 Context 是链式构建的,每个 Interface() 或 Str() 调用都可被拦截或跳过,而且 zerolog.GlobalLevel() 和 zerolog.LevelFieldName 等机制让动态过滤更轻量。
但注意:如果你已在项目中重度依赖 logrus 的 Hook 生态(比如 Sentry、Elasticsearch 输出),切换成本不低;且 zerolog 默认不打印 caller 信息,需手动加 zerolog.CallerMarshalFunc。
- 简单过滤可用
zerolog.FilterFunc,它在序列化前接收Event,可调用e.Discard()或修改e.fields - 若需按环境开关过滤(如 dev 不过滤、prod 过滤),用
zerolog.SetGlobalLevel()+ 自定义 writer 更灵活 - 别忽略
zerolog.ErrorStacks(true)—— 它会影响 panic 日志结构,和字段过滤逻辑可能冲突
Format() 这道门,让数据以干净状态跨出去。


















