go Cloud Run + Cloud Trace + OpenTelemetry のセットアップ 最終更新: 2022-08-08
TL;DR
GCPのドキュメントにはほとんど役に立つことは書いてない
Cloud Trace の Exporter を OpenTelemetry に設定した後はただ OpenTelemetry 使うだけなので Cloud Trace ほぼ関係ない
Cloud Run なら端折れることが多いがドキュメントには書かれてない
opentelemetry-go-contrib が神
基本情報
Observability周りのあれこれの規格を標準化してベンダーロックインを減らそうというもの
Cloud Trace
GCPの分散トレース用コンポーネント
Cloud Run + Cloud Trace
Cloud RunはデフォルトでCloud Traceに対応している
Cloud Run サービスへの受信リクエストが発生すると、Cloud Trace に表示可能なトレースが自動生成されます。
独自のスパンや属性などをトレースに追加することもできる
Cloud Traceのクライアントライブラリを使う
Go + Cloud Trace
GoにはCloud Trace 独自のクライアントライブラリはない(あるにはあるが一般向けじゃない)
OpenTelemetry のクライアントライブラリを推奨されている
Cloud Trace は OpenTelemetry のバックエンドとして接続される
Application --> OpenTelemetry --> Cloud Trace
つまり、Go + Cloud Run + Cloud Trace をやるためには
OpenTelemetryのクライアントライブラリをセットアップして
OpenTelemetryがデータをCloud Traceへ送信するようにして
そのあとはCloud Traceのことは忘れてOpenTelemetryを使ってトレースする
OpenTelemetryのクライアントライブラリをセットアップ
code:tracing.go
import (
"log"
"contrib.rocks/apps/api/internal/config"
"contrib.rocks/libs/goutils/env"
cloudtrace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
// これはあってもなくてもいい。アプリケーションの各地でTracer作るのが面倒だったので共通化した
var DefaultTracer = otel.Tracer("")
func InitTraceProvider(cfg *config.Config) *sdktrace.TracerProvider {
// Cloud Trace用のExporterを生成
exporter, err := cloudtrace.New()
if err != nil {
log.Fatal(err)
}
// otel.TracerProviderにexporterが渡るようにする
tpOpts := []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exporter)}
if cfg.Env == env.EnvDevelopment {
tpOpts = append(tpOpts, sdktrace.WithSampler(sdktrace.AlwaysSample()))
}
tp := sdktrace.NewTracerProvider(tpOpts...)
otel.SetTracerProvider(tp)
return tp
}
github.com/GoogleCloudPlatform/opentelemetry-operations-go
Open TelemetryのAPIと互換性のある SpanExporter を生成する
Cloud Run の上なら New() しとけば Application Default Credentials 的なアレでプロジェクトIDとか自動検出される
このへんのドキュメントがマジで無い
sdktrace.WithBatcher(exporter)
逐次送信ではなく非同期でいい感じにまとめてトレースを送信するオプション
ここで exporter を渡すことでスパンの送り先が Cloud Traceになる
sdktrace.WithSampler(sdktrace.AlwaysSample()
開発モードのときはデバッグのために毎回送るが、これをオプションに入れなければいい感じにサンプリングされるはず
sdktrace.NewTracerProvider(tpOpts...)
これで Cloud Traceと裏でつながった TracerProvider を生成する
otel.SetTracerProvider(tp)
OpenTelemetryのライブラリに Cloud Traceと裏でつながった TracerProvider を渡す
あとはOpenTelemetryにトレースさせればよい
OpenTelemetryを使ってトレース
code:server.go
func StartServer() error {
// ...
// さっき作った TracerProvider
tp := tracing.InitTraceProvider(cfg)
defer tp.Shutdown(context.Background())
r := gin.Default()
// otelgin.Middlewareがいい感じにやってくれる
r.Use(otelgin.Middleware("api", otelgin.WithTracerProvider(tp)))
// ...
return r.Run(fmt.Sprintf(":%s", cfg.Port))
}
基本的にopentelemetry-go-contribのサンプルコードを読めばわかった
otelgin.Middleware
いろいろやってくれる
Cloud Runが自動で作成している RootSpan を引っこ抜いてきたりあれこれ属性を追加してくれたり
ginでエラーを返していたらそのエラー情報も紐付けたり
defer tp.Shutdown(context.Background())
サーバーが終了するときには未送信のスパンをまとめて送ってから終わるようにするための関数
各種コードでのカスタムスパン
これもopentelemetry-go-contribのサンプルコードを読めばわかった。
code:api/image.go
func (api *API) Get(c *gin.Context) {
ctx, span := tracing.DefaultTracer.Start(c.Request.Context(), "api.image.Get")
defer span.End()
// ...
}
ctx, span := tracing.DefaultTracer.Start(c.Request.Context(), "api.image.Get")
tracing.DefaultTracer は最初に作っておいた Tracer。この場で otel.Tracer(name) してもいい
スパンの入れ子構造を作るには Context を引き継いでいく必要がある
Start() から返された新しい Context を、次のスパンでは引数に渡すことになる
defer span.End()
もちろんスパンは閉じなきゃいけない
defer のおかげで関数単位でのスパンはめちゃ楽