go 标准 log 包不支持 hook、level 和 formatter,无法按错误类型条件触发报警;应使用 zerolog 等库,通过 hookfunc 在 error 级别异步发送告警,并确保日志结构化携带关键字段。

Go 标准 log 包不支持 Hook,别硬套 logrus 那套思路
Go 的 log 包是纯同步、无扩展点的简单封装,它没有 Hook、没有 Formatter、也没有 Level 概念。试图给 log.Printf 塞个“错误触发报警”的钩子,本质是在对抗设计——要么自己包一层,要么换库。标准库里连 SetOutput 都只接受 io.Writer,没法拦截“是不是错误”这个语义。
常见错误现象:log.SetOutput(&MyHookWriter{}) 后发现所有日志都进去了,但无法区分 ERROR 和 INFO;或者用 log.Panicf 试图触发告警,结果服务直接挂了,根本没机会发通知。
- 真正需要的是「按错误类型/关键字/panic 状态」做条件判断,而不是日志输出管道劫持
- 如果坚持用标准
log,只能靠调用方自律:统一用log.Printf("[ERROR] %s", msg),再自己解析前缀——脆弱且易漏 - 更现实的做法:用
zap或zerolog替代,它们原生支持Level+Hook(比如zerolog.Hook接口)
用 zerolog.Hook 实现 ERROR 级别自动报警
zerolog 是目前 Go 生态中对 Hook 支持最干净的结构化日志库,它的 Hook 是一个函数接口,接收 zerolog.Event 和 level,在写入前可读取字段、判断逻辑、触发外部动作。
使用场景:HTTP handler 中遇到数据库超时,想立刻发钉钉 Webhook,同时保留本地 JSON 日志。
立即学习“go语言免费学习笔记(深入)”;
func alertOnErrHook() zerolog.Hook {
return zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
if level == zerolog.ErrorLevel {
// 这里加你的报警逻辑:HTTP POST、发邮件、写 Kafka……
go func() {
_ = sendDingTalkAlert(msg) // 自定义函数,非阻塞
}()
}
})
}
- 必须用
zerolog.Error().Msg("xxx")触发,不能只靠字符串含 "error" —— 因为msg参数只是最终拼接后的文本,Event才有完整结构 - 注意
go func()异步调用:避免报警慢拖垮主流程;但也要小心 panic 没 recover 导致 goroutine 消失 - 别在 Hook 里调
e.Send()或改e,Hook 只读;写日志是后续 pipeline 的事
panic 时如何补发错误报警(绕过 defer 捕获盲区)
HTTP handler 里 defer recover() 能捕获 panic,但中间件链路深时容易漏;而 zerolog.Hook 对 panic 本身无感知——因为 panic 不走日志路径。真正可靠的方式是注册 recover + 主动打 error 日志。
常见错误现象:服务 panic 后进程退出,没留任何错误线索,监控只看到 CPU 突降。
- 在 main 函数开头加
defer全局兜底:defer func() { if r := recover(); r != nil { log.Error().Interface("panic", r).Msg("global panic caught") } }() - 别只打
fmt.Printf:标准输出可能被重定向或丢弃;必须走你配置好的带 Hook 的 logger - 如果用了
http.Server,建议配合server.RegisterOnShutdown做最后 flush,但 panic 时不触发,所以兜底recover仍是唯一选择
报警触发后别忽略上下文丢失问题
Hook 里拿到的 msg 是扁平字符串,Event 虽有字段,但如果你没在打日志时显式传,比如 log.Error().Str("user_id", uid).Msg("db timeout"),那 Hook 里就拿不到 user_id —— 报警消息会变成“db timeout”,毫无定位价值。
- 报警函数里不要只拼
msg,优先从e.Fields()提取关键字段(zerologv1.30+ 支持e.GetLevel()、e.GetMessage()、e.GetFields()) - 字段命名要一致:统一用
error存错误对象,stack存堆栈(可用github.com/pkg/errors或errors.WithStack) - 别在 Hook 里做耗时序列化(如把整个
map[string]interface{}转 JSON 发 HTTP),先取必要字段,再异步发
最麻烦的不是怎么发报警,而是报警消息里有没有那个能让人 5 秒内定位到代码行和用户 ID 的字段。这得从第一条 .Error().Str("order_id", oid).Msg("pay failed") 就开始写对。










