zerolog icon indicating copy to clipboard operation
zerolog copied to clipboard

gRPC interceptor?

Open ghost opened this issue 5 years ago • 2 comments

I have swapped out logrus in my application for zerolog, for performance gain, but I am using gRPC and I am unable to find zerolog for gRPC. I am not talking about the grpclog but gRPC interceptor logger: https://github.com/grpc-ecosystem/go-grpc-middleware/tree/master/logging

Is there some project that provides this implementation for zerolog?

I've built a very simple implementation inspired by the logrus from grpc middleware.

package ctx_zerolog

import (
	"context"
	"github.com/rs/zerolog"
)

type ctxKey struct{}

var key ctxKey

type wrapper struct {
	logger zerolog.Logger
}

func New(ctx context.Context, log zerolog.Logger) context.Context {
	return context.WithValue(ctx, key, &wrapper{log})
}

func Set(ctx context.Context, changes zerolog.Context) {
	l, ok := ctx.Value(key).(*wrapper)
	if !ok || l == nil {
		return
	}
	l.logger = changes.Logger()
}

func Get(ctx context.Context) zerolog.Context {
	l, ok := ctx.Value(key).(*wrapper)
	if !ok || l == nil {
		return zerolog.Nop().With()
	}
	return l.logger.With()
}
package grpc_zerolog

import (
	"fmt"
	"github.com/rs/zerolog"
	"google.golang.org/grpc/grpclog"
)

func ReplaceGrpcLogger(logger zerolog.Logger) {
	grpclog.SetLoggerV2(wrap(logger))
}

func wrap(l zerolog.Logger) *bridge {
	return &bridge{l}
}

type bridge struct {
	zerolog.Logger
}

func (b *bridge) Info(args ...interface{}) {
	b.Logger.Info().Msg(fmt.Sprint(args...))
}

func (b *bridge) Infoln(args ...interface{}) {
	b.Logger.Info().Msg(fmt.Sprint(args...) + "\n")
}

func (b *bridge) Infof(format string, args ...interface{}) {
	b.Logger.Info().Msgf(format, args...)
}

func (b *bridge) Warning(args ...interface{}) {
	b.Logger.Warn().Msg(fmt.Sprint(args...))
}

func (b *bridge) Warningln(args ...interface{}) {
	b.Logger.Warn().Msg(fmt.Sprint(args...) + "\n")
}

func (b *bridge) Warningf(format string, args ...interface{}) {
	b.Logger.Warn().Msgf(format, args...)
}

func (b *bridge) Error(args ...interface{}) {
	b.Logger.Error().Msg(fmt.Sprint(args...))
}

func (b *bridge) Errorln(args ...interface{}) {
	b.Logger.Error().Msg(fmt.Sprint(args...) + "\n")
}

func (b *bridge) Errorf(format string, args ...interface{}) {
	b.Logger.Error().Msgf(format, args...)
}

func (b *bridge) Fatal(args ...interface{}) {
	b.Logger.Fatal().Msg(fmt.Sprint(args...))
}

func (b *bridge) Fatalln(args ...interface{}) {
	b.Logger.Fatal().Msg(fmt.Sprint(args...) + "\n")
}

func (b *bridge) Fatalf(format string, args ...interface{}) {
	b.Logger.Fatal().Msgf(format, args...)
}

func (b *bridge) V(verbosity int) bool {
	// verbosity values:
	// 0 = info
	// 1 = warning
	// 2 = error
	// 3 = fatal
	switch b.GetLevel() {
	case zerolog.PanicLevel:
		return verbosity > 3
	case zerolog.FatalLevel:
		return verbosity == 3
	case zerolog.ErrorLevel:
		return verbosity == 2
	case zerolog.WarnLevel:
		return verbosity == 1
	case zerolog.InfoLevel:
		return verbosity == 0
	case zerolog.DebugLevel:
		return true
	case zerolog.TraceLevel:
		return true
	default:
		return false
	}
}

package grpc_zerolog

import (
	"context"
	"github.com/grpc-ecosystem/go-grpc-middleware"
	"github.com/rs/zerolog"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"ctx_zerolog"
	"path"
	"time"
)

func UnaryServerInterceptor(logger zerolog.Logger) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		start := time.Now()
		newCtx := initCtx(ctx, logger, info.FullMethod, start)
		resp, err := handler(newCtx, req)
		code := status.Code(err)
		level := ctl(code)
		with := ctx_zerolog.Get(newCtx).Str("grpc.code", code.String()).Dur("grpc.time_ms", time.Since(start))
		if err != nil {
			with = with.Err(err)
		}
		doLog(with.Logger(), level, "finished unary call")
		return resp, err
	}
}

func StreamServerInterceptor(logger zerolog.Logger) grpc.StreamServerInterceptor {
	return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		start := time.Now()
		newCtx := initCtx(stream.Context(), logger, info.FullMethod, start)
		wrapped := grpc_middleware.WrapServerStream(stream)
		wrapped.WrappedContext = newCtx
		err := handler(srv, wrapped)
		code := status.Code(err)
		level := ctl(code)
		with := ctx_zerolog.Get(newCtx).Str("grpc.code", code.String()).Dur("grpc.time_ms", time.Since(start))
		if err != nil {
			with = with.Err(err)
		}
		doLog(with.Logger(), level, "finished streaming call")
		return err
	}
}

func doLog(logger zerolog.Logger, level zerolog.Level, msg string) {
	switch level {
	case zerolog.DebugLevel:
		logger.Debug().Msg(msg)
	case zerolog.InfoLevel:
		logger.Info().Msg(msg)
	case zerolog.WarnLevel:
		logger.Warn().Msg(msg)
	case zerolog.ErrorLevel:
		logger.Error().Msg(msg)
	case zerolog.FatalLevel:
		logger.Fatal().Msg(msg)
	case zerolog.PanicLevel:
		logger.Panic().Msg(msg)
	}
}

func initCtx(ctx context.Context, logger zerolog.Logger, fullMethodString string, start time.Time) context.Context {
	service := path.Dir(fullMethodString)[1:]
	method := path.Base(fullMethodString)
	with := logger.With().
		Str("grpc.service", service).
		Str("grpc.method", method).
		Str("grpc.start_time", start.Format(time.RFC3339))
	if d, ok := ctx.Deadline(); ok {
		with = with.Str("grpc.request.deadline", d.Format(time.RFC3339))
	}
	return ctx_zerolog.New(ctx, with.Logger())
}

func ctl(code codes.Code) zerolog.Level {
	switch code {
	case codes.OK:
		return zerolog.DebugLevel
	case codes.Canceled:
		return zerolog.DebugLevel
	case codes.Unknown:
		return zerolog.InfoLevel
	case codes.InvalidArgument:
		return zerolog.DebugLevel
	case codes.DeadlineExceeded:
		return zerolog.InfoLevel
	case codes.NotFound:
		return zerolog.DebugLevel
	case codes.AlreadyExists:
		return zerolog.DebugLevel
	case codes.PermissionDenied:
		return zerolog.InfoLevel
	case codes.Unauthenticated:
		return zerolog.InfoLevel
	case codes.ResourceExhausted:
		return zerolog.DebugLevel
	case codes.FailedPrecondition:
		return zerolog.DebugLevel
	case codes.Aborted:
		return zerolog.DebugLevel
	case codes.OutOfRange:
		return zerolog.DebugLevel
	case codes.Unimplemented:
		return zerolog.WarnLevel
	case codes.Internal:
		return zerolog.WarnLevel
	case codes.Unavailable:
		return zerolog.WarnLevel
	case codes.DataLoss:
		return zerolog.WarnLevel
	default:
		return zerolog.InfoLevel
	}
}

ghost avatar Jan 23 '20 19:01 ghost

Hello! I've written some interceptors for my own use. It independent from go-grpc-miggleware, and it gives possibility to advance it without PRs to go-grpc-middleware, but together with it cannot use the testing framework, I'll will glade to have some ideas how to write the tests for gRPC interceptors. Welcome to use and contribute: https://github.com/pereslava/grpc_zerolog, Documentation at pkg.go.dev

pereslava avatar Feb 08 '21 06:02 pereslava

Take a look https://github.com/grpc-ecosystem/go-grpc-middleware/blob/main/interceptors/logging/examples/zerolog/example_test.go

zs-dima avatar Sep 30 '23 19:09 zs-dima