应使用 ctx.values() 存取请求 id 和耗时:入口设 start := time.now() 并 ctx.values().set("req_id", uuid.newstring()),出口用 duration := time.since(start) 与 ctx.values().getstring("req_id") 构造结构化日志,且所有提前返回路径须调 ctx.stopexecution() 防重复执行。

中间件里怎么拿到请求 ID 和耗时做日志字段
别依赖手动拼接或全局计时器。Iris 的 ctx.Values() 是唯一安全的请求级上下文载体,而耗时必须用 time.Now() 在中间件入口记起点、出口算差值。常见错误是把 start time 存进 ctx.Request().Context() —— 它不被 Iris 生命周期管理,且无法跨中间件自动传递。
正确做法:
- 在日志中间件开头写:
start := time.Now() - 用
ctx.Values().Set("req_id", uuid.NewString())生成并存入请求 ID(需引入github.com/google/uuid) - 在
ctx.Next()后读取:duration := time.Since(start),再和ctx.Values().GetString("req_id")一起写入日志结构体 - 不要在中间件里调
ctx.GetHeader("X-Request-ID")就直接用——客户端可能没传,得 fallback 到自动生成
怎么避免日志中间件重复打印或漏掉错误响应
关键在执行顺序和中断控制。ctx.StopExecution() 后仍调 ctx.Next() 不会 panic,但会导致后续中间件重复执行、日志打两遍;而没调 ctx.StopExecution() 却已写响应(比如 401),业务 handler 还会继续跑,可能触发 DB 查询或发消息,日志里就只剩“成功”假象。
安全模式:
- 所有提前返回路径(鉴权失败、参数校验错、签名无效)都必须:写响应 +
ctx.StopExecution(),且确保ctx.Next()不在这些分支内 - 日志语句统一放在
ctx.Next()之后,用ctx.Response().StatusCode()获取最终状态码,而不是猜 - 如果用了 recover 中间件,它必须注册在最外层(
app.Use(recover.New())),否则 panic 日志会丢失
结构化日志字段该塞哪些才实用
别堆砌字段,重点覆盖可观测三要素:谁(身份)、干了啥(行为)、结果如何(状态)。Iris 本身不提供日志序列化,你得自己构造 map 或用第三方库(如 zerolog)。
最小可用字段集:
-
req_id:从ctx.Values().GetString("req_id")取,用于全链路追踪 -
method/path:直接用ctx.Method()和ctx.Path() -
status:用ctx.Response().StatusCode(),不是硬编码 -
duration_ms:用float64(duration.Microseconds()) / 1000.0,单位统一为毫秒 -
user_id:如果鉴权中间件已写入ctx.Values().Set("user_id", uid),这里就读出来;没写就留空,别强转或 panic - 避免写
body或headers全量——太重,GC 压力大,也容易泄露敏感信息
为什么不能用 fmt.Printf 或 log.Println 直接打结构化日志
因为它们输出的是纯文本,没法被日志采集系统(如 Filebeat、Fluent Bit)自动解析成字段。你看到的是一整行字符串,想查 status == 500 得靠正则或 grep,效率低还不可靠。
真正要做的:
- 用
json.Marshal()把日志 map 转成 JSON 字符串,再fmt.Print出去(开发环境够用) - 生产环境推荐
zerolog.New(os.Stdout).With().Timestamp().Logger(),它默认输出 JSON,字段可嵌套,支持 level 控制 - 注意:Iris 的
app.Logger()是框架内置 logger,只适合打启动、配置类信息,不适合每请求结构化日志——它没字段化能力,也不带req_id - 如果用了集中式日志(如 SLS、ELK),确保每行 JSON 是单条完整记录,别换行或拼接,否则采集端会切碎
req_id 的生成时机、ctx.StopExecution() 的位置,这三个点最容易被跳过,一漏就导致日志对不上请求、排查时来回翻查。











