實現一個Prometheus exporter
- 2022 年 7 月 1 日
- 筆記
- Prometheus
Prometheus 官方和社區提供了非常多的exporter,涵蓋資料庫、中間件、OS、存儲、硬體設備等,具體可查看exporters、exporterhub.io,通過這些 exporter 基本可以覆蓋80%的監控需求,依然有小部分需要通過自定義腳本或者訂製、修改社區exporter實現。本文我們將學習如何通過go編寫一個簡單的expoter用於暴露OS的負載。
要實現的三個load指標如下:
exporter的核心是http服務,對外暴露exporter本身運行時指標和監控資訊。我們可以直接通過net/http
暴力實現,更好的方式是使用Prometheus 官方提供的client library 來簡化一部分工作。
client library官方支援語言:
也有社區支援的其他語言庫如C、C++、PHP等
獲取數據源
在使用client library暴露數據之前,我們得先找到數據源,以linux為例要獲取系統負載我們可以讀取/proc目錄下的loadavg文件。涉及到各類作業系統指標的獲取可以參考官方的node-exporter,這裡我們給他寫成load包,等會直接調用GetLoad()就能拿到數據了。
package collect
import (
"fmt"
"io/ioutil"
"strconv"
"strings"
)
// The path of the proc filesystem.
var procPath = "/proc/loadavg"
// Read loadavg from /proc.
func GetLoad() (loads []float64, err error) {
data, err := ioutil.ReadFile(procPath)
if err != nil {
return nil, err
}
loads, err = parseLoad(string(data))
if err != nil {
return nil, err
}
return loads, nil
}
// Parse /proc loadavg and return 1m, 5m and 15m.
func parseLoad(data string) (loads []float64, err error) {
loads = make([]float64, 3)
parts := strings.Fields(data)
if len(parts) < 3 {
return nil, fmt.Errorf("unexpected content in %s", procPath)
}
for i, load := range parts[0:3] {
loads[i], err = strconv.ParseFloat(load, 64)
if err != nil {
return nil, fmt.Errorf("could not parse load '%s': %w", load, err)
}
}
return loads, nil
}
通過client_golang暴露指標
開通我們提到exporter要暴露的指標包含兩部分,一是本身的運行時資訊,另一個監控的metrics。而運行時資訊client_golang
已經幫我們實現了,我們要做的是通過client_golang
包將監控數據轉換為metrics後再暴露出來。
一個最基礎使用client_golang
包示例如下:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":2112", nil)
}
promhttp.Handler()
封裝了本身的 go 運行時 metrics,並按照metircs後接value的格式在前端輸出。
當我們訪問2112埠的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 7
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.15.14"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
...
如何暴露自定義metrics呢?
先看如下的示例:
package main
import (
"net/http"
"time"
"log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func recordMetrics() {
go func() {
for {
opsProcessed.Inc()
time.Sleep(2 * time.Second)
}
}()
}
var (
opsProcessed = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "myapp",
Name: "processed_ops_total",
Help: "The total number of processed events",
})
)
func main() {
prometheus.MustRegister(opsProcessed)
recordMetrics()
http.Handle("/metrics", promhttp.Handler())
log.Print("export /metrics on port :8085")
http.ListenAndServe(":8085", nil)
}
示例來自於官方倉庫,做了稍加修改。可以看到使用NewCounter方法可以很快地幫我們創建一個Prometheus Counter數據類型實例。
Counter介面的定義包含了Counter本身的特性-只能增加即Inc和Add,同時還包含Meterics、Collector介面
Collector還包含2個方法,待會我們寫自己的Collector時需要實現這兩個方法。
type Collector interface {
Describe(chan<- *Desc)
Collect(chan<- Metric)
}
CounterOpts 來源於metrics.go 的Ops結構體定義了構成metrics的基本結構。
接著將opsProcessed這個Counter進行註冊,所謂註冊也就是讓Handler
跟蹤這個Counter中的metircs和collector
運行後,訪問/metircs可以看到自定義指標myapp_processed_ops_total通過定時的Inc()調用來更新value
# HELP myapp_processed_ops_total The total number of processed events
# TYPE myapp_processed_ops_total counter
myapp_processed_ops_total 15
下面我們通過自定義collector實現一個簡易的exporter
目錄結構如下:
# tree .
.
├── collect
│ ├── collector.go
│ └── loadavg.go
├── go.mod
├── go.sum
└── main.go
loadavg.go即上面的獲取數據源。
collector.go如下:
package collect
import (
"log"
"github.com/prometheus/client_golang/prometheus"
)
var namespace = "node"
type loadavgCollector struct {
metrics []typedDesc
}
type typedDesc struct {
desc *prometheus.Desc
valueType prometheus.ValueType
}
func NewloadavgCollector() *loadavgCollector {
return &loadavgCollector{
metrics: []typedDesc{
{prometheus.NewDesc(namespace+"_load1", "1m load average.", nil, nil), prometheus.GaugeValue},
{prometheus.NewDesc(namespace+"_load5", "5m load average.", , nil), prometheus.GaugeValue},
{prometheus.NewDesc(namespace+"_load15", "15m load average.", nil, nil), prometheus.GaugeValue},
},
}
}
//Each and every collector must implement the Describe function.
//It essentially writes all descriptors to the prometheus desc channel.
func (collector *loadavgCollector) Describe(ch chan<- *prometheus.Desc) {
//Update this section with the each metric you create for a given collector
ch <- collector.metrics[1].desc
}
//Collect implements required collect function for all promehteus collectors
func (collector *loadavgCollector) Collect(ch chan<- prometheus.Metric) {
//Implement logic here to determine proper metric value to return to prometheus
//for each descriptor or call other functions that do so.
loads, err := GetLoad()
if err != nil {
log.Print("get loadavg error: ", err)
}
//Write latest value for each metric in the prometheus metric channel.
//Note that you can pass CounterValue, GaugeValue, or UntypedValue types here.
for i, load := range loads {
ch <- prometheus.MustNewConstMetric(collector.metrics[i].desc, prometheus.GaugeValue, load)
}
}
collector中每一個要暴露的metrics都需要包含一個metrics描述即desc,都需要符合prometheus.Desc結構,我們可以直接使用NewDesc來創建。這裡我們創建了三個metircs_name分別為node_load1、node_load5、node_15以及相應的描述,也可以加上對應的label。
接著實現collector的兩個方法Describe、Collect分別寫入對應的發送channel,其中prometheus.Metric的通道傳入的值還包括三個load的value
最後在主函數中註冊collector
prometheus.MustRegister(collect.NewloadavgCollector())
在Prometheus每個請求周期到達時都會使用GetLoad()獲取數據,轉換為metircs,發送給Metrics通道,http Handler處理和返回。
實現一個指標豐富、可靠性高的exporter感覺還是有一些困難的,需要對Go的一些特性以及Prometheus client包有較深入的了解。本文是對exporter編寫的簡單嘗試,如實現邏輯、方式或理解不準確可參考開源exporter和官方文檔。
文章涉及程式碼可查看:exporter
通過部落格閱讀:iqsing.github.io