Carpe Diem

備忘録

gRPC の Unary Interceptor の基本的な使い方

概要

gRPCのInterceptorのClient側、Server側の基本的な使い方を紹介します。

環境

Interceptorの種類

以下の4つがあります。今回はUnaryの方の使い方を説明します。

サーバサイド

クライアントサイド

UnaryServerInterceptor

まずはよくあるサーバ側の設定です。

type UnaryServerInterceptor func(
  ctx context.Context,
  req interface{},
  info *UnaryServerInfo,
  handler UnaryHandler
) (resp interface{}, err error)

と定義されているので、この通りの関数を用意します。
それぞれ説明すると

変数 意味
ctx context。クライアントでセットされたmetadataとか入ってる
req クライアントからのrequest body
info server info。メソッド名とか分かる
handler クライアントから呼ばれたgRPCメソッド
resp response body。handlerを実行すると吐き出される

具体的な実装

サーバにきたリクエストのログ、レスポンスのログを吐き出すようなinterceptorを用意します。

func LogBodyInterceptor() grpc.UnaryServerInterceptor {
        return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
                // 1. request bodyをprint
                fmt.Printf("%+v\n", req)

                // 2. リクエストされたgrpcメソッドを実行
                resp, err := handler(ctx, req)
                if err != nil {
                        return nil, err
                }

                // 3. response bodyをprint
                fmt.Printf("%+v\n", resp)

                return res, nil
        }
}

ポイント

  • handlerの前に書けばgrpcメソッドを実行する前に処理される
  • handlerの後に書けばgrpcメソッドを実行した後に処理される

今回は分かりやすくこのように書いていますが、実際は3. response bodyをprintはエラー時もログを吐きたいと思うのでdeferで実行するのが普通ですね。

grpcサーバにセットする方法

こんな感じにNewServerする時の引数に与えます。

s := grpc.NewServer(
        grpc.UnaryInterceptor(LogBodyInterceptor()),
)

UnaryClientInterceptor

次にクライアント側の例です。

type UnaryClientInterceptor func(
  ctx context.Context,
  method string,
  req, reply interface{},
  cc *ClientConn,
  invoker UnaryInvoker,
  opts ...CallOption
) error

と定義されているので、この通りの関数を用意します。
それぞれ説明すると

変数 意味
ctx context。サーバに渡したいmetadataとかセットする
method gRPCのメソッド名
req request body
reply サーバからのresponse body
cc client connectio
invoker コールするgRPCメソッド。これを実行するとRPCが実行される
opts コール時のオプション。サーバからのresponse header, trailerはここから取得

具体的な実装

metadataにトークンを設定してサーバに送る例です。

func ClientIAuthnterceptor() grpc.UnaryClientInterceptor {
        return func(
                ctx context.Context,
                method string,
                req interface{},
                reply interface{},
                cc *grpc.ClientConn,
                invoker grpc.UnaryInvoker,
                opts ...grpc.CallOption,
        ) error {
                md := metadata.Pairs("x-auth-token", "hogehoge")
                ctx = metadata.NewOutgoingContext(ctx, md)

                err := invoker(ctx, method, req, reply, cc, opts...)
                return err
        }
}

ポイント

  • invokerの前に書けばgrpcメソッドをコールする前に処理される
  • invokerの後に書けばgrpcメソッドをコールした後に処理される

なので以下のような順になります。

func interceptor() {
  // 2
  invoker()
  // 3
}

func main() {
  // 1
  resp, err := someRPCCall()
  // 4
}

grpcクライアントにセットする方法

こんな感じにDialOptionとしてセットしていきます。

conn, err := grpc.Dial(
        address,
        grpc.WithInsecure(),
        grpc.WithUnaryInterceptor(ClientAuthInterceptor()),
)
if err != nil {
        log.Fatal(err)
}
defer conn.Close()

具体的な用途は?

  • ログ
  • 認証・認可
  • 特定のメソッドだけ何かさせたい時に
  • x-request-idなどトレーシングに

などでしょうか。metadataはHTTPでいうHeaderに近いので、同じような役割で使えると思います。

まとめ

Unary Interceptorの基本的な使い方を説明しました。