Skip to content
Prometheus 客户端埋点 (Instrumen...

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_totalmyapp_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)) 直接查 _countquantile 标签
适用场景 绝大多数场景(首选) 单实例精确分位、无法定义桶的场景

💡 默认选择 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_idrequest_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_idrequest_idtrace_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 查找开销

参考资料: