From f03aa0220c1cac46f3dc18ac8b34cbad0b9ea6ca Mon Sep 17 00:00:00 2001 From: Matthieu Vachon Date: Tue, 29 Sep 2026 15:10:34 -0400 Subject: [PATCH] Add server.WithCodeLevelFunc to override gRPC code-to-log-level mapping Standard server's grpc_zap interceptor always maps codes.ResourceExhausted to Info with no way to override it, which is noisy for services doing admission control (e.g. substreams tier2 backpressure refusals). Callers can now supply their own code-to-level mapping via server.Options; default behavior (package Verbosity) is unchanged when unset. --- CHANGELOG.md | 1 + server/options.go | 26 ++++++++++++++++++++++++++ server/standard/server.go | 16 +++++++++++++++- server/standard/server_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1bece..a8c1bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `List` to `server.HealthGRPCHandler` and `server/connectrpc.HealthGRPCHandler`, implementing the `grpc.health.v1.Health/List` RPC added to the `HealthServer` interface in gRPC-Go v1.82.0. Both return a single entry keyed by the empty service name, matching the server-wide health check these handlers expose. +- Add `server.WithCodeLevelFunc` to let callers of `dgrpc/server/standard` override the gRPC-code-to-zap-level mapping used for the request-completion log line, instead of only the global `standard.Verbosity` knob. Useful for services (e.g. ones doing admission control) where a code like `codes.ResourceExhausted` is routine and shouldn't default to `Info`. ### Changed diff --git a/server/options.go b/server/options.go index 2d219c3..63cdf41 100644 --- a/server/options.go +++ b/server/options.go @@ -10,7 +10,9 @@ import ( "connectrpc.com/connect" "github.com/rs/cors" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "google.golang.org/grpc" + "google.golang.org/grpc/codes" ) type Options struct { @@ -29,6 +31,11 @@ type Options struct { PostStreamInterceptors []grpc.StreamServerInterceptor ConnectExtraInterceptors []connect.Interceptor + // CodeLevelFunc, when set, overrides the default gRPC-code-to-zap-level mapping + // used by the `dgrpc/server/standard` server for its request-completion log line. + // See WithCodeLevelFunc. + CodeLevelFunc func(codes.Code) zapcore.Level + Registrator func(gs *grpc.Server) SecureTLSConfig *tls.Config @@ -308,6 +315,25 @@ func WithGRPCServerOptions(opts ...grpc.ServerOption) Option { } } +// WithCodeLevelFunc lets you override how gRPC status codes are mapped to zap log +// levels for the standard server's request-completion log line ("finished +// unary/streaming call with code X"). +// +// By default that mapping is controlled by the package-level `standard.Verbosity` +// variable, which applies the same code-to-level rules to every service using +// `dgrpc/server/standard`. A caller that gives specific meaning to a code — for +// example using `codes.ResourceExhausted` for routine admission-control +// backpressure — can use this option to log it at a different level without +// affecting other `dgrpc` users. +// +// **Important** Only taken into consideration by the `dgrpc/server/standard` +// server, ignored by all other server implementations. +func WithCodeLevelFunc(f func(codes.Code) zapcore.Level) Option { + return func(options *Options) { + options.CodeLevelFunc = f + } +} + // OverrideTraceID option can be used to force the generation of a new fresh trace ID // for every gRPC request entering the middleware func OverrideTraceID() Option { diff --git a/server/standard/server.go b/server/standard/server.go index 8cf3465..66398af 100644 --- a/server/standard/server.go +++ b/server/standard/server.go @@ -59,6 +59,9 @@ var notReadyResponse = map[string]interface{}{"is_ready": false} // For now, this controls server logging of gRCP code to zap level which // reduce some of the case into `INFO` level and some more like `OK` on `DEBUG` // level. +// +// A single server can instead override the whole gRPC-code-to-zap-level mapping, +// ignoring Verbosity entirely, via server.WithCodeLevelFunc. var Verbosity = 3 // StandardServer is actually a thin wrapper struct around an `*http.StandardServer` and a @@ -353,7 +356,7 @@ func newGRPCServer(options *server.Options) *grpc.Server { zopts := []grpc_zap.Option{ grpc_zap.WithDurationField(grpc_zap.DurationToTimeMillisField), grpc_zap.WithDecider(defaultLoggingDecider), - grpc_zap.WithLevels(defaultServerCodeLevel), + grpc_zap.WithLevels(codeLevelFunc(options)), } // Zap base server interceptor @@ -444,6 +447,17 @@ func defaultLoggingDecider(fullMethodName string, err error) bool { return true } +// codeLevelFunc returns the gRPC-code-to-zap-level mapping to use, preferring +// a caller-supplied override (see server.WithCodeLevelFunc) over the built-in +// Verbosity-based default. +func codeLevelFunc(options *server.Options) func(codes.Code) zapcore.Level { + if options.CodeLevelFunc != nil { + return options.CodeLevelFunc + } + + return defaultServerCodeLevel +} + func defaultServerCodeLevel(code codes.Code) zapcore.Level { // Our default is Verbosity 3 (which essentially means Info) and higher means more verbose // while lower means less verbose. diff --git a/server/standard/server_test.go b/server/standard/server_test.go index 9fc5f6b..65d7915 100644 --- a/server/standard/server_test.go +++ b/server/standard/server_test.go @@ -6,6 +6,9 @@ import ( "github.com/streamingfast/dgrpc/server" "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc/codes" ) func TestHealthOver_isActive(t *testing.T) { @@ -33,3 +36,24 @@ func TestHealthOver_isActive(t *testing.T) { }) } } + +func TestCodeLevelFunc(t *testing.T) { + t.Run("defaults to defaultServerCodeLevel when unset", func(t *testing.T) { + options := &server.Options{} + assert.Equal(t, defaultServerCodeLevel(codes.ResourceExhausted), codeLevelFunc(options)(codes.ResourceExhausted)) + }) + + t.Run("uses caller-supplied override when set", func(t *testing.T) { + options := &server.Options{ + CodeLevelFunc: func(code codes.Code) zapcore.Level { + if code == codes.ResourceExhausted { + return zap.DebugLevel + } + return zap.InfoLevel + }, + } + + assert.Equal(t, zap.DebugLevel, codeLevelFunc(options)(codes.ResourceExhausted)) + assert.Equal(t, zap.InfoLevel, codeLevelFunc(options)(codes.NotFound)) + }) +}