gorm内置prometheus插件仅暴露连接池指标,因其只调用db.stats()获取openconnections等池状态,未hook sql执行钩子,故不采集单条sql耗时、类型或表名。

为什么 GORM 内置的 prometheus 插件只暴露连接池指标,不采集 SQL 耗时
GORM v2+ 提供的 gorm.io/plugin/prometheus 插件本质是个轻量 exporter,它只调用 db.Stats() 获取连接池状态(如 OpenConnections、IdleConnections),并不介入 SQL 执行路径。这意味着它完全不感知单条 SQL 的执行时间、类型或表名——你看到的指标全是“池子本身”,不是“SQL 本身”。
常见误判是以为启用插件后就能监控慢查询,结果 /metrics 里只有 gorm_open_connections 这类指标,没有延迟分布。
- 它不 hook
Process或After钩子,所以无法计时 - 它不解析 SQL 文本,因此无法提取
sql_type或table_name - 它默认使用
prometheus.DefaultRegisterer,但如果你项目用了自定义 registry(比如 GoFrame 的全局 registry),它可能根本没被暴露出来
如何用 GORM callbacks 正确埋点 SQL 耗时(Histogram)
必须用 callback 机制,在语句真正执行前后打点,且必须用 Histogram——别用 Gauge 存单次耗时,也别用 Summary。
示例注册逻辑:
sqlDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "gorm_sql_duration_seconds",
Help: "SQL execution latency distribution",
Buckets: []float64{0.01, 0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 10.0},
},
[]string{"sql_type", "table_name"},
)
prometheus.MustRegister(sqlDuration)
db.Callback().Create().After("gorm:after_create").Register("metrics:sql_duration", func(db *gorm.DB) {
if db.Error == nil {
elapsed := time.Since(db.Statement.StartTime)
sqlType := "create"
tableName := db.Statement.Schema.Table
sqlDuration.WithLabelValues(sqlType, tableName).Observe(elapsed.Seconds())
}
})
// 同样要为 Update/Delete/Query 单独注册,不能只挂一个 callback.Process()
- 务必为每种操作(
Create、Update、Delete、Query)分别注册,避免漏掉 Raw SQL 或事务内多语句 -
db.Statement.Schema.Table是安全的归一化表名;不要用db.Statement.SQL.String()做 label,会爆炸性生成高基数指标 - 如果用了事务,每条语句都会触发一次 callback,这正是你想要的——细粒度计时,不是整个事务只记一次
/metrics 看不到 GORM 指标?先检查 registry 是否一致
最常踩的坑:你在 GORM 插件或 callback 里调用 prometheus.MustRegister(...),但 HTTP handler 用的是 promhttp.HandlerFor(registry, nil),而 registry 是你自己 new 出来的,和 DefaultRegisterer 不是一个实例。
现象:curl http://localhost:8080/metrics 里只有 go_runtime 指标,没有 gorm_*。
- 确认所有指标注册都指向同一个 registry 实例,例如:
myRegistry := prometheus.NewRegistry(),然后统一传给promhttp.HandlerFor(myRegistry, nil)和gorm.RegisterPlugin(...)的配置 - GORM 插件初始化时支持传入 registry:
prometheus.NewPlugin(prometheus.Config{Registry: myRegistry}) - callback 中注册指标也要用同一 registry:
myRegistry.MustRegister(sqlDuration) - 别混用
promauto.NewHistogramVec(它默认用 DefaultRegisterer)和手动 registry
长耗时 SQL 监控的关键:标签设计与分桶合理性
哪怕计时逻辑完全正确,标签乱设或分桶太窄也会让指标失效。
错误示例:sqlDuration.WithLabelValues("SELECT", "users WHERE id = ?", "2026-08-19").Observe(...) —— 这会为每个 ID 生成独立时间序列,几万用户就几万个 series,Prometheus OOM。
- 表名用
db.Statement.Schema.Table,固定值如"users";SQL 类型用硬编码字符串,如"select_by_id"或"batch_update",而不是原始 SQL - Buckets 必须覆盖真实 P99 延迟:OLTP 场景建议
[]float64{0.01, 0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 10.0};如果是报表类慢查询,把上限拉到 60 或 300 - 避免加无意义 label,比如
host或pod_name—— Prometheus 自带instancelabel,再加反而冗余
真正难的不是写那几行 callback,而是想清楚你要回答什么问题:是“哪个表的 SELECT 最慢”,还是“哪类业务操作拖垮了 P95”——指标结构得服务于这个问题,而不是堆砌字段。











