じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

Golang のEcho でMiddlewareを使ってPrometheus Exporter を実装する

はじめに

もし、アプリケーションに実装できるならそれが良いです。独自に実装などせずにエンドポイントにて500 Internal Server Errorが多発していればアラートをすれば良いので...。

こちらの続編になります。 syu-m-5151.hatenablog.com

本エントリーでは、GolangでEchoフレームワークを使用し、Prometheus ExporterをMiddlewareとして実装する方法について説明します。Prometheus Middlewareは、自動でMetrics を生成します。これにより、アプリケーションのパフォーマンス監視や問題解析が容易になります。

利用しているコードはこちら

github.com

コードを解説するんじゃい

シンプルだけど解説をします。環境構築などは前回のエントリーに任せます。 Echoフレームワークを使ってGolangでシンプルなWebアプリケーションを作成し、Prometheus Exporterをミドルウェアとして実装する例です。

package main

import (
    "math/rand"
    "net/http"
    "time"

    "github.com/labstack/echo-contrib/prometheus"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    prom "github.com/prometheus/client_golang/prometheus"
)

// Prometheus のメトリクスを定義しています。
// これらのメトリクスは、3-shake.com への外部アクセスの情報を収集するために使用されます。
var (
    externalAccessDuration = prom.NewHistogram(
        prom.HistogramOpts{
            Name:    "external_access_duration_seconds",
            Help:    "Duration of external access to 3-shake.com",
            Buckets: prom.DefBuckets,
        },
    )

    lastExternalAccessStatusCode = prom.NewGauge(
        prom.GaugeOpts{
            Name: "last_external_access_status_code",
            Help: "Last status code of external access to 3-shake.com",
        },
    )
)

// init 関数内で、メトリクスを Prometheus に登録しています。
func init() {
    prom.MustRegister(externalAccessDuration)
    prom.MustRegister(lastExternalAccessStatusCode)
}

// 3-shake.com の外部アクセスを計測するミドルウェアを作成します。
func measureExternalAccess(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        // HTTP クライアントを作成し、タイムアウトを 10 秒に設定します。
        client := &http.Client{Timeout: 10 * time.Second}
        // 現在の時刻を取得し、アクセス開始時間として保持します。
        startTime := time.Now()
        // 3-shake.com に対して HTTP GET リクエストを送信します。
        resp, err := client.Get("https://3-shake.com")
        // アクセス開始時間から現在の時刻までの経過時間を計算し、duration に格納します。
        duration := time.Since(startTime)
        // エラーが発生しない場合(リクエストが成功した場合)
        if err == nil {
            // アクセス時間(duration)をヒストグラムメトリクスに追加します。
            externalAccessDuration.Observe(duration.Seconds())
            // ステータスコードをゲージメトリクスに設定します。
            lastExternalAccessStatusCode.Set(float64(resp.StatusCode))
            // レスポンスのボディを閉じます。
            resp.Body.Close()
        }
        // 次のミドルウェアまたはハンドラ関数に処理を移します。
        return next(c)
    }
}

func unstableEndpoint(c echo.Context) error {
    // 0 から 4 までのランダムな整数を生成します。
    randomNumber := rand.Intn(5)

    // 生成された整数が 4 の場合、HTTP ステータスコード 500 を返します。
    if randomNumber == 4 {
        return c.String(http.StatusInternalServerError, "Something went wrong!")
    }

    // それ以外の場合、HTTP ステータスコード 200 を返します。
    return c.String(http.StatusOK, "Success!")
}

func main() {
    e := echo.New()

    // ミドルウェアの設定
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    // Prometheus ミドルウェアを有効にします。
    p := prometheus.NewPrometheus("echo", nil)
    p.Use(e)

    // 3-shake.com への外部アクセスを計測するミドルウェアを追加します。
    e.Use(measureExternalAccess)

    // ルートのエンドポイントを設定します。
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World!")
    })
    // /unstable エンドポイントを設定します。
    // 20% の確率で HTTP ステータスコード 500 を返します。
    e.GET("/unstable", unstableEndpoint)

    // サーバーを開始します。
    e.Start(":2121")
}

var

varで3-shake.com への外部アクセスの情報を収集するための Prometheus メトリクスを定義していきます。

var (
    externalAccessDuration = prom.NewHistogram(
        prom.HistogramOpts{
            Name:    "external_access_duration_seconds",
            Help:    "Duration of external access to 3-shake.com",
            Buckets: prom.DefBuckets,
        },
    )

    lastExternalAccessStatusCode = prom.NewGauge(
        prom.GaugeOpts{
            Name: "last_external_access_status_code",
            Help: "Last status code of external access to 3-shake.com",
        },
    )
)

echo.labstack.com

init

init 関数でメトリクスを Prometheus に登録します。

func init() {
    prom.MustRegister(externalAccessDuration)
    prom.MustRegister(lastExternalAccessStatusCode)
}

measureExternalAccess

measureExternalAccess関数で3-shake.com への外部アクセスを計測するミドルウェアを定義します。こちらの方がEcho Likeな定義の仕方だと思うので好きです。Echo のカスタムミドルウェアで、リクエストが処理される前に 3-shake.com への外部アクセスを計測する役割を持っています。

func measureExternalAccess(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        // HTTP クライアントを作成し、タイムアウトを 10 秒に設定します。
        client := &http.Client{Timeout: 10 * time.Second}
        // 現在の時刻を取得し、アクセス開始時間として保持します。
        startTime := time.Now()
        // 3-shake.com に対して HTTP GET リクエストを送信します。
        resp, err := client.Get("https://3-shake.com")
        // アクセス開始時間から現在の時刻までの経過時間を計算し、duration に格納します。
        duration := time.Since(startTime)
        // エラーが発生しない場合(リクエストが成功した場合)
        if err == nil {
            // アクセス時間(duration)をヒストグラムメトリクスに追加します。
            externalAccessDuration.Observe(duration.Seconds())
            // ステータスコードをゲージメトリクスに設定します。
            lastExternalAccessStatusCode.Set(float64(resp.StatusCode))
            // レスポンスのボディを閉じます。
            resp.Body.Close()
        }
        // 次のミドルウェアまたはハンドラ関数に処理を移します。
        return next(c)
    }
}

unstableEndpoint

ちゃんと、メトリクス値が取得できているか確認したいのでunstableEndpointというエンドポイントを追加し、リクエストのうち約 5 回に 1 回失敗するように実装しました。このエンドポイントは、リクエストが成功した場合には HTTP ステータスコード 200 を返し、失敗した場合には HTTP ステータスコード 500 を返します。

func unstableEndpoint(c echo.Context) error {
    // 0 から 4 までのランダムな整数を生成します。
    randomNumber := rand.Intn(5)
    // 生成された整数が 4 の場合、HTTP ステータスコード 500 を返します。
    if randomNumber == 4 {
        return c.String(http.StatusInternalServerError, "Something went wrong!")
    }
    // それ以外の場合、HTTP ステータスコード 200 を返します。
    return c.String(http.StatusOK, "Success!")
}

curl してくるワンライナー(fish)も用意しましたので思う存分curl してください。

for i in (seq 1 50); curl http://localhost:2121/unstable; echo ""; end

Middlewareを適用

寂しいのでPrometheus 以外のMiddlewareをEchoインスタンスに適用しました。

  • middleware.Logger() : リクエストのログを出力するMiddleware
  • middleware.Recover() : パニックを回復してアプリケーションがクラッシュしないようにするMiddleware
   e.Use(middleware.Logger())
    e.Use(middleware.Recover())

Prometheus Middlewareを適用

これだけなんです。 Echo用の新しいPrometheus Middlewareインスタンスを作成します。作成したPrometheus MiddlewareインスタンスをEchoインスタンスに適用します。

 // Prometheusミドルウェアを適用する
    p := prometheus.NewPrometheus("echo", nil)
    p.Use(e)

EchoのMiddlewareについて

Middlewareは、リクエストとレスポンスの処理の前後にカスタムロジックを実行するための仕組みを提供しています。EchoのMiddlewareは、コードの再利用性、可読性、そして機能の分離を向上させるために役立ちます。

sequenceDiagram participant Client participant Logger participant Middleware1 participant Recovery participant Prometheus participant CORS participant Handler Client->>Logger: Request Logger->>Middleware1: Processed Request Middleware1->>Recovery: Processed Request Recovery->>Prometheus: Processed Request Prometheus->>CORS: Processed Request CORS->>Handler: Processed Request Handler->>CORS: Response CORS->>Prometheus: Processed Response Prometheus->>Recovery: Processed Response Recovery->>Middleware1: Processed Response Middleware1->>Logger: Processed Response Logger->>Client: Processed Response

echo.labstack.com

Echo Echo Middleware の特徴

Middlewareは、複数のMiddleware関数を組み合わせて実行することができます。これにより、機能を組み合わせてカスタム処理パイプラインを構築することができます。これらは登録された順序で実行されます。これにより、処理の流れを明確にし、簡単に制御できるようになります。また、Echoでは、これらをグローバルに適用することも、特定のルートに適用することもできます。これにより、アプリケーション全体または特定のエンドポイントに対してカスタム処理を適用できます。Echoは、いくつかの組み込みミドルウェアを提供していますが独自のカスタムミドルウェアを作成してアプリケーションに適用することもできます。

e := echo.New()
e.Use(middleware.Logger()) # 登録された順序で実行されるぞ
e.GET("/",getHallo,middleware.Recover()) # e.GET("/", <Handler>, <Middleware...>) で特定のルートにだけMiddlewareを登録できる
e.Use(LoggingMiddleware) # 独自で実装するカスタムミドルウェア

echo.labstack.com

再び、Docker Compose での実行するんじゃろがい

完全に同じことやってるのでこちらを参考にしてください

syu-m-5151.hatenablog.com

見れたぞぉおおおお

http://localhost:2121/metrics の結果もこちらに記載しておきます

github.com

Golang のEcho でMiddlewareを使ってアプリケーションのPrometheus Exporter を実装することができました。アラートの設定方法については他のブログを参照してください。

さいごに

以上で、Echo フレームワークを使って、Prometheus メトリクスを追加し、さらに不安定なエンドポイントを作成する方法を解説しました。この知識を活かして、みなさんのアプリケーションにメトリクスを取得する機能を追加して、可観測性を向上させましょう!

全然、関係ないけど翻訳に携わってコンテナセキュリティのブラックボックス感が多少薄まるのでみんな読んでくれ...