OpenTelemetry+Go 計装サンプル大全 with Cloud Trace ~意外なつまづきポイントを添えて~
はじめに
こんにちは。クラウドエース株式会社で主にアプリケーション開発を担当している水野です。
今回は、Go 言語でトレース計装する際のサンプル集をご紹介します。
OpenTelemetry は、アプリケーションのオブザーバビリティを実現するための OSS です。
対象読者
手動計装は、アプリケーションコードを煩雑化してしまう可能性もあるため
「できるだけ最小限な構成で手軽に導入したい」という方もいると思います。
そんな方に すぐ活用できる!ような記事となっています。
使用技術
- Golang
- OpenTelemetry
- Google Cloud
- Cloud Trace
- Cloud Logging
- Cloud Pub/Sub
- Cloud SQL
方針
- アプリケーション実装は最小限にする(ライブラリの力をできるだけ借りる)
- 計装箇所
- API (incoming, outgoing)
- http
- grpc
- RDB
- sql
- メッセージキュー
- publish
- subscribe
- API (incoming, outgoing)
- その他
- ログとの連携
OpenTelemetryのセットアップ
まずは、トレース計装するための OpenTelemetry のセットアップです。
主に以下のような設定をしています。(各設定の説明は割愛します)
import (
"context"
"log"
texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
"go.opentelemetry.io/contrib/detectors/gcp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.9.0"
)
// 呼び出し側はtp.Shutdown()でクリーンアップを行う
func InitializeTracer() *sdktrace.TracerProvider {
ctx := context.Background()
exporter, err := texporter.New(texporter.WithProjectID("PROJECT_ID"))
if err != nil {
log.Println(err)
}
res, err := resource.New(ctx,
resource.WithDetectors(gcp.NewDetector()),
resource.WithAttributes(
semconv.ServiceNameKey.String("アプリケーションサービス名"),
),
)
if err != nil {
log.Fatalln(err)
}
tp := sdktrace.NewTracerProvider(
// エキスポーターをCloud Traceに設定
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
// ここを設定しないとトレース伝搬されない
otel.SetTextMapPropagator(propagation.TraceContext{})
return tp
}
API (outgoing request)
http
OpenTelemetry が公式で提供している opentelemetry-go-contrib/instrumentation という OSS があり、以下のようなライブラリがあります。
イメージとしては、このようなトレースを取得できます。
こちらは、/api/v1/employee/attendances/:date
というAPIのリクエスト内で、/api/v1/attendances/detail
と/api/v1/attendance/:attendanceID
の外部APIを呼び出しています。
incoming
Gin を使用したサンプルをご紹介します。
以下のコードを追加するだけです。簡単!
func main() {
r := gin.Default()
// Request.Context()をgin.Contextに引き渡す
+ r.ContextWithFallback = true
+ r.Use(otelgin.Middleware("アプリケーションサービス名"))
r.GET("/ping", ping)
r.Run()
}
func ping(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
}
outgoing
net/http を使用したサンプルをご紹介します。
こちらも以下のコードを追加するだけです。簡単!
func DoRequest(ctx context.Context) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
if err != nil {
log.Fatal(err)
}
+ client := http.Client{
+ Transport: otelhttp.NewTransport(http.DefaultTransport),
+ }
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
}
grpc
OpenTelemetry が公式で otelgrpc を提供していますので、こちらのサンプルをご紹介します。
こちらも修正が最小限でシンプルですね。
incoming
s := grpc.NewServer(
+ grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
outgoing
conn, err := grpc.Dial("127.0.0.1:7777",
grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
if err != nil {
log.Fatalf("did not connect: %s", err)
}
defer func() { _ = conn.Close() }()
RDB
sql
残念ながら OpenTelemetry が公式でサポートしていません。
そのため、サードパーティライブラリをご紹介します。
- sqlcommenter(Googleが提供しているライブラリ)
- XSAM/otelsql(OpenTelemetryチームからレビューを受けたライブラリ)
- uptrace/otelsql(Uptraceが提供しているライブラリ)
今回は sqlcommenter を使用したサンプルをご紹介します。
こちらも簡単な修正で導入ができます。
dbURI := "root:root@tcp(127.0.0.1:3306/dbName)?parseTime=true"
- dbPool, err := sql.Open("mysql", dbURI)
+ dbPool, err := gosql.Open("mysql", dbURI, sqlcommentercore.CommenterOptions{})
トレースはこのように取得できます。
さらに、sqlcommenterは、Cloud Trace と Cloud SQL の Query Insights を自動連携してくれるため、以下のような調査が簡単にできます。これは地味に嬉しいですね。
- 問題のある SQL クエリがリクエスト全体にどれくらい影響でているか
- アプリケーションのどこが原因で SQL クエリに問題がでているか
メッセージキュー(Pub/Sub)
残念ながら OpenTelemetry が公式でサポートしていません。また、適切なサードパーティライブラリも現状ないため、自力で実装する必要があります。
でも、ご安心ください。1,2行修正するだけです。
詳しい内容の解説は、Google 山口さんの記事が丁寧にまとまっています。よろしければこちらもご覧ください。
こちらでは、基本的にGoogle 山口さんの記事から必要な部分を抜粋して、ご紹介します。
以下、サンプルです。
publisher
msg := pubsub.Message{Data: m}
...(中略)...
if msg.Attributes == nil {
msg.Attributes = make(map[string]string)
}
+ otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(msg.Attributes))
subscriber
if msg.Attributes != nil {
+ propagator := otel.GetTextMapPropagator()
+ ctx = propagator.Extract(ctx, propagation.MapCarrier(msg.Attributes))
}
そして、このようなトレースが取得できます。
その他
ログとの連携
ログとの連携を行うことで以下のようなことができます。
- リクエスト内のどのタイミングでログが出力されたか
- リクエスト内で出力したログのグループ化
今回は、Cloud Logging を使用したサンプルをご紹介します。
OpenTelemetry のライブラリでトレースとスパンを取得し、そこに構造化ログを紐づけるカタチです。
func NewLogger(ctx context.Context) *slog.Logger {
l := slog.New(slog.NewJSONHandler(os.Stdout, nil))
return setTrace(ctx, l)
}
func setTrace(ctx context.Context, l *slog.Logger) *slog.Logger {
projectID := "PROJECT_ID"
// ここでスパンとトレースを取得
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
if !sc.IsValid() {
return l
}
traceStr := fmt.Sprintf("projects/%s/traces/%s", projectID, sc.TraceID().String())
return l.With(
// ログにトレースを紐づける
slog.String("logging.googleapis.com/trace", traceStr),
// スパンも紐づけたい場合
slog.String("logging.googleapis.com/spanId", sc.SpanID().String()),
)
}
Cloud Logging での詳しい解説は、公式ドキュメントをご覧ください。
このようにトレースが取得でき、リクエスト内のどのタイミングでログが出力されたかが明確になります。
また、トレースIDでログを絞り込みすることで、エラーになったリクエスト内でどのエラーログを出力しているかも一目で分かります。
おわりに
Go 言語でトレース計装する際のサンプル集をご紹介しました。
手動計装でも最小限のアプリケーション変更でトレース導入できそうですね。ぜひ皆さんも試してみてください。
Discussion