12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

品川Advent Calendar 2019

Day 22

Prometheus の exporter 自作

Last updated at Posted at 2019-12-22

はじめに

品川 Advent Calendar 2019 の22日目です。

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

取得するメトリクス

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

自作してみる

hirano00o/sample-exporter: Sample Exporter for Prometheus
作成にあたっては、クライアントライブラリの examplenode_exporter参考にしました。

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

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 は、メモリ以外にも情報取得したくなったときに、簡単に追加できるようになるベースです。

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

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
}

結果

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

$ 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 で長期安定化試験のときに見られると便利ですしね。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?