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 实现流程
- 在项目中引入对应的 Prometheus 客户端库(Go:
prometheus/client_golang;Java:simpleclient;Python:prometheus_client)。 - 定义业务指标(注册到默认注册表或自定义注册表)。
- 在 HTTP 服务中添加
/metrics端点(通常使用库自带的 Handler)。 - 在业务逻辑中更新指标值(如
inc()、set()、observe())。 - 启动服务,验证
/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:自定义指标采集流程
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 超时与优雅关闭: 为
/metricsHTTP 服务设置 ReadTimeout / WriteTimeout(Go 示例需手动配置),并注册 shutdown hook 释放注册表。 - 自定义注册表隔离: 对于多组件应用,使用独立
prometheus.NewRegistry()避免指标名称冲突。 - 指标缓存: 若指标更新远快于 scrape 间隔,使用
prometheus.NewCounterFunc或NewGaugeFunc在拉取时计算值,减少实时更新开销。