package grpc_zap import ( "context" "time" grpc_logging "github.com/grpc-ecosystem/go-grpc-middleware/logging" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" "go.uber.org/zap/zapcore" "google.golang.org/grpc/codes" ) var ( defaultOptions = &options{ levelFunc: DefaultCodeToLevel, shouldLog: grpc_logging.DefaultDeciderMethod, codeFunc: grpc_logging.DefaultErrorToCode, durationFunc: DefaultDurationToField, messageFunc: DefaultMessageProducer, } ) type options struct { levelFunc CodeToLevel shouldLog grpc_logging.Decider codeFunc grpc_logging.ErrorToCode durationFunc DurationToField messageFunc MessageProducer } func evaluateServerOpt(opts []Option) *options { optCopy := &options{} *optCopy = *defaultOptions optCopy.levelFunc = DefaultCodeToLevel for _, o := range opts { o(optCopy) } return optCopy } func evaluateClientOpt(opts []Option) *options { optCopy := &options{} *optCopy = *defaultOptions optCopy.levelFunc = DefaultClientCodeToLevel for _, o := range opts { o(optCopy) } return optCopy } type Option func(*options) // CodeToLevel function defines the mapping between gRPC return codes and interceptor log level. type CodeToLevel func(code codes.Code) zapcore.Level // DurationToField function defines how to produce duration fields for logging type DurationToField func(duration time.Duration) zapcore.Field // WithDecider customizes the function for deciding if the gRPC interceptor logs should log. func WithDecider(f grpc_logging.Decider) Option { return func(o *options) { o.shouldLog = f } } // WithLevels customizes the function for mapping gRPC return codes and interceptor log level statements. func WithLevels(f CodeToLevel) Option { return func(o *options) { o.levelFunc = f } } // WithCodes customizes the function for mapping errors to error codes. func WithCodes(f grpc_logging.ErrorToCode) Option { return func(o *options) { o.codeFunc = f } } // WithDurationField customizes the function for mapping request durations to Zap fields. func WithDurationField(f DurationToField) Option { return func(o *options) { o.durationFunc = f } } // WithMessageProducer customizes the function for message formation. func WithMessageProducer(f MessageProducer) Option { return func(o *options) { o.messageFunc = f } } // DefaultCodeToLevel is the default implementation of gRPC return codes and interceptor log level for server side. func DefaultCodeToLevel(code codes.Code) zapcore.Level { switch code { case codes.OK: return zap.InfoLevel case codes.Canceled: return zap.InfoLevel case codes.Unknown: return zap.ErrorLevel case codes.InvalidArgument: return zap.InfoLevel case codes.DeadlineExceeded: return zap.WarnLevel case codes.NotFound: return zap.InfoLevel case codes.AlreadyExists: return zap.InfoLevel case codes.PermissionDenied: return zap.WarnLevel case codes.Unauthenticated: return zap.InfoLevel // unauthenticated requests can happen case codes.ResourceExhausted: return zap.WarnLevel case codes.FailedPrecondition: return zap.WarnLevel case codes.Aborted: return zap.WarnLevel case codes.OutOfRange: return zap.WarnLevel case codes.Unimplemented: return zap.ErrorLevel case codes.Internal: return zap.ErrorLevel case codes.Unavailable: return zap.WarnLevel case codes.DataLoss: return zap.ErrorLevel default: return zap.ErrorLevel } } // DefaultClientCodeToLevel is the default implementation of gRPC return codes to log levels for client side. func DefaultClientCodeToLevel(code codes.Code) zapcore.Level { switch code { case codes.OK: return zap.DebugLevel case codes.Canceled: return zap.DebugLevel case codes.Unknown: return zap.InfoLevel case codes.InvalidArgument: return zap.DebugLevel case codes.DeadlineExceeded: return zap.InfoLevel case codes.NotFound: return zap.DebugLevel case codes.AlreadyExists: return zap.DebugLevel case codes.PermissionDenied: return zap.InfoLevel case codes.Unauthenticated: return zap.InfoLevel // unauthenticated requests can happen case codes.ResourceExhausted: return zap.DebugLevel case codes.FailedPrecondition: return zap.DebugLevel case codes.Aborted: return zap.DebugLevel case codes.OutOfRange: return zap.DebugLevel case codes.Unimplemented: return zap.WarnLevel case codes.Internal: return zap.WarnLevel case codes.Unavailable: return zap.WarnLevel case codes.DataLoss: return zap.WarnLevel default: return zap.InfoLevel } } // DefaultDurationToField is the default implementation of converting request duration to a Zap field. var DefaultDurationToField = DurationToTimeMillisField // DurationToTimeMillisField converts the duration to milliseconds and uses the key `grpc.time_ms`. func DurationToTimeMillisField(duration time.Duration) zapcore.Field { return zap.Float32("grpc.time_ms", durationToMilliseconds(duration)) } // DurationToDurationField uses a Duration field to log the request duration // and leaves it up to Zap's encoder settings to determine how that is output. func DurationToDurationField(duration time.Duration) zapcore.Field { return zap.Duration("grpc.duration", duration) } func durationToMilliseconds(duration time.Duration) float32 { return float32(duration.Nanoseconds()/1000) / 1000 } // MessageProducer produces a user defined log message type MessageProducer func(ctx context.Context, msg string, level zapcore.Level, code codes.Code, err error, duration zapcore.Field) // DefaultMessageProducer writes the default message func DefaultMessageProducer(ctx context.Context, msg string, level zapcore.Level, code codes.Code, err error, duration zapcore.Field) { // re-extract logger from newCtx, as it may have extra fields that changed in the holder. ctxzap.Extract(ctx).Check(level, msg).Write( zap.Error(err), zap.String("grpc.code", code.String()), duration, ) }