Fiber结构化日志中间件必须用ctx.Next()前后打点采集字段,配合zerolog异步输出并捕获panic;直接使用fiber.Logger或原生log无法获取request_id、latency等上下文,且不支持JSON格式、自定义字段和状态码分流。

Fiber 是 Go 语言的高性能 Web 框架,其中间件机制与 Express 或 Gin 类似,但设计更轻量、链式调用更明确。记录接口日志不是靠“插件”或“starter”,而是通过标准中间件函数 + ctx.Next() 控制执行时机完成的。
为什么不能直接套用 Logrus 或 Zap 的默认 logger?
因为 Fiber 的 ctx 对象不自动携带请求 ID、耗时、状态码等上下文字段;原生 logger 打印的只是静态字符串,没法结构化提取 method、path、status、latency。直接写 log.Info("req") 会导致日志散乱、无法按路径聚合、查不到慢接口。
fiber.Logger 中间件默认行为和坑点
Fiber 官方提供了 fiber.Logger,但它默认只输出到控制台,且格式固定(如 2026/09/30 12:00:00 | 200 | 1.23ms | GET | /api/user),不支持:
- 输出到文件或第三方日志系统(如 Loki、ELK)
- 添加自定义字段(如 user_id、trace_id)
- 按状态码分流(比如 ERROR 单独告警)
- JSON 结构化输出(zap 或 zerolog 友好格式)
如果你只用 fiber.Logger() 而不做封装,线上出问题时,你仍然得 grep 文本日志,没法做字段检索。
怎么写一个可落地的结构化日志中间件?
核心是:在 ctx.Next() 前后打点,手动采集字段,再交给结构化 logger(如 zerolog)输出。关键步骤如下:
- 用
ctx.Locals或ctx.Context().Value注入request_id(建议用fiber.UUID()生成) - 记录开始时间:
start := time.Now() - 调用
ctx.Next(),让后续 handler 执行 - 计算耗时:
latency := time.Since(start) - 从
ctx提取真实字段:ctx.Method()、ctx.Path()、ctx.Response().StatusCode()、ctx.Get("User-Agent") - 用
zerolog.Ctx(ctx.Context()).Info().Str("path", path).Int("status", status).Dur("latency", latency).Msg("http")输出
示例片段(使用 github.com/rs/zerolog):
func StructuredLogger() fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
c.Locals("req_id", fiber.UUID())
<pre class="brush:php;toolbar:false;"> err := c.Next()
status := c.Response().StatusCode()
latency := time.Since(start)
zerolog.Ctx(c.Context()).Info().
Str("req_id", c.Locals("req_id").(string)).
Str("method", c.Method()).
Str("path", c.Path()).
Int("status", status).
Dur("latency", latency).
Str("ua", c.Get("User-Agent")).
Msg("http")
return err
}
}
容易被忽略的细节:异步写日志和 panic 捕获
上面的中间件在 panic 时不会触发 Msg("http"),导致失败请求“静默消失”。真正生产可用的版本必须:
- 包一层 defer 捕获 panic,并记录 status = 500 和错误堆栈
- 日志输出走异步 channel(zerolog.New(os.Stdout).With().Timestamp().Logger() 默认同步,高并发下会阻塞 HTTP 响应)
- 避免在日志中序列化 c.Body() 或 c.Request().String() —— 这会读取并消耗 request body,导致下游 handler 拿不到原始数据











