はじめに
Prometheus でアプリケーションの構築をしているとどうしてもこの値が取りたいのに... と思うことが多々ある。Pushgateway も選択肢として上げられるが今回は選択肢を増やしてほしいという意味でもExporterの実装方法について検討していきます。ExporterはPrometheusのpull モデルに適合し、監視対象のライフサイクルと一貫性があり、スケーラビリティと自動検出の利点を享受できるため、Pushgatewayよりも推奨される方法です。ただし、特定のユースケース(サービスレベルのバッチジョブなど)では、Pushgatewayの使用が適切な場合もあります。Pushgatewayを使う際には以下の問題点があるので注意が必要です。
- 複数のインスタンスを1つのPushgatewayで監視すると、単一障害点とボトルネックが発生する可能性がある。
- Prometheusの自動インスタンスヘルスチェックが利用できなくなる。
- Pushgatewayは一度プッシュされたデータを忘れず、手動でAPIを通じて削除しない限り永久にPrometheusで公開されてしまう。
Exporter の実装と運用はそこそこ手間になるので最適な方法を選んでほしいです。この辺はCloudを利用しても同じような問題があるので注意しながらやっていきましょう。
サンプルコードはこちらです。サンプルコード自体は雑多な作業リポジトリにおいてあるのでご注意ください。また、アプリケーション自体のリソースを確認するのにEcho のミドルウェアを使用していません。自身の利用しているライブラリーにPrometheus のエンドポイントを提供する機能がないか調べておきましょう。gRPCのGo Server にも同様の機能があります。あと、外部のリソースが確認したいだけならBlackbox exporterという選択肢もあります。
Exporterとは
Exporterは、Prometheusがメトリクスを収集するために使用するプログラムです。Exporterは、アプリケーションやインフラストラクチャからメトリクスを収集し、Prometheusが理解できる形式に変換して提供します。公式ドキュメントで提供されているExporter一覧は、こちらを参照してください。
Prometheusの公式クライアントライブラリ
Prometheusは、いくつかの言語用の公式クライアントライブラリを提供しており、これを使用してExporterを実装することができます。今回はGoで実装していくのこちらが参考になると思います。
やっていくぞ!おら!
やっていきます
環境構築
# Go のモジュールを作成する。必要なライブラリーはのちほど`go mod tidy` で持ってくる。
go mod init prometheus-go-exporter
実装について
以下をmain.go に配置して実行(go run main.go)してください。以下のコードはEchoを利用したWebサーバーにPrometheusのExporterを実装し、3-shake.comへのアクセスを計測しています。 http://localhost:2121/3-shake-status や http://localhost:2121/metrics で値を取得できていると思います。
package main import ( "fmt" "net/http" "time" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) // Prometheusのメトリクスを定義しています。 // これらのメトリクスは、HTTPリクエストの情報や3-shake.comへのアクセス情報を収集するために使用されます。 var ( httpRequestsTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Number of HTTP requests processed", }, []string{"method", "path"}, ) httpRequestDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Duration of HTTP requests", Buckets: prometheus.DefBuckets, }, []string{"method", "path"}, ) httpRequestSize = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_size_bytes", Help: "Size of HTTP requests", Buckets: prometheus.ExponentialBuckets(128, 2, 10), }, []string{"method", "path"}, ) httpResponseSize = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_response_size_bytes", Help: "Size of HTTP responses", Buckets: prometheus.ExponentialBuckets(128, 2, 10), }, []string{"method", "path"}, ) httpResponseTime = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "http_response_time_seconds", Help: "Time of the last HTTP response", }, []string{"method", "path"}, ) externalAccessDuration = prometheus.NewHistogram( prometheus.HistogramOpts{ Name: "external_access_duration_seconds", Help: "Duration of external access to 3-shake.com", Buckets: prometheus.DefBuckets, }, ) lastExternalAccessStatusCode = prometheus.NewGauge( prometheus.GaugeOpts{ Name: "last_external_access_status_code", Help: "Last status code of external access to 3-shake.com", }, ) ) // init関数内で、メトリクスをPrometheusに登録しています。 func init() { registerMetrics() } // registerMetrics関数では、Prometheusにメトリクスを登録しています。 // これにより、Prometheusがメトリクスを収集できるようになります。 func registerMetrics() { prometheus.MustRegister(httpRequestsTotal) prometheus.MustRegister(httpRequestDuration) prometheus.MustRegister(httpRequestSize) prometheus.MustRegister(httpResponseSize) prometheus.MustRegister(httpResponseTime) prometheus.MustRegister(externalAccessDuration) prometheus.MustRegister(lastExternalAccessStatusCode) } // updateMetrics関数では、受信したHTTPリクエストのメトリクスを更新しています。 // これにより、各リクエストに関する情報が収集されます。 func updateMetrics(method, path string, requestSize, responseSize int, duration time.Duration) { httpRequestsTotal.WithLabelValues(method, path).Inc() httpRequestDuration.WithLabelValues(method, path).Observe(duration.Seconds()) httpRequestSize.WithLabelValues(method, path).Observe(float64(requestSize)) httpResponseSize.WithLabelValues(method, path).Observe(float64(responseSize)) httpResponseTime.WithLabelValues(method, path).Set(float64(time.Now().Unix())) } // prometheusMiddleware関数では、Echoのミドルウェアとして、受信したHTTPリクエストに関するメトリクスを更新する機能を追加しています。 func prometheusMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { startTime := time.Now() err := next(c) duration := time.Since(startTime) requestSize := c.Request().ContentLength responseSize := c.Response().Size updateMetrics(c.Request().Method, c.Path(), int(requestSize), int(responseSize), duration) return err } } // measureExternalAccess関数では、3-shake.comへの外部アクセスを定期的に計測し、そのアクセス時間とステータスコードをメトリクスに格納しています。 // この関数はメイン関数内で呼び出され、別のゴルーチンで実行されます。 func measureExternalAccess() { client := &http.Client{Timeout: 10 * time.Second} go func() { for { startTime := time.Now() resp, err := client.Get("https://3-shake.com") duration := time.Since(startTime) if err == nil { externalAccessDuration.Observe(duration.Seconds()) lastExternalAccessStatusCode.Set(float64(resp.StatusCode)) resp.Body.Close() } time.Sleep(1 * time.Minute) } }() } func main() { // Echo instance e := echo.New() // Middleware for Prometheus Exporter e.Use(prometheusMiddleware) // Enable request logger e.Use(middleware.Logger()) e.GET("/3-shake-status", func(c echo.Context) error { status := lastExternalAccessStatusCode.Desc().String() return c.String(http.StatusOK, fmt.Sprintf("Last 3-shake.com access status: %s", status)) }) // Prometheus Exporter endpoint e.GET("/metrics", echo.WrapHandler(promhttp.Handler())) // Measure external access to 3-shake.com measureExternalAccess() // Start the server e.Start(":2121") }
コードを解説するんじゃい
解説をします。
Prometheusのメトリクスを定義
Prometheusのメトリクスを定義しています。これらのメトリクスは、HTTPリクエストの情報や3-shake.comへのアクセス情報を収集するために使用されます。
var ( // ... (省略) externalAccessDuration = prometheus.NewHistogram( prometheus.HistogramOpts{ Name: "external_access_duration_seconds", Help: "Duration of external access to 3-shake.com", Buckets: prometheus.DefBuckets, }, ) lastExternalAccessStatusCode = prometheus.NewGauge( prometheus.GaugeOpts{ Name: "last_external_access_status_code", Help: "Last status code of external access to 3-shake.com", }, ) )
init関数
init
関数内で、メトリクスをPrometheusに登録しています。
func init() {
registerMetrics()
}
registerMetrics関数
registerMetrics
関数では、Prometheusにメトリクスを登録しています。これにより、Prometheusがメトリクスを収集できるようになります。
func registerMetrics() { // ... (省略) prometheus.MustRegister(externalAccessDuration) prometheus.MustRegister(lastExternalAccessStatusCode) }
updateMetrics関数
updateMetrics関数
では、受信したHTTPリクエストのメトリクスを更新しています。これにより、各リクエストに関する情報が収集されます。
func updateMetrics(method, path string, requestSize, responseSize int, duration time.Duration) { // ... (省略) }
prometheusMiddleware関数
prometheusMiddleware関数
では、Echoのミドルウェアとして、受信したHTTPリクエストに関するメトリクスを更新する機能を追加しています。
func prometheusMiddleware(next echo.HandlerFunc) echo.HandlerFunc { // ... (省略: ) }
measureExternalAccess関数
measureExternalAccess関数
では、3-shake.comへの外部アクセスを定期的に計測し、そのアクセス時間とステータスコードをメトリクスに格納しています。この関数はメイン関数内で呼び出され、別のゴルーチンで実行されます。
func measureExternalAccess() { client := &http.Client{Timeout: 10 * time.Second} go func() { for { startTime := time.Now() resp, err := client.Get("https://3-shake.com") duration := time.Since(startTime) if err == nil { externalAccessDuration.Observe(duration.Seconds()) lastExternalAccessStatusCode.Set(float64(resp.StatusCode)) resp.Body.Close() } time.Sleep(1 * time.Minute) } }() }
var 配下ってことぉおおお
Prometheusのメトリクスを定義しています。この辺の実装はよく悩むと思うので公式の実装とかたくさん読むと何をどれに使えばよいかの勘所が掴めると思います。実際に使わないと差が分からないのでとっとと手を動かすのがオススメです。
httpRequestsTotal
処理されたHTTPリクエストの総数をカウントするメトリクスです。prometheus.NewCounterVec
関数を使用して定義され、リクエストのメソッド(GET、POSTなど)とパス(リソースへのURLパス)によってラベル付けされます。
httpRequestDuration
HTTPリクエストの処理時間を記録するメトリクスです。prometheus.NewHistogramVec
関数を使用して定義され、リクエストのメソッドとパスによってラベル付けされます。デフォルトのバケットは、prometheus.DefBucketsを使用して設定されます。
httpRequestSize
HTTPリクエストのサイズ(バイト単位)を記録するメトリクスです。prometheus.NewHistogramVec
関数を使用して定義され、リクエストのメソッドとパスによってラベル付けされます。バケットは、prometheus.ExponentialBuckets
関数を使用して設定されます。
httpResponseSize
HTTPレスポンスのサイズ(バイト単位)を記録するメトリクスです。prometheus.NewHistogramVec
関数を使用して定義され、リクエストのメソッドとパスによってラベル付けされます。バケットは、同様にprometheus.ExponentialBuckets
関数を使用して設定されます。
httpResponseTime
HTTPレスポンスの時間を記録するメトリクスです。このメトリクスは、prometheus.NewGaugeVec
関数を使用して定義され、リクエストのメソッドとパスによってラベル付けされます。
externalAccessDuration
これは、3-shake.comへの外部アクセスの持続時間を記録するメトリクスです。このメトリクスは、prometheus.NewHistogram
関数を使用して定義されます。デフォルトのバケットは、prometheus.DefBuckets
関数を使用して設定されます。
Docker Compose での実行するんじゃろがい
prometheus.yml
まず、prometheus.yml
を作成します。このファイルには、Prometheusがどのようにメトリクスを収集するかについての設定が含まれています。
global: scrape_interval: 15s scrape_configs: - job_name: 'echo_exporter' static_configs: - targets: ['echo_exporter:2121']
docker-compose.yml
次に、docker-compose.yml
を作成します。このファイルには、PrometheusとGolangのEchoアプリケーションを実行するために必要なコンテナの設定が含まれています。
version: '3.8' services: echo_exporter: build: context: . dockerfile: Dockerfile_exporter ports: - "2121:2121" prometheus: image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml command: - "--config.file=/etc/prometheus/prometheus.yml" ports: - "9090:9090"
Dockerfile
Dockerfileを作成して、Echoアプリケーションをコンテナで実行できるようにします。別に動けばよいのでなんか工夫とかはしてないです。本番でやるときはうまくマルチステージビルドとか使って下さい。
# Use the official Golang image as the base image FROM golang:1.20 # Set the working directory WORKDIR /app # Copy go.mod and go.sum to download dependencies COPY go.mod go.sum ./ # Download dependencies RUN go mod download # Copy the source code COPY . . # Build the application RUN go build -o main . # Expose the port the application will run on EXPOSE 2121 # Run the application CMD ["./main"]
docker compose の実行
2023 年 6 月末から、Compose V1 はサポートされなくなり、すべての Docker Desktop バージョンから削除されるので注意してほしいです。ちなみにcompose がdocker
コマンドに入るようになったのでdocker-compose
特別にインストールせずとも実行可能になりました。
# デーモン化しちゃう docker compose up -d
Dockerfileを使用してecho_exporterサービスがビルドされ、PrometheusとGolangのEchoアプリケーションをそれぞれのコンテナで起動します。Prometheusは、echo_exporterサービスからメトリクスを収集し、ポート9090でアクセスできるようになります。
last_external_access_status_code を確認するには起動した状態でこちらを参考にしてください。
一回、シャットダウンしたので以下のようなグラフが出力されていますね。。長くなったのでこれで終わります。
さいごに
実は Echo において Prometheus は、HTTP リクエストのメトリックを生成することができるミドルウェアを提供しているので基本的な部分でコードを書く必要がありません。もし、アプリケーションに実装できるならそれが良いです。独自に実装などせずにエンドポイントにて500 Internal Server Error
が多発していればアラートをすれば良いだけなので...。もし、インフラのコードがアプリに組み込めないもしくはプロダクションコードは開発側しか触れない時には協力を仰いで下さい。開発側との人間関係に問題があったりセキュリティ上の課題がある場合には別の手段を考えましょう。
package main import ( "github.com/labstack/echo/v4" "github.com/labstack/echo-contrib/prometheus" ) func main() { e := echo.New() // Enable metrics middleware p := prometheus.NewPrometheus("echo", nil) p.Use(e) e.Logger.Fatal(e.Start(":1323")) }
と書くだけで外部リソースへのアクセス以外のメトリクスは提供できます。また、外部リソースに対してもいくつかの構造体を持っているのでこれらも効率的に提供できます。
本当に関係ないんですけど2023年4月19日にECサイト構築・運用セキュリティガイドラインを読み解く会 というのをやるので興味あれば!
続編のブログも書いておきました。