新手玩GO語言,自己動手寫一個prometheus的exporter

最近想動手寫寫GO,正好自己在用prometheus當作監控平台的基底

就想說來寫個prometheus的exporter吧

這篇文章的目的是讓一個GO新手也能從無到有的做一個prometheus的exporter

如果想直接看code請參考:https://github.com/del680202/dummy_exporter

首先,prometheus的exporter在監控平台中扮演著什麼角色

可以參考下圖

prometheus+grafana是現在主流的監控搭配之一

prometheus自身擔任database的角色,搭配grafana我們可以可視化的監控各種時間序列數據

但是prometheus的數據從哪來呢,他還需要一個exporter的角色,讓prometheus可以拿到想監控的數據

現在主流的軟體平台幾乎都有人製作專屬的exporter了,像是spark/flink/mysql等等

上github用exporter當關鍵字搜尋就可以很容易找到

有趣的是甚至還有人做了bitcoin exporter

只要能自己客製化prometheus的exporter,那麼任何東西都能以時間序列數據呈現及監控

有此念頭因此開始研究怎麼寫prometheus exporter

雖然prometheus exporter支持多種語言,但是目前主流看到的都來是GO為主

自己上網找了幾個範例,想要嘗試修改

嘗試了半天怎麼編譯都不成功,最後決定從了解怎麼配置GO的環境開始入手

題外話,我自己目前學習go是從下面兩個網站著手,這裏順便分享一下

配置GO環境

首先要安裝GO,我筆電是mac 就先從下載安裝GO開始吧

GO下載位置:https://golang.org/dl/

安裝好之後先確定環境變數

$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/terrence/Library/Caches/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/terrence/golang"
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
...

這邊有兩個重要的環境變數,GOROOTGOPATH

之前拿別人的範例來改的時候,因為忽略了這兩個環境變數的設定,所以編譯一直出現dependency找不到的問題

會出現類似下面的錯誤

server.go:6:2: cannot find package "github.com/[username]/[appname]/models" in any of:
    /usr/local/opt/go/libexec/src/github.com/[username]/[appname]/models (from $GOROOT)
    /Users/[username]/src/github.com/[username]/[appname]/models (from $GOPATH)

這邊先配置go的環境變數

$ vim ~/.bash_profile
...
export GOROOT=/usr/local/go  
export GOPATH=$HOME/golang
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

$ source ~/.bash_profile

設置了三個變數

  • GOROOT: go的安裝路徑
  • GOPATH: go的工作根目錄,一定要有,不然import時會有各種奇怪的dependency錯誤
  • PATH: 增加$GOROOT/bin$GOPATH/bin這樣就可以直接用go的各種工具

這邊值得一提的$GOPATH/bin的配置,一開始我不知道怎麼配,使用go安裝的工具都無法用

GOPATH底下在作業過程中會產生三個目錄pkg, src, bin

其中src目錄,裡面存放各種專案目錄跟dependency,也是我們要進行開發的地方

而bin目錄,當使用go get還是go install時,會把可執行檔放進去

舉個例子,假設我想用glide這個工具去管理dependency

我要先用如下指令去安裝glide

$ go get github.com/Masterminds/glide

然後我就可以看到glide這個工具被放到$GOPATH/bin下面去了,而我$GOPATH/bin已經設定到PATH環境變數去了

因此接下來我就能直接使用glide這個工具

建立專案目錄

好了,GO的環境配置好了,接下來是要開發一個dummy_exporter當作範例

首先我要建立一個新的GO專案目錄,目前常看的的格式是$GOPATH/src/DOMAIN/USER_NAME/PROJECT_NAME

  • DOMAIN: 你放專案的網站,常見是github.com
  • USER_NAME: 假設我打算之後把專案同步到github,那就是我的github帳號
  • PROJECT_NAME: 你的專案名稱

當然也是可以做成比較扁平的架構, $GOPATH/src/PROJECT_NAME

不過這會影響的是自己之後import的路徑 這也是我剛開始踩到的雷

在go的專案常常會看到如下用法

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

go的import路徑會是先去GOROOT下面找,下一個則是去GOPATH/src下面找

由於現在go的一些dependency管理工具,會用DOMAIN/USER_NAME/PROJECT_NAME的形式展開

所以import的用法也常常是DOMAIN/USER_NAME/PROJECT_NAME的格式

所以這邊,我們還是循這種命名方式去建立我們的專案目錄

# 建立專案目錄,並移動過去
$ USER_NAME=del680202
$ PROJECT_NAME=dummy_exporter
$ mkdir -p $GOPATH/src/github.com/$USER_NAME/$PROJECT_NAME
$ cd $GOPATH/src/github.com/$USER_NAME/$PROJECT_NAME

安裝dep並初始化專案

到了專案目錄下之後,我們要開始開發一個新的GO專案

這邊我們選用dep當作我們的dependency管理工具

現在許多專案也慢慢從glide開始移植到dep去了

參考:日文注意

安裝方法如下

$ go get -v github.com/golang/dep
$ go install -v github.com/golang/dep/cmd/dep

安裝好後使用init指令去初始化專案

$ dep init

初始化好後會產生三個東西

  • vendor
  • Gopkg.toml
  • Gopkg.lock

其中vendor是用來存放第三方dependency套件的

Gopkg.toml等不用自己編寫,這是depglide最大不同的地方,這工具是直接參考source code去下載dependency

開始寫一個dummy exporter

這裡就不細講GO的語法了,首先產生一個main.go

$ vim main.go
main.go
package main

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

func main() {
        fmt.Println(`
  This is a dummy example of prometheus exporter
  Access: http://127.0.0.1:8081
  `)

        // Define parameters

        metricsPath := "/metrics"
        listenAddress := ":8081"

        // Launch http service

        http.Handle(metricsPath, prometheus.Handler())
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                w.Write([]byte(`<html>
             <head><title>Dummy Exporter</title></head>
             <body>
             <h1>Dummy Exporter</h1>
             <p><a href='` + metricsPath + `'>Metrics</a></p>
             </body>
             </html>`))
        })
        fmt.Println(http.ListenAndServe(listenAddress, nil))
}

上面的code就是一個最簡單的prometheus exporter

只使用了github.com/prometheus/client_golang/prometheus這個外部包

其中用了http包去起了一個http service並用prometheus.Handler當作http handler

有了這支程式之後,可以用dep去下載對應的包

$ dep ensure

之後可以在vendor目錄下看到github.com/prometheus/client_golang/prometheus的套件被下載下來了

確認套件的方法可以使用dep status

$ dep status
PROJECT                                           CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beorn7/perks                           branch master  branch master  4c0e845   4c0e845  1
github.com/golang/protobuf                        v1.0.0         v1.0.0         9255415   v1.0.0   1
github.com/matttproud/golang_protobuf_extensions  v1.0.0         v1.0.0         3247c84   v1.0.0   1
github.com/prometheus/client_golang               v0.8.0         v0.8.0         c5b7fcc   v0.8.0   1
github.com/prometheus/client_model                branch master  branch master  99fa1f4   99fa1f4  1
github.com/prometheus/common                      branch master  branch master  e4aa40a   e4aa40a  3
github.com/prometheus/procfs                      branch master  branch master  54d17b5   54d17b5  4

也可以輸出成圖片

$ dep status -dot | dot -Tpng -o pi-dependency.png

測試dummy exporter

測試的方法很簡單
可以用go run直接執行

$ go run main.go

或是編譯後在執行

$ go build
$ ./dummy_exporter    # build的結果會跟專案目錄同名

執行後看到如下訊息就是正確的

  This is a dummy example of prometheus exporter
  Access: http://127.0.0.1:8081
  

加入自己的metrics

有了一個dummy exporter之後試著加入自己的metrics吧

想要加入自己定義的metrics,需在啟動http service前註冊自己的exporter物件到prometheus

func main() {
...
  //註冊自己的exporter

  metricsPrefix := "dummy"
    exporter := NewExporter(metricsPrefix)
    prometheus.MustRegister(exporter)

    // Launch http service

    http.Handle(metricsPath, prometheus.Handler())
...

其中NewExporter是我們自己定義的

//定義兩個metric輸出

type Exporter struct {
    gauge    prometheus.Gauge
    gaugeVec prometheus.GaugeVec
}

func NewExporter(metricsPrefix string) *Exporter {
    gauge := prometheus.NewGauge(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"})

    gaugeVec := *prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_vec_metric",
        Help:      "This is a dummy gauga vece metric"},
        []string{"myLabel"})

    return &Exporter{
        gauge:    gauge,
        gaugeVec: gaugeVec,
    }
}

首先定義一個data struct,裡面包含prometheus的屬性,其他定義方法請參考flink_exporter

struct的結構不是太重要,只要包含prometheus的屬性就行了

prometheus目前支援4種類型的metrics

  • Counter: 重點方法inc ,一個累計型的metric
  • CounterVec: Counter支援Label
  • Gauge: 重點方法set,自己設定各種value 最常用
  • GaugeVec: Gauge支援Label
  • Histogram: 重點方法Observe,集計型的metric
  • HistogramVec: Histogram支援Label
  • Summary: 重點方法Observe,集計型的metric
  • SummaryVec: Summary支援Label

四種屬性都有vec的版本,差別在於支不支援Lable

沒Label: dummy_gauge_metric 0
有Label: dummy_gauge_vec_metric{myLabel="hello"} 0

有Label的話在使用grafana的時候可以設定一些條件來過濾,端看自己用途

這邊只是示範,只用了Gauge跟GaugeVec當範例

而重要的是NewExporter裡面對metrics的定義

gauge := prometheus.NewGauge(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"})
gaugeVec := *prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_vec_metric",
        Help:      "This is a dummy gauga vece metric"},
        []string{"myLabel"})

可以看到塞給Exporter的值來自prometheus的NewGauge等方法

參數

  • Namespace: 顯示在metric前面的名字
  • Name: 顯示在exporter上的名字
  • Help: 說明文字
  • Label名字:Vec類型才會有

下面舉個例子

prometheus.GaugeOpts{
        Namespace: "dummy",
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"}

用上面的參數做出來的metrics,在exporter上看起來會像是下面這樣

# HELP dummy_gauge_metric This is a dummy gauge metric
# TYPE dummy_gauge_metric gauge
dummy_gauge_metric 0

定義好exporter之後,接下來問題是怎麼收集/更新metrics

我們需要替這個exporter定義兩個方法

  • Collect: 實際收集metrics的function
  • Describe: 描述metrics

這兩個方法不能省略,一定要有

Describe理論上不用做什麼特別的事,只要讓exporter metrics呼叫Describe方法就好

而Collect則是要實作對metrics的收集

範例如下


//實作收集Metric的邏輯,這邊簡單起見直接設定成0

func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
    e.gauge.Set(float64(0))
    e.gaugeVec.WithLabelValues("hello").Set(float64(0))
    e.gauge.Collect(ch)
    e.gaugeVec.Collect(ch)
}

// 讓exporter的prometheus屬性呼叫Describe方法

func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
    e.gauge.Describe(ch)
    e.gaugeVec.Describe(ch)
}

如此一來就可以在dummy exporter上看到我們自己定義的dummy metrics了

最後,完整的code如下

main.go
package main

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

type Exporter struct {
    gauge    prometheus.Gauge
    gaugeVec prometheus.GaugeVec
}

func NewExporter(metricsPrefix string) *Exporter {
    gauge := prometheus.NewGauge(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"})

    gaugeVec := *prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_vec_metric",
        Help:      "This is a dummy gauga vece metric"},
        []string{"myLabel"})

    return &Exporter{
        gauge:    gauge,
        gaugeVec: gaugeVec,
    }
}

func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
    e.gauge.Set(float64(0))
    e.gaugeVec.WithLabelValues("hello").Set(float64(0))
    e.gauge.Collect(ch)
    e.gaugeVec.Collect(ch)
}

func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
    e.gauge.Describe(ch)
    e.gaugeVec.Describe(ch)
}

func main() {
    fmt.Println(`
  This is a dummy example of prometheus exporter
  Access: http://127.0.0.1:8081
  `)

    // Define parameters

    metricsPath := "/metrics"
    listenAddress := ":8081"
    metricsPrefix := "dummy"

    // Register dummy exporter, not necessary

    exporter := NewExporter(metricsPrefix)
    prometheus.MustRegister(exporter)

    // Launch http service

    http.Handle(metricsPath, prometheus.Handler())
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`<html>
             <head><title>Dummy Exporter</title></head>
             <body>
             <h1>Dummy Exporter</h1>
             <p><a href='` + metricsPath + `'>Metrics</a></p>
             </body>
             </html>`))
    })
    fmt.Println(http.ListenAndServe(listenAddress, nil))
}
comments powered by Disqus