為 Go 應(yīng)用添加 Prometheus 監(jiān)控指標(biāo)
前面我們了解了如何儀表化應(yīng)用,接下來我們將學(xué)習(xí)使用 Prometheus 的 Go 客戶端庫來為一個 Go 應(yīng)用程序添加和暴露監(jiān)控指標(biāo)。
創(chuàng)建應(yīng)用
我們首先從一個最簡單的 Go 應(yīng)用程序開始,在端口 8080 的 /metrics 端點(diǎn)上暴露客戶端庫的默認(rèn)注冊表,暫時還沒有跟蹤任何其他自定義的監(jiān)控指標(biāo)。
先創(chuàng)建一個名為 instrument-demo 的目錄,在該目錄下面初始化項(xiàng)目:
- ☸ ➜ mkdir instrument-demo && cd instrument-demo
- ☸ ➜ go mod init github.com/cnych/instrument-demo
上面的命令會在 instrument-demo 目錄下面生成一個 go.mod 文件,在同目錄下面新建一個 main.go 的入口文件,內(nèi)容如下所示:
- 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)
- }
然后執(zhí)行下面的命令下載 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
然后直接執(zhí)行 go run 命令啟動服務(wù):
- ☸ ➜ go run main.go
然后我們可以在瀏覽器中訪問 http://localhost:8080/metrics 來獲得默認(rèn)的監(jiān)控指標(biāo)數(shù)據(jù):
- # 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
我們并沒有在代碼中添加什么業(yè)務(wù)邏輯,但是可以看到依然有一些指標(biāo)數(shù)據(jù)輸出,這是因?yàn)?Go 客戶端庫默認(rèn)在我們暴露的全局默認(rèn)指標(biāo)注冊表中注冊了一些關(guān)于 promhttp 處理器和運(yùn)行時間相關(guān)的默認(rèn)指標(biāo),根據(jù)不同指標(biāo)名稱的前綴可以看出:
- go_*:以 go_ 為前綴的指標(biāo)是關(guān)于 Go 運(yùn)行時相關(guān)的指標(biāo),比如垃圾回收時間、goroutine 數(shù)量等,這些都是 Go 客戶端庫特有的,其他語言的客戶端庫可能會暴露各自語言的其他運(yùn)行時指標(biāo)。
- promhttp_*:來自 promhttp 工具包的相關(guān)指標(biāo),用于跟蹤對指標(biāo)請求的處理。
這些默認(rèn)的指標(biāo)是非常有用,但是更多的時候我們需要自己控制,來暴露一些自定義指標(biāo)。這就需要我們?nèi)?shí)現(xiàn)自定義的指標(biāo)了。
添加自定義指標(biāo)
接下來我們來自定義一個的 gauge 指標(biāo)來暴露當(dāng)前的溫度。創(chuàng)建一個新的文件 custom-metric/main.go,內(nèi)容如下所示:
- package main
- import (
- "net/http"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- )
- func main() {
- // 創(chuàng)建一個沒有任何 label 標(biāo)簽的 gauge 指標(biāo)
- temp := prometheus.NewGauge(prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- })
- // 在默認(rèn)的注冊表中注冊該指標(biāo)
- prometheus.MustRegister(temp)
- // 設(shè)置 gauge 的值為 39
- temp.Set(39)
- // 暴露指標(biāo)
- http.Handle("/metrics", promhttp.Handler())
- http.ListenAndServe(":8080", nil)
- }
上面文件中和最初的文件就有一些變化了:
- 我們使用 prometheus.NewGauge() 函數(shù)創(chuàng)建了一個自定義的 gauge 指標(biāo)對象,指標(biāo)名稱為 home_temperature_celsius,并添加了一個注釋信息。
- 然后使用 prometheus.MustRegister() 函數(shù)在默認(rèn)的注冊表中注冊了這個 gauge 指標(biāo)對象。
- 通過調(diào)用 Set() 方法將 gauge 指標(biāo)的值設(shè)置為 39。
- 然后像之前一樣通過 HTTP 暴露默認(rèn)的注冊表。
需要注意的是除了 prometheus.MustRegister() 函數(shù)之外還有一個 prometheus.Register() 函數(shù),一般在 golang 中我們會將 Mustxxx 開頭的函數(shù)定義為必須滿足條件的函數(shù),如果不滿足會返回一個 panic 而不是一個 error 操作,所以如果這里不能正常注冊的話會拋出一個 panic。
現(xiàn)在我們來運(yùn)行這個程序:
- ☸ ➜ go run ./custom-metric
啟動后重新訪問指標(biāo)接口 http://localhost:8080/metrics,仔細(xì)對比我們會發(fā)現(xiàn)多了一個名為 home_temperature_celsius 的指標(biāo):
- ...
- # HELP home_temperature_celsius The current temperature in degrees Celsius.
- # TYPE home_temperature_celsius gauge
- home_temperature_celsius 42
- ...
這樣我們就實(shí)現(xiàn)了添加一個自定義的指標(biāo)的操作,整體比較簡單,當(dāng)然在實(shí)際的項(xiàng)目中需要結(jié)合業(yè)務(wù)來確定添加哪些自定義指標(biāo)。
自定義注冊表
前面我們是使用 prometheus.MustRegister() 函數(shù)來將指標(biāo)注冊到全局默認(rèn)注冊中,此外我們還可以使用 prometheus.NewRegistry() 函數(shù)來創(chuàng)建和使用自己的非全局的注冊表。
既然有全局的默認(rèn)注冊表,為什么我們還需要自定義注冊表呢?這主要是因?yàn)椋?/p>
- 全局變量通常不利于維護(hù)和測試,創(chuàng)建一個非全局的注冊表,并明確地將其傳遞給程序中需要注冊指標(biāo)的地方,這也一種更加推薦的做法。
- 全局默認(rèn)注冊表包括一組默認(rèn)的指標(biāo),我們有時候可能希望除了自定義的指標(biāo)之外,不希望暴露其他的指標(biāo)。
下面的示例程序演示了如何創(chuàng)建、使用和暴露一個非全局注冊表對象,創(chuàng)建一個文件 custom-registry/main.go,內(nèi)容如下所示:
- package main
- import (
- "net/http"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- )
- func main() {
- // 創(chuàng)建一個自定義的注冊表
- registry := prometheus.NewRegistry()
- // 可選: 添加 process 和 Go 運(yùn)行時指標(biāo)到我們自定義的注冊表中
- registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
- registry.MustRegister(prometheus.NewGoCollector())
- // 創(chuàng)建一個簡單呃 gauge 指標(biāo)。
- temp := prometheus.NewGauge(prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- })
- // 使用我們自定義的注冊表注冊 gauge
- registry.MustRegister(temp)
- // 設(shè)置 gague 的值為 39
- temp.Set(39)
- // 暴露自定義指標(biāo)
- http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry}))
- http.ListenAndServe(":8080", nil)
- }
上面我們沒有使用全局默認(rèn)的注冊表了,而是創(chuàng)建的一個自定義的注冊表:
- 首先使用 prometheus.NewRegistry() 函數(shù)創(chuàng)建我們自己的注冊表對象。
- 然后使用自定義注冊表對象上面的 MustRegister() 哈是來注冊 guage 指標(biāo),而不是調(diào)用 prometheus.MustRegister() 函數(shù)(這會使用全局默認(rèn)的注冊表)。
- 如果我們希望在自定義注冊表中也有進(jìn)程和 Go 運(yùn)行時相關(guān)的指標(biāo),我們可以通過實(shí)例化 Collector 收集器來添加他們。
- 最后在暴露指標(biāo)的時候必須通過調(diào)用 promhttp.HandleFor() 函數(shù)來創(chuàng)建一個專門針對我們自定義注冊表的 HTTP 處理器,為了同時暴露前面示例中的 promhttp_* 相關(guān)的指標(biāo),我們還需要在 promhttp.HandlerOpts 配置對象的 Registry 字段中傳遞我們的注冊表對象。
同樣我們重新運(yùn)行上面的自定義注冊表程序:
- ☸ ➜ go run ./custom-metric
啟動后再次訪問指標(biāo)接口 http://localhost:8080/metrics,可以發(fā)現(xiàn)和上面示例中的指標(biāo)數(shù)據(jù)是相同的。
指標(biāo)定制
Gauges
前面的示例我們已經(jīng)了解了如何添加 gauge 類型的指標(biāo),創(chuàng)建了一個沒有任何標(biāo)簽的指標(biāo),直接使用 prometheus.NewGauge() 函數(shù)即可實(shí)例化一個 gauge 類型的指標(biāo)對象,通過 prometheus.GaugeOpts 對象可以指定指標(biāo)的名稱和注釋信息:
- queueLength := prometheus.NewGauge(prometheus.GaugeOpts{
- Name: "queue_length",
- Help: "The number of items in the queue.",
- })
我們知道 gauge 類型的指標(biāo)值是可以上升或下降的,所以我們可以為 gauge 指標(biāo)設(shè)置一個指定的值,所以 gauge 指標(biāo)對象暴露了 Set()、Inc()、Dec()、Add() 和 Sub() 這些函數(shù)來更改指標(biāo)值:
- // 使用 Set() 設(shè)置指定的值
- 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 儀表盤經(jīng)常被用來暴露 Unix 的時間戳樣本值,所以也有一個方便的方法來將 gauge 設(shè)置為當(dāng)前的時間戳:
- demoTimestamp.SetToCurrentTime()
最終 gauge 指標(biāo)會被渲染成如下所示的數(shù)據(jù):
- # HELP queue_length The number of items in the queue.
- # TYPE queue_length gauge
- queue_length 42
Counters
要創(chuàng)建一個 counter 類型的指標(biāo)和 gauge 比較類似,只是用 prometheus.NewCounter() 函數(shù)來初始化指標(biāo)對象:
- totalRequests := prometheus.NewCounter(prometheus.CounterOpts{
- Name: "http_requests_total",
- Help: "The total number of handled HTTP requests.",
- })
我們知道 counter 指標(biāo)只能隨著時間的推移而不斷增加,所以我們不能為其設(shè)置一個指定的值或者減少指標(biāo)值,所以該對象下面只有 Inc() 和 Add() 兩個函數(shù):
- totalRequests.Inc() // +1:Increment the counter by 1.
- totalRequests.Add(23) // +n:Increment the counter by 23.
當(dāng)服務(wù)進(jìn)程重新啟動的時候,counter 指標(biāo)值會被重置為 0,不過不用擔(dān)心數(shù)據(jù)錯亂,我們一般會使用的 rate() 函數(shù)會自動處理。
最終 counter 指標(biāo)會被渲染成如下所示的數(shù)據(jù):
- # HELP http_requests_total The total number of handled HTTP requests.
- # TYPE http_requests_total counter
- http_requests_total 7734
Histograms
創(chuàng)建直方圖指標(biāo)比 counter 和 gauge 都要復(fù)雜,因?yàn)樾枰渲冒延^測值歸入的 bucket 的數(shù)量,以及每個 bucket 的上邊界。Prometheus 中的直方圖是累積的,所以每一個后續(xù)的 bucket 都包含前一個 bucket 的觀察計(jì)數(shù),所有 bucket 的下限都從 0 開始的,所以我們不需要明確配置每個 bucket 的下限,只需要配置上限即可。
同樣要創(chuàng)建直方圖指標(biāo)對象,我們使用 prometheus.NewHistogram() 函數(shù)來進(jìn)行初始化:
- requestDurations := prometheus.NewHistogram(prometheus.HistogramOpts{
- Name: "http_request_duration_seconds",
- Help: "A histogram of the HTTP request durations in seconds.",
- // Bucket 配置:第一個 bucket 包括所有在 0.05s 內(nèi)完成的請求,最后一個包括所有在10s內(nèi)完成的請求。
- Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
- })
這里和前面不一樣的地方在于除了指定指標(biāo)名稱和幫助信息之外,還需要配置 Buckets。如果我們手動去枚舉所有的 bucket 可能很繁瑣,所以 Go 客戶端庫為為我們提供了一些輔助函數(shù)可以幫助我們生成線性或者指數(shù)增長的 bucket,比如 prometheus.LinearBuckets() 和 prometheus.ExponentialBuckets() 函數(shù)。
直方圖會自動對數(shù)值的分布進(jìn)行分類和計(jì)數(shù),所以它只有一個 Observe() 方法,每當(dāng)你在代碼中處理要跟蹤的數(shù)據(jù)時,就會調(diào)用這個方法。例如,如果你剛剛處理了一個 HTTP 請求,花了 0.42 秒,則可以使用下面的代碼來跟蹤。
- requestDurations.Observe(0.42)
由于跟蹤持續(xù)時間是直方圖的一個常見用例,Go 客戶端庫就提供了輔助函數(shù),用于對代碼的某些部分進(jìn)行計(jì)時,然后自動觀察所產(chǎn)生的持續(xù)時間,將其轉(zhuǎn)化為直方圖,如下代碼所示:
- // 啟動一個計(jì)時器
- timer := prometheus.NewTimer(requestDurations)
- // [...在應(yīng)用中處理請求...]
- // 停止計(jì)時器并觀察其持續(xù)時間,將其放進(jìn) requestDurations 的直方圖指標(biāo)中去
- timer.ObserveDuration()
直方圖指標(biāo)最終會生成如下所示的數(shù)據(jù):
- # 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
每個配置的存儲桶最終作為一個帶有 _bucket 后綴的計(jì)數(shù)器時間序列,使用 le(小于或等于) 標(biāo)簽指示該存儲桶的上限,具有上限的隱式存儲桶 +Inf 也暴露于比最大配置的存儲桶邊界花費(fèi)更長的時間的請求,還包括使用后綴 _sum 累積總和和計(jì)數(shù) _count 的指標(biāo),這些時間序列中的每一個在概念上都是一個 counter 計(jì)數(shù)器(只能上升的單個值),只是它們是作為直方圖的一部分創(chuàng)建的。
Summaries
創(chuàng)建和使用摘要與直方圖非常類似,只是我們需要指定要跟蹤的 quantiles 分位數(shù)值,而不需要處理 bucket 桶,比如我們想要跟蹤 HTTP 請求延遲的第 50、90 和 99 個百分位數(shù),那么我們可以創(chuàng)建這樣的一個摘要對象:
- 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個百分位數(shù),最大絕對誤差為0.05。
- 0.9: 0.01, // 第90個百分位數(shù),最大絕對誤差為0.01。
- 0.99: 0.001, // 第90個百分位數(shù),最大絕對誤差為0.001。
- },
- },
- )
這里和前面不一樣的地方在于使用 prometheus.NewSummary() 函數(shù)初始化摘要指標(biāo)對象的時候,需要通過 prometheus.SummaryOpts{} 對象的 Objectives 屬性指定想要跟蹤的分位數(shù)值。
同樣摘要指標(biāo)對象創(chuàng)建后,跟蹤持續(xù)時間的方式和直方圖是完全一樣的,使用一個 Observe() 函數(shù)即可:
- requestDurations.Observe(0.42)
雖然直方圖桶可以跨維度匯總(如端點(diǎn)、HTTP 方法等),但這對于匯總 quantiles 分位數(shù)值來說在統(tǒng)計(jì)學(xué)上是無效的。例如,你不能對兩個單獨(dú)的服務(wù)實(shí)例的第 90 百分位延遲進(jìn)行平均,并期望得到一個有效的整體第 90 百分位延遲。如果需要按維度進(jìn)行匯總,那么我們需要使用直方圖而不是摘要指標(biāo)。
摘要指標(biāo)最終生成的指標(biāo)數(shù)據(jù)與直方圖非常類似,不同之處在于使用 quantile 標(biāo)簽來表示分位數(shù)序列,并且這些序列沒有擴(kuò)展指標(biāo)名稱的后綴:
- # 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
標(biāo)簽
到目前為止,我們還沒有為指標(biāo)對象添加任何的標(biāo)簽,要創(chuàng)建具有標(biāo)簽維度的指標(biāo),我們可以調(diào)用類似于 NewXXXVec() 的構(gòu)造函數(shù)來初始化指標(biāo)對象:
- NewGauge() 變成 NewGaugeVec()
- NewCounter() 變成 NewCounterVec()
- NewSummary() 變成 NewSummaryVec()
- NewHistogram() 變成 NewHistogramVec()
這些函數(shù)允許我們指定一個額外的字符串切片參數(shù),提供標(biāo)簽名稱的列表,通過它來拆分指標(biāo)。
例如,為了按照房子以及測量溫度的房間來劃分我們早期的溫度表指標(biāo),可以這樣創(chuàng)建指標(biāo)。
- temp := prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- },
- // 兩個標(biāo)簽名稱,通過它們來分割指標(biāo)。
- []string{"house", "room"},
- )
然后要訪問一個特有標(biāo)簽的子指標(biāo),需要在設(shè)置其值之前,用 house 和 room 標(biāo)簽的各自數(shù)值,對產(chǎn)生的 gauge 向量調(diào)用 WithLabelValues() 方法來處理下:
- // 為 home=ydzs 和 room=living-room 設(shè)置指標(biāo)值
- temp.WithLabelValues("ydzs", "living-room").Set(27)
如果你喜歡在選擇的子指標(biāo)中明確提供標(biāo)簽名稱,可以使用效率稍低的 With() 方法來代替:
- temp.With(prometheus.Labels{"house": "ydzs", "room": "living-room"}).Set(66)
不過需要注意如果向這兩個方法傳遞不正確的標(biāo)簽數(shù)量或不正確的標(biāo)簽名稱,這兩個方法都會觸發(fā) panic。
下面是我們按照 house 和 room 標(biāo)簽維度區(qū)分指標(biāo)的完整示例,創(chuàng)建一個名為 label-metric/main.go 的新文件,內(nèi)容如下所示:
- package main
- import (
- "net/http"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- )
- func main() {
- // 創(chuàng)建帶 house 和 room 標(biāo)簽的 gauge 指標(biāo)對象
- temp := prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- },
- // 指定標(biāo)簽名稱
- []string{"house", "room"},
- )
- // 注冊到全局默認(rèn)注冊表中
- prometheus.MustRegister(temp)
- // 針對不同標(biāo)簽值設(shè)置不同的指標(biāo)值
- 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)
- // 暴露自定義的指標(biāo)
- http.Handle("/metrics", promhttp.Handler())
- http.ListenAndServe(":8080", nil)
- }
上面代碼非常清晰了,運(yùn)行下面的程序:
- ☸ ➜ go run ./label-metric
啟動完成后重新訪問指標(biāo)端點(diǎn) http://localhost:8080/metrics,可以找到 home_temperature_celsius 指標(biāo)不同標(biāo)簽維度下面的指標(biāo)值:
- ...
- # 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
- ...
注意:當(dāng)使用帶有標(biāo)簽維度的指標(biāo)時,任何標(biāo)簽組合的時間序列只有在該標(biāo)簽組合被訪問過至少一次后才會出現(xiàn)在 /metrics 輸出中,這對我們在 PromQL 查詢的時候會產(chǎn)生一些問題,因?yàn)樗M承r間序列一直存在,我們可以在程序第一次啟動時,將所有重要的標(biāo)簽組合預(yù)先初始化為默認(rèn)值。
同樣的方式在其他幾個指標(biāo)類型中使用標(biāo)簽的方法與上面的方式一致。

































