Prometheus 客户端埋点 (Instrumentation)
本文档涵盖 Prometheus 客户端埋点的命名规范、Go 客户端库
client_golang的完整使用指南,以及埋点最佳实践。
符号说明
| 符号 | 含义 |
|---|---|
| 🚨 | 陷阱 / Pitfall |
| 💡 | 最佳实践 / Best practice |
| 🔬 | 深入原理 / Deep-dive |
| ⚡ | 性能提示 / Performance note |
1. 通用命名规范
💡 指标命名格式: <namespace>_<subsystem>_<name>_<unit>
namespace: 应用/组织名(如myapp)subsystem: 子系统名(如api)name: 指标含义(如requests)unit: 单位(seconds,bytes,total,ratio)- 全部小写,snake_case
例如:myapp_api_requests_total、myapp_db_query_duration_seconds
2. Go 客户端库 (client_golang) — 完整指南
client_golang 是 Prometheus 官方提供的 Go 语言客户端库,也是 Prometheus 自身使用的埋点库。
2.1 安装与导入
go get github.com/prometheus/client_golang核心包的职责分工:
import (
"github.com/prometheus/client_golang/prometheus" // 核心:定义指标类型
"github.com/prometheus/client_golang/prometheus/promauto" // 便捷:自动注册
"github.com/prometheus/client_golang/prometheus/promhttp" // HTTP:暴露 /metrics 端点
"github.com/prometheus/client_golang/prometheus/collectors" // 内置:Go runtime 采集器
)| 包 | 职责 |
|---|---|
prometheus |
定义指标、注册、MustRegister、Describe/Collect 接口 |
promauto |
工厂函数,创建指标时自动注册到默认 Registry |
promhttp |
HTTP Handler,暴露 /metrics 端点 |
collectors |
Go runtime 指标采集器(可选启用) |
testutil |
测试辅助:比较指标值、断言期望值 |
2.2 注册机制:promauto vs 手动注册
两种创建指标的方式,本质区别在于是否需要手动注册:
方式一:promauto(✅ 推荐日常使用)
import "github.com/prometheus/client_golang/prometheus/promauto"
var totalReqs = promauto.NewCounter(prometheus.CounterOpts{
Name: "myapp_requests_total",
Help: "Total number of requests.",
})
// ↑ 自动注册到 prometheus.DefaultRegisterer,一步到位方式二:手动注册(适合需要自定义 Registry 的场景)
import "github.com/prometheus/client_golang/prometheus"
var totalReqs = prometheus.NewCounter(prometheus.CounterOpts{
Name: "myapp_requests_total",
Help: "Total number of requests.",
})
func init() {
prometheus.MustRegister(totalReqs) // 必须手动注册,否则 /metrics 看不到
}🚨 忘记
MustRegister是指标不出现的头号原因。 如果不调用注册,指标静默丢失。💡 需要多个独立的 Registry(例如单元测试隔离、多租户)时用手动注册 +
prometheus.NewRegistry()。日常业务代码用promauto即可。🔬 深入原理:
prometheus.DefaultRegisterer是一个全局的*Registry单例。promauto内部调用MustRegister注册到这个全局 Registry。promhttp.Handler()也是从这个全局 Registry 读取指标并渲染成文本格式。
2.3 Counter — 只增不减的计数器
API 方法:
// 无标签 Counter
counter := promauto.NewCounter(prometheus.CounterOpts{
Name: "myapp_events_total",
Help: "Total events processed.",
})
counter.Inc() // +1
counter.Add(3.14) // +3.14(接受 float64)
// 带标签 Counter Vec
counterVec := promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "myapp_http_requests_total",
Help: "Total HTTP requests.",
},
[]string{"method", "endpoint", "status"},
)
counterVec.WithLabelValues("GET", "/api/users", "200").Inc()
counterVec.With(prometheus.Labels{"method": "POST", "endpoint": "/api/orders", "status": "201"}).Inc()
// 预先绑定标签(适合循环中反复使用、减少查找开销)
reqCounter := counterVec.WithLabelValues("GET", "/api/users", "200")
for i := 0; i < 100; i++ {
reqCounter.Inc() // 直接操作,不用每次 WithLabelValues
}CounterVec 的三种标签绑定方式:
| 方式 | 签名 | 适用场景 |
|---|---|---|
WithLabelValues(vals ...string) |
可变参数,按顺序匹配 | 标签值已知、顺序明确 |
With(labels prometheus.Labels) |
map[string]string |
标签较多时,按名赋值更清晰 |
MustCurryWith(labels prometheus.Labels) |
预设部分标签,返回新的 CounterVec | 中间件场景:提前固定 service 标签 |
// MustCurryWith 示例:提前固定 service 标签
baseCounter := promauto.NewCounterVec(
prometheus.CounterOpts{Name: "myapp_requests_total", Help: "..."},
[]string{"service", "method", "status"},
)
// 预先咖喱化,只保留 method 和 status 两个维度
httpCounter, err := baseCounter.CurryWith(prometheus.Labels{"service": "myapp"})
if err != nil { /* label mismatch */ }
// 使用时只需要填 method 和 status
httpCounter.WithLabelValues("GET", "200").Inc()2.4 Gauge — 可增可减的仪表盘
// 无标签 Gauge
gauge := promauto.NewGauge(prometheus.GaugeOpts{
Name: "myapp_active_connections",
Help: "Current number of active connections.",
})
gauge.Set(42) // 设为 42
gauge.Inc() // +1 → 43
gauge.Dec() // -1 → 42
gauge.Add(10) // +10 → 52
gauge.Sub(5) // -5 → 47
// 带标签 Gauge Vec
gaugeVec := promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "myapp_queue_depth",
Help: "Current queue depth per queue.",
},
[]string{"queue_name"},
)
gaugeVec.WithLabelValues("orders").Set(150)
gaugeVec.WithLabelValues("notifications").Set(30)
// 💡 常用模式:回调函数动态取值
type Server struct {
activeClients promauto.Gauge
}
func NewServer() *Server {
s := &Server{
activeClients: promauto.NewGauge(prometheus.GaugeOpts{
Name: "myapp_active_clients",
Help: "Current connected clients.",
}),
}
return s
}
func (s *Server) OnConnect() {
s.activeClients.Inc()
}
func (s *Server) OnDisconnect() {
s.activeClients.Dec()
}2.5 Histogram — 分布统计(⭐ 最推荐)
histogramVec := promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "myapp_http_request_duration_seconds",
Help: "HTTP request latencies in seconds.",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
// 可选字段:
// ConstLabels: prometheus.Labels{"version": "v2"}, // 所有样本都带的固定标签
// NativeHistogramBucketFactor: 1.1, // 启用原生直方图(实验性)
},
[]string{"method", "endpoint"},
)
// === 四种观测方式 ===
// ① 直接记录值(适合已知延迟的场景)
histogramVec.WithLabelValues("GET", "/api/users").Observe(0.235)
// ② Timer 方式(推荐,自动计 start→now 的耗时)
timer := prometheus.NewTimer(histogramVec.WithLabelValues("POST", "/api/orders"))
// ... 执行逻辑 ...
timer.ObserveDuration()
// ③ TimerFunc 方式(测量一个函数的执行时间)
func processOrder() { /* ... */ }
prometheus.NewTimer(histogramVec.WithLabelValues("POST", "/api/orders")).ObserveDuration()
// 等价于:defer prometheus.NewTimer(...).ObserveDuration()
// ④ ObserveDuration 的 defer 模式(最常用)
func handleRequest() {
timer := prometheus.NewTimer(histogramVec.WithLabelValues("GET", "/api/users"))
defer timer.ObserveDuration()
// ... 业务逻辑 ...
}💡 桶定义策略:
// 策略 1:对数分布(适合覆盖大范围延迟)
prometheus.DefBuckets // 默认 [.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]
// 策略 2:线性分布(适合集中在某个区间的场景)
prometheus.LinearBuckets(start, width, count)
// prometheus.LinearBuckets(0.1, 0.1, 10) → [0.1, 0.2, 0.3, ..., 1.0]
// 策略 3:指数分布
prometheus.ExponentialBuckets(start, factor, count)
// prometheus.ExponentialBuckets(0.1, 2, 8) → [0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8]
// 策略 4:自定义(对齐 SLO 阈值,最推荐)
[]float64{0.01, 0.05, 0.1, 0.2, 0.5, 1, 2}⚡ 桶的数量影响性能和存储。 每个桶 + 每对标签组合 = 一条时间序列。比如 10 个桶 × 3 个 method × 5 个 endpoint = 150 条时间序列。在标签基数高的场景下,桶数越少越好。
🔬 深入原理: Histogram 底层为每个桶自动生成三条时间序列:
_bucket{le="x"},_sum,_count。其中_bucket是 Counter 类型,_sum和_count也是 Counter。这意味着rate()对它们同样有效。
2.6 Summary — 客户端分位数(特定场景使用)
summaryVec := promauto.NewSummaryVec(
prometheus.SummaryOpts{
Name: "myapp_request_duration_seconds",
Help: "Request duration summary.",
Objectives: map[float64]float64{
0.5: 0.05, // P50,最大误差 5%
0.9: 0.01, // P90,最大误差 1%
0.99: 0.001, // P99,最大误差 0.1%
},
MaxAge: 10 * time.Minute, // 滑动窗口大小
AgeBuckets: 5, // 窗口内的桶数
},
[]string{"method"},
)
// 用法与 Histogram 相同
summaryVec.WithLabelValues("GET").Observe(0.15)| 特性 | Histogram | Summary |
|---|---|---|
| 分位数计算位置 | 服务端(Prometheus) | 客户端(应用进程) |
| 跨实例聚合 | ✅ 可以 sum 后 histogram_quantile | ❌ 分位数不可聚合 |
| CPU 开销 | 低 | 中(客户端内部排序) |
| 精度 | 受桶宽度影响 | 高精度 |
| PromQL 查询 | histogram_quantile(0.99, sum(rate(bucket[5m])) by (le)) |
直接查 _count 或 quantile 标签 |
| 适用场景 | 绝大多数场景(首选) | 单实例精确分位、无法定义桶的场景 |
💡 默认选择 Histogram。 只有当你需要在单个实例上获取高精度分位数,且不需要跨实例聚合时,才用 Summary。
2.7 暴露 /metrics 端点
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// 基础用法:单一路由
http.Handle("/metrics", promhttp.Handler())
// 高级用法:自定义 Handler(控制行为)
handler := promhttp.HandlerFor(
prometheus.DefaultGatherer, // 可替换为自定义 Registry
promhttp.HandlerOpts{
EnableOpenMetrics: false,
MaxRequestsInFlight: 5,
Timeout: 10 * time.Second,
ErrorLog: log.New(os.Stderr, "promhttp: ", log.LstdFlags),
Registry: prometheus.DefaultRegisterer,
},
)
http.Handle("/metrics", handler)
http.ListenAndServe(":8080", nil)
}2.8 完整的 HTTP 中间件示例
package main
import (
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// ① 定义指标(包级别变量)
var (
requestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "myapp_http_requests_total",
Help: "Total HTTP requests processed.",
},
[]string{"method", "endpoint", "status"},
)
requestsInFlight = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "myapp_http_requests_in_flight",
Help: "Current number of in-flight HTTP requests.",
},
)
requestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "myapp_http_request_duration_seconds",
Help: "HTTP request latencies in seconds.",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "endpoint"},
)
responseSize = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "myapp_http_response_size_bytes",
Help: "HTTP response sizes in bytes.",
Buckets: prometheus.ExponentialBuckets(100, 10, 8), // 100B → 1GB
},
[]string{"method", "endpoint"},
)
)
// ② responseWriter 包装器 —— 捕获状态码和响应大小
type responseWriter struct {
http.ResponseWriter
statusCode int
size int
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.size += n
return n, err
}
// ③ 中间件函数
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(v)
}))
defer timer.ObserveDuration()
requestsInFlight.Inc()
defer requestsInFlight.Dec()
rw := newResponseWriter(w)
next.ServeHTTP(rw, r)
statusLabel := http.StatusText(rw.statusCode)
if statusLabel == "" {
statusLabel = "unknown"
}
requestsTotal.WithLabelValues(r.Method, r.URL.Path, statusLabel).Inc()
responseSize.WithLabelValues(r.Method, r.URL.Path).Observe(float64(rw.size))
})
}
// ④ 业务 Handler
func helloHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond)
w.Write([]byte("hello, prometheus!"))
}
// ⑤ 主函数
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)
mux.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", metricsMiddleware(mux))
}2.9 自定义 Collector — 从业务对象直接导出指标
当指标值来自现有业务对象而非手动 Inc()/Observe() 时,实现 prometheus.Collector 接口更自然:
type ConnectionPool struct {
openConnections int
maxConnections int
waitQueueLength int
openConnDesc *prometheus.Desc
maxConnDesc *prometheus.Desc
waitQueueDesc *prometheus.Desc
}
func NewConnectionPool(max int) *ConnectionPool {
pool := &ConnectionPool{
maxConnections: max,
openConnDesc: prometheus.NewDesc(
"db_pool_connections_open",
"Number of currently open connections.",
nil, nil,
),
maxConnDesc: prometheus.NewDesc(
"db_pool_connections_max",
"Maximum connection pool size.",
nil, nil,
),
waitQueueDesc: prometheus.NewDesc(
"db_pool_wait_queue_length",
"Number of callers waiting for a connection.",
nil, nil,
),
}
prometheus.MustRegister(pool)
return pool
}
// Describe —— 向 Registry 描述自己有哪些指标
func (p *ConnectionPool) Describe(ch chan<- *prometheus.Desc) {
ch <- p.openConnDesc
ch <- p.maxConnDesc
ch <- p.waitQueueDesc
}
// Collect —— 采集当前值发送给 Registry
func (p *ConnectionPool) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(p.openConnDesc, prometheus.GaugeValue, float64(p.openConnections))
ch <- prometheus.MustNewConstMetric(p.maxConnDesc, prometheus.GaugeValue, float64(p.maxConnections))
ch <- prometheus.MustNewConstMetric(p.waitQueueDesc, prometheus.GaugeValue, float64(p.waitQueueLength))
}💡 何时用自定义 Collector? 当指标值天然由业务对象持有(如连接池大小、缓存命中率),且你不想在每次状态变化时手动
Set()。用 Collector 让 Prometheus 每次 scrape 时主动拉取最新值。🔬 深入原理:
/metrics请求到达时,promhttp.Handler()会遍历所有注册的 Collector,调用它们的Collect(ch)方法,将它们产出的 Metric 序列化为 Prometheus text 格式。这就是 “Pull 模型” 在代码层面的实现。
2.10 Go Runtime 指标
Prometheus 默认不收集 Go runtime 指标。需要手动注册:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
func init() {
// 采集 Go runtime 指标(goroutine 数、GC 次数/耗时、内存分配等)
prometheus.MustRegister(collectors.NewGoCollector(
collectors.WithGoCollectorRuntimeMetrics(
collectors.GoRuntimeMetricsRule{Matcher: regexp.MustCompile("/.*")},
),
))
// 采集进程级别指标(CPU、内存、文件描述符)
prometheus.MustRegister(collectors.NewProcessCollector(
collectors.ProcessCollectorOpts{},
))
}💡 Go 1.20+ 起,runtime 指标通过
runtime/metrics包提供了更丰富的指标,NewGoCollector会自动使用。
启用后你会看到这些新增指标:
| 指标前缀 | 内容 |
|---|---|
go_goroutines |
当前 goroutine 数量 |
go_memstats_alloc_bytes |
已分配的堆内存 |
go_gc_duration_seconds |
GC 暂停时长(Summary) |
go_gc_* |
GC 次数、CPU 占比等 |
process_cpu_seconds_total |
进程累计 CPU 时间 |
process_resident_memory_bytes |
常驻内存 |
process_open_fds |
打开的文件描述符数 |
2.11 gRPC 拦截器(Interceptor)
import (
"context"
"time"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
var (
grpcRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "grpc_server_requests_total",
Help: "Total gRPC requests.",
},
[]string{"service", "method", "code"},
)
grpcRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "grpc_server_request_duration_seconds",
Help: "gRPC request duration.",
Buckets: prometheus.DefBuckets,
},
[]string{"service", "method"},
)
grpcRequestsInFlight = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "grpc_server_requests_in_flight",
Help: "Current in-flight gRPC requests.",
},
)
)
// Unary 拦截器
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
grpcRequestDuration.WithLabelValues(getService(info.FullMethod), getMethod(info.FullMethod)).Observe(v)
}))
defer timer.ObserveDuration()
grpcRequestsInFlight.Inc()
defer grpcRequestsInFlight.Dec()
resp, err := handler(ctx, req)
code := status.Code(err).String()
grpcRequestsTotal.WithLabelValues(getService(info.FullMethod), getMethod(info.FullMethod), code).Inc()
return resp, err
}
}
func getService(fullMethod string) string {
parts := strings.SplitN(strings.TrimPrefix(fullMethod, "/"), "/", 2)
if len(parts) > 0 {
return parts[0]
}
return "unknown"
}
func getMethod(fullMethod string) string {
parts := strings.SplitN(strings.TrimPrefix(fullMethod, "/"), "/", 2)
if len(parts) > 1 {
return parts[1]
}
return fullMethod
}2.12 测试
import (
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
)
func TestMyCounter(t *testing.T) {
// 使用独立 Registry,避免测试之间互相污染
registry := prometheus.NewRegistry()
counter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "test_requests_total",
Help: "Test counter.",
})
registry.MustRegister(counter)
counter.Inc()
counter.Add(2)
// 断言指标值
expected := `
# HELP test_requests_total Test counter.
# TYPE test_requests_total counter
test_requests_total 3
`
if err := testutil.CollectAndCompare(counter, expected); err != nil {
t.Fatal(err)
}
// 或者直接比较数值
if v := testutil.ToFloat64(counter); v != 3 {
t.Fatalf("expected 3, got %v", v)
}
}2.13 高级主题:ConstLabels 与 CurryWith
ConstLabels — 所有样本自动附加的标签:
counter := promauto.NewCounter(prometheus.CounterOpts{
Name: "myapp_requests_total",
Help: "Total requests.",
ConstLabels: prometheus.Labels{"version": "v2.1.0", "region": "us-east-1"},
})
// 每条指标自动带 version="v2.1.0", region="us-east-1"CurryWith — 逐层固定标签(中间件友好):
// 定义基础指标(3 个标签维度)
baseReq := promauto.NewCounterVec(
prometheus.CounterOpts{Name: "myapp_requests_total", Help: "..."},
[]string{"service", "method", "status"},
)
// 在初始化时固定 service="user-api"
userReq, _ := baseReq.CurryWith(prometheus.Labels{"service": "user-api"})
// 使用时只填剩余标签
userReq.WithLabelValues("GET", "200").Inc()2.14 常见问题与调试
| 问题 | 原因 | 解决方案 |
|---|---|---|
指标在 /metrics 中不出现 |
① 忘记注册 ② 注册但从未调用任何方法 | ① MustRegister ② 对 Counter 预先 Inc(0) 让指标从 0 出现 |
panic: duplicate metrics |
两个指标的 Name 相同 | 检查是否重复定义,或使用不同的 Name |
CurryWith 返回 error |
咖喱化的标签在原 Vec 的标签列表中不存在 | 检查标签名拼写是否与 []string 中定义的一致 |
| 标签基数过高导致内存爆炸 | 将 user_id、request_id 等无界值作为标签 |
用 metric_relabel_configs 在 Prometheus 侧 drop,或在代码层聚合 |
| Timer 测量不准 | 在 goroutine 中启动 Timer,在另一个 goroutine 中 Observe | 在同一 goroutine 中用 defer 确保配对 |
2.15 项目目录结构建议
myapp/
├── main.go
├── metrics/
│ └── metrics.go # 所有指标定义集中放在一个包
├── middleware/
│ └── metrics.go # HTTP/gRPC 指标中间件
└── handler/
└── user.go # 业务 handler(通过 metrics 包引用指标)metrics/metrics.go 示例:
package metrics
import "github.com/prometheus/client_golang/prometheus/promauto"
import "github.com/prometheus/client_golang/prometheus"
var (
HTTPRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{Name: "myapp_http_requests_total", Help: "..."},
[]string{"method", "endpoint", "status"},
)
HTTPRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{Name: "myapp_http_request_duration_seconds", Help: "...", Buckets: prometheus.DefBuckets},
[]string{"method", "endpoint"},
)
DBQueryDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{Name: "myapp_db_query_duration_seconds", Help: "...", Buckets: prometheus.DefBuckets},
[]string{"operation", "table"},
)
)3. 客户端埋点最佳实践总结
| 实践 | 说明 |
|---|---|
💡 Counter 加 _total 后缀 |
社区约定俗成,一眼能识别指标类型 |
| 💡 Histogram bucket 对齐 SLO | 如 SLO 是 P99<500ms,在 100ms/250ms/500ms/1s 设桶 |
| 🚨 标签基数不要过高 | 避免 user_id、request_id、trace_id 等无界值作为标签 |
💡 用 _seconds 作为时间单位 |
统一用秒,不要混用毫秒 |
💡 先 rate() 再 sum() |
不要先 sum() 再 rate()——会搞乱 Counter 重置检测 |
| 🚨 不要在客户端算分位数 | 留给 Histogram + histogram_quantile() 在服务端聚合 |
| 💡 初始化标签组合 | 对已知标签值预先 Inc(0),让指标从 0 开始,避免图表断线 |
| 💡 指标定义集中管理 | 放在单独的 metrics 包中,避免散落在各处 |
💡 用 promauto 而非手动注册 |
简洁、自动、不易出错,日常开发首选 |
| 💡 测试用独立 Registry | prometheus.NewRegistry() 隔离测试,避免互相污染 |
| 💡 启用 Go runtime 指标 | 注册 collectors.NewGoCollector() + NewProcessCollector() |
💡 Timer 始终用 defer |
确保即使 panic 也能正常 Observe |
🚨 避免在循环中重复 WithLabelValues |
先 WithLabelValues 绑定好再用,减少 map 查找开销 |
参考资料: