Prometheus の exporter 自作

# はじめに
[品川 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/shinagawa) の22日目です。

[Prometheus](https://prometheus.io/) はご存知の通り、監視対象にアクセスしてデータを収集し、モニタリングや通知が可能な Pull 型の監視ソフトウェアの1つです。
監視対象は、データを取得して Prometheus にレスポンスする exporter や、バッチやアプリケーション等が Push したデータを蓄積しておき、Prometheus にレスポンスする PushGateway を用意しておく必要があります。
exporter は、公式非公式を含め [多数公開](https://prometheus.io/docs/instrumenting/exporters/) されており、また exporter を自作するための [クライアントライブラリも公開](https://prometheus.io/docs/instrumenting/clientlibs/) されています。
exporter を自作する機会は、今のところ全く無いのですが、いつか来たるべき日に備えて、今更ですがクライアントライブラリを利用して自作してみたいと思います。

# 取得するメトリクス
とりあえず Linux のメモリ使用率を取得する exporter を作ります。
Linux の場合、メモリ使用率は `/proc/meminfo` を見ればわかるので、ここを読み取るような作りにします。

# 自作してみる
[hirano00o/sample-exporter: Sample Exporter for Prometheus](https://github.com/hirano00o/sample-exporter)
作成にあたっては、クライアントライブラリの [example](https://github.com/prometheus/client_golang/blob/master/examples/random/main.go) や [node_exporter](https://github.com/prometheus/node_exporter) を**参考**にしました。

`node_exporter` では、取得する情報を引数で指定してフィルタしたりしてましたが、今回取得情報に関する引数は取らないので、こんな感じになりました。

```go:sample-exporter/main.go
func main() {
	flag.Parse()

	c, err := collector.NewSampleCollector()
	if err != nil {
		log.Fatal(err)
	}
	// NewしたCollectorを登録する。ここに登録したもののメトリクスが取得できる
	prometheus.MustRegister(c)

	http.Handle("/metrics", promhttp.Handler())

	log.Println("Listening on ", *addr)
	if err := http.ListenAndServe(*addr, nil); err != nil {
		log.Fatal(err)
	}
}
```

`collect.go` は、メモリ以外にも情報取得したくなったときに、簡単に追加できるようになるベースです。

```go:sample-exporter/collector/collect.go
var (
...
	factories      = make(map[string]func() (Collector, error))
	collectorState = make(map[string]int)
)

// memory.goでinit()で呼ぶ
func registCollector(collector string, f func() (Collector, error)) {
	factories[collector] = f
	collectorState[collector] = 0
}

type SampleCollector struct {
	Collectors map[string]Collector  // keyがstring型, valueはCollectorと言う名のinterface型
}

// memory.goでも利用するし、メモリ以外にもCPU等取得情報を追加したいときに備えて、interfaceで定義しておく
type Collector interface {
	Update(ch chan<- prometheus.Metric) error
}

func NewSampleCollector() (*SampleCollector, error) {
	collectors := make(map[string]Collector)
	for k, _ := range collectorState {
		f, err := factories[k]()
		if err != nil {
			return nil, err
		}
		collectors[k] = f
	}
	// 今回はメモリだけだが、CPU等の他情報の取得も簡単に追加できる
	return &SampleCollector{Collectors: collectors}, nil
}

// Describe と Collect は、Collector interface に実装されている
// https://godoc.org/github.com/prometheus/client_golang/prometheus#Collector
func (sc SampleCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- scrapeDurationDesc
	ch <- scrapeSuccessDesc
}

// goroutine で複数の(今回はメモリだけだが)情報を並列に取得(execute)
func (sc SampleCollector) Collect(ch chan<- prometheus.Metric) {
	wg := sync.WaitGroup{}
	wg.Add(len(sc.Collectors))
	for name, c := range sc.Collectors {
		go func(name string, c Collector) {
			execute(name, c, ch)
			wg.Done()
		}(name, c)
	}
	wg.Wait()
}

func execute(name string, c Collector, ch chan<- prometheus.Metric) {
	begin := time.Now()
	err := c.Update(ch)
	duration := time.Since(begin)
	var success float64

	if err != nil {
		log.Printf("ERROR: %s collector failed after %fs: %s", name, duration.Seconds(), err.Error())
		success = 0
	}
	success = 1
	// 集計したい情報は chan に Metric を渡す
	ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), name)
	ch <- prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, success, name)
}
```

メモリ情報の取得は、`node_exporter` では、`/proc/meminfo` をパースしていましたが、今回は下記ライブラリを利用しました。
[shirou/gopsutil: psutil for golang](https://github.com/shirou/gopsutil/)

```go:sample-exporter/collector/memory.go
const (
	subsystem = "memory"
)

type memoryCollector struct{}

// MustRegister するために、init() で collect.go の factories に追加する
func init() {
	registCollector(subsystem, NewMemoryCollector)
}

func NewMemoryCollector() (Collector, error) {
	return &memoryCollector{}, nil
}

func (c *memoryCollector) Update(ch chan<- prometheus.Metric) error {
	var metricType prometheus.ValueType

	// メモリ情報の取得
	v, err := mem.VirtualMemory()
	if err != nil {
		return fmt.Errorf("could not get memory info: %s", err)
	}

...
	for i := 0; i < t.NumField(); i++ {
...
		if strings.Contains(t.Field(i).Name, "Total") == true {
			// Total (例えばMemTotal)が入っていたら、Counter
			// メトリクスの種類は https://prometheus.io/docs/concepts/metric_types/
			metricType = prometheus.CounterValue
		} else {
			metricType = prometheus.GaugeValue
		}
...
		// 集計したい情報なので chan に Metric を渡す
		ch <- prometheus.MustNewConstMetric(
			prometheus.NewDesc(
				// メトリクス名を BuildFQName() で作成し、指定
				prometheus.BuildFQName(namespace, subsystem, t.Field(i).Name),
				fmt.Sprintf("Memory information filed %s", t.Field(i).Name),
				nil, nil,
			),
			// メトリクスタイプと値を指定、値はfloat64
			metricType, f64,
		)
	}
	return nil
}
```

# 結果
長いので一部だけ出力しましたが、ちゃんと取得できてそうですね・

```bash
$ curl http://localhost:9090/metrics | grep -i "sample_memory_UsedPercent"
# HELP sample_memory_UsedPercent Memory information filed UsedPercent
# TYPE sample_memory_UsedPercent counter
sample_memory_UsedPercent 1.9555757729632135
$ curl http://localhost:9090/metrics | grep -i "sample_memory_UsedPercent"
# HELP sample_memory_UsedPercent Memory information filed UsedPercent
# TYPE sample_memory_UsedPercent counter
sample_memory_UsedPercent 1.9468305687203793
```

# おわりに
結果的に、`node_exporter` の劣化版みたいな感じになってしまいましたが、意外と簡単に exporter を作ることができました。
例えば会社で、全プロジェクトで汎用的に簡単に使える exporter を作って配布して、とりあえず入れておけば OK みたいな形にするとかですかね。
grafana で長期安定化試験のときに見られると便利ですしね。