前面我们了解了如何仪表化应用,接下来我们将学习使用 Prometheus 的 Go 客户端库来为一个 Go 应用程序添加和暴露监控指标。

创建应用

/metrics
instrument-demo
☸ ➜ mkdir instrument-demo && cd instrument-demo
☸ ➜ go mod init github.com/cnych/instrument-demo

instrument-demogo.modmain.go
package main

import (
 "net/http"

 "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    // Serve the default Prometheus metrics registry over HTTP on /metrics.
 http.Handle("/metrics", promhttp.Handler())
 http.ListenAndServe(":8080", nil)
}

然后执行下面的命令下载 Prometheus 客户端库依赖:

☸ ➜ export GOPROXY="https://goproxy.cn"
☸ ➜ go mod tidy
go: finding module for package github.com/prometheus/client_golang/prometheus/promhttp
go: found github.com/prometheus/client_golang/prometheus/promhttp in github.com/prometheus/client_golang v1.11.0
go: downloading google.golang.org/protobuf v1.26.0-rc.1

go run
☸ ➜ go run main.go

http://localhost:8080/metrics
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 6
......
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 8
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 1
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

promhttp
go_*go_promhttp_*promhttp

这些默认的指标是非常有用,但是更多的时候我们需要自己控制,来暴露一些自定义指标。这就需要我们去实现自定义的指标了。

添加自定义指标

gaugecustom-metric/main.go
package main

import (
 "net/http"

 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    // 创建一个没有任何 label 标签的 gauge 指标
 temp := prometheus.NewGauge(prometheus.GaugeOpts{
  Name: "home_temperature_celsius",
  Help: "The current temperature in degrees Celsius.",
 })

 // 在默认的注册表中注册该指标
 prometheus.MustRegister(temp)

 // 设置 gauge 的值为 39
 temp.Set(39)

 // 暴露指标
 http.Handle("/metrics", promhttp.Handler())
 http.ListenAndServe(":8080", nil)
}

上面文件中和最初的文件就有一些变化了:

prometheus.NewGauge()home_temperature_celsiusprometheus.MustRegister()Set()
prometheus.MustRegister()prometheus.Register()Mustxxx

现在我们来运行这个程序:

☸ ➜ go run ./custom-metric

http://localhost:8080/metricshome_temperature_celsius
...
# HELP home_temperature_celsius The current temperature in degrees Celsius.
# TYPE home_temperature_celsius gauge
home_temperature_celsius 42
...

这样我们就实现了添加一个自定义的指标的操作,整体比较简单,当然在实际的项目中需要结合业务来确定添加哪些自定义指标。

自定义注册表

prometheus.MustRegister()prometheus.NewRegistry()

既然有全局的默认注册表,为什么我们还需要自定义注册表呢?这主要是因为:

  • 全局变量通常不利于维护和测试,创建一个非全局的注册表,并明确地将其传递给程序中需要注册指标的地方,这也一种更加推荐的做法。
  • 全局默认注册表包括一组默认的指标,我们有时候可能希望除了自定义的指标之外,不希望暴露其他的指标。
custom-registry/main.go
package main

import (
 "net/http"

 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
 // 创建一个自定义的注册表
 registry := prometheus.NewRegistry()
 // 可选: 添加 process 和 Go 运行时指标到我们自定义的注册表中
 registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
 registry.MustRegister(prometheus.NewGoCollector())

 // 创建一个简单呃 gauge 指标。
 temp := prometheus.NewGauge(prometheus.GaugeOpts{
  Name: "home_temperature_celsius",
  Help: "The current temperature in degrees Celsius.",
 })

 // 使用我们自定义的注册表注册 gauge
 registry.MustRegister(temp)

 // 设置 gague 的值为 39
 temp.Set(39)

 // 暴露自定义指标
 http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry}))
 http.ListenAndServe(":8080", nil)
}

上面我们没有使用全局默认的注册表了,而是创建的一个自定义的注册表:

prometheus.NewRegistry()MustRegister()prometheus.MustRegister()Collectorpromhttp.HandleFor()promhttp_*promhttp.HandlerOptsRegistry

同样我们重新运行上面的自定义注册表程序:

☸ ➜ go run ./custom-metric

http://localhost:8080/metrics

指标定制

Gauges

prometheus.NewGauge()prometheus.GaugeOpts
queueLength := prometheus.NewGauge(prometheus.GaugeOpts{
 Name: "queue_length",
 Help: "The number of items in the queue.",
})

Set()Inc()Dec()Add()Sub()
// 使用 Set() 设置指定的值
queueLength.Set(0)

// 增加或减少
queueLength.Inc()   // +1:Increment the gauge by 1.
queueLength.Dec()   // -1:Decrement the gauge by 1.
queueLength.Add(23) // Increment by 23.
queueLength.Sub(42) // Decrement by 42.

另外 gauge 仪表盘经常被用来暴露 Unix 的时间戳样本值,所以也有一个方便的方法来将 gauge 设置为当前的时间戳:

demoTimestamp.SetToCurrentTime()

最终 gauge 指标会被渲染成如下所示的数据:

# HELP queue_length The number of items in the queue.
# TYPE queue_length gauge
queue_length 42

Counters

prometheus.NewCounter()
totalRequests := prometheus.NewCounter(prometheus.CounterOpts{
 Name: "http_requests_total",
 Help: "The total number of handled HTTP requests.",
})

Inc()Add()
totalRequests.Inc()   // +1:Increment the counter by 1.
totalRequests.Add(23) // +n:Increment the counter by 23.

rate()

最终 counter 指标会被渲染成如下所示的数据:

# HELP http_requests_total The total number of handled HTTP requests.
# TYPE http_requests_total counter
http_requests_total 7734

Histograms

创建直方图指标比 counter 和 gauge 都要复杂,因为需要配置把观测值归入的 bucket 的数量,以及每个 bucket 的上边界。Prometheus 中的直方图是累积的,所以每一个后续的 bucket 都包含前一个 bucket 的观察计数,所有 bucket 的下限都从 0 开始的,所以我们不需要明确配置每个 bucket 的下限,只需要配置上限即可。

prometheus.NewHistogram()
requestDurations := prometheus.NewHistogram(prometheus.HistogramOpts{
  Name:    "http_request_duration_seconds",
  Help:    "A histogram of the HTTP request durations in seconds.",
  // Bucket 配置:第一个 bucket 包括所有在 0.05s 内完成的请求,最后一个包括所有在10s内完成的请求。
  Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
})

prometheus.LinearBuckets()prometheus.ExponentialBuckets()
Observe()
requestDurations.Observe(0.42)

由于跟踪持续时间是直方图的一个常见用例,Go 客户端库就提供了辅助函数,用于对代码的某些部分进行计时,然后自动观察所产生的持续时间,将其转化为直方图,如下代码所示:

// 启动一个计时器
timer := prometheus.NewTimer(requestDurations)

// [...在应用中处理请求...]

// 停止计时器并观察其持续时间,将其放进 requestDurations 的直方图指标中去
timer.ObserveDuration()

直方图指标最终会生成如下所示的数据:

# HELP http_request_duration_seconds A histogram of the HTTP request durations in seconds.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 4599
http_request_duration_seconds_bucket{le="0.1"} 24128
http_request_duration_seconds_bucket{le="0.25"} 45311
http_request_duration_seconds_bucket{le="0.5"} 59983
http_request_duration_seconds_bucket{le="1"} 60345
http_request_duration_seconds_bucket{le="2.5"} 114003
http_request_duration_seconds_bucket{le="5"} 201325
http_request_duration_seconds_bucket{le="+Inf"} 227420
http_request_duration_seconds_sum 88364.234
http_request_duration_seconds_count 227420

_bucketle(小于或等于)+Inf_sum_count

Summaries

创建和使用摘要与直方图非常类似,只是我们需要指定要跟踪的 quantiles 分位数值,而不需要处理 bucket 桶,比如我们想要跟踪 HTTP 请求延迟的第 50、90 和 99 个百分位数,那么我们可以创建这样的一个摘要对象:

requestDurations := prometheus.NewSummary(prometheus.SummaryOpts{
    Name:       "http_request_duration_seconds",
    Help:       "A summary of the HTTP request durations in seconds.",
    Objectives: map[float64]float64{
      0.5: 0.05,   // 第50个百分位数,最大绝对误差为0.05。
      0.9: 0.01,   // 第90个百分位数,最大绝对误差为0.01。
      0.99: 0.001, // 第90个百分位数,最大绝对误差为0.001。
    },
  },
)

prometheus.NewSummary()prometheus.SummaryOpts{}Objectives
Observe()
requestDurations.Observe(0.42)

虽然直方图桶可以跨维度汇总(如端点、HTTP 方法等),但这对于汇总 quantiles 分位数值来说在统计学上是无效的。例如,你不能对两个单独的服务实例的第 90 百分位延迟进行平均,并期望得到一个有效的整体第 90 百分位延迟。如果需要按维度进行汇总,那么我们需要使用直方图而不是摘要指标。

quantile
# HELP http_request_duration_seconds A summary of the HTTP request durations in seconds.
# TYPE http_request_duration_seconds summary
http_request_duration_seconds{quantile="0.5"} 0.052
http_request_duration_seconds{quantile="0.90"} 0.564
http_request_duration_seconds{quantile="0.99"} 2.372
http_request_duration_seconds_sum 88364.234
http_request_duration_seconds_count 227420

标签

NewXXXVec()
NewGauge()NewGaugeVec()NewCounter()NewCounterVec()NewSummary()NewSummaryVec()NewHistogram()NewHistogramVec()

这些函数允许我们指定一个额外的字符串切片参数,提供标签名称的列表,通过它来拆分指标。

例如,为了按照房子以及测量温度的房间来划分我们早期的温度表指标,可以这样创建指标。

temp := prometheus.NewGaugeVec(
  prometheus.GaugeOpts{
    Name: "home_temperature_celsius",
    Help: "The current temperature in degrees Celsius.",
  },
  // 两个标签名称,通过它们来分割指标。
  []string{"house", "room"},
)

houseroomWithLabelValues()
// 为 home=ydzs 和 room=living-room 设置指标值
temp.WithLabelValues("ydzs", "living-room").Set(27)

With()
temp.With(prometheus.Labels{"house": "ydzs", "room": "living-room"}).Set(66)

不过需要注意如果向这两个方法传递不正确的标签数量或不正确的标签名称,这两个方法都会触发 panic。

houseroomlabel-metric/main.go
package main

import (
 "net/http"

 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    // 创建带 house 和 room 标签的 gauge 指标对象
 temp := prometheus.NewGaugeVec(
  prometheus.GaugeOpts{
   Name: "home_temperature_celsius",
   Help: "The current temperature in degrees Celsius.",
  },
  // 指定标签名称
  []string{"house", "room"},
 )

 // 注册到全局默认注册表中
 prometheus.MustRegister(temp)

 // 针对不同标签值设置不同的指标值
 temp.WithLabelValues("cnych", "living-room").Set(27)
 temp.WithLabelValues("cnych", "bedroom").Set(25.3)
 temp.WithLabelValues("ydzs", "living-room").Set(24.5)
 temp.WithLabelValues("ydzs", "bedroom").Set(27.7)

 // 暴露自定义的指标
 http.Handle("/metrics", promhttp.Handler())
 http.ListenAndServe(":8080", nil)
}

上面代码非常清晰了,运行下面的程序:

☸ ➜ go run ./label-metric

http://localhost:8080/metricshome_temperature_celsius
...
# HELP home_temperature_celsius The current temperature in degrees Celsius.
# TYPE home_temperature_celsius gauge
home_temperature_celsius{house="cnych",room="bedroom"} 25.3
home_temperature_celsius{house="cnych",room="living-room"} 27
home_temperature_celsius{house="ydzs",room="bedroom"} 27.7
home_temperature_celsius{house="ydzs",room="living-room"} 24.5
...

/metrics

同样的方式在其他几个指标类型中使用标签的方法与上面的方式一致。