Prometheus 用のメトリクスをパースする

Prometheus が収集するメトリクス（[Exposition formats](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md)）を直接パースできた方が何かと便利（データ分析等で）かと思い、試しに実装してみました。

# メトリクスのパース

メトリクスをパースする機能は Prometheus に用意されているので、そのライブラリを使って実装すると次のようになりました。

```go:sample1/main.go
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/prometheus/common/expfmt"
)

func main() {
    // メトリクス
	data := `
sample_total 10
test_metric{label="t1", proc="a1"} 123.4
`

	p := expfmt.TextParser{}
	// パース処理
	mf, err := p.TextToMetricFamilies(strings.NewReader(data))

	if err != nil {
		log.Fatal(err)
	}

	for k, v := range mf {
		fmt.Printf("key=%s, name=%s, type=%s \n", k, v.GetName(), v.GetType())
		// ラベルの取得と出力
		for _, l := range v.GetMetric()[0].Label {
			fmt.Printf("label name=%s, value=%s \n", l.GetName(), l.GetValue())
		}
		// 値の取得と出力
		fmt.Printf("untyped value=%f \n", v.GetMetric()[0].Untyped.GetValue())

		fmt.Println("---")
	}
}
```

実行結果は以下の通りです。

##### 実行結果
```sh
$ cd sample1

$ go run main.go
key=sample_total, name=sample_total, type=UNTYPED 
untyped value=10.000000 
---
key=test_metric, name=test_metric, type=UNTYPED 
label name=label, value=t1 
label name=proc, value=a1 
untyped value=123.400000 
---
```

# Histogram の抽出と算出

次に、CoreDNS のメトリクスから次のような Histogram 形式のデータだけを対象にして、増加した分を算出する処理を実装してみました。

```prometheus:Histogram 例
# HELP coredns_dns_request_duration_seconds Histogram of the time (in seconds) each request took per zone.
# TYPE coredns_dns_request_duration_seconds histogram
coredns_dns_request_duration_seconds_bucket{server="dns://:53",view="",zone=".",le="0.00025"} 22
coredns_dns_request_duration_seconds_bucket{server="dns://:53",view="",zone=".",le="0.0005"} 22
...省略
```

Histogram は ```GetType``` の戻り値が ```dto.MetricType_HISTOGRAM``` かどうかで判定できます。

また、Histogram の場合は ```_bucket``` 部分が除外された名称になります。

```go:sample2/main.go
package main

import (
	"fmt"
	"log"
	"os"
	"strings"

	dto "github.com/prometheus/client_model/go"
	"github.com/prometheus/common/expfmt"
)
// ラベルの連結
func joinLabel(ps []*dto.LabelPair) string {
	rs := []string{}

	for _, p := range ps {
		rs = append(rs, fmt.Sprintf("%s:%s", p.GetName(), p.GetValue()))
	}

	return strings.Join(rs, ",")
}

func main() {
	file := os.Args[1]
	f, err := os.Open(file)

	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	p := expfmt.TextParser{}

	mf, err := p.TextToMetricFamilies(f)

	if err != nil {
		log.Fatal(err)
	}

	for k, v := range mf {
		// Histogram の判定
		if v.GetType() == dto.MetricType_HISTOGRAM {
			for _, m := range v.GetMetric() {
				hist := m.GetHistogram()

				label := joinLabel(m.GetLabel())
				sum := hist.GetSampleSum() //合計の取得

				lastCount := uint64(0)

				for _, b := range hist.GetBucket() {
                    // 差分の算出
					count := b.GetCumulativeCount() - lastCount
					lastCount = b.GetCumulativeCount()

					if count > 0 {
						fmt.Printf(
							"name=%s, label=[%s], sum=%f, le=%f, count=%d \n",
							k, label, sum, b.GetUpperBound(), count,
						)
					}
				}
			}
		}
	}
}
```

実行結果は次のようになりました。

##### 実行結果
```sh
$ cd sample2

$ go run main.go coredns_metrics.txt
name=coredns_forward_request_duration_seconds, label=[rcode:NOERROR,to:192.168.5.3:53], sum=3.013131, le=0.001000, count=1 
name=coredns_forward_request_duration_seconds, label=[rcode:NOERROR,to:192.168.5.3:53], sum=3.013131, le=0.002000, count=1 
name=coredns_forward_request_duration_seconds, label=[rcode:NOERROR,to:192.168.5.3:53], sum=3.013131, le=0.004000, count=2 
name=coredns_forward_request_duration_seconds, label=[rcode:NOERROR,to:192.168.5.3:53], sum=3.013131, le=1.024000, count=3 
name=coredns_dns_request_duration_seconds, label=[server:dns://:53,view:,zone:.], sum=4.017252, le=0.000250, count=22 
name=coredns_dns_request_duration_seconds, label=[server:dns://:53,view:,zone:.], sum=4.017252, le=0.001000, count=2 
name=coredns_dns_request_duration_seconds, label=[server:dns://:53,view:,zone:.], sum=4.017252, le=0.004000, count=2 
name=coredns_dns_request_duration_seconds, label=[server:dns://:53,view:,zone:.], sum=4.017252, le=0.008000, count=1 
name=coredns_dns_request_duration_seconds, label=[server:dns://:53,view:,zone:.], sum=4.017252, le=1.024000, count=4 
name=coredns_health_request_duration_seconds, label=[], sum=0.380166, le=0.000250, count=3 
name=coredns_health_request_duration_seconds, label=[], sum=0.380166, le=0.002500, count=680 
name=coredns_health_request_duration_seconds, label=[], sum=0.380166, le=0.025000, count=1 
name=coredns_dns_response_size_bytes, label=[proto:udp,server:dns://:53,view:,zone:.], sum=3501.000000, le=100.000000, count=11 
name=coredns_dns_response_size_bytes, label=[proto:udp,server:dns://:53,view:,zone:.], sum=3501.000000, le=200.000000, count=20 
name=coredns_dns_request_size_bytes, label=[proto:udp,server:dns://:53,view:,zone:.], sum=1957.000000, le=100.000000, count=31 
```