Go标准库log包无日志级别语义,无法实现错误等级分流;必须使用zerolog或slog等支持结构化level字段的库,通过LevelFilter或HandlerOptions按级别路由输出,并确保level字段为小写JSON顶层键以兼容采集器。

Go日志中间件无法靠log.Printf实现错误等级分流
标准库log包没有级别语义,log.Printf和log.Println输出的全是“普通日志”,既不能按Warn/Error过滤,也无法把Error单独写入stderr或独立文件。强行用前缀字符串(如"[ERROR] ")模拟分级,会导致日志分析工具(如Loki、Fluent Bit)无法提取level字段,后续告警、监控、ES聚合全部失效。
必须用zerolog或slog初始化带级别的全局Logger
分流存储的前提是日志条目本身带level结构化字段。推荐用zerolog(零分配、API直观)或slog(Go 1.21+内置、JSON友好):
-
zerolog需显式启用级别字段:zerolog.SetGlobalLevel(zerolog.InfoLevel),否则Warn()/Error()调用会被静默丢弃 -
slog需在Handler中指定最低输出级:&slog.HandlerOptions{Level: slog.LevelInfo},否则DebugContext日志不会输出 - 两者都支持输出到不同
io.Writer:比如把Error日志写入os.Stderr,其余写入os.Stdout,再由容器运行时或日志采集器(如Fluentd)按流分离
示例(zerolog):
import github.com/rs/zerolog
// 初始化:Error日志走stderr,其余走stdout
errorWriter := zerolog.ConsoleWriter{Out: os.Stderr}
infoWriter := zerolog.ConsoleWriter{Out: os.Stdout}
log.Logger = zerolog.New(zerolog.MultiLevelWriter(
zerolog.LevelFilter(zerolog.ErrorLevel, errorWriter),
zerolog.LevelFilter(zerolog.InfoLevel, infoWriter),
)).With().Timestamp().Logger()
HTTP中间件里不能直接用log.Error,要从context.Context取logger实例
中间件必须保证每个请求的日志携带唯一request_id、能区分debug/info/warn/error,并且不污染全局logger。正确做法是:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 在最外层中间件生成
request_id,用context.WithValue注入ctx - 用
zerolog.Ctx(ctx)或slog.With获取请求专属logger,避免并发写冲突 - 不要在中间件里调用
log.Error(...)——它用的是全局logger,没request_id,也没trace上下文 - panic恢复时,必须用
Ctx(ctx).Fatal().Stack().Msg("panic recovered"),否则堆栈丢失
真正分流不是靠代码写多路io.Writer,而是靠日志采集层识别level字段
生产环境几乎不直接在Go进程里把Error日志写进/var/log/app/error.log——这会引入文件锁、权限、轮转、磁盘满等运维问题。正确路径是:
- 所有日志统一输出到
stdout/stderr(结构化JSON格式) - 容器运行时(Docker/K8s)把两路流分别捕获为
logs/stdout和logs/stderr - Fluent Bit或Vector配置
level字段提取规则,把"level":"error"的日志发往告警通道(如Slack/Webhook),其余存入Loki或ES - 如果硬要本地文件分流,
zerolog提供LevelFilter+os.OpenFile组合,但务必设os.O_APPEND | os.O_CREATE,且禁用Caller和Stack避免性能损耗
关键点:level字段必须出现在每条日志的JSON顶层,且值为小写字符串("error"而非"ERROR"),否则采集器匹配失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










