🐙

【Go】 DynamoDB アクセス時に発生するエラーをアプリケーション全体で一律でログ出力したい

2023/07/08に公開

DynamoDB アクセス時に発生するエラーをアプリケーション全体で一律でログ出力したく、学習を兼ねて記事を書きます。

目次

  • やりたかったこと
  • http.Transport
  • Go のインターフェース
  • サンプル

やりたかったこと

アプリケーション全体を通してエラーハンドリングは実装していますが、エラー時のログ出力の抜け漏れを疑い、DynamoDB 利用時に発生するエラーを漏れなく全てログ出力したいです。
出力するエラーは、DynamoDB 利用側が起因となるエラー(例えば、DynamoDB へのリクエストのスループットが、アカウントのスループット上限を超過する時に発生する RequestLimitExceeded や、必須パラメータが指定されていない時に発生する ValidationException 等)とします。
DynamoDB 側でのエラー(Internal Server ErrorService Unavailable)や、ネットワーク起因によるエラーはログ出力しなくて良いです。

やったこと(やっていること)

DynamoDB の API 呼び出しに使用している HTTP クライアントに任意のエラー時にログ出力する処理を追加しました。
これにより、呼び出し元に手を入れずに対応することができます。

具体的には、AWS SDK が内部で使用する HTTP Client とそのプロパティである Transport の構造体を独自に定義します。
HTTP クライアントとしてい使用される http.Client 構造体には、HTTPリクエストのタイムアウト値を設定する Timeout、リダイレクト前に呼ばれる関数を定義する CheckRedirect、任意のCookieを設定する Jar 、そしてTransportがあります。
この Transport プロパティは RoundTrip メソッドを持つ http.RoundTripper インターフェースを実装します。
RoundTrip のシグネチャは以下のとおりです

RoundTrip(*Request) (*Response, error)

RoundTrip executes a single HTTP transaction, returning the Response for the request req.

https://pkg.go.dev/net/http#RoundTripper

RoundTrip メソッドは HTTP リクエストを引数として HTTP レスポンスと error を返します。
リトライ処理やロギング 等の任意の処理を、HTTP リクエストの前後に追加することができます。

AWS SDK v2 の内部では、各サービスの API リクエストで使用する http.Client, http.Transport 構造体を定義しています。
AWS SDK を使用しつつ、HTTP リクエストをキャプチャして特定の条件でログ出力する処理を実装するには、AWS SDK の利用側で RoundTrip メソッド、RoundTripper インターフェースを実装した Transport 構造体を定義・生成して、DynamoDB へのアクセスするためのクライアント作成時に設定します。

サンプル

DynamoDB の API リクエストにより発生する UserError 時にログ出力するサンプルです。
HTTP のステータスコードで判定してログ出力しています。

DynamoDB にアクセスするためのクライアント

// DynamoDB の API レスポンスの型定義
type LowLevelAPIErrorResponseBody struct {
	ErrType string `json:"__type"`
	Message string `json:"message"`
}

type CustomTransport struct {
	Transport http.RoundTripper
}

func NewCustomTransport() *CustomTransport {
	ct := &CustomTransport{
		Transport: &http.Transport{
  		TLSHandshakeTimeout: 1 * time.Second, // チューニングする HTTP リクエストの設定項目を定義
    },
	}
	return ct
}

// DynamoDB へのリクエストで発生する UserError をログに出力する対応
// DynamoDB の UserError メトリクスの切り分けのために使用する.
func (ct *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	res, err := ct.Transport.RoundTrip(req) // Transport に実装されている RoundTrip により HTTP リクエストの結果を取得
	if err != nil {
		return res, err
	}

  // HTTP StatusCode が 400 Bad Request の場合にログ出力する
	if res.StatusCode == http.StatusBadRequest {
    var data []byte
		if data, err := io.ReadAll(res.Body); err != nil {
			return res, err
		}
		errBody := &LowLevelAPIErrorResponseBody{}
		if err = json.Unmarshal(data, errBody); err != nil {
			return res, err
		}
		logger.Log.Infof("DynamoDB 400 Error = %v, %v", errBody.ErrType, errBody.Message)
	}

	return res, err
}

DynamoDB Client インスタンスの生成側

func NewClient(ctx context.Context) (*Client, error) {
	cfg, err := config.LoadDefaultConfig(
		ctx,
		config.WithRegion(region),
		config.WithHTTPClient(&http.Client{
			Transport: NewCustomTransport(), // ここで定義した構造体を使用
			Timeout:   timeout * time.Second,
		}),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to load dynamodb config err = %w", err)
	}
	return &Client{dynamodb.NewFromConfig(cfg)}, nil
}

感想

  • 型パズルというか Interface パズルを解きながら理解を進めているような感覚で、Go の Interface が完全に分からなくなった状態です。

参考とさせていただいた記事・ブログ・公式サイト

GitHubで編集を提案

Discussion