👁️

AWS SDK for Go v1 の API 呼び出しごとに OpenTelemetry の Span を自動で作成する

2022/10/16に公開約3,500字

はじめに

S3, SQS, DynamoDB など AWS のサービスを活用した Go アプリケーションを運用する際、 OpenTelemetry などを使って分散トレーシングを導入したくなることがあると思います。

otelaws としてそのためのライブラリが公開されており、基本的にはこれを導入するだけで対応できます。
しかし、これは AWS SDK for Go v2 のみに対応しており、 v1 には対応していません。
この記事では v2 にバージョンアップできない場合などに使える代替案を紹介します。

まず v2 ではどのように対応されているのか、コードを読んで確認します。
以下のファイルを読み込めば大体の内容がわかります。

https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/aws.go

v2 では otelaws.AppendMiddlewares という関数をクライアントオブジェクトに対して呼び出してあげれば自動で Span が作成されるようです。

ところで AWS SDK for Go の middleware は v2 で導入された仕組みです。 (AWS の発表)
v1 には middleware は存在しないので、このコードをそのまま利用するのは難しそうです。

しかし v1 には Handlers という構造体があり、これが middleware のような役割を果たしています
詳しくは builders.flash の記事が参考になります。

この記事ではこの Handlers を使用して、自動で Span を作る仕組みを紹介します。

実装

ここでは Exporter として Jaeger を使うことにします。
また、実際の AWS 環境にはリクエストは飛ばさず LocalStack を使って動作確認することにします。

これらを起動する Docker Compose の設定は以下のようになります。

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "14268:14268" # for exporter
      - "16686:16686" # for browser
  localstack:
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"

Handlers を用いて自動で Span を作成するための関数は以下のように実装できます。

import "github.com/aws/aws-sdk-go/aws/client"

func instrument(client *client.Client) {
	serviceName := client.ServiceName

	client.Handlers.Send.PushFront(func(r *request.Request) {
		operationName := r.Operation.Name

		ctx, _ := tracer.Start(r.Context(), fmt.Sprintf("%s:%s", serviceName, operationName))

		r.SetContext(ctx)
	})

	client.Handlers.Complete.PushBack(func(r *request.Request) {
		span := trace.SpanFromContext(r.Context())
		defer span.End()

		if r.Error != nil {
			span.SetStatus(codes.Error, r.Error.Error())
			span.RecordError(r.Error)
		}
	})
}

実際の otelaws では timestamp、span kind、その他にも複数の attribute が設定されますが、ここでは省略しています。
ここは v2 でも v1 でも対して変わらないので、以下のコードを参考にすることによって自分で簡単に設定できるのではないかと思います。

https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/aws.go

先ほど作成した関数は以下のように呼び出すことができます。

sess := session.Must(session.NewSession(&aws.Config{
	Endpoint:         aws.String("http://localhost:4566"),
	Region:           aws.String("us-east-1"),
	S3ForcePathStyle: aws.Bool(true),
}))

{
	service := s3.New(sess)
	instrument(service.Client) // ここで Handlers にフックが設定される

	_, _ = service.ListBucketsWithContext(ctx, &s3.ListBucketsInput{})
}

{
	service := dynamodb.New(sess) // ここで Handlers にフックが設定される
	instrument(service.Client)

	_, _ = service.ListTablesWithContext(ctx, &dynamodb.ListTablesInput{})
	_, _ = service.PutItemWithContext(ctx, &dynamodb.PutItemInput{
		TableName: aws.String("not-found"),
		Item:      map[string]*dynamodb.AttributeValue{"id": {S: aws.String("1")}},
	})
}

このコードを呼び出した結果は Jaeger で見ると以下のようになります。

最後に

実際に動作するコードの全体は https://github.com/lambdasawa/zenn/tree/main/snippet/aws-sdk-go-v1-otel-auto-instrumentation にあります。
必要に応じて参照してください。

AWS SDK Go for v2 を使えればこのようなことをしなくても良いですが、諸事情で v1 を使う判断をすることもあると思います。
そのような場合にこの記事が役立てば幸いです。

GitHubで編集を提案

Discussion

ログインするとコメントできます