From ef6044e50d84a92caa7006201e1111c855eaa49a Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Tue, 3 Feb 2026 18:58:07 +0900 Subject: [PATCH 01/10] grpclog: add GRPC_GO_COMPONENT_LOG_LEVEL for controling component logs --- README.md | 22 +++ grpclog/component.go | 101 ++++++++++++- grpclog/component_test.go | 278 ++++++++++++++++++++++++++++++++++++ grpclog/grpclog.go | 23 ++- grpclog/internal/grpclog.go | 6 + grpclog/loggerv2.go | 83 ++++++++--- 6 files changed, 491 insertions(+), 22 deletions(-) create mode 100644 grpclog/component_test.go diff --git a/README.md b/README.md index f9a88d597ec4..70ac30afe539 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,28 @@ $ export GRPC_GO_LOG_VERBOSITY_LEVEL=99 $ export GRPC_GO_LOG_SEVERITY_LEVEL=info ``` +Additionally, you can configure log severity levels for individual components +using the `GRPC_GO_COMPONENT_LOG_LEVEL` environment variable. The format is a +comma-separated list of `component:LEVEL` pairs like this: + +```console +$ export GRPC_GO_COMPONENT_LOG_LEVEL=dns:ERROR,transport:WARNING +``` + +When both `GRPC_GO_LOG_SEVERITY_LEVEL` and `GRPC_GO_COMPONENT_LOG_LEVEL` are +set, component-specific settings take precedence over `GRPC_GO_LOG_SEVERITY_LEVEL` +for the specified components. Components not listed in +`GRPC_GO_COMPONENT_LOG_LEVEL` will use the `GRPC_GO_LOG_SEVERITY_LEVEL` setting. +For example: + +```console +$ export GRPC_GO_LOG_SEVERITY_LEVEL=ERROR +$ export GRPC_GO_COMPONENT_LOG_LEVEL=dns:INFO +``` + +In this case, the `dns` component will log at `INFO` level, while all other +components will log at `ERROR` level. + ### The RPC failed with error `"code = Unavailable desc = transport is closing"` This error means the connection the RPC is using was closed, and there are many diff --git a/grpclog/component.go b/grpclog/component.go index f1ae080dcb81..efc35015d6fc 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -20,33 +20,78 @@ package grpclog import ( "fmt" + "strings" ) +type severityLevel int + +const ( + severityInfo severityLevel = iota + severityWarning + severityError + severityFatal +) + +func parseComponentLogLevel(logLevel string) map[string]severityLevel { + if logLevel == "" { + return nil + } + + logLevels := make(map[string]severityLevel) + for part := range strings.SplitSeq(logLevel, ",") { + kv := strings.Split(part, ":") + if len(kv) < 2 { + continue + } + component := kv[0] + if component == "" { + continue + } + level := kv[1] + switch level { + case "INFO", "info": + logLevels[component] = severityInfo + case "WARNING", "warning": + logLevels[component] = severityWarning + case "ERROR", "error": + logLevels[component] = severityError + default: + logLevels[component] = severityFatal + } + } + return logLevels +} + // componentData records the settings for a component. type componentData struct { name string + + infoDepth func(depth int, args ...any) + warningDepth func(depth int, args ...any) + errorDepth func(depth int, args ...any) + fatalDepth func(depth int, args ...any) } var cache = map[string]*componentData{} func (c *componentData) InfoDepth(depth int, args ...any) { args = append([]any{"[" + string(c.name) + "]"}, args...) - InfoDepth(depth+1, args...) + c.infoDepth(depth+1, args...) } func (c *componentData) WarningDepth(depth int, args ...any) { args = append([]any{"[" + string(c.name) + "]"}, args...) - WarningDepth(depth+1, args...) + c.warningDepth(depth+1, args...) } func (c *componentData) ErrorDepth(depth int, args ...any) { args = append([]any{"[" + string(c.name) + "]"}, args...) - ErrorDepth(depth+1, args...) + c.errorDepth(depth+1, args...) } func (c *componentData) FatalDepth(depth int, args ...any) { args = append([]any{"[" + string(c.name) + "]"}, args...) - FatalDepth(depth+1, args...) + c.fatalDepth(depth+1, args...) } func (c *componentData) Info(args ...any) { @@ -101,6 +146,9 @@ func (c *componentData) V(l int) bool { return V(l) } +func noopDepth(_ int, _ ...any) { +} + // Component creates a new component and returns it for logging. If a component // with the name already exists, nothing will be created and it will be // returned. SetLoggerV2 will panic if it is called with a logger created by @@ -109,7 +157,50 @@ func Component(componentName string) DepthLoggerV2 { if cData, ok := cache[componentName]; ok { return cData } - c := &componentData{componentName} + c := &componentData{ + name: componentName, + infoDepth: InfoDepth, + warningDepth: WarningDepth, + errorDepth: ErrorDepth, + fatalDepth: FatalDepth, + } + + if level, ok := componentLogLevels[componentName]; ok { + c.fatalDepth = func(depth int, args ...any) { + componentFatalDepth(depth+1, args...) + } + switch level { + case severityInfo: + c.infoDepth = func(depth int, args ...any) { + componentInfoDepth(depth+1, args...) + } + c.warningDepth = func(depth int, args ...any) { + componentWarningDepth(depth+1, args...) + } + c.errorDepth = func(depth int, args ...any) { + componentErrorDepth(depth+1, args...) + } + case severityWarning: + c.infoDepth = noopDepth + c.warningDepth = func(depth int, args ...any) { + componentWarningDepth(depth+1, args...) + } + c.errorDepth = func(depth int, args ...any) { + componentErrorDepth(depth+1, args...) + } + case severityError: + c.infoDepth = noopDepth + c.warningDepth = noopDepth + c.errorDepth = func(depth int, args ...any) { + componentErrorDepth(depth+1, args...) + } + case severityFatal: + c.infoDepth = noopDepth + c.warningDepth = noopDepth + c.errorDepth = noopDepth + } + } + cache[componentName] = c return c } diff --git a/grpclog/component_test.go b/grpclog/component_test.go new file mode 100644 index 000000000000..23edc828cdc2 --- /dev/null +++ b/grpclog/component_test.go @@ -0,0 +1,278 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package grpclog + +import ( + "bytes" + "slices" + "strings" + "testing" + + "google.golang.org/grpc/grpclog/internal" +) + +func TestParseComponentLogLevel(t *testing.T) { + tests := []struct { + name string + input string + want map[string]severityLevel + }{ + { + name: "returns nil for empty input", + input: "", + want: nil, + }, + { + name: "parses single component with INFO level", + input: "dns:INFO", + want: map[string]severityLevel{ + "dns": severityInfo, + }, + }, + { + name: "parses single component with WARNING level", + input: "xds:WARNING", + want: map[string]severityLevel{ + "xds": severityWarning, + }, + }, + { + name: "parses single component with ERROR level", + input: "authz:ERROR", + want: map[string]severityLevel{ + "authz": severityError, + }, + }, + { + name: "all-lowercase level names are recognized", + input: "dns:error,xds:warning,authz:info", + want: map[string]severityLevel{ + "dns": severityError, + "xds": severityWarning, + "authz": severityInfo, + }, + }, + { + name: "mixed-case level names are treated as unknown and default to FATAL", + input: "dns:Error,xds:Warning,authz:Info", + want: map[string]severityLevel{ + "dns": severityFatal, + "xds": severityFatal, + "authz": severityFatal, + }, + }, + { + name: "parses multiple comma-separated components with different levels", + input: "dns:ERROR,xds:WARNING,authz:INFO", + want: map[string]severityLevel{ + "dns": severityError, + "xds": severityWarning, + "authz": severityInfo, + }, + }, + { + name: "entry without colon separator is skipped", + input: "dns:ERROR,invalid,xds:WARNING", + want: map[string]severityLevel{ + "dns": severityError, + "xds": severityWarning, + }, + }, + { + name: "entry with empty component name before colon is skipped", + input: ":ERROR,dns:WARNING", + want: map[string]severityLevel{ + "dns": severityWarning, + }, + }, + { + name: "unrecognized level name defaults to FATAL", + input: "dns:UNKNOWN,xds:ERROR", + want: map[string]severityLevel{ + "dns": severityFatal, + "xds": severityError, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseComponentLogLevel(tt.input) + if tt.want == nil { + if got != nil { + t.Errorf("got %v for input %q, want nil", got, tt.input) + } + return + } + if len(got) != len(tt.want) { + t.Errorf("got %v for input %q, want %v", got, tt.input, tt.want) + return + } + for k, v := range tt.want { + if gv, ok := got[k]; !ok || gv != v { + t.Errorf("got %v for key %q in input %q, want %v", gv, k, tt.input, v) + } + } + }) + } +} + +func TestLogger(t *testing.T) { + tests := []struct { + name string + componentLogLevel string + logSeverityLevel string + components []string + want []string + }{ + { + name: "empty GRPC_GO_LOG_SEVERITY_LEVEL and GRPC_GO_COMPONENT_LOG_LEVEL defaults to ERROR for all logs", + componentLogLevel: "", + logSeverityLevel: "", + components: []string{"dns", "xds", "authz", "balancer"}, + want: []string{ + "ERROR: [dns] dns-error", + "ERROR: [xds] xds-error", + "ERROR: [authz] authz-error", + "ERROR: [balancer] balancer-error", + }, + }, + { + name: "INFO GRPC_GO_LOG_SEVERITY_LEVEL and empty GRPC_GO_COMPONENT_LOG_LEVEL logs all levels", + componentLogLevel: "", + logSeverityLevel: "INFO", + components: []string{"dns", "xds", "authz", "balancer"}, + want: []string{ + "INFO: [dns] dns-info", + "WARNING: [dns] dns-warning", + "ERROR: [dns] dns-error", + "INFO: [xds] xds-info", + "WARNING: [xds] xds-warning", + "ERROR: [xds] xds-error", + "INFO: [authz] authz-info", + "WARNING: [authz] authz-warning", + "ERROR: [authz] authz-error", + "INFO: [balancer] balancer-info", + "WARNING: [balancer] balancer-warning", + "ERROR: [balancer] balancer-error", + }, + }, + { + name: "GRPC_GO_COMPONENT_LOG_LEVEL overrides ERROR GRPC_GO_LOG_SEVERITY_LEVEL for configured components", + componentLogLevel: "dns:INFO,xds:WARNING,authz:ERROR", + logSeverityLevel: "ERROR", + components: []string{"dns", "xds", "authz", "balancer"}, + want: []string{ + "INFO: [dns] dns-info", + "WARNING: [dns] dns-warning", + "ERROR: [dns] dns-error", + "WARNING: [xds] xds-warning", + "ERROR: [xds] xds-error", + "ERROR: [authz] authz-error", + "ERROR: [balancer] balancer-error", + }, + }, + { + name: "GRPC_GO_COMPONENT_LOG_LEVEL overrides INFO GRPC_GO_LOG_SEVERITY_LEVEL for configured components", + componentLogLevel: "dns:INFO,xds:WARNING,authz:ERROR", + logSeverityLevel: "INFO", + components: []string{"dns", "xds", "authz", "balancer"}, + want: []string{ + "INFO: [dns] dns-info", + "WARNING: [dns] dns-warning", + "ERROR: [dns] dns-error", + "WARNING: [xds] xds-warning", + "ERROR: [xds] xds-error", + "ERROR: [authz] authz-error", + "INFO: [balancer] balancer-info", + "WARNING: [balancer] balancer-warning", + "ERROR: [balancer] balancer-error", + }, + }, + { + name: "empty GRPC_GO_LOG_SEVERITY_LEVEL with GRPC_GO_COMPONENT_LOG_LEVEL defaults to ERROR for unconfigured components", + componentLogLevel: "dns:INFO,xds:WARNING,authz:ERROR", + logSeverityLevel: "", + components: []string{"dns", "xds", "authz", "balancer"}, + want: []string{ + "INFO: [dns] dns-info", + "WARNING: [dns] dns-warning", + "ERROR: [dns] dns-error", + "WARNING: [xds] xds-warning", + "ERROR: [xds] xds-error", + "ERROR: [authz] authz-error", + "ERROR: [balancer] balancer-error", + }, + }, + { + name: "FATAL GRPC_GO_COMPONENT_LOG_LEVEL suppresses all non-fatal logs", + componentLogLevel: "dns:FATAL", + logSeverityLevel: "INFO", + components: []string{"dns", "balancer"}, + want: []string{ + "INFO: [balancer] balancer-info", + "WARNING: [balancer] balancer-warning", + "ERROR: [balancer] balancer-error", + }, + }, + } + originalComponentLogLevels := componentLogLevels + originalLoggerV2 := internal.LoggerV2Impl + originalDepthLoggerV2 := internal.DepthLoggerV2Impl + originalComponentLoggerV2 := internal.ComponentLoggerV2Impl + originalComponentDepthLoggerV2 := internal.ComponentDepthLoggerV2Impl + originalCache := cache + + t.Cleanup(func() { + componentLogLevels = originalComponentLogLevels + internal.LoggerV2Impl = originalLoggerV2 + internal.DepthLoggerV2Impl = originalDepthLoggerV2 + internal.ComponentLoggerV2Impl = originalComponentLoggerV2 + internal.ComponentDepthLoggerV2Impl = originalComponentDepthLoggerV2 + cache = originalCache + }) + + const logTimestampPrefixLen = len("2006/01/02 15:04:05 ") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cache = map[string]*componentData{} + + var buf bytes.Buffer + initLogger(&buf, tt.logSeverityLevel, "", "", tt.componentLogLevel) + + for _, name := range tt.components { + logger := Component(name) + logger.Infof("%s-info", name) + logger.Warningf("%s-warning", name) + logger.Errorf("%s-error", name) + } + + var got []string + for line := range strings.SplitSeq(buf.String(), "\n") { + if line == "" { + continue + } + got = append(got, line[logTimestampPrefixLen:]) + } + if !slices.Equal(got, tt.want) { + t.Errorf("got %v, want %v", got, tt.want) + } + }) + } +} diff --git a/grpclog/grpclog.go b/grpclog/grpclog.go index db320105e64e..b07835e49209 100644 --- a/grpclog/grpclog.go +++ b/grpclog/grpclog.go @@ -21,16 +21,37 @@ // In the default logger, severity level can be set by environment variable // GRPC_GO_LOG_SEVERITY_LEVEL, verbosity level can be set by // GRPC_GO_LOG_VERBOSITY_LEVEL. +// +// Per-component severity level can be set by GRPC_GO_COMPONENT_LOG_LEVEL with +// format "component1:LEVEL,component2:LEVEL" (e.g., "dns:error,xds:warning"). +// Component-specific settings take precedence over GRPC_GO_LOG_SEVERITY_LEVEL +// for that component. package grpclog import ( + "io" "os" "google.golang.org/grpc/grpclog/internal" ) +var componentLogLevels map[string]severityLevel + func init() { - SetLoggerV2(newLoggerV2()) + initLogger( + os.Stderr, + os.Getenv("GRPC_GO_LOG_SEVERITY_LEVEL"), + os.Getenv("GRPC_GO_LOG_VERBOSITY_LEVEL"), + os.Getenv("GRPC_GO_LOG_FORMATTER"), + os.Getenv("GRPC_GO_COMPONENT_LOG_LEVEL"), + ) +} + +func initLogger(w io.Writer, logSeverityLevel, logVerbosityLevel, logFormatter, componentLogLevel string) { + componentLogLevels = parseComponentLogLevel(componentLogLevel) + config := loggerV2Config(logVerbosityLevel, logFormatter) + setLoggerV2(newLoggerV2(w, config, logSeverityLevel)) + setComponentLoggerV2(newComponentLoggerV2(w, config)) } // V reports whether verbosity level l is at least the requested verbose level. diff --git a/grpclog/internal/grpclog.go b/grpclog/internal/grpclog.go index 59c03bc14c2a..cc91805d5523 100644 --- a/grpclog/internal/grpclog.go +++ b/grpclog/internal/grpclog.go @@ -24,3 +24,9 @@ var LoggerV2Impl LoggerV2 // DepthLoggerV2Impl is the logger used for the depth log functions. var DepthLoggerV2Impl DepthLoggerV2 + +// ComponentLoggerV2Impl is the logger used for non-depth per-component log functions. +var ComponentLoggerV2Impl LoggerV2 + +// ComponentDepthLoggerV2Impl is the logger used for depth per-component log functions. +var ComponentDepthLoggerV2Impl DepthLoggerV2 diff --git a/grpclog/loggerv2.go b/grpclog/loggerv2.go index 892dc13d164b..2746ef9e4bde 100644 --- a/grpclog/loggerv2.go +++ b/grpclog/loggerv2.go @@ -30,6 +30,39 @@ import ( // LoggerV2 does underlying logging work for grpclog. type LoggerV2 internal.LoggerV2 +func componentInfoDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.InfoDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Infoln(args...) + } +} + +func componentWarningDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.WarningDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Warningln(args...) + } +} + +func componentErrorDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.ErrorDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Errorln(args...) + } +} + +func componentFatalDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.FatalDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Fatalln(args...) + } + os.Exit(1) +} + // SetLoggerV2 sets logger that is used in grpc to a V2 logger. // Not mutex-protected, should be called before any gRPC functions. func SetLoggerV2(l LoggerV2) { @@ -38,6 +71,8 @@ func SetLoggerV2(l LoggerV2) { } internal.LoggerV2Impl = l internal.DepthLoggerV2Impl, _ = l.(internal.DepthLoggerV2) + internal.ComponentLoggerV2Impl = l + internal.ComponentDepthLoggerV2Impl, _ = l.(internal.DepthLoggerV2) } // NewLoggerV2 creates a loggerV2 with the provided writers. @@ -55,35 +90,51 @@ func NewLoggerV2WithVerbosity(infoW, warningW, errorW io.Writer, v int) LoggerV2 return internal.NewLoggerV2(infoW, warningW, errorW, internal.LoggerV2Config{Verbosity: v}) } +func loggerV2Config(level, formatter string) internal.LoggerV2Config { + var v int + if vl, err := strconv.Atoi(level); err == nil { + v = vl + } + + jsonFormat := strings.EqualFold(formatter, "json") + + return internal.LoggerV2Config{ + Verbosity: v, + FormatJSON: jsonFormat, + } +} + // newLoggerV2 creates a loggerV2 to be used as default logger. // All logs are written to stderr. -func newLoggerV2() LoggerV2 { +func newLoggerV2(w io.Writer, config internal.LoggerV2Config, level string) LoggerV2 { errorW := io.Discard warningW := io.Discard infoW := io.Discard - logLevel := os.Getenv("GRPC_GO_LOG_SEVERITY_LEVEL") - switch logLevel { + switch level { case "", "ERROR", "error": // If env is unset, set level to ERROR. - errorW = os.Stderr + errorW = w case "WARNING", "warning": - warningW = os.Stderr + warningW = w case "INFO", "info": - infoW = os.Stderr + infoW = w } - var v int - vLevel := os.Getenv("GRPC_GO_LOG_VERBOSITY_LEVEL") - if vl, err := strconv.Atoi(vLevel); err == nil { - v = vl - } + return internal.NewLoggerV2(infoW, warningW, errorW, config) +} - jsonFormat := strings.EqualFold(os.Getenv("GRPC_GO_LOG_FORMATTER"), "json") +func newComponentLoggerV2(w io.Writer, config internal.LoggerV2Config) LoggerV2 { + return internal.NewLoggerV2(w, io.Discard, io.Discard, config) +} - return internal.NewLoggerV2(infoW, warningW, errorW, internal.LoggerV2Config{ - Verbosity: v, - FormatJSON: jsonFormat, - }) +func setLoggerV2(l LoggerV2) { + internal.LoggerV2Impl = l + internal.DepthLoggerV2Impl, _ = l.(internal.DepthLoggerV2) +} + +func setComponentLoggerV2(l LoggerV2) { + internal.ComponentLoggerV2Impl = l + internal.ComponentDepthLoggerV2Impl, _ = l.(internal.DepthLoggerV2) } // DepthLoggerV2 logs at a specified call frame. If a LoggerV2 also implements From 324a3947f6461ffbfbf55c2fb8f257f880b9ab41 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Tue, 24 Feb 2026 17:03:10 +0900 Subject: [PATCH 02/10] grpclog: wrap long comment lines in grpclog.go --- grpclog/internal/grpclog.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/grpclog/internal/grpclog.go b/grpclog/internal/grpclog.go index cc91805d5523..1c522c93f392 100644 --- a/grpclog/internal/grpclog.go +++ b/grpclog/internal/grpclog.go @@ -25,8 +25,10 @@ var LoggerV2Impl LoggerV2 // DepthLoggerV2Impl is the logger used for the depth log functions. var DepthLoggerV2Impl DepthLoggerV2 -// ComponentLoggerV2Impl is the logger used for non-depth per-component log functions. +// ComponentLoggerV2Impl is the logger used for non-depth +// per-component log functions. var ComponentLoggerV2Impl LoggerV2 -// ComponentDepthLoggerV2Impl is the logger used for depth per-component log functions. +// ComponentDepthLoggerV2Impl is the logger used for depth +// per-component log functions. var ComponentDepthLoggerV2Impl DepthLoggerV2 From c0d5868ef6af76457194de31eaf4cef1fe22e5f6 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Tue, 24 Feb 2026 20:52:23 +0900 Subject: [PATCH 03/10] grpclog: rename parseComponentLogLevel to parseComponentLogLevels --- grpclog/component.go | 2 +- grpclog/component_test.go | 2 +- grpclog/grpclog.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grpclog/component.go b/grpclog/component.go index efc35015d6fc..04d8b89fad53 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -32,7 +32,7 @@ const ( severityFatal ) -func parseComponentLogLevel(logLevel string) map[string]severityLevel { +func parseComponentLogLevels(logLevel string) map[string]severityLevel { if logLevel == "" { return nil } diff --git a/grpclog/component_test.go b/grpclog/component_test.go index 23edc828cdc2..82e3b2a0c48b 100644 --- a/grpclog/component_test.go +++ b/grpclog/component_test.go @@ -112,7 +112,7 @@ func TestParseComponentLogLevel(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := parseComponentLogLevel(tt.input) + got := parseComponentLogLevels(tt.input) if tt.want == nil { if got != nil { t.Errorf("got %v for input %q, want nil", got, tt.input) diff --git a/grpclog/grpclog.go b/grpclog/grpclog.go index b07835e49209..b37ff34c0d26 100644 --- a/grpclog/grpclog.go +++ b/grpclog/grpclog.go @@ -48,7 +48,7 @@ func init() { } func initLogger(w io.Writer, logSeverityLevel, logVerbosityLevel, logFormatter, componentLogLevel string) { - componentLogLevels = parseComponentLogLevel(componentLogLevel) + componentLogLevels = parseComponentLogLevels(componentLogLevel) config := loggerV2Config(logVerbosityLevel, logFormatter) setLoggerV2(newLoggerV2(w, config, logSeverityLevel)) setComponentLoggerV2(newComponentLoggerV2(w, config)) From 5eda13160cacea0e871953ed54ae5f1e1d2d3a02 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Mon, 2 Mar 2026 14:45:15 +0900 Subject: [PATCH 04/10] grpclog: add mutex to protect component cache from concurrent access --- grpclog/component.go | 16 +++++++++++++--- grpclog/component_test.go | 6 +++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/grpclog/component.go b/grpclog/component.go index 04d8b89fad53..0b589a0e62eb 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -21,6 +21,7 @@ package grpclog import ( "fmt" "strings" + "sync" ) type severityLevel int @@ -72,7 +73,14 @@ type componentData struct { fatalDepth func(depth int, args ...any) } -var cache = map[string]*componentData{} +type componentCache struct { + mu sync.Mutex + data map[string]*componentData +} + +var cache = componentCache{ + data: map[string]*componentData{}, +} func (c *componentData) InfoDepth(depth int, args ...any) { args = append([]any{"[" + string(c.name) + "]"}, args...) @@ -154,7 +162,9 @@ func noopDepth(_ int, _ ...any) { // returned. SetLoggerV2 will panic if it is called with a logger created by // Component. func Component(componentName string) DepthLoggerV2 { - if cData, ok := cache[componentName]; ok { + cache.mu.Lock() + defer cache.mu.Unlock() + if cData, ok := cache.data[componentName]; ok { return cData } c := &componentData{ @@ -201,6 +211,6 @@ func Component(componentName string) DepthLoggerV2 { } } - cache[componentName] = c + cache.data[componentName] = c return c } diff --git a/grpclog/component_test.go b/grpclog/component_test.go index 82e3b2a0c48b..1206824f3252 100644 --- a/grpclog/component_test.go +++ b/grpclog/component_test.go @@ -236,7 +236,7 @@ func TestLogger(t *testing.T) { originalDepthLoggerV2 := internal.DepthLoggerV2Impl originalComponentLoggerV2 := internal.ComponentLoggerV2Impl originalComponentDepthLoggerV2 := internal.ComponentDepthLoggerV2Impl - originalCache := cache + originalCache := cache.data t.Cleanup(func() { componentLogLevels = originalComponentLogLevels @@ -244,14 +244,14 @@ func TestLogger(t *testing.T) { internal.DepthLoggerV2Impl = originalDepthLoggerV2 internal.ComponentLoggerV2Impl = originalComponentLoggerV2 internal.ComponentDepthLoggerV2Impl = originalComponentDepthLoggerV2 - cache = originalCache + cache.data = originalCache }) const logTimestampPrefixLen = len("2006/01/02 15:04:05 ") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cache = map[string]*componentData{} + cache.data = map[string]*componentData{} var buf bytes.Buffer initLogger(&buf, tt.logSeverityLevel, "", "", tt.componentLogLevel) From 1b7e50f017bd626c4daa2370bfea761721741352 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Mon, 2 Mar 2026 15:07:38 +0900 Subject: [PATCH 05/10] grpclog: remove unused parameter names in noopDepth --- grpclog/component.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpclog/component.go b/grpclog/component.go index 0b589a0e62eb..3735412913eb 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -154,7 +154,7 @@ func (c *componentData) V(l int) bool { return V(l) } -func noopDepth(_ int, _ ...any) { +func noopDepth(int, ...any) { } // Component creates a new component and returns it for logging. If a component From 76662e9bd9a8770fe8902e10c8fdbe14428bef71 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Mon, 2 Mar 2026 17:07:47 +0900 Subject: [PATCH 06/10] grpclog: move logging documentation from README to package doc comment --- README.md | 31 ++----------------------------- grpclog/grpclog.go | 32 ++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 70ac30afe539..274f25d5793b 100644 --- a/README.md +++ b/README.md @@ -71,35 +71,8 @@ Please update to the latest version of gRPC-Go using ### How to turn on logging -The default logger is controlled by environment variables. Turn everything on -like this: - -```console -$ export GRPC_GO_LOG_VERBOSITY_LEVEL=99 -$ export GRPC_GO_LOG_SEVERITY_LEVEL=info -``` - -Additionally, you can configure log severity levels for individual components -using the `GRPC_GO_COMPONENT_LOG_LEVEL` environment variable. The format is a -comma-separated list of `component:LEVEL` pairs like this: - -```console -$ export GRPC_GO_COMPONENT_LOG_LEVEL=dns:ERROR,transport:WARNING -``` - -When both `GRPC_GO_LOG_SEVERITY_LEVEL` and `GRPC_GO_COMPONENT_LOG_LEVEL` are -set, component-specific settings take precedence over `GRPC_GO_LOG_SEVERITY_LEVEL` -for the specified components. Components not listed in -`GRPC_GO_COMPONENT_LOG_LEVEL` will use the `GRPC_GO_LOG_SEVERITY_LEVEL` setting. -For example: - -```console -$ export GRPC_GO_LOG_SEVERITY_LEVEL=ERROR -$ export GRPC_GO_COMPONENT_LOG_LEVEL=dns:INFO -``` - -In this case, the `dns` component will log at `INFO` level, while all other -components will log at `ERROR` level. +See the [grpclog package documentation](https://pkg.go.dev/google.golang.org/grpc/grpclog) +for details on how to configure logging. ### The RPC failed with error `"code = Unavailable desc = transport is closing"` diff --git a/grpclog/grpclog.go b/grpclog/grpclog.go index b37ff34c0d26..7314685df218 100644 --- a/grpclog/grpclog.go +++ b/grpclog/grpclog.go @@ -18,14 +18,30 @@ // Package grpclog defines logging for grpc. // -// In the default logger, severity level can be set by environment variable -// GRPC_GO_LOG_SEVERITY_LEVEL, verbosity level can be set by -// GRPC_GO_LOG_VERBOSITY_LEVEL. -// -// Per-component severity level can be set by GRPC_GO_COMPONENT_LOG_LEVEL with -// format "component1:LEVEL,component2:LEVEL" (e.g., "dns:error,xds:warning"). -// Component-specific settings take precedence over GRPC_GO_LOG_SEVERITY_LEVEL -// for that component. +// The default logger is controlled by environment variables. Turn +// everything on like this: +// +// export GRPC_GO_LOG_VERBOSITY_LEVEL=99 +// export GRPC_GO_LOG_SEVERITY_LEVEL=info +// +// Additionally, you can configure log severity levels for individual +// components using the GRPC_GO_COMPONENT_LOG_LEVEL environment +// variable. The format is a comma-separated list of component:LEVEL +// pairs like this: +// +// export GRPC_GO_COMPONENT_LOG_LEVEL=dns:ERROR,transport:WARNING +// +// When both GRPC_GO_LOG_SEVERITY_LEVEL and GRPC_GO_COMPONENT_LOG_LEVEL +// are set, component-specific settings take precedence over +// GRPC_GO_LOG_SEVERITY_LEVEL for the specified components. Components +// not listed in GRPC_GO_COMPONENT_LOG_LEVEL will use the +// GRPC_GO_LOG_SEVERITY_LEVEL setting. For example: +// +// export GRPC_GO_LOG_SEVERITY_LEVEL=ERROR +// export GRPC_GO_COMPONENT_LOG_LEVEL=dns:INFO +// +// In this case, the dns component will log at INFO level, while all +// other components will log at ERROR level. package grpclog import ( From f566320f8a6ea4c2da16f205437cbd391be22702 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Mon, 2 Mar 2026 17:35:31 +0900 Subject: [PATCH 07/10] grpclog: move component log severity types and parsing to grpclog.go --- grpclog/component.go | 40 ---------------------------------------- grpclog/grpclog.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/grpclog/component.go b/grpclog/component.go index 3735412913eb..31e1c449e8d0 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -20,49 +20,9 @@ package grpclog import ( "fmt" - "strings" "sync" ) -type severityLevel int - -const ( - severityInfo severityLevel = iota - severityWarning - severityError - severityFatal -) - -func parseComponentLogLevels(logLevel string) map[string]severityLevel { - if logLevel == "" { - return nil - } - - logLevels := make(map[string]severityLevel) - for part := range strings.SplitSeq(logLevel, ",") { - kv := strings.Split(part, ":") - if len(kv) < 2 { - continue - } - component := kv[0] - if component == "" { - continue - } - level := kv[1] - switch level { - case "INFO", "info": - logLevels[component] = severityInfo - case "WARNING", "warning": - logLevels[component] = severityWarning - case "ERROR", "error": - logLevels[component] = severityError - default: - logLevels[component] = severityFatal - } - } - return logLevels -} - // componentData records the settings for a component. type componentData struct { name string diff --git a/grpclog/grpclog.go b/grpclog/grpclog.go index 7314685df218..50ffbbbe9bb0 100644 --- a/grpclog/grpclog.go +++ b/grpclog/grpclog.go @@ -47,10 +47,50 @@ package grpclog import ( "io" "os" + "strings" "google.golang.org/grpc/grpclog/internal" ) +type severityLevel int + +const ( + severityInfo severityLevel = iota + severityWarning + severityError + severityFatal +) + +func parseComponentLogLevels(logLevel string) map[string]severityLevel { + if logLevel == "" { + return nil + } + + logLevels := make(map[string]severityLevel) + for part := range strings.SplitSeq(logLevel, ",") { + kv := strings.Split(part, ":") + if len(kv) < 2 { + continue + } + component := kv[0] + if component == "" { + continue + } + level := kv[1] + switch level { + case "INFO", "info": + logLevels[component] = severityInfo + case "WARNING", "warning": + logLevels[component] = severityWarning + case "ERROR", "error": + logLevels[component] = severityError + default: + logLevels[component] = severityFatal + } + } + return logLevels +} + var componentLogLevels map[string]severityLevel func init() { From d7230060d349ec2a70d34ef85c5b60a020374f07 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Mon, 2 Mar 2026 19:26:24 +0900 Subject: [PATCH 08/10] grpclog: skip unrecognized component log levels instead of defaulting to fatal --- grpclog/component.go | 4 ---- grpclog/component_test.go | 16 +++++++--------- grpclog/grpclog.go | 3 +-- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/grpclog/component.go b/grpclog/component.go index 31e1c449e8d0..3d1d9dc89c05 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -164,10 +164,6 @@ func Component(componentName string) DepthLoggerV2 { c.errorDepth = func(depth int, args ...any) { componentErrorDepth(depth+1, args...) } - case severityFatal: - c.infoDepth = noopDepth - c.warningDepth = noopDepth - c.errorDepth = noopDepth } } diff --git a/grpclog/component_test.go b/grpclog/component_test.go index 1206824f3252..f96b0c886408 100644 --- a/grpclog/component_test.go +++ b/grpclog/component_test.go @@ -69,13 +69,9 @@ func TestParseComponentLogLevel(t *testing.T) { }, }, { - name: "mixed-case level names are treated as unknown and default to FATAL", + name: "mixed-case level names are treated as unknown and skipped", input: "dns:Error,xds:Warning,authz:Info", - want: map[string]severityLevel{ - "dns": severityFatal, - "xds": severityFatal, - "authz": severityFatal, - }, + want: map[string]severityLevel{}, }, { name: "parses multiple comma-separated components with different levels", @@ -102,10 +98,9 @@ func TestParseComponentLogLevel(t *testing.T) { }, }, { - name: "unrecognized level name defaults to FATAL", + name: "unrecognized level name is skipped", input: "dns:UNKNOWN,xds:ERROR", want: map[string]severityLevel{ - "dns": severityFatal, "xds": severityError, }, }, @@ -220,11 +215,14 @@ func TestLogger(t *testing.T) { }, }, { - name: "FATAL GRPC_GO_COMPONENT_LOG_LEVEL suppresses all non-fatal logs", + name: "unrecognized GRPC_GO_COMPONENT_LOG_LEVEL falls back to global default", componentLogLevel: "dns:FATAL", logSeverityLevel: "INFO", components: []string{"dns", "balancer"}, want: []string{ + "INFO: [dns] dns-info", + "WARNING: [dns] dns-warning", + "ERROR: [dns] dns-error", "INFO: [balancer] balancer-info", "WARNING: [balancer] balancer-warning", "ERROR: [balancer] balancer-error", diff --git a/grpclog/grpclog.go b/grpclog/grpclog.go index 50ffbbbe9bb0..09004ffaf6af 100644 --- a/grpclog/grpclog.go +++ b/grpclog/grpclog.go @@ -58,7 +58,6 @@ const ( severityInfo severityLevel = iota severityWarning severityError - severityFatal ) func parseComponentLogLevels(logLevel string) map[string]severityLevel { @@ -85,7 +84,7 @@ func parseComponentLogLevels(logLevel string) map[string]severityLevel { case "ERROR", "error": logLevels[component] = severityError default: - logLevels[component] = severityFatal + continue } } return logLevels From 1058fd9c53dfd6f6ad861178d34fcfaeb73e54cc Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Mon, 2 Mar 2026 19:39:13 +0900 Subject: [PATCH 09/10] grpclog: move component depth functions from loggerv2.go to component.go --- grpclog/component.go | 36 ++++++++++++++++++++++++++++++++++++ grpclog/loggerv2.go | 34 ---------------------------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/grpclog/component.go b/grpclog/component.go index 3d1d9dc89c05..4ef5685e8874 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -20,7 +20,10 @@ package grpclog import ( "fmt" + "os" "sync" + + "google.golang.org/grpc/grpclog/internal" ) // componentData records the settings for a component. @@ -114,6 +117,39 @@ func (c *componentData) V(l int) bool { return V(l) } +func componentInfoDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.InfoDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Infoln(args...) + } +} + +func componentWarningDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.WarningDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Warningln(args...) + } +} + +func componentErrorDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.ErrorDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Errorln(args...) + } +} + +func componentFatalDepth(depth int, args ...any) { + if internal.ComponentDepthLoggerV2Impl != nil { + internal.ComponentDepthLoggerV2Impl.FatalDepth(depth, args...) + } else { + internal.ComponentLoggerV2Impl.Fatalln(args...) + } + os.Exit(1) +} + func noopDepth(int, ...any) { } diff --git a/grpclog/loggerv2.go b/grpclog/loggerv2.go index 2746ef9e4bde..5611e2f8236b 100644 --- a/grpclog/loggerv2.go +++ b/grpclog/loggerv2.go @@ -20,7 +20,6 @@ package grpclog import ( "io" - "os" "strconv" "strings" @@ -30,39 +29,6 @@ import ( // LoggerV2 does underlying logging work for grpclog. type LoggerV2 internal.LoggerV2 -func componentInfoDepth(depth int, args ...any) { - if internal.ComponentDepthLoggerV2Impl != nil { - internal.ComponentDepthLoggerV2Impl.InfoDepth(depth, args...) - } else { - internal.ComponentLoggerV2Impl.Infoln(args...) - } -} - -func componentWarningDepth(depth int, args ...any) { - if internal.ComponentDepthLoggerV2Impl != nil { - internal.ComponentDepthLoggerV2Impl.WarningDepth(depth, args...) - } else { - internal.ComponentLoggerV2Impl.Warningln(args...) - } -} - -func componentErrorDepth(depth int, args ...any) { - if internal.ComponentDepthLoggerV2Impl != nil { - internal.ComponentDepthLoggerV2Impl.ErrorDepth(depth, args...) - } else { - internal.ComponentLoggerV2Impl.Errorln(args...) - } -} - -func componentFatalDepth(depth int, args ...any) { - if internal.ComponentDepthLoggerV2Impl != nil { - internal.ComponentDepthLoggerV2Impl.FatalDepth(depth, args...) - } else { - internal.ComponentLoggerV2Impl.Fatalln(args...) - } - os.Exit(1) -} - // SetLoggerV2 sets logger that is used in grpc to a V2 logger. // Not mutex-protected, should be called before any gRPC functions. func SetLoggerV2(l LoggerV2) { From 619a19e9b005ee1f16ee23d526340d513b2714f7 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Mon, 2 Mar 2026 20:35:27 +0900 Subject: [PATCH 10/10] grpclog: precompute component log prefix to avoid repeated string concatenation --- grpclog/component.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/grpclog/component.go b/grpclog/component.go index 4ef5685e8874..2609becee4c7 100644 --- a/grpclog/component.go +++ b/grpclog/component.go @@ -28,7 +28,8 @@ import ( // componentData records the settings for a component. type componentData struct { - name string + name string + logPrefix string infoDepth func(depth int, args ...any) warningDepth func(depth int, args ...any) @@ -46,22 +47,22 @@ var cache = componentCache{ } func (c *componentData) InfoDepth(depth int, args ...any) { - args = append([]any{"[" + string(c.name) + "]"}, args...) + args = append([]any{c.logPrefix}, args...) c.infoDepth(depth+1, args...) } func (c *componentData) WarningDepth(depth int, args ...any) { - args = append([]any{"[" + string(c.name) + "]"}, args...) + args = append([]any{c.logPrefix}, args...) c.warningDepth(depth+1, args...) } func (c *componentData) ErrorDepth(depth int, args ...any) { - args = append([]any{"[" + string(c.name) + "]"}, args...) + args = append([]any{c.logPrefix}, args...) c.errorDepth(depth+1, args...) } func (c *componentData) FatalDepth(depth int, args ...any) { - args = append([]any{"[" + string(c.name) + "]"}, args...) + args = append([]any{c.logPrefix}, args...) c.fatalDepth(depth+1, args...) } @@ -165,6 +166,7 @@ func Component(componentName string) DepthLoggerV2 { } c := &componentData{ name: componentName, + logPrefix: "[" + componentName + "]", infoDepth: InfoDepth, warningDepth: WarningDepth, errorDepth: ErrorDepth,