本番環境で使えるgRPC-Gateway【後編】

[VISITS Technologies Advent Calendar 2019](https://qiita.com/advent-calendar/2019/visits) 14日目は@istshが担当します。

この記事は[本番環境で使えるgRPC-Gateway【前編】](https://qiita.com/istsh/items/030933950f9fe8c82844)の続きで、gRPC Clientの実装についてまとめました。
また、前回と同様に、[サンプルコード](https://github.com/istsh/go-grpc-sample)を使って解説してきます。

# goファイルを生成
[前編 - goファイルを生成](https://qiita.com/istsh/items/030933950f9fe8c82844#go%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E7%94%9F%E6%88%90) で、`login.pb.gw.go`も生成されていると思います。
`--grpc-gateway_out`オプションを付けると、gRPC-Gateway用のgoファイルが生成されます。

# gRPC Clientの実装
一部抜粋しています。コード全体を見たい場合は[サンプルコード](https://github.com/istsh/go-grpc-sample)を見てください。

```app/cmd/client/main.go
func run() error {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	mux := runtime.NewServeMux(
		runtime.WithMetadata(RequestIDAnnotator),
		runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{OrigName: false, EmitDefaults: true}),
	)

	if err := registerServiceHandlers(ctx, mux); err != nil {
		return err
	}

	handler := handlers.CORS(
		handlers.AllowedOrigins([]string{"*"}),
		handlers.AllowedMethods([]string{http.MethodPost, http.MethodGet, http.MethodPut, http.MethodDelete}),
		handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept-Encoding", "Accept"}),
	)(mux)

	addr := ":8080"
	fmt.Printf("http server started on %s\n", addr)
	// Start HTTP server (and proxy calls to gRPC server endpoint)
	return http.ListenAndServe(addr, handler)
}

func main() {
	flag.Parse()
	defer glog.Flush()

	if err := run(); err != nil {
		glog.Fatal(err)
	}
}
```

## Annotator, Marshallerの設定
```go
mux := runtime.NewServeMux(
    runtime.WithMetadata(RequestIDAnnotator),
    runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{OrigName: false, EmitDefaults: true}),
)
```
前者は、HTTPリクエストヘッダーの`X-Request-ID`の値を取得し、gRPCのmetadataに詰めて返す関数です。
`RequestIDAnnotator`の実装については後述します。

後者は、JSON形式でレスポンスを返すように定義しています。
ここでは、`OrigName`と`EmitDefaults`に値を設定しています。

- OrigName
    - false: キャメルケースで返す。(具体的には、各フィールドのjson nameで返す)
    - true: protoで定義した名前で返す。
- EmitDefaults
    - 0やfalseなどのゼロ値になっているものもJSONで出力することができます。



## HTTPとgRPCの連携を追加する
```go
func registerServiceHandlers(ctx context.Context, mux *runtime.ServeMux) error {
	opts := grpcDialOptions()

	if err := pbv1.RegisterLoginServiceHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts); err != nil {
		return err
	}

	if err := pbv1.RegisterUserServiceHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts); err != nil {
		return err
	}

	return nil
}
```
HTTPリクエストをgRPC Serverに渡すための定義です。
`grpcDialOptions`の実装については後述します。

## CORSの設定
```go
handler := handlers.CORS(
	handlers.AllowedOrigins([]string{"*"}),
	handlers.AllowedMethods([]string{http.MethodPost, http.MethodGet, http.MethodPut, http.MethodDelete}),
	handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept-Encoding", "Accept"}),
)(mux)
```
[github.com/gorilla/handlers](https://github.com/gorilla/handlers)が、簡潔に定義できる関数を用意してくれているので採用しています。

## RequestIDAnnotatorの実装
```go
// RequestIDAnnotator takes requestID from http request header and sets it to metadata.
func RequestIDAnnotator(ctx context.Context, req *http.Request) metadata.MD {
	requestID := req.Header.Get(interceptor.XRequestIDKey)
	if requestID == "" {
		requestID = xid.New().String()
	}

	return metadata.New(map[string]string{
		interceptor.XRequestIDKey: requestID,
	})
}
```
前述の通り、HTTPリクエストヘッダーの`X-Request-ID`の値を取得し、gRPCのmetadataに詰めて返す関数です。
注意すべきなのは、metadataのキーです。
理由は、`metadata.New`の中でmapのキーが`strings.ToLower`で小文字に変換されるからです。
metadataから値を取る時に、`md[key]`と書く場合は気をつけてください。
ただし、`md.Get(key)`で取る場合は、Getの中で`strings.ToLower`を実行してくれるので、気にしなくて大丈夫です。

## grpcDialOptionsの実装
```app/cmd/client/main.go
func grpcDialOptions() []grpc.DialOption {
	l := logrus.New()
	l.SetFormatter(&logrus.JSONFormatter{})
	decider := func(ctx context.Context, fullMethodName string) bool {
		return true
	}
	startTimeFunc := func() time.Time {
		return time.Now()
	}
	durationFunc := func(startTime time.Time) time.Duration {
		return time.Now().Sub(startTime)
	}

	opts := []grpc.DialOption{
		grpc.WithInsecure(), // 証明書のエラーを無視する
		grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)), // gzip圧縮
		grpc.WithUnaryInterceptor(interceptor.PayloadUnaryClientInterceptor(logrus.NewEntry(l), decider, startTimeFunc, durationFunc)),
	}

	return opts
}
```

### PayloadUnaryClientInterceptor
```app/infrastructure/interceptor/payload_interceptor.go
// PayloadUnaryClientInterceptor returns a new unary client interceptor that logs the paylods of requests and responses.
func PayloadUnaryClientInterceptor(entry *logrus.Entry, decider grpc_logging.ClientPayloadLoggingDecider, startTimeFunc func() time.Time, durationFunc func(startTime time.Time) time.Duration) grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		if !decider(ctx, method) {
			return invoker(ctx, method, req, reply, cc, opts...)
		}

		startTime := startTimeFunc()
		err := invoker(ctx, method, req, reply, cc, opts...)
		fields, level := newLogFields(ctx, method, req, reply, durationFunc(startTime), err)

		levelLogf(
			entry.WithFields(fields),
			level,
			"client request/response payload logged as grpc.response.content")
		return err
	}
}

func newLogFields(ctx context.Context, method string, reqPbMsg, resPbMsg interface{}, duration time.Duration, err error) (logrus.Fields, logrus.Level) {
	code := grpc_logging.DefaultErrorToCode(err)
	level := grpc_logrus.DefaultCodeToLevel(code)
	durField, durVal := grpc_logrus.DefaultDurationToField(duration)

	fields := logrus.Fields{
		"system":       "grpc",
		"span.kind":    "client",
		"grpc.service": path.Dir(method)[1:],
		"grpc.method":  path.Base(method),
		"grpc.code":    code.String(),
		durField:       durVal,
	}

	if md, ok := metadata.FromOutgoingContext(ctx); ok {
		if values := md.Get("grpcgateway-user-agent"); len(values) > 0 {
			fields["user-agent"] = values[0]
		}
		if values := md.Get("x-forwarded-for"); len(values) > 0 {
			fields["remote_ip"] = values[0]
		}
		if values := md.Get(XRequestIDKey); len(values) > 0 {
			fields["request_id"] = values[0]
		}
	}
	if p, ok := reqPbMsg.(proto.Message); ok {
		fields["grpc.request.content"] = &jsonpbRequestMarshalleble{p}
	}
	if p, ok := resPbMsg.(proto.Message); ok {
		fields["grpc.response.content"] = &jsonpbResponseMarshalleble{p}
	}
	if err != nil {
		fields[logrus.ErrorKey] = err.Error()
	}

	return fields, level
}
```
(一部抜粋)

ここでは簡潔に説明するので、詳細は[コード](https://github.com/istsh/go-grpc-sample/blob/develop/app/infrastructure/interceptor/payload_interceptor.go)を見ていただきたいと思います。
このインターセプターは、各Serviceの実行後に、リクエスト/レスポンスパラメータを標準出力に出力するものです。
実行時間やエラーメッセージ、リクエストID、userAgentなども合わせて出力しています。
また、ログインなどでパスワードが送られてきたときにマスクさせる、といった処理もこのファイル内に実装してあります。

# まとめ
[【前編】](https://qiita.com/istsh/items/030933950f9fe8c82844)に続き、gRPC Clientの実装を紹介しました。
インターセプターは、[github.com/grpc-ecosystem/go-grpc-middleware](https://github.com/grpc-ecosystem/go-grpc-middleware)を参考に実装したので、場合によってはこのリポジトリの実装で事足りるかもしれません。
自分が探した限りでは、RequestIDやUserAgent、リクエストとレスポンスをまとめて標準出力するなどといったことを実現してくれるライブラリは見つけられなかったので実装することにしました。
同じ課題を抱えている人に少しでも参考になれば嬉しいです。

後日【gRPC-Gatewayでカスタムエラーを返す方法】についても投稿するのでお楽しみに！