分页监控需识别分页意图,必须在Gin中间件提取page/size并透传至GORM上下文,通过callback钩子按page_type和offset_range打标上报Prometheus,避免指标失真。

分页查询为什么必须单独监控
GORM 的 Limit 和 Offset 本身不触发 SQL 执行,真正耗时在 Find、Count 或 Scan 阶段。但分页场景下,Count + Find 常成对出现,且 Offset 越大,MySQL 的 filesort 和 skip 开销越明显——尤其当没走覆盖索引时,单次查询可能从 5ms 涨到 800ms。
你不能只监控“SQL 总耗时”,而要识别出“这是分页逻辑引发的慢查询”。
- 分页相关操作必须打上 page_type="list" 或 page_type="count" 标签
- 避免把 db.Limit(20).Offset(10000).Find(&users) 和普通 db.Where("id = ?").First() 统一归为 select 类型,否则指标会失真
- Offset 值本身不宜直接打标(高基数),但可归类:比如 offset_range="0-100"、"101-1000"、"1001+"Gin 中间件里怎么捕获分页上下文
Gin 中间件是唯一能提前看到请求参数(如 page、size)并绑定到 DB 上下文的位置。GORM 的 *gorm.DB 是无状态的,你得靠 c.Set("page_ctx", PageCtx{Page: p, Size: s}) 把分页参数透传下去。
- 不要在 handler 里解析 query 再调 db.Limit().Offset() —— 这样中间件完全感知不到分页意图
- 推荐封装一个 BindPagination(c *gin.Context) (int, int, error) 函数统一校验和提取,失败直接 c.AbortWithStatusJSON(400, ...)
- 若用 GORM Callback 注入监控,需确保 callback 能拿到中间件塞进 c 的 page_ctx,例如:ctx := c.MustGet("page_ctx").(PageCtx)
- 别用 c.Request.URL.Query() 在 callback 里重复解析——URL 已被 Gin 解析过一次,再 parse 是冗余且易出错的
如何让 GORM 分页操作自动上报 Prometheus Histogram
GORM v2 的 callback 机制支持在 process 阶段前后埋点,但必须注册到具体钩子,否则会漏掉 Count 或只计 Find。
- 注册两个 callback:callback.Query().Before("gorm:query").Register("log_page_count", countHook) 和 callback.Query().After("gorm:query").Register("log_page_find", findHook)
- 在 hook 函数里,通过 db.Statement.Settings["page_ctx"] 取出中间件传入的分页信息(需提前 db.WithContext(c.Request.Context()).Session(&gorm.Session{Context: c.Request.Context()}))
- 上报时 label 必须含:sql_type="paged_select"、table_name="user"、page_type="count"、offset_range="1001+"
- prometheus.NewHistogramVec 的 buckets 建议加宽:分页查询延迟波动大,建议包含 30.0 秒桶(应对深分页毛刺)
为什么 /metrics 里看不到分页指标
最常见原因是:你在中间件或 callback 里创建了新的 prometheus.Registry,但 promhttp.Handler() 默认只暴露 prometheus.DefaultRegisterer。
- 检查是否写了 reg := prometheus.NewRegistry() 却没传给 promhttp.HandlerFor(reg, ...)
- 更稳妥的做法是复用默认 registry:prometheus.MustRegister(sqlPageDuration),而不是 reg.MustRegister(...)
- 另一个坑:GORM callback 注册顺序错误,比如 callback.Process() 被多次调用,导致同一查询被计时 2 次以上,直观看就是分页指标数值翻倍、分布异常
- 最后确认:你的分页操作是否真的走到了注册的 callback?比如用了 db.Raw("SELECT ... LIMIT ? OFFSET ?").Scan(),它绕过了 GORM 查询 callback,必须单独处理
page、size)并绑定到 DB 上下文的位置。GORM 的 *gorm.DB 是无状态的,你得靠 c.Set("page_ctx", PageCtx{Page: p, Size: s}) 把分页参数透传下去。
- 不要在 handler 里解析 query 再调 db.Limit().Offset() —— 这样中间件完全感知不到分页意图
- 推荐封装一个 BindPagination(c *gin.Context) (int, int, error) 函数统一校验和提取,失败直接 c.AbortWithStatusJSON(400, ...)
- 若用 GORM Callback 注入监控,需确保 callback 能拿到中间件塞进 c 的 page_ctx,例如:ctx := c.MustGet("page_ctx").(PageCtx)
- 别用 c.Request.URL.Query() 在 callback 里重复解析——URL 已被 Gin 解析过一次,再 parse 是冗余且易出错的
如何让 GORM 分页操作自动上报 Prometheus Histogram
GORM v2 的 callback 机制支持在 process 阶段前后埋点,但必须注册到具体钩子,否则会漏掉 Count 或只计 Find。
- 注册两个 callback:callback.Query().Before("gorm:query").Register("log_page_count", countHook) 和 callback.Query().After("gorm:query").Register("log_page_find", findHook)
- 在 hook 函数里,通过 db.Statement.Settings["page_ctx"] 取出中间件传入的分页信息(需提前 db.WithContext(c.Request.Context()).Session(&gorm.Session{Context: c.Request.Context()}))
- 上报时 label 必须含:sql_type="paged_select"、table_name="user"、page_type="count"、offset_range="1001+"
- prometheus.NewHistogramVec 的 buckets 建议加宽:分页查询延迟波动大,建议包含 30.0 秒桶(应对深分页毛刺)
为什么 /metrics 里看不到分页指标
最常见原因是:你在中间件或 callback 里创建了新的 prometheus.Registry,但 promhttp.Handler() 默认只暴露 prometheus.DefaultRegisterer。
- 检查是否写了 reg := prometheus.NewRegistry() 却没传给 promhttp.HandlerFor(reg, ...)
- 更稳妥的做法是复用默认 registry:prometheus.MustRegister(sqlPageDuration),而不是 reg.MustRegister(...)
- 另一个坑:GORM callback 注册顺序错误,比如 callback.Process() 被多次调用,导致同一查询被计时 2 次以上,直观看就是分页指标数值翻倍、分布异常
- 最后确认:你的分页操作是否真的走到了注册的 callback?比如用了 db.Raw("SELECT ... LIMIT ? OFFSET ?").Scan(),它绕过了 GORM 查询 callback,必须单独处理
prometheus.Registry,但 promhttp.Handler() 默认只暴露 prometheus.DefaultRegisterer。
- 检查是否写了 reg := prometheus.NewRegistry() 却没传给 promhttp.HandlerFor(reg, ...)
- 更稳妥的做法是复用默认 registry:prometheus.MustRegister(sqlPageDuration),而不是 reg.MustRegister(...)
- 另一个坑:GORM callback 注册顺序错误,比如 callback.Process() 被多次调用,导致同一查询被计时 2 次以上,直观看就是分页指标数值翻倍、分布异常
- 最后确认:你的分页操作是否真的走到了注册的 callback?比如用了 db.Raw("SELECT ... LIMIT ? OFFSET ?").Scan(),它绕过了 GORM 查询 callback,必须单独处理
分页监控真正的难点不在埋点,而在“识别出哪次查询是分页意图驱动的”。参数来自 HTTP、执行落在 DB、指标暴露在 HTTP /metrics,三端上下文必须靠 Gin 的 c 显式串联,缺一不可。


















