0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GCPの Cloud Traceを OpenTelemetry ベースで 動作確認した際の 最小限のGo言語実装

Last updated at Posted at 2024-12-12

第三開発部所属の恩田です。久しぶりに記事を書いてみました。

対象者

  • OpenTelemetry で可観測性を上げたいエンジニア
  • 利用環境は Cloud Trace (GCP) である
  • 取り敢えず動かしたい

概要

最近 Cloud Traceを Go言語のコードに適用した。

過去にも同等の経験がありその際は簡単だったので、すぐに済むと高を括っていたが、現在推奨の連携は Open Telemetry ベースの実装となりやや複雑化している。幾つかのサンプル実装を目にしたが、個人的には最小セットのコードが欲しくそれ以上の機能は一旦不要なので、必要なものだけピックアップした最小セットに近いものを作成した。

なお、公式のサンプルコードも GCP版
OpenTelemetry版 とあるが、何れも Metricsや Logの実装も含めていたり、それらのシャットダウン処理を束ねるクリーンアップ関数を用意して後処理を効率化したりと要所々々で色を付けているので、そこは一旦カットした(本番適用時のデザインとしては秀逸なので追って適用)

余談だが、Metricsを利用すると任意の値を元に Cloud Monitoring上の独自の Metricsとしてグラフが作成され可観測性が上がるので、興味があればそちらも試してみて欲しい。

実装

結論から述べれば後述の実装がほぼ最小セットである。動かすために追加した部分は2箇所だ。

  • ① 環境誤差を環境変数として読み込む部分と、それを .env ファイル越しに設定する部分
  • ② 模擬的な計測対象としての関数(spanXXという形式の名称)とその共通機能を抽出した spanFunc

この辺りは本質ではないが、動作確認のために必要となっている。

本当に骨組みだけ知りたければ OpenTelemetryの Go Instruction が参考になる。

また、動作確認用のコードなので、見通し重視でエラーハンドリングは適当だ。もし実際に利用するならその辺りも補強した上で利用して欲しい。

解説

主な関数の構造としては以下の形式になっている。

  • main 環境変数より各種情報を収集する
  • control 主な処理の流れをコントロールする
  • newTracerProvider トレース制御を行う Tracer のファクトリを初期化する

基本的に newTracerProvider で生成する TracerProviderExporter を入れ替えれば様々なベンダーのメトリクスに連携ができるようになっており、それ以外は汎用的な構造をしている。

ベンダー一覧 を見ると 2024/12/04現在で 76ベンダーを確認できる。これらの共通項を提示している訳で OpenTelemetryは偉大である。

main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"go.opentelemetry.io/otel/attribute"

	gcptrace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
	"github.com/joho/godotenv"

	"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.4.0"
	"go.opentelemetry.io/otel/trace"
)

// Prerequisites:
//   - ServiceAccount with "Cloud Trace Agent" role
//   - Enabling "Cloud Trace API"
//
// See:
//   - https://github.com/GoogleCloudPlatform/golang-samples/blob/main/opentelemetry/instrumentation/app/setup.go
func main() {
	// ① 環境誤差を環境変数として読み込む部分と、それを `.env` ファイル越しに設定する部分
	if err := godotenv.Load(".env"); err != nil {
		log.Fatal(err)
	}
	projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
	serviceName := os.Getenv("SERVICE_NAME")
	credFilePath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")

	fmt.Printf("GOOGLE_CLOUD_PROJECT: [%s], SERVICE_NAME: [%s], GOOGLE_APPLICATION_CREDENTIALS: [%s] \n", projectID, serviceName, credFilePath)

	if err := control(projectID, serviceName); err != nil {
		log.Fatal(err)
	}
}

func control(projectID, serviceName string) error {
	ctx := context.Background()

	// TraceProvider の設定
	tp, err := newTracerProvider(ctx, projectID, serviceName)
	if err != nil {
		return err
	}
	defer func(ctx context.Context) {
		_ = tp.ForceFlush(ctx)
		_ = tp.Shutdown(ctx)
	}(ctx)
	otel.SetTracerProvider(tp)

	// Trace情報の収集
	t := tp.Tracer("trace-1")
	if err := span1(ctx, t); err != nil {
		return err
	}
	return nil
}

func newTracerProvider(ctx context.Context, projectID, serviceName string) (*sdktrace.TracerProvider, error) {
	// 1. 出力先に責任を持つ Exporterを用意し、GCPプロジェクトに設定
	// See: https://opentelemetry.io/ecosystem/vendors/
	exporter, err := gcptrace.New(gcptrace.WithProjectID(projectID))
	if err != nil {
		return nil, err
	}
	// 2. 資源情報に責任を持つ Resourceを用意し、サービスを設定
	res, err := resource.New(ctx,
		resource.WithDetectors(gcp.NewDetector()),
		resource.WithAttributes(
			semconv.ServiceNameKey.String(serviceName),
		),
	)
	if err != nil {
		return nil, err
	}
	// 3. サンプリング率の設定
	sampler := sdktrace.AlwaysSample() // sdktrace.NeverSample(), sdktrace.TraceIDRatioBased(0.1) are another options

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
		sdktrace.WithSampler(sampler),
	)
	// 4. トレースコンテキストの伝播方法を Contextベースに指定
	otel.SetTextMapPropagator(propagation.TraceContext{})
	return tp, nil
}

// ↓ ③ 模擬的な計測対象としての関数(spanXXという形式の名称)とその共通機能を抽出した `spanFunc`
func spanFunc(ctx context.Context, t trace.Tracer, spanName string, latency time.Duration, f func(ctx context.Context) error) error {
	chCtx, span := t.Start(ctx, spanName)
	span.SetAttributes(attribute.String("expectedLatency", latency.String())) // Optional
	defer span.End()
	time.Sleep(latency)
	return f(chCtx)
}

func span1(ctx context.Context, t trace.Tracer) error {
	return spanFunc(ctx, t, "span-1", 10*time.Millisecond, func(ctx context.Context) error {
		_ = span11(ctx, t)
		_ = span12(ctx, t)
		return nil
	})
}

func span11(ctx context.Context, t trace.Tracer) error {
	return spanFunc(ctx, t, "span-11", 20*time.Millisecond, func(ctx context.Context) error {
		_ = span111(ctx, t)
		return nil
	})
}

func span111(ctx context.Context, t trace.Tracer) error {
	return spanFunc(ctx, t, "span-111", 5*time.Millisecond, func(ctx context.Context) error {
		return nil
	})
}

func span12(ctx context.Context, t trace.Tracer) error {
	return spanFunc(ctx, t, "span-12", 30*time.Millisecond, func(ctx context.Context) error {
		return nil
	})
}

.env
GOOGLE_CLOUD_PROJECT="xxxxx"
SERVICE_NAME="xxxx"
GOOGLE_APPLICATION_CREDENTIALS="xxxx.json"

実行方法

環境変数の用意

下記の環境変数を埋める。 .env ファイルに定義しておけば都度引数として渡す必要が無い。

# 名前 設定内容
1 GOOGLE_CLOUD_PROJECT GCPのプロジェクトID
2 SERVICE_NAME Trace上に表示されるサービスの論理名
3 GOOGLE_APPLICATION_CREDENTIALS ローカル実行時に参照するクレデンシャルファイルのパス

権限設定

IAMで上記の #3で指定したサービスアカウントに以下の権限を付けておく

  • Cloud Trace エージェント

API有効化

以下のAPIを有効化しておく

  • Cloud Trace API

ハマりどころ

今回実際にハマった部分を晒しておく。分かり易いエラーメッセージを出力してくれる優しい人達ばかりではないので注意して欲しい。

  • IAM設定が抜けていてエラーが出る
  • API許可が抜けていて無言で Traceが出ない
  • TraceProvider.ForceFlush(ctx) が無いため Traceに出ない
  • sdktrace.AlwaysSample() が無いため Traceに出ない
  • trace パッケージが以下の3種存在するので混乱する
    • gcpに出力する Exporter定義のための trace
    • Trace処理を実際に実装している otel SDKの trace
    • インターフェイスを提供する otel本体の trace

なお、TraceProvider.ForceFlush(ctx)tracer.Trace.AlwaysSample() の部分は稼働確認後にプロジェクトの適性に合わせて再設定して欲しい。そうしないと全てが都度漏れなく出力されてしまい、性能に支障が出てしまう。

出力先

管理コンソール(GUI)上の Cloud Traceを確認すると、 Traceエクスプローラ 画面上に青いドットとして出力され、選択するとトレース情報が出力される。
image.png

image.png

また、属性情報を付与した場合は、トレースを選択すると表示される属性欄に記載される。例えば、Repository 上の関数で複数件の登録を行った際に、その件数を属性として設定しておく等の利用法が考えられる。

image.png

実装の拡張

一通りの稼働を確認できたら、Span に追加の情報を載せて可観測性を上げると良い。具体的には以下のような機能が用意されている。 Attributes を代表としてサンプル実装にも載せたので、これらを適切に使って解像度の高いトレースを実現したい。

# 名称 説明 Link
1 Attributes Spanに追加の属性情報を付与する
フィルタリング等で利用可能
Span Attributes
2 Events 期間内に特殊なイベントが発生した場合に付与する。ロック制御・再試行等 Events
3 Status 異常なステータスをマークする Set span status
4 Record Error 異常なステータスと共にエラーを記録する Record errors
5 Propagation 別のマイクロサービス等、プロセス外の対象に伝搬させる Propagators and Context

Otelの全貌

ついでに全体構成を分析していったら、結構大掛かりだったので解像度の粗い画像で雰囲気だけでも晒してみる。

image.png

これで何が言いたいのかといえば、「これだけ複雑なら初見が苦労するはずだよ」ということ。全貌の詳細は 別途まとめて記事化しようと考えている。

まとめ

簡単そうに見えるトレース出力が意外と難しかった。苦労したくない皆様はこのサンプルを動かしてみた上で、問題なく稼働したならエレガントなコードにリファクタリングして利用することで工数を最小化して欲しい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?