ロギング

ecosystem の middleware にロギングあり

https://github.com/grpc-ecosystem/go-grpc-middleware/tree/master/logging/logrus

導入に詰まったところ

exsample_test.go には

var (
	logrusLogger *logrus.Logger
	customFunc   grpc_logrus.CodeToLevel
)

// Initialization shows a relatively complex initialization sequence.
func Example_initialization() {
	// Logrus entry is used, allowing pre-definition of certain fields by the user.
	logrusEntry := logrus.NewEntry(logrusLogger)
	// Shared options for the logger, with a custom gRPC code to log level function.
	opts := []grpc_logrus.Option{
		grpc_logrus.WithLevels(customFunc),
	}
	// Make sure that log statements internal to gRPC library are logged using the logrus Logger as well.
	grpc_logrus.ReplaceGrpcLogger(logrusEntry)
	// Create a server, make sure we put the grpc_ctxtags context before everything else.
	_ = grpc.NewServer(
		grpc_middleware.WithUnaryServerChain(
			grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
			grpc_logrus.UnaryServerInterceptor(logrusEntry, opts...),
		),
		grpc_middleware.WithStreamServerChain(
			grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
			grpc_logrus.StreamServerInterceptor(logrusEntry, opts...),
		),
	)
}

とあるけど, 当然 logrusLogger や customFunc は初期化が必要。

func の方はデフォルトのものを用意してくれてる。logger フォーマットとか level 分けとかは任意。

func init() {
	logrusLogger = logrus.New()
  if isDebuggable {
		logrusLogger.Level = logrus.DebugLevel
	}
	logrusLogger.Formatter = &logrus.JSONFormatter{
		FieldMap: logrus.FieldMap{
			logrus.FieldKeyTime:  "timestamp",
			logrus.FieldKeyLevel: "level",
			logrus.FieldKeyMsg:   "message",
		},
		TimestampFormat: time.RFC3339Nano,
	}
	logrusLogger.Out = os.Stdout
	codeToLevel = grpc_logrus.DefaultCodeToLevel
}

エラーハンドリング

単純に return err すると, status Unknown で返る。外部パッケージを扱う時のハンドリングはそれでいいかも。アプリ内でコンテキストを分けたい場合は以下。

// CharactorNotFoundError charactor not found
func CharactorNotFoundError(id uint64) error {
	return status.Errorf(codes.NotFound, "user not found [id: %d]", id)
}

// CharactorAlreadyExistsError charactor already exists
func CharactorAlreadyExistsError(id uint64) error {
	return status.Errorf(codes.AlreadyExists, "user already exists [id: %d]", id)
}

// RuntimeError internal server error
func RuntimeError(err error) error {
	return status.Error(codes.Internal, err.Error())
}

E2E

↓に書いてる通りで動く。ゴルーチンで grpc サーバーを立てて, client ダミーを作ってレスポンスを見れる。httptest より早くて楽ちん。