Repository: grpc-ecosystem/go-grpc-middleware (go)
Issue: callback grpc_logrus.DefaultMessageProduce ignores TraceLevel
today if I use the option `grpc_logrus.WithLevels` and set one or more codes to be logged on `logrus.TraceLevel` I can't see the messages.

The root cause is `grpc_logrus.DefaultMessageProducer` that ignores TraceLevel

possible workaround: use `grpc_logrus.WithMessageProducer` to set a callback that can hand Trace
Pull request description: add support to trace on grpc_logrus.DefaultMessageProducer
today if I use the option `grpc_logrus.WithLevels` and set one or more codes to be logged on `logrus.TraceLevel` I can't see the messages.

The root cause is `grpc_logrus.DefaultMessageProducer` that ignores TraceLevel

possible workaround: use `grpc_logrus.WithMessageProducer` to set a callback that can hand Trace

this PR closes #546 546

Patch:
diff --git a/auth/doc.go b/auth/doc.go
index 0550f02..b8ae65b 100644
--- a/auth/doc.go
+++ b/auth/doc.go
@@ -4,7 +4,7 @@
 /*
 `grpc_auth` a generic server-side auth middleware for gRPC.
 
-Server Side Auth Middleware
+# Server Side Auth Middleware
 
 It allows for easy assertion of `:authorization` headers in gRPC calls, be it HTTP Basic auth, or
 OAuth2 Bearer tokens.
diff --git a/doc.go b/doc.go
index 718e100..a80b572 100644
--- a/doc.go
+++ b/doc.go
@@ -4,7 +4,7 @@
 /*
 `grpc_middleware` is a collection of gRPC middleware packages: interceptors, helpers and tools.
 
-Middleware
+# Middleware
 
 gRPC is a fantastic RPC middleware, which sees a lot of adoption in the Golang world. However, the
 upstream gRPC codebase is relatively bare bones.
@@ -13,7 +13,7 @@ This package, and most of its child packages provides commonly needed middleware
 client-side interceptors for retires, server-side interceptors for input validation and auth,
 functions for chaining said interceptors, metadata convenience methods and more.
 
-Chaining
+# Chaining
 
 By default, gRPC doesn't allow one to have more than one interceptor either on the client nor on
 the server side. `grpc_middleware` provides convenient chaining methods
@@ -42,7 +42,7 @@ These interceptors will be executed from left to right: monitoring and then retr
 
 The retry interceptor will call every interceptor that follows it whenever when a retry happens.
 
-Writing Your Own
+# Writing Your Own
 
 Implementing your own interceptor is pretty trivial: there are interfaces for that. But the interesting
 bit exposing common data to handlers (and other middleware), similarly to HTTP Middleware design.
diff --git a/go.mod b/go.mod
index f9123e0..07e96f8 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
 	golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
 	google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215 // indirect
 	google.golang.org/grpc v1.29.1
+	gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e // indirect
 )
 
 go 1.14
diff --git a/go.sum b/go.sum
index e7d8e4e..c801cb7 100644
--- a/go.sum
+++ b/go.sum
@@ -155,5 +155,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e h1:3i3ny04XV6HbZ2N1oIBw1UBYATHAOpo4tfTF83JM3Z0=
+gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/logging/logrus/options.go b/logging/logrus/options.go
index f6352c4..4ca8a3b 100644
--- a/logging/logrus/options.go
+++ b/logging/logrus/options.go
@@ -212,19 +212,6 @@ func DefaultMessageProducer(ctx context.Context, format string, level logrus.Lev
 	if err != nil {
 		fields[logrus.ErrorKey] = err
 	}
-	entry := ctxlogrus.Extract(ctx).WithContext(ctx).WithFields(fields)
-	switch level {
-	case logrus.DebugLevel:
-		entry.Debugf(format)
-	case logrus.InfoLevel:
-		entry.Infof(format)
-	case logrus.WarnLevel:
-		entry.Warningf(format)
-	case logrus.ErrorLevel:
-		entry.Errorf(format)
-	case logrus.FatalLevel:
-		entry.Fatalf(format)
-	case logrus.PanicLevel:
-		entry.Panicf(format)
-	}
+
+	ctxlogrus.Extract(ctx).WithContext(ctx).WithFields(fields).Logf(level, format)
 }
diff --git a/logging/zap/doc.go b/logging/zap/doc.go
index ffa6b5c..8441c1f 100644
--- a/logging/zap/doc.go
+++ b/logging/zap/doc.go
@@ -17,7 +17,6 @@ the full request/response payload needs to be written with care, this can signif
 
 ZAP can also be made as a backend for gRPC library internals. For that use `ReplaceGrpcLoggerV2`.
 
-
 *Server Interceptor*
 Below is a JSON formatted example of a log that would be logged by the server interceptor:
 
@@ -68,7 +67,6 @@ Below is a JSON formatted example of a log that would be logged by the payload i
 
 Note - due to implementation ZAP differs from Logrus in the "grpc.request.content" object by having an inner "msg" object.
 
-
 Please see examples and tests for examples of use.
 Please see settable_test.go for canonical integration through "zaptest" with golang testing infrastructure.
 */
diff --git a/ratelimit/doc.go b/ratelimit/doc.go
index 2589a1d..a5adb9d 100644
--- a/ratelimit/doc.go
+++ b/ratelimit/doc.go
@@ -1,7 +1,7 @@
 /*
 `ratelimit` a generic server-side ratelimit middleware for gRPC.
 
-Server Side Ratelimit Middleware
+# Server Side Ratelimit Middleware
 
 It allows to do grpc rate limit by your own rate limiter (e.g. token bucket, leaky bucket, etc.)
 
diff --git a/recovery/doc.go b/recovery/doc.go
index 2806de5..e324a2c 100644
--- a/recovery/doc.go
+++ b/recovery/doc.go
@@ -4,7 +4,7 @@
 /*
 `grpc_recovery` are interceptors that recover from gRPC handler panics.
 
-Server Side Recovery Middleware
+# Server Side Recovery Middleware
 
 By default a panic will be converted into a gRPC error with `code.Internal`.
 
diff --git a/retry/doc.go b/retry/doc.go
index f8ba719..da05ede 100644
--- a/retry/doc.go
+++ b/retry/doc.go
@@ -4,7 +4,7 @@
 /*
 `grpc_retry` provides client-side request retry logic for gRPC.
 
-Client-Side Request Retry Interceptor
+# Client-Side Request Retry Interceptor
 
 It allows for automatic retry, inside the generated gRPC code of requests based on the gRPC status
 of the reply. It supports unary (1:1), and server stream (1:n) requests.
@@ -12,7 +12,7 @@ of the reply. It supports unary (1:1), and server stream (1:n) requests.
 By default the interceptors *are disabled*, preventing accidental use of retries. You can easily
 override the number of retries (setting them to more than 0) with a `grpc.ClientOption`, e.g.:
 
- myclient.Ping(ctx, goodPing, grpc_retry.WithMax(5))
+	myclient.Ping(ctx, goodPing, grpc_retry.WithMax(5))
 
 Other default options are: retry on `ResourceExhausted` and `Unavailable` gRPC codes, use a 50ms
 linear backoff with 10% jitter.
diff --git a/tags/doc.go b/tags/doc.go
index 960638d..778bbdb 100644
--- a/tags/doc.go
+++ b/tags/doc.go
@@ -1,7 +1,7 @@
 /*
 `grpc_ctxtags` adds a Tag object to the context that can be used by other middleware to add context about a request.
 
-Request Context Tags
+# Request Context Tags
 
 Tags describe information about the request, and can be set and used by other middleware, or handlers. Tags are used
 for logging and tracing of requests. Tags are populated both upwards, *and* downwards in the interceptor-handler stack.
diff --git a/tags/fieldextractor.go b/tags/fieldextractor.go
index a4073ab..e6c5907 100644
--- a/tags/fieldextractor.go
+++ b/tags/fieldextractor.go
@@ -35,9 +35,9 @@ func CodeGenRequestFieldExtractor(fullMethod string, req interface{}) map[string
 // TagBasedRequestFieldExtractor is a function that relies on Go struct tags to export log fields from requests.
 // These are usually coming from a protoc-plugin, such as Gogo protobuf.
 //
-//  message Metadata {
-//     repeated string tags = 1 [ (gogoproto.moretags) = "log_field:\"meta_tags\"" ];
-//  }
+//	message Metadata {
+//	   repeated string tags = 1 [ (gogoproto.moretags) = "log_field:\"meta_tags\"" ];
+//	}
 //
 // The tagName is configurable using the tagName variable. Here it would be "log_field".
 func TagBasedRequestFieldExtractor(tagName string) RequestFieldExtractorFunc {
diff --git a/tracing/opentracing/doc.go b/tracing/opentracing/doc.go
index 7a58efc..e30d3c5 100644
--- a/tracing/opentracing/doc.go
+++ b/tracing/opentracing/doc.go
@@ -4,7 +4,7 @@
 /*
 `grpc_opentracing` adds OpenTracing
 
-OpenTracing Interceptors
+# OpenTracing Interceptors
 
 These are both client-side and server-side interceptors for OpenTracing. They are a provider-agnostic, with backends
 such as Zipkin, or Google Stackdriver Trace.
@@ -17,6 +17,5 @@ All server-side spans are tagged with grpc_ctxtags information.
 For more information see:
 http://opentracing.io/documentation/
 https://github.com/opentracing/specification/blob/master/semantic_conventions.md
-
 */
 package grpc_opentracing
diff --git a/validator/doc.go b/validator/doc.go
index 7205520..0059801 100644
--- a/validator/doc.go
+++ b/validator/doc.go
@@ -4,7 +4,7 @@
 /*
 `grpc_validator` is a generic request contents validator server-side middleware for gRPC.
 
-Request Validator Middleware
+# Request Validator Middleware
 
 Validating input is important, and hard. It also causes a lot of boilerplate code. This middleware
 checks for the existence of a `Validate` method on each of the messages of a gRPC request. This
@@ -16,7 +16,6 @@ While it is generic, it was intended to be used with https://github.com/mwitkow/
 a Go protocol buffers codegen plugin that creates the `Validate` methods (including nested messages)
 based on declarative options in the `.proto` files themselves. For example:
 
-
 	syntax = "proto3";
 	package validator.examples;
 	import "github.com/mwitkow/go-proto-validators/validator.proto";


Test patch:
diff --git a/logging/logrus/options_test.go b/logging/logrus/options_test.go
index 8df3489..8e4396a 100644
--- a/logging/logrus/options_test.go
+++ b/logging/logrus/options_test.go
@@ -1,13 +1,90 @@
 package grpc_logrus
 
 import (
+	"context"
+	"errors"
 	"testing"
 	"time"
 
+	"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
+	"github.com/sirupsen/logrus"
+	"github.com/sirupsen/logrus/hooks/test"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"google.golang.org/grpc/codes"
 )
 
 func TestDurationToTimeMillisField(t *testing.T) {
 	_, val := DurationToTimeMillisField(time.Microsecond * 100)
 	assert.Equal(t, val.(float32), float32(0.1), "sub millisecond values should be correct")
 }
+
+func TestDefaultMessageProducer(t *testing.T) {
+	t.Parallel()
+
+	errNotFound := errors.New("not found")
+	testcases := []struct {
+		label  string
+		format string
+		level  logrus.Level
+		code   codes.Code
+		err    error
+		fields logrus.Fields
+	}{
+		{
+			label:  "should emit info message without error",
+			format: "test",
+			level:  logrus.InfoLevel,
+			code:   codes.OK,
+			fields: logrus.Fields{
+				"foo": "bar",
+			},
+		},
+		{
+			label:  "should emit info message with error",
+			format: "test",
+			level:  logrus.InfoLevel,
+			code:   codes.NotFound,
+			err:    errNotFound,
+			fields: logrus.Fields{
+				"foo":           "bar",
+				logrus.ErrorKey: errNotFound,
+			},
+		},
+		{
+			label:  "should emit trace message without error",
+			format: "test",
+			level:  logrus.TraceLevel,
+			code:   codes.OK,
+			fields: logrus.Fields{
+				"foo": "bar",
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.label, func(t *testing.T) {
+			t.Parallel()
+
+			logger, hook := test.NewNullLogger()
+
+			logger.SetLevel(logrus.TraceLevel)
+
+			logrusEntry := logger.WithField("foo", "bar")
+
+			ctx := ctxlogrus.ToContext(context.Background(), logrusEntry)
+
+			DefaultMessageProducer(ctx, tc.format, tc.level, tc.code, tc.err, logrus.Fields{})
+
+			lastEntry := hook.LastEntry()
+
+			require.NotNil(t, lastEntry)
+
+			assert.Equal(t, "test", lastEntry.Message)
+			assert.Equal(t, tc.fields, lastEntry.Data)
+			assert.Equal(t, tc.level, lastEntry.Level)
+		})
+	}
+
+}
diff --git a/tracing/opentracing/interceptors_test.go b/tracing/opentracing/interceptors_test.go
index 7dc7cfa..0c45dce 100644
--- a/tracing/opentracing/interceptors_test.go
+++ b/tracing/opentracing/interceptors_test.go
@@ -337,4 +337,4 @@ func (jaegerFormatExtractor) Extract(carrier interface{}) (mocktracer.MockSpanCo
 		return rval, err
 	}
 	return rval, nil
-}
\ No newline at end of file
+}