4. 采集应用自定义指标:使用 Prometheus 客户端库暴露 /metrics 端点

自定义指标是 APM 监控的“最后一公里”。业务状态(当前在线用户、订单处理速率、缓存命中率)往往无法通过基础设施指标(CPU、内存)直接反映,必须由应用层主动暴露。Prometheus 的 Pull 模型要求应用在 /metrics 端点输出符合文本格式的指标数据。本章以 Go、Java、Python 三种语言为例,演示如何使用官方客户端库暴露自定义业务指标,并验证数据正确性。

4.1 原理与指标类型

Prometheus 客户端库提供四种核心指标类型,映射不同的业务语义:

类型语义典型业务示例
Counter只增不减的累计值请求总数、错误总数、任务执行次数
Gauge可升可降的瞬时值当前在线用户数、内存使用量、队列深度
Histogram分桶统计分布请求延迟分布(<1ms、<5ms、<10ms、……)
Summary预计算分位数95% 响应时间、99% 响应时间

客户端库会自动维护指标并响应 HTTP 请求。/metrics 端点输出的文本行格式如下(Prometheus exposition format):

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 1027
http_requests_total{method="POST",status="500"} 13
选择指南: 计数器适合累加事件;仪表盘适合实时值;直方图与摘要都用于延迟,但直方图可聚合(推荐),摘要不可跨实例聚合。生产环境优先使用 Histogram。

4.2 实现流程

  1. 在项目中引入对应的 Prometheus 客户端库(Go: prometheus/client_golang;Java: simpleclient;Python: prometheus_client)。
  2. 定义业务指标(注册到默认注册表或自定义注册表)。
  3. 在 HTTP 服务中添加 /metrics 端点(通常使用库自带的 Handler)。
  4. 在业务逻辑中更新指标值(如 inc()、set()、observe())。
  5. 启动服务,验证 /metrics 端点返回的指标数据。
每步的常见陷阱已在后续“常见错误”中说明,阅读代码前建议先浏览 4.6 节。

4.3 代码示例:三种语言的业务指标暴露

以下示例均模拟一个“用户登录服务”,暴露三个自定义指标:

  • login_total(Counter):累计登录次数,带 status 标签
  • active_users(Gauge):当前活跃用户数
  • login_duration_seconds(Histogram):登录耗时分布

4.3.1 Go(client_golang)

package main

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    loginTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{Name: "login_total", Help: "Total number of logins."},
        []string{"status"},
    )
    activeUsers = prometheus.NewGauge(
        prometheus.GaugeOpts{Name: "active_users", Help: "Current active users."},
    )
    loginDuration = prometheus.NewHistogram(
        prometheus.HistogramOpts{
            Name:    "login_duration_seconds",
            Help:    "Login duration in seconds.",
            Buckets: prometheus.DefBuckets,
        },
    )
)

func init() {
    prometheus.MustRegister(loginTotal)
    prometheus.MustRegister(activeUsers)
    prometheus.MustRegister(loginDuration)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    timer := prometheus.NewTimer(loginDuration)
    defer timer.ObserveDuration()

    // 模拟登录业务
    status := "success"
    loginTotal.WithLabelValues(status).Inc()
    activeUsers.Inc()
    // 实际业务逻辑...
}

func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/login", loginHandler)
    http.ListenAndServe(":8080", nil)
}

4.3.2 Java(simpleclient + simpleclient_httpserver)

import io.prometheus.client.Counter;
import io.prometheus.client.Gauge;
import io.prometheus.client.Histogram;
import io.prometheus.client.exporter.HTTPServer;

public class LoginMetrics {
    static final Counter loginTotal = Counter.build()
        .name("login_total").help("Total number of logins.")
        .labelNames("status").register();
    static final Gauge activeUsers = Gauge.build()
        .name("active_users").help("Current active users.").register();
    static final Histogram loginDuration = Histogram.build()
        .name("login_duration_seconds").help("Login duration in seconds.")
        .register();

    public static void main(String[] args) throws Exception {
        new HTTPServer(8081);
        // 业务线程中更新指标
        loginTotal.labels("success").inc();
        activeUsers.inc();
        Histogram.Timer timer = loginDuration.startTimer();
        // 模拟登录...
        timer.observeDuration();
    }
}

4.3.3 Python(prometheus_client)

from prometheus_client import Counter, Gauge, Histogram, start_http_server
import time

login_total = Counter('login_total', 'Total number of logins', ['status'])
active_users = Gauge('active_users', 'Current active users')
login_duration = Histogram('login_duration_seconds', 'Login duration in seconds')

def login():
    with login_duration.time():
        # 模拟登录
        time.sleep(0.1)
        login_total.labels(status='success').inc()
        active_users.inc()

if __name__ == '__main__':
    start_http_server(8082)
    while True:
        login()
        time.sleep(1)

4.4 验证指标数据

启动服务后,使用 curl 或浏览器访问 /metrics 端点:

# Go 示例运行在 8080 端口
curl http://localhost:8080/metrics

返回内容片段:

# HELP login_total Total number of logins
# TYPE login_total counter
login_total{status="success"} 42
# HELP active_users Current active users
# TYPE active_users gauge
active_users 15
# HELP login_duration_seconds Login duration in seconds
# TYPE login_duration_seconds histogram
login_duration_seconds_bucket{le="0.005"} 0
login_duration_seconds_bucket{le="0.01"} 0
login_duration_seconds_bucket{le="0.025"} 5
login_duration_seconds_bucket{le="+Inf"} 42
login_duration_seconds_count 42
login_duration_seconds_sum 3.1415

Prometheus Server 可通过 scrape_configs 的 targets 添加该端口自动采集。

验证小技巧: 在浏览器打开 /metrics 后按 F5 刷新,观察 Counter 值是否递增;或使用 watch -n 1 'curl -s http://localhost:8080/metrics | grep login_total' 实时观察变化。

4.5 内联 SVG:自定义指标采集流程

自定义指标采集流程 应用服务 (Go / Java / Python) Prometheus 客户端库 管理指标状态 /metrics 端点 HTTPServer 响应 Prometheus Server 定期 Pull(scrape) 存储 / 告警 业务逻辑更新指标 Inc() / Set() / Observe() 图例 应用代码 客户端库 Prometheus Server

4.6 调试与常见错误

错误现象根源解决方案
curl 返回 404/metrics 未注册路由检查 Go 的 http.Handle("/metrics", ...) 或 Java 的 HTTPServer 端口是否启动
指标数据为空指标未注册到默认注册表使用 prometheus.MustRegister()(Go)/ .register()(Java)/ 自动注册(Python)
标签重复或拼写错误标签名大小写敏感,值类型不匹配确保所有 WithLabelValues 传入的标签顺序与定义一致
高基数标签导致性能下降将用户 ID、请求 ID 等无限增长的值作为标签标签必须是有限的枚举值,或改用 labelnames 仅用于缓存键
端口冲突多个应用使用相同 /metrics 端口每个应用使用独立端口,或通过反向代理添加路径区分
生产环境警示: 不要将 /metrics 端口暴露到公网,防止内部指标泄露。建议使用内部网络或绑定 127.0.0.1 配合反向代理认证。

4.7 性能与稳定性优化

  • 减少锁竞争: 高并发场景下,Counter 的 Inc() 内部使用原子操作;Histogram 的 Observe() 操作较重,可考虑使用 Precomputed Histogram 或异步批量上报。
  • 避免不必要的高基数: 标签基数超过 1000 即可能造成 Prometheus 内存膨胀,建议限制标签值枚举上限。
  • HTTP 超时与优雅关闭: 为 /metrics HTTP 服务设置 ReadTimeout / WriteTimeout(Go 示例需手动配置),并注册 shutdown hook 释放注册表。
  • 自定义注册表隔离: 对于多组件应用,使用独立 prometheus.NewRegistry() 避免指标名称冲突。
  • 指标缓存: 若指标更新远快于 scrape 间隔,使用 prometheus.NewCounterFunc 或 NewGaugeFunc 在拉取时计算值,减少实时更新开销。