從零開始寫一個Exporter

我是碼客發表於2019-06-25

前言

上一篇文章中已經給大家整體的介紹了開源監控系統Prometheus,其中Exporter作為整個系統的Agent端,通過HTTP介面暴露需要監控的資料。那麼如何將使用者指標通過Exporter的形式暴露出來呢?比如說線上,請求失敗數,異常請求等指標可以通過Exporter的形式暴露出來,從而基於這些指標做告警監控。

 

演示環境

$ uname -a
Darwin 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64
$ go version
go version go1.12.4 darwin/amd64

 

四類指標介紹

Prometheus定義了4種不同的指標型別:Counter(計數器),Gauge(儀表盤),Histogram(直方圖),Summary(摘要)。

其中Exporter返回的樣本資料中會包含資料型別的說明,例如:

# TYPE node_network_carrier_changes_total counter
node_network_carrier_changes_total{device="br-01520cb4f523"} 1

這四類指標的特徵為:

Counter:只增不減(除非系統發生重啟,或者使用者程式有異常)的計數器。常見的監控指標如http_requests_total, node_cpu都是Counter型別的監控指標。一般推薦在定義為Counter的指標末尾加上_total作為字尾。

Gauge:可增可減的儀表盤。Gauge型別的指標側重於反應系統當前的狀態。因此此類指標的資料可增可減。常見的例如node_memory_MemAvailable_bytes(可用記憶體)。

HIstogram:分析資料分佈的直方圖。顯示資料的區間分佈。例如統計請求耗時在0-10ms的請求數量和10ms-20ms的請求數量分佈。

Summary: 分析資料分佈的摘要。顯示資料的中位數,9分數等。

 

實戰

接下來我將用Prometheus提供的Golang SDK 編寫包含上述四類指標的Exporter,示例的編寫修改自SDK的example。由於example中示例比較複雜,我會精簡一下,儘量讓大家用最小的學習成本能夠領悟到Exporter開發的精髓。第一個例子會演示Counter和Gauge的用法,第二個例子演示Histogram和Summary的用法。

Counter和Gauge用法演示:

package main

import (
    "flag"
    "log"
    "net/http"

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

var addr = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")

func main() {
    flag.Parse()
    http.Handle("/metrics", promhttp.Handler())
    log.Fatal(http.ListenAndServe(*addr, nil))
}

上述程式碼就是一個通過0.0.0.0:8080/metrics 暴露golang資訊的原始Exporter,沒有包含任何的使用者自定義指標資訊。接下來往裡面新增Counter和Gauge型別指標:

 1 func recordMetrics() {
 2     go func() {
 3         for {
 4             opsProcessed.Inc()
 5             myGague.Add(11)
 6             time.Sleep(2 * time.Second)
 7         }
 8     }()
 9 }
10 
11 var (
12     opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
13         Name: "myapp_processed_ops_total",
14         Help: "The total number of processed events",
15     })
16     myGague = promauto.NewGauge(prometheus.GaugeOpts{
17         Name: "my_example_gauge_data",
18         Help: "my example gauge data",
19         ConstLabels:map[string]string{"error":""},
20     })
21 )

在上面的main函式中新增recordMetrics方法呼叫。curl 127.0.0.1:8080/metrics 能看到自定義的Counter型別指標myapp_processed_ops_total 和 Gauge 型別指標my_example_gauge_data。

# HELP my_example_gauge_data my example gauge data
# TYPE my_example_gauge_data gauge
my_example_gauge_data{error=""} 44
# HELP myapp_processed_ops_total The total number of processed events
# TYPE myapp_processed_ops_total counter
myapp_processed_ops_total 4

其中#HELP 是程式碼中的Help欄位資訊,#TYPE 說明欄位的型別,例如my_example_gauge_data是gauge型別指標。my_example_gauge_data是指標名稱,大括號括起來的error是該指標的維度,44是該指標的值。需要特別注意的是第12行和16行用的是promauto包的NewXXX方法,例如:

func NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
    c := prometheus.NewCounter(opts)
    prometheus.MustRegister(c)
    return c
}

可以看到該函式是會自動呼叫MustRegister方法,如果用的是prometheus包的NewCounter則需要再自行呼叫MustRegister註冊收集的指標。其中Couter型別指標有以下的內建介面:

type Counter interface {
    Metric
    Collector

    // Inc increments the counter by 1. Use Add to increment it by arbitrary
    // non-negative values.
    Inc()
    // Add adds the given value to the counter. It panics if the value is <
    // 0.
    Add(float64)
}

可以通過Inc()介面給指標直接進行+1操作,也可以通過Add(float64)給指標加上某個值。還有繼承自Metric和Collector的一些描述介面,這裡不做展開。

Gauge型別的內建介面有:

type Gauge interface {
    Metric
    Collector

    // Set sets the Gauge to an arbitrary value.
    Set(float64)
    // Inc increments the Gauge by 1. Use Add to increment it by arbitrary
    // values.
    Inc()
    // Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary
    // values.
    Dec()
    // Add adds the given value to the Gauge. (The value can be negative,
    // resulting in a decrease of the Gauge.)
    Add(float64)
    // Sub subtracts the given value from the Gauge. (The value can be
    // negative, resulting in an increase of the Gauge.)
    Sub(float64)

    // SetToCurrentTime sets the Gauge to the current Unix time in seconds.
    SetToCurrentTime()
}

需要注意的是Gauge提供了Sub(float64)的減操作介面,因為Gauge是可增可減的指標。Counter因為是隻增不減的指標,所以只有加的介面。

 

Histogram和Summary用法演示:

 1 package main
 2 
 3 import (
 4     "flag"
 5     "fmt"
 6     "log"
 7     "math"
 8     "math/rand"
 9     "net/http"
10     "time"
11 
12     "github.com/prometheus/client_golang/prometheus"
13     "github.com/prometheus/client_golang/prometheus/promhttp"
14 )
15 
16 var (
17     addr              = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")
18     uniformDomain     = flag.Float64("uniform.domain", 0.0002, "The domain for the uniform distribution.")
19     normDomain        = flag.Float64("normal.domain", 0.0002, "The domain for the normal distribution.")
20     normMean          = flag.Float64("normal.mean", 0.00001, "The mean for the normal distribution.")
21     oscillationPeriod = flag.Duration("oscillation-period", 10*time.Minute, "The duration of the rate oscillation period.")
22 )
23 
24 var (
25     rpcDurations = prometheus.NewSummaryVec(
26         prometheus.SummaryOpts{
27             Name:       "rpc_durations_seconds",
28             Help:       "RPC latency distributions.",
29             Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
30         },
31         []string{"service","error_code"},
32     )
33     rpcDurationsHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
34         Name:    "rpc_durations_histogram_seconds",
35         Help:    "RPC latency distributions.",
36         Buckets: prometheus.LinearBuckets(0, 5, 20),
37     })
38 )
39 
40 func init() {
41     // Register the summary and the histogram with Prometheus's default registry.
42     prometheus.MustRegister(rpcDurations)
43     prometheus.MustRegister(rpcDurationsHistogram)
44     // Add Go module build info.
45     prometheus.MustRegister(prometheus.NewBuildInfoCollector())
46 }
47 
48 func main() {
49     flag.Parse()
50 
51     start := time.Now()
52 
53     oscillationFactor := func() float64 {
54         return 2 + math.Sin(math.Sin(2*math.Pi*float64(time.Since(start))/float64(*oscillationPeriod)))
55     }
56 
57     go func() {
58         i := 1
59         for {
60             time.Sleep(time.Duration(75*oscillationFactor()) * time.Millisecond)
61             if (i*3) > 100 {
62                 break
63             }
64             rpcDurations.WithLabelValues("normal","400").Observe(float64((i*3)%100))
65             rpcDurationsHistogram.Observe(float64((i*3)%100))
66             fmt.Println(float64((i*3)%100), " i=", i)
67             i++
68         }
69     }()
70 
71     go func() {
72         for {
73             v := rand.ExpFloat64() / 1e6
74             rpcDurations.WithLabelValues("exponential", "303").Observe(v)
75             time.Sleep(time.Duration(50*oscillationFactor()) * time.Millisecond)
76         }
77     }()
78 
79     // Expose the registered metrics via HTTP.
80     http.Handle("/metrics", promhttp.Handler())
81     log.Fatal(http.ListenAndServe(*addr, nil))
82 }

第25-32行定義了一個Summary型別指標,其中有service和errro_code兩個維度。第33-37行定義了一個Histogram型別指標,從0開始,5為寬度,有20個直方。也就是0-5,6-10,11-15 .... 等20個範圍統計。

其中直方圖HIstogram指標的相關結果為:

 1 # HELP rpc_durations_histogram_seconds RPC latency distributions.
 2 # TYPE rpc_durations_histogram_seconds histogram
 3 rpc_durations_histogram_seconds_bucket{le="0"} 0
 4 rpc_durations_histogram_seconds_bucket{le="5"} 1
 5 rpc_durations_histogram_seconds_bucket{le="10"} 3
 6 rpc_durations_histogram_seconds_bucket{le="15"} 5
 7 rpc_durations_histogram_seconds_bucket{le="20"} 6
 8 rpc_durations_histogram_seconds_bucket{le="25"} 8
 9 rpc_durations_histogram_seconds_bucket{le="30"} 10
10 rpc_durations_histogram_seconds_bucket{le="35"} 11
11 rpc_durations_histogram_seconds_bucket{le="40"} 13
12 rpc_durations_histogram_seconds_bucket{le="45"} 15
13 rpc_durations_histogram_seconds_bucket{le="50"} 16
14 rpc_durations_histogram_seconds_bucket{le="55"} 18
15 rpc_durations_histogram_seconds_bucket{le="60"} 20
16 rpc_durations_histogram_seconds_bucket{le="65"} 21
17 rpc_durations_histogram_seconds_bucket{le="70"} 23
18 rpc_durations_histogram_seconds_bucket{le="75"} 25
19 rpc_durations_histogram_seconds_bucket{le="80"} 26
20 rpc_durations_histogram_seconds_bucket{le="85"} 28
21 rpc_durations_histogram_seconds_bucket{le="90"} 30
22 rpc_durations_histogram_seconds_bucket{le="95"} 31
23 rpc_durations_histogram_seconds_bucket{le="+Inf"} 33
24 rpc_durations_histogram_seconds_sum 1683
25 rpc_durations_histogram_seconds_count 33

xxx_count反應當前指標的記錄總數,xxx_sum表示當前指標的總數。不同的le表示不同的區間,後面的數字是從開始到這個區間的總數。例如le="30"後面的10表示有10個樣本落在0-30區間,那麼26-30這個區間一共有多少個樣本呢,只需要用len="30" - len="25",即2個。也就是27和30這兩個點。

Summary相關的結果如下:

 1 # HELP rpc_durations_seconds RPC latency distributions.
 2 # TYPE rpc_durations_seconds summary
 3 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.5"} 7.176288428497417e-07
 4 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.9"} 2.6582266087185467e-06
 5 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.99"} 4.013935374172691e-06
 6 rpc_durations_seconds_sum{error_code="303",service="exponential"} 0.00015065426336339398
 7 rpc_durations_seconds_count{error_code="303",service="exponential"} 146
 8 rpc_durations_seconds{error_code="400",service="normal",quantile="0.5"} 51
 9 rpc_durations_seconds{error_code="400",service="normal",quantile="0.9"} 90
10 rpc_durations_seconds{error_code="400",service="normal",quantile="0.99"} 99
11 rpc_durations_seconds_sum{error_code="400",service="normal"} 1683
12 rpc_durations_seconds_count{error_code="400",service="normal"} 33

其中sum和count指標的含義和上面Histogram一致。拿第8-10行指標來說明,第8行的quantile 0.5 表示這裡指標的中位數是51,9分數是90。

 

自定義型別

如果上面Counter,Gauge,Histogram,Summary四種內建指標都不能滿足我們要求時,我們還可以自定義型別。只要實現了Collect介面的方法,然後呼叫MustRegister即可:

func MustRegister(cs ...Collector) {
    DefaultRegisterer.MustRegister(cs...)
}

type Collector interface {
    Describe(chan<- *Desc)
    Collect(chan<- Metric)
}

 

總結

文章通過Prometheus內建的Counter(計數器),Gauge(儀表盤),Histogram(直方圖),Summary(摘要)演示了Exporter的開發,最後提供了自定義型別的實現方法。

 

參考

https://prometheus.io/docs/guides/go-application/

https://yunlzheng.gitbook.io/prometheus-book/parti-prometheus-ji-chu/promql/prometheus-metrics-types

https://songjiayang.gitbooks.io/prometheus/content/concepts/metric-types.html

 

 

相關文章