🐡

さくらのクラウドのモニタリングスイートに golang で直接 Prometheus Remote Write する

に公開

この記事はさくらインターネットアドベントカレンダー3 の三日目の記事です。
https://qiita.com/advent-calendar/2025/sakura

さくらのクラウドにはモニタリングスイートという機能があり、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