Go でトレースと指標を生成する

このドキュメントでは、オープンソースの OpenTelemetry フレームワークを使用してトレースと指標データを収集するように Go アプリを変更する方法と、構造化 JSON ログを標準出力に出力する方法について説明します。このドキュメントでは、インストールして実行できるサンプルアプリについても説明します。このアプリは、指標、トレース、ログを生成するように構成されています。

コンテキストについて

OpenTelemetry のコンテキストは、プロセス内の API 間で実行スコープの値を伝達するメカニズムです。コンテキストの重要な用途は、現在アクティブなスパンを保持し、新しいスパンの作成時にその親として変更または参照できるようにすることです。要約すると、次のようになります。

  • コンテキストとは、プロセス内の API 間で実行スコープの値(現在のアクティブなスパンなど)を伝播するメカニズムを指します。

  • スパン コンテキストは、トレース ID、スパン ID、トレースのフラグと状態を含む、すべてのスパンの不変オブジェクトです。

  • 伝播とは、サービスとプロセスの間でコンテキストを移動するメカニズムです。

Go 標準ライブラリの context.Context も API の境界を越えてスコープ値を保持します。通常、サーバーのハンドラ関数は受信 Context を受け取り、呼び出しチェーンを介して、送信リクエストを行うすべてのクライアントに渡します。

Go 標準ライブラリ context.Context は、Go での OpenTelemetry コンテキストの実装として使用されます。

始める前に

Cloud Logging API, Cloud Monitoring API, and Cloud Trace API API を有効にします。

API を有効にする

アプリを計測してトレース、指標、ログを収集する

アプリを計測して、トレースと指標データを収集し、構造化 JSON を標準出力に出力するには、このドキュメントの以降のセクションで説明する手順を実施します。

  1. main 関数を構成する
  2. OpenTelemetry を構成する
  3. 構造化ロギングを構成する
  4. HTTP サーバーに計測を追加する
  5. トレーススパンをログおよび指標にリンクする
  6. HTTP クライアントに計測を追加する
  7. 構造化ログを書き込む

main 関数を構成する

構造化ログを書き込むようにアプリを構成し、OpenTelemetry を使用して指標とトレースデータを収集するには、main 関数を更新して Go 構造化ロギング パッケージ slog と OpenTelemetry を構成します。

次のコードサンプルは、setupLogging()setupOpenTelemetry() の 2 つのヘルパー関数を呼び出す main 関数を示しています。次のヘルパー関数は、ロギング パッケージと OpenTelemetry を構成します。

func main() {
	ctx := context.Background()

	// Setup logging
	setupLogging()

	// Setup metrics, tracing, and context propagation
	shutdown, err := setupOpenTelemetry(ctx)
	if err != nil {
		slog.ErrorContext(ctx, "error setting up OpenTelemetry", slog.Any("error", err))
		os.Exit(1)
	}

	// Run the http server, and shutdown and flush telemetry after it exits.
	slog.InfoContext(ctx, "server starting...")
	if err = errors.Join(runServer(), shutdown(ctx)); err != nil {
		slog.ErrorContext(ctx, "server exited with error", slog.Any("error", err))
		os.Exit(1)
	}
}

ロギング パッケージを構成した後、ログをトレースデータにリンクするには、Go Context をロガーに渡す必要があります。詳細については、このドキュメントの構造化ログを書き込むをご覧ください。

OpenTelemetry を構成する

OTLP プロトコルを使用してトレースと指標を収集し、エクスポートするには、グローバル TracerProvider インスタンスと MeterProvider インスタンスを構成します。次のコードサンプルは、main 関数から呼び出される setupOpenTelemetry 関数を示しています。

func setupOpenTelemetry(ctx context.Context) (shutdown func(context.Context) error, err error) {
	var shutdownFuncs []func(context.Context) error

	// shutdown combines shutdown functions from multiple OpenTelemetry
	// components into a single function.
	shutdown = func(ctx context.Context) error {
		var err error
		for _, fn := range shutdownFuncs {
			err = errors.Join(err, fn(ctx))
		}
		shutdownFuncs = nil
		return err
	}

	// Configure Context Propagation to use the default W3C traceparent format
	otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())

	// Configure Trace Export to send spans as OTLP
	texporter, err := autoexport.NewSpanExporter(ctx)
	if err != nil {
		err = errors.Join(err, shutdown(ctx))
		return
	}
	tp := trace.NewTracerProvider(trace.WithBatcher(texporter))
	shutdownFuncs = append(shutdownFuncs, tp.Shutdown)
	otel.SetTracerProvider(tp)

	// Configure Metric Export to send metrics as OTLP
	mreader, err := autoexport.NewMetricReader(ctx)
	if err != nil {
		err = errors.Join(err, shutdown(ctx))
		return
	}
	mp := metric.NewMeterProvider(
		metric.WithReader(mreader),
	)
	shutdownFuncs = append(shutdownFuncs, mp.Shutdown)
	otel.SetMeterProvider(mp)

	return shutdown, nil
}

上記のコードサンプルでは、トレース コンテキストの伝播W3C トレース コンテキスト形式を使用するようグローバル TextMapPropagator を構成しています。この構成により、トレース内でスパンが正しい親子関係を持つことが保証されます。

保留中のすべてのテレメトリーがフラッシュされ、接続を正常に終了するため、setupOpenTelemetry 関数は、これらのアクションを実行する shutdown という名前の関数を返します。

構造化ロギングを構成する

標準出力に書き込まれる JSON 形式のログにトレース情報を含めるには、Go 構造化ロギング パッケージ slog を構成します。次のコードサンプルは、main 関数から呼び出される setupLogging 関数を示しています。

func setupLogging() {
	// Use json as our base logging format.
	jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: replacer})
	// Add span context attributes when Context is passed to logging calls.
	instrumentedHandler := handlerWithSpanContext(jsonHandler)
	// Set this handler as the global slog handler.
	slog.SetDefault(slog.New(instrumentedHandler))
}

上記のコードは handlerWithSpanContext 関数を呼び出します。この関数は Context インスタンスから情報を抽出し、その情報を属性としてログに追加します。これらの属性を使用して、ログをトレースに関連付けることができます。

  • logging.googleapis.com/trace: ログエントリに関連付けられているトレースのリソース名。
  • logging.googleapis.com/spanId: ログエントリに関連付けられているトレースのスパン ID。
  • logging.googleapis.com/trace_sampled: このフィールドの値は true または false にする必要があります。

これらのフィールドの詳細については、LogEntry 構造体をご覧ください。

func handlerWithSpanContext(handler slog.Handler) *spanContextLogHandler {
	return &spanContextLogHandler{Handler: handler}
}

// spanContextLogHandler is an slog.Handler which adds attributes from the
// span context.
type spanContextLogHandler struct {
	slog.Handler
}

// Handle overrides slog.Handler's Handle method. This adds attributes from the
// span context to the slog.Record.
func (t *spanContextLogHandler) Handle(ctx context.Context, record slog.Record) error {
	// Get the SpanContext from the golang Context.
	if s := trace.SpanContextFromContext(ctx); s.IsValid() {
		// Add trace context attributes following Cloud Logging structured log format described
		// in https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
		record.AddAttrs(
			slog.Any("logging.googleapis.com/trace", s.TraceID()),
		)
		record.AddAttrs(
			slog.Any("logging.googleapis.com/spanId", s.SpanID()),
		)
		record.AddAttrs(
			slog.Bool("logging.googleapis.com/trace_sampled", s.TraceFlags().IsSampled()),
		)
	}
	return t.Handler.Handle(ctx, record)
}

func replacer(groups []string, a slog.Attr) slog.Attr {
	// Rename attribute keys to match Cloud Logging structured log format
	switch a.Key {
	case slog.LevelKey:
		return slog.Any("severity", a.Value)
	case slog.TimeKey:
		return slog.Any("timestamp", a.Value)
	case slog.MessageKey:
		return slog.Any("message", a.Value)
	}
	return a
}

HTTP サーバーに計測を追加する

HTTP サーバーによって処理されるリクエストにトレースと指標の計測を追加するには、OpenTelemetry を使用します。次のサンプルでは、otelhttp ハンドラを使用してコンテキストを伝播し、トレースと指標の計測を行います。

func runServer() error {
	handleHTTP("/single", handleSingle)
	handleHTTP("/multi", handleMulti)

	return http.ListenAndServe(":8080", nil)
}

// handleHTTP handles the http HandlerFunc on the specified route, and uses
// otelhttp for context propagation, trace instrumentation, and metric
// instrumentation.
func handleHTTP(route string, handleFn http.HandlerFunc) {
	instrumentedHandler := otelhttp.NewHandler(otelhttp.WithRouteTag(route, handleFn), route)

	http.Handle(route, instrumentedHandler)
}

上記のコードでは、otelhttp ハンドラが TracerProviderMeterProviderTextMapPropagator の各グローバル インスタンスを使用しています。これらのインスタンスは、setupOpenTelemetry 関数によって構成されます。

トレーススパンをログおよび指標にリンクする

サーバーとクライアントのスパンをリンクし、指標とログを関連付けるには、Go Context インスタンスを HTTP リクエストに渡します。また、ログを書き込むときにも、そのインスタンスを渡します。次の例は、Go Context インスタンスを抽出し、そのインスタンスをロガーと callSingle 関数に渡すルートハンドラを示しています。この関数は送信 HTTP リクエストを行います。

func handleMulti(w http.ResponseWriter, r *http.Request) {
	subRequests := 3 + rand.Intn(4)
	// Write a structured log with the request context, which allows the log to
	// be linked with the trace for this request.
	slog.InfoContext(r.Context(), "handle /multi request", slog.Int("subRequests", subRequests))

	// Make 3-7 http requests to the /single endpoint.
	for i := 0; i < subRequests; i++ {
		if err := callSingle(r.Context()); err != nil {
			http.Error(w, err.Error(), http.StatusBadGateway)
			return
		}
	}

	fmt.Fprintln(w, "ok")
}

上記のコードでは、関数呼び出し r.Context() によって HTTP リクエストから Go Context が取得されます。

HTTP クライアントに計測を追加する

トレース コンテキストを送信 HTTP リクエストに挿入し、トレースと指標の計測を追加するには、otelhttp.Get 関数を呼び出します。次の例では、callSingle 関数がこのアクションを実行します。

func callSingle(ctx context.Context) error {
	// otelhttp.Get makes an http GET request, just like net/http.Get.
	// In addition, it records a span, records metrics, and propagates context.
	res, err := otelhttp.Get(ctx, "http://localhost:8080/single")
	if err != nil {
		return err
	}

	return res.Body.Close()
}

上記のコードでは、otelhttp ハンドラが TracerProviderMeterProviderTextMapPropagator の各グローバル インスタンスを使用しています。これらのインスタンスは、setupOpenTelemetry 関数によって構成されます。

構造化ログを書き込む

トレースにリンクする構造化ログを書き込むには、Go の構造化ロギング パッケージ slog を使用して、Go の Context インスタンスをロガーに渡します。ログをスパンにリンクするには、Go Context インスタンスが必要です。たとえば、次のステートメントは、slogInfoContext メソッドを呼び出す方法と、subRequests フィールドを JSON インスタンスに追加する方法を示しています。

slog.InfoContext(r.Context(), "handle /multi request", slog.Int("subRequests", subRequests))

テレメトリーを収集するように構成されたサンプルアプリを実行する

サンプルアプリでは、JSON ログ、OTLP 指標、トレースなど、ベンダーに依存しない形式を使用しています。テレメトリーを Google Cloud にルーティングするため、このサンプルでは Google エクスポータで構成された OpenTelemetry Collector を使用します。アプリの負荷生成ツールがアプリのルートにリクエストを発行します。

アプリをダウンロードしてデプロイする

サンプルを実行するには、次の操作を行います。

  1. Google Cloud コンソールで、「Cloud Shell をアクティブにする」をクリックします。

    Cloud Shell をアクティブにする

    Google Cloud コンソールの下部で Cloud Shell セッションが開始し、コマンドライン プロンプトが表示されます。Cloud Shell はシェル環境です。Google Cloud CLI がすでにインストールされており、現在のプロジェクトの値もすでに設定されています。セッションが初期化されるまで数秒かかることがあります。

  2. リポジトリのクローンを作成します。

    git clone https://github.com/GoogleCloudPlatform/golang-samples
    
  3. OpenTelemetry ディレクトリに移動します。

    cd golang-samples/opentelemetry/instrumentation
    
  4. サンプルをビルドして実行します。

    docker compose up --abort-on-container-exit
    

    Cloud Shell で実行していない場合は、認証情報ファイルを指す GOOGLE_APPLICATION_CREDENTIALS 環境変数を使用してアプリケーションを実行します。アプリケーションのデフォルト認証情報は、$HOME/.config/gcloud/application_default_credentials.json にある認証情報ファイルを提供します。

    # Set environment variables
    export GOOGLE_CLOUD_PROJECT="PROJECT_ID"
    export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/gcloud/application_default_credentials.json"
    export USERID="$(id -u)"
    
    # Run
    docker compose -f docker-compose.yaml -f docker-compose.creds.yaml up --abort-on-container-exit
    

指標を表示する

サンプルアプリの OpenTelemetry 計測は、Metrics Explorer で表示可能な Prometheus 指標を生成します。

  • Prometheus/http_server_duration/histogram は、サーバー リクエストの所要時間を記録し、結果をヒストグラムに保存します。

  • Prometheus/http_server_request_content_length_total/counter は、/multi/single の HTTP ルートのリクエスト コンテンツの長さを記録します。この指標の測定値は累積値です。つまり、各値は値の収集が開始してからの合計を表します。

  • Prometheus/http_server_response_content_length_total/counter は、/multi/single の HTTP ルートのレスポンス コンテンツの長さを記録します。この指標の測定値は累積値です。

サンプルアプリによって生成された指標を表示する手順は次のとおりです。
  1. Google Cloud コンソールのナビゲーション パネルで [Monitoring] を選択し、次に [ Metrics Explorer] を選択します。

    Metrics Explorer に移動

  2. [指標] 要素の [指標を選択] メニューを展開してフィルタバーに「http_server」と入力し、サブメニューを使用して特定のリソースタイプと指標を選択します。
    1. [有効なリソース] メニューで、[Prometheus Target] を選択します。
    2. [有効な指標カテゴリ] メニューで、[Http] を選択します。
    3. [ACTIVE METRICS] メニューで指標を選択します。
    4. [適用] をクリックします。
  3. データの表示方法を構成します。

    指標の測定値が累積の場合、Metrics Explorer はアライメント期間ごとに測定データを自動的に正規化し、グラフに率を表示します。詳細については、種類、タイプ、変換をご覧ください。

    2 つの counter 指標など、integer 値または double 値が測定されると、Metrics Explorer はすべての時系列を自動的に合計します。HTTP ルート /multi/single のデータを表示するには、[集計] エントリの最初のメニューを [なし] に設定します。

    グラフの構成の詳細については、Metrics Explorer 使用時の指標の選択をご覧ください。

トレースを表示する

トレースデータを表示するには、次の操作を行います。

  1. Google Cloud コンソールのナビゲーション パネルで [トレース] を選択し、次に [Trace エクスプローラ] を選択します。

    [Trace エクスプローラ] に移動

  2. 散布図で、URI が /multi のトレースを選択します。
  3. [トレースの詳細] パネルのガントチャートで、/multi というラベルのスパンを選択します。

    パネルが開き、HTTP リクエストに関する情報が表示されます。詳細には、メソッド、ステータス コード、バイト数、呼び出し元のユーザー エージェントが含まれます。

  4. このトレースに関連付けられているログを表示するには、[ログとイベント] タブを選択します。

    このタブには、個々のログが表示されます。ログエントリの詳細を表示するには、ログエントリを開きます。[ログを表示] をクリックし、ログ エクスプローラを使用してログを表示することもできます。

Cloud Trace エクスプローラの使用方法について詳しくは、トレースを検索して調査するをご覧ください。

ログを表示する

ログ エクスプローラではログを調査できます。また、関連するトレース(存在する場合)を確認することもできます。

  1. Google Cloud コンソールのナビゲーション パネルで、[ロギング] を選択してから、[ログ エクスプローラ] を選択します。

    [ログ エクスプローラ] に移動

  2. handle /multi request の説明を含むログを見つけます。

    ログの詳細を表示するには、ログエントリを開きます。jsonPayload フィールドに、subRequests というラベルの付いたエントリがあります。このエントリは、handleMulti 関数のステートメントによって追加されました。

  3. 「handle /multi request」メッセージを含むログエントリの [ トレース] をクリックし、[トレースの詳細表示] を選択します。

    [トレースの詳細] パネルが開き、選択したトレースが表示されます。

ログ エクスプローラの使用方法については、ログ エクスプローラを使用してログを表示するをご覧ください。

次のステップ