さくらのクラウドのモニタリングスイートに golang で直接 Prometheus Remote Write する
この記事はさくらインターネットアドベントカレンダー3 の三日目の記事です。
さくらのクラウドにはモニタリングスイートという機能があり、Metrics,Logging,Tracing の3つの機能が提供されています。このうち Metrics 機能については、Prometheus Remote Write で書き込むことが出来ます。
Prometheus Remote Write というプロトコルは一般にはなじみがないプロトコルなため、ついつい otelcol を経由して書き込みたくなってしまうかもしれません。しかし、Prometheus Remote Write は Protobuf を Snappy で圧縮したものを HTTP で送るというシンプルなプロトコルなので、実は golang 等で書かれたアプリケーションサーバーから簡単に直接送ることができます。
具体的なコードは以下のようになります。
package main
import (
"bytes"
"flag"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/prompb"
)
func main() {
remoteWriteURL := flag.String("url", "", "remote write URL")
credentials := flag.String("credentials", "", "bearer token for Authorization header")
flag.Parse()
if *remoteWriteURL == "" {
slog.Error("remote write URL is required")
return
}
if *credentials == "" {
slog.Error("credentials are required")
return
}
now := time.Now()
ts := &prompb.TimeSeries{
Labels: []prompb.Label{
{Name: "__name__", Value: "test_metric"}, // これがメトリクス名となります
{Name: "job", Value: "sync-worker"}, // こっちはラベル
},
Samples: []prompb.Sample{
{
Value: 123.45,
Timestamp: now.UnixMilli(),
},
},
}
slog.Info(
"sending remote write",
slog.Any("ts", ts))
req := &prompb.WriteRequest{
Timeseries: []prompb.TimeSeries{*ts},
}
if err := sendRemoteWrite(*remoteWriteURL, *credentials, req); err != nil {
slog.Error("remote write failed",
slog.Any("error", err))
} else {
slog.Info("remote write succeeded")
}
}
func sendRemoteWrite(remoteWriteURL, credentials string, wr *prompb.WriteRequest) error {
// protobuf にシリアライズ
data, err := proto.Marshal(wr)
if err != nil {
return fmt.Errorf("failed to marshal write request: %w", err)
}
// snappy 圧縮
compressed := snappy.Encode(nil, data)
// HTTP client
httpClient := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequest("POST", remoteWriteURL, bytes.NewReader(compressed))
if err != nil {
return fmt.Errorf("failed to create remote write request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+credentials)
req.Header.Set("Content-Encoding", "snappy")
req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
req.Header.Set("User-Agent", "remote-writer/0.1")
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("remote write request failed: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
slog.Info("remote write finished",
slog.String("url", remoteWriteURL),
slog.Int("status_code", resp.StatusCode))
if resp.StatusCode/100 != 2 {
body, err := io.ReadAll(resp.Body)
return fmt.Errorf("failed to write remote: %w, status=%d, body=%s", err, resp.StatusCode, string(body))
}
_, _ = io.Copy(io.Discard, resp.Body)
return nil
}
ここで、gogo/protobuf というライブラリは見慣れないものかもしれません。Protocol Buffers を扱うための便利ライブラリですが、
Deprecated になっています。しかし、Prometheus のライブラリ自体が gogo/protobuf に依存しているため、そのまま使っています。#11908 として Prometheus 内でも議論が続けられています。
接続情報の取得と動作確認
さて、このコードを使って実際に動作確認してみましょう。
さくらのクラウドのモニタリングスイートのコントロールパネルから Endpoint URL と Credential を取得します。

コレを元に以下のように実行します。

結果として、以下のように送信出来ていることが確認できます。

まとめ
Prometheus Remote Write を golang で直接送信することが容易であることを示しました。
参考
- castai/promwrite は prometheus remote write を行うための golang ライブラリです
Discussion