Runner in the High

技術のことをかくこころみ

GinのOpenCensusミドルウェアを作ってCloud Loggngのログがリクエスト単位にグルーピングされるようにする

OpenCensusはdeprecatedされているので本当はOpenTelemetryを使った方がいいのだけれど、やったのでメモがてらに残しておく。アプリケーションの実行環境はGAEでやった。

追記: OpenTelemetryでやる方法も書きました izumisy.work

まずはログのグルーピング(Span)をリクエスト毎に開始するためのミドルウェアを作る。

package middleware

import (
    "github.com/gin-gonic/gin"
    "go.opencensus.io/exporter/stackdriver/propagation"
    "go.opencensus.io/trace"
)

//
// ルーティングの実装ではUseを使って以下のミドルウェアを登録しておく
// router.Use(Tracer())
//

func Tracer() gin.HandlerFunc {
    return func(c *gin.Context) {
        ctxWithSpan, span := trace.StartSpan(c, "yourapp/trace")
        defer span.End()

        httpFormat := propagation.HTTPFormat{}
        if sc, ok := httpFormat.SpanContextFromRequest(c.Request); ok {
            ctxWithSpan, span = trace.StartSpanWithRemoteParent(c, c.FullPath(), sc)
        }

        c.Request = c.Request.WithContext(ctxWithSpan)
        c.Next()
    }
}

TracerミドルウェアによってContextにグルーピングのために必要な情報がセットされるので、それを取得する関数も用意しておく。

gin.Context の場合にはRequestからContextを取り出すようにしているのがポイント。

package log

import (
    "context"

    "github.com/gin-gonic/gin"
    "go.opencensus.io/trace"
)

type Tracer struct {
    SpanID  string
    TraceID string
}

func ExtractTracer(ctx context.Context) Tracer {
    sc := trace.SpanContext{}

    if ginCtx, ok := ctx.(*gin.Context); ok {
        sc = trace.FromContext(ginCtx.Request.Context()).SpanContext()
    } else {
        sc = trace.FromContext(ctx).SpanContext()
    }

    return Tracer{
        SpanID:  sc.SpanID.String(),
        TraceID: sc.TraceID.String(),
    }
}

あとはCloud Loggingにデータを送信するタイミングで、上で作ったExtractTracerを用いてContextからSpanIDとTraceIDを取り出し、それぞれ適切な形でセットしてやればok

package log

import (
    "fmt"
    "os"
    "log"

    "cloud.google.com/go/logging"
)

func Log(ctx context.Context, severity logging.Severity, timestamp time.Time, text string) {
    projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
    client, err := logging.NewClient(ctx, projectID)
    if err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }

    tracer := ExtractTracer(ctx)
    logger = client.Logger("ServeHTTP")
    logger.Log(logging.Entry{
        Timestamp: timestamp,
        Severity:  severity,
        Payload:   text,
        SpanID:    tracer.SpanID,
        Trace:     fmt.Sprintf("projects/%s/traces/%s", projectID, tracer.TraceID),
    })
}

DeNAが出しているaelogというパッケージを使えばこの辺全部よしなにやってくれるっぽい。

Ginでなにもせず使えるかは試してない。ミドルウェアの型定義的に gin.WrapH とかでラップしてUseしてあげれば使えるような気もするけど、どうなんだろう github.com

追記: 調べたらこんなのもあった。 github.com Cloud Loggingとのつなぎこみだけ自分で作ってこういうのを使うでもいいかも。