Prometheus Pushgateway に プログラムからメトリクスをpushする [Protocol buffer編]


## 概要

Pushgateway にプログラムからメトリクスをpushする方法を紹介します。

push可能なメトリクスのフォーマットは`Text`と`Protocol buffer`の2種類あります。
本記事では`Protocol buffer`フォーマットでpushする方法を紹介します。

`Text`フォーマットでのpushについては以下記事をご参照ください。
[Prometheus Pushgateway に プログラムからメトリクスをpushする [Text format編]](https://qiita.com/loftkun/items/a79af040c4699990c6e1)

## 各言語向けライブラリ

「Prometheus」のクライアントライブラリに「Pushgateway」へのメトリクスpushメソッドがしれっと実装されていますので若干盲点ですが、各言語向けのライブラリがあります。
通常は以下のライブラリを使うのが良いと思います。

- [Pushing metrics | Prometheus](https://prometheus.io/docs/instrumenting/pushing/)

なお、全てのライブラリを確認したわけではありませんが`Protocol buffer`フォーマットでpushする処理は見当たりませんでした。

また、ネット上を見渡しても`Protocol buffer`のバイト列をPOSTする処理や、後述するデリミタ情報を付与することにより、Pushgatewayにメトリクスをpushできることを示しているコードは見当たりませんでした。このあたりの実装を見かけた方はぜひコメント欄で教えて下さい。

ということで今回自作してみました。まずは今回参照したドキュメントを紹介します。

## Prometheus Client Data の仕様

以下ドキュメントにPrometheus Client Dataのフォーマットが記載されています。

- [Prometheus Client Data Exposition Format - Google ドキュメント](https://docs.google.com/document/d/1ZjyKiKxZV83VI9ZKAXRGKaUKK2BIWCT7oiGBKDBpjEY/edit)

`Protocol buffer`フォーマットでpushする場合は以下に気をつけると良さそうです。

- `32-Bit Varint-Encoded Record Length-Delimited` をほどこす必要がある
  - Protocol buffer のバイト列全体のLengthをバイト列先頭に付与すると良さそうです。
- protoファイル(`metrics.proto`)は以下リポジトリで公開されている
  - [prometheus/client_model: Data model artifacts for Prometheus.](https://github.com/prometheus/client_model)
  - ちなみにGo言語用の構造体定義(`metrics.pb.go`)も用意されているのでGo言語で実装する場合はgo getして入手するだけで良いです。
  - 念の為お使いのバージョンの[PushGatewayの依存モジュール](https://github.com/prometheus/pushgateway/blob/master/go.mod)のclient_modelと同じバージョンのものを使うと良いかと思います。
- `Content-Type`ヘッダの値は`application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`とする

続いて実装例を紹介します。

## 実装例

```golang
package main

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/golang/protobuf/proto"
	"github.com/matttproud/golang_protobuf_extensions/pbutil"

	dto "github.com/prometheus/client_model/go"
)

func main() {

	// metrics
	m := &dto.MetricFamily{
		Name: proto.String("some_metric"),
		Help: proto.String("Just an example."),
		Type: dto.MetricType_COUNTER.Enum(),
		Metric: []*dto.Metric{
			{
				Label: []*dto.LabelPair{
					{
						Name:  proto.String("label1"),
						Value: proto.String("val1"),
					},
					{
						Name:  proto.String("label2"),
						Value: proto.String("val2"),
					},
				},
				Counter: &dto.Counter{
					Value: proto.Float64(123.456),
				},
			},
		},
	}
	fmt.Println("metric:", m)

	// byte列を見てみよう
	dump(m)

	// push
	push(m)

}

func dump(m proto.Message) {
	// byte列に変換
	marshaled, _ := proto.Marshal(m)
	fmt.Println("metric marshaled: ", marshaled)

	// 逆変換の例
	unmarshaled := &dto.MetricFamily{}
	_ = proto.Unmarshal(marshaled, unmarshaled)
	fmt.Println("metric unmarshaled: ", unmarshaled)
}

func push(m proto.Message) {

	// lenghをデリミタとしてbyte列の先頭に付与する
	buf := &bytes.Buffer{}
	_, err := pbutil.WriteDelimited(buf, m)
	fmt.Println("metric delimited: ", buf.Bytes())

	// post
	err = post(buf.Bytes())
	if err != nil {
		fmt.Println("err: ", err)
	}
}

func post(in []byte) error {

	endpoint := "http://localhost:9091/metrics/job/some_job/instance/some_instance"
	contentType := "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"

	res, err := http.Post(endpoint, contentType, bytes.NewBuffer(in))
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		b, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return errors.New(res.Status)
		}
		return fmt.Errorf("%s: %s", res.Status, b)
	}
	return err
}
```

`dump(m)`はメトリクスのpush処理には不要な処理で、バイト列の比較用のために書いたコードです。

## 実行結果

実行すると以下のようなログを出力します。

```sh
metric: name:"some_metric" help:"Just an example." type:COUNTER metric:<label:<name:"label1" value:"val1" > label:<name:"label2" value:"val2" > counter:<value:123.456 > > 
metric marshaled:  [10 11 115 111 109 101 95 109 101 116 114 105 99 18 16 74 117 115 116 32 97 110 32 101 120 97 109 112 108 101 46 24 0 34 43 10 14 10 6 108 97 98 101 108 49 18 4 118 97 108 49 10 14 10 6 108 97 98 101 108 50 18 4 118 97 108 50 26 9 9 119 190 159 26 47 221 94 64]
metric unmarshaled:  name:"some_metric" help:"Just an example." type:COUNTER metric:<label:<name:"label1" value:"val1" > label:<name:"label2" value:"val2" > counter:<value:123.456 > > 
metric delimited:  [78 10 11 115 111 109 101 95 109 101 116 114 105 99 18 16 74 117 115 116 32 97 110 32 101 120 97 109 112 108 101 46 24 0 34 43 10 14 10 6 108 97 98 101 108 49 18 4 118 97 108 49 10 14 10 6 108 97 98 101 108 50 18 4 118 97 108 50 26 9 9 119 190 159 26 47 221 94 64]
```

`metric marshaled`と`metric delimited`のバイト列を比較すると、delimitedの方は先頭にlength値を表す値が追加されていることが分かります。
このlength値がないとPushgateway側でunmarshalに失敗しレスポンスコード400が返ってきます。

なお、pushが成功するとPushgatewayのWebUIには以下のように表示されます。

![push結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21106/f0d748d9-eebd-acda-4502-190f946397a8.png)