Skip to content

grpclog: add GRPC_GO_COMPONENT_LOG_LEVEL for controling component logs#8885

Open
110y wants to merge 11 commits intogrpc:masterfrom
110y:grpclog-component-log-severity
Open

grpclog: add GRPC_GO_COMPONENT_LOG_LEVEL for controling component logs#8885
110y wants to merge 11 commits intogrpc:masterfrom
110y:grpclog-component-log-severity

Conversation

@110y
Copy link
Copy Markdown
Contributor

@110y 110y commented Feb 6, 2026

Fixes #3937

Add support for the GRPC_GO_COMPONENT_LOG_LEVEL environment variable, which allows users to configure log severity levels on a per-component basis.
The format is a comma-separated list of component:LEVEL pairs (e.g., dns:ERROR,xds:WARNING,authz:INFO).

When both GRPC_GO_LOG_SEVERITY_LEVEL and GRPC_GO_COMPONENT_LOG_LEVEL are set,
component-specific settings take precedence for the specified components.
Components without an explicit override continue to use the existing global severity level logger.

A separate component logger (ComponentLoggerV2Impl) is introduced so that components configured with a less restrictive severity than the global logger (e.g., component=INFO while global=ERROR) can still produce output.

RELEASE NOTES:

  • grpclog: Add GRPC_GO_COMPONENT_LOG_LEVEL environment variable for per-component log severity configuration. Users can now set different severity levels for individual gRPC components (e.g., GRPC_GO_COMPONENT_LOG_LEVEL=dns:ERROR,xds:WARNING).

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 70.31250% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.88%. Comparing base (ad2d82e) to head (5492a39).

Files with missing lines Patch % Lines
grpclog/component.go 68.65% 14 Missing and 7 partials ⚠️
grpclog/grpclog.go 70.27% 3 Missing and 8 partials ⚠️
grpclog/loggerv2.go 75.00% 2 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8885      +/-   ##
==========================================
- Coverage   83.06%   82.88%   -0.18%     
==========================================
  Files         410      410              
  Lines       32598    32702     +104     
==========================================
+ Hits        27076    27105      +29     
- Misses       4053     4088      +35     
- Partials     1469     1509      +40     
Files with missing lines Coverage Δ
grpclog/loggerv2.go 61.53% <75.00%> (+1.53%) ⬆️
grpclog/grpclog.go 63.91% <70.27%> (-1.60%) ⬇️
grpclog/component.go 60.00% <68.65%> (-17.28%) ⬇️

... and 24 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@110y
Copy link
Copy Markdown
Contributor Author

110y commented Feb 6, 2026

@easwars Please take a look, thanks 🙏

@eshitachandwani eshitachandwani added this to the 1.80 Release milestone Feb 9, 2026
@eshitachandwani eshitachandwani added the Type: Feature New features or improvements in behavior label Feb 9, 2026
@eshitachandwani eshitachandwani self-requested a review February 9, 2026 10:46
@eshitachandwani eshitachandwani self-assigned this Feb 9, 2026
Copy link
Copy Markdown
Member

@eshitachandwani eshitachandwani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the delay , I am still in process of reviewing the PR , but posting a few quick comments I had.

Comment thread grpclog/internal/grpclog.go Outdated
// 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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please make sure the comments are wrapped at 80 cols

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 324a394

Comment thread grpclog/component.go Outdated
}
level := kv[1]
switch level {
case "INFO", "info":
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here , instead of just checking for all uppercase and all lower case , can we instead convert the input to all lowercase and then just check for info. This is similar to what we do for balancer and resolver registries. But I also see that this is how log levels are checked earlier so maybe @easwars knows if there is a particular reason for that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you mentioned, and as I mentioned in the issue regarding component logging: #3937 (comment),
this code follows what the existing GRPC_GO_LOG_SEVERITY_LEVEL does for case-insensitive level matching.
I also thought it might be better to use strings.ToLower for matching, but I would say we should make two environment variables have the same logic for case-insensitivity.

@easwars What do you think about this? 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easwars

Let me notify in case of you missed this comment 🙏

Comment thread grpclog/component.go Outdated
case "ERROR", "error":
logLevels[component] = severityError
default:
logLevels[component] = severityFatal
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the log level does not match any, we might want to let the global default take over for the specific component instead of level being Fatal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought it would be better to let the global default logger take over if the log level for a component does not match any know level.
However, I found that the existing GRPC_GO_LOG_SEVERITY_LEVEL implicitly behaves like the "FATAL" level if an unknown level string is passed:

grpc-go/grpclog/loggerv2.go

Lines 109 to 124 in ef6044e

func newLoggerV2(w io.Writer, config internal.LoggerV2Config, level string) LoggerV2 {
errorW := io.Discard
warningW := io.Discard
infoW := io.Discard
switch level {
case "", "ERROR", "error": // If env is unset, set level to ERROR.
errorW = w
case "WARNING", "warning":
warningW = w
case "INFO", "info":
infoW = w
}
return internal.NewLoggerV2(infoW, warningW, errorW, config)
}

So, I made the new GRPC_GO_COMPONENT_LOG_LEVEL behave similarly.

I think it would be fine to make the new GRPC_GO_COMPONENT_LOG_LEVEL behave differently from the existing GRPC_GO_LOG_SEVERITY_LEVEL and let the global default logger take over if the log level for a component does not match any as @eshitachandwani mentioned.

@easwars What do you think about this? 👀

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you say that it implicitly behaves like fatal if an unknown string is passed? My reading of it is that passing an unknown string should just lead to nothing being logged because all three writers would be set to io.Discard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry I missed that internal.NewLoggerV2 uses the error writer also for the fatal writer:

fatalW := errorW

I made it to use global default logger when it gets an unrecognized string for a component by this commit: d723006

@easwars easwars removed the request for review from Pranjali-2501 February 24, 2026 05:43
Comment thread grpclog/component.go Outdated
severityFatal
)

func parseComponentLogLevel(logLevel string) map[string]severityLevel {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func parseComponentLogLevel(logLevel string) map[string]severityLevel {
func parseComponentLogLevels(logLevels string) map[string]severityLevel {

Copy link
Copy Markdown
Contributor Author

@110y 110y Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: c0d5868
Because the variable logLevel is already used in subsequent processing and the name of environment variable is GRPC_GO_COMPONENT_LOG_LEVEL (does not have s), I leave the argument as it was.

Comment thread grpclog/loggerv2.go Outdated
Comment thread grpclog/loggerv2.go

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if my understanding is wrong but if we pass io.Discard in error and warning writer, it is never going to print the error or warning logs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because combineLoggers:

warningW = combineLoggers(infoW, warningW)
errorW = combineLoggers(errorW, warningW)
returns the non-io.Discard writer when the other side is io.Discard, warning and error logs will be printed correctly.

This is also the same behavior as the existing newLoggerV2:

grpc-go/grpclog/loggerv2.go

Lines 109 to 124 in 324a394

func newLoggerV2(w io.Writer, config internal.LoggerV2Config, level string) LoggerV2 {
errorW := io.Discard
warningW := io.Discard
infoW := io.Discard
switch level {
case "", "ERROR", "error": // If env is unset, set level to ERROR.
errorW = w
case "WARNING", "warning":
warningW = w
case "INFO", "info":
infoW = w
}
return internal.NewLoggerV2(infoW, warningW, errorW, config)
}
when the level is set to "INFO" — it only sets infoW = w and leaves warningW and errorW as io.Discard, relying on combineLoggers to propagate the writer upward.

@eshitachandwani
Copy link
Copy Markdown
Member

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces a valuable feature for per-component log severity control in gRPC-Go. The implementation correctly handles the precedence of component-specific settings over global settings and provides a separate component logger to allow more verbose logging for specific components. However, there are a few critical issues regarding Go version compatibility and thread safety that need to be addressed before merging.

Comment thread grpclog/component.go Outdated
Comment thread grpclog/component_test.go
Comment thread grpclog/component.go Outdated
Comment on lines 157 to 204
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Access to the global cache map is not synchronized. Since Component is a public function that can be called concurrently from multiple goroutines (e.g., during lazy initialization of different gRPC components), this can lead to a concurrent map write panic. Although the original code had this issue, the PR significantly increases the logic within the critical section, making the race more likely. A sync.Mutex or sync.RWMutex should be used to protect the cache.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As gemini mentioned, my implementation does not use a mutex because the original implementation does not use a mutex to protect cache.

@eshitachandwani @easwars What do you think about this? 👀

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a mutex would mean every component trying to initialize the logger would have to acquire the mutex and it will significantly increase the latency, so I dont think using mutex is a good idea.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mutex is only going to be used when the component is created and not everytime the component is used to log a message. So, I don't think it is going to significantly increase the latency. I think we can go ahead with having the mutex.

Copy link
Copy Markdown
Contributor Author

@110y 110y Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: 5eda131

@easwars easwars self-assigned this Feb 25, 2026
Comment thread README.md Outdated
Comment on lines +82 to +101
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consolidate all this information in the godoc for the grpclog package (including the existing paragraph in this section) and simply point to the godoc from here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: 76662e9

Comment thread grpclog/component.go Outdated
case "ERROR", "error":
logLevels[component] = severityError
default:
logLevels[component] = severityFatal
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you say that it implicitly behaves like fatal if an unknown string is passed? My reading of it is that passing an unknown string should just lead to nothing being logged because all three writers would be set to io.Discard.

Comment thread grpclog/component.go Outdated
logLevels[component] = severityInfo
case "WARNING", "warning":
logLevels[component] = severityWarning
case "ERROR", "error":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we consider an empty string to mean log at ERROR as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easwars
So, do you men if we have GRPC_GO_COMPONENT_LOG_LEVEL as dns:INFO,xds:,authz:ERROR, the log level for xds component should be ERROR instead of make it use the global default logger, right?

Comment thread grpclog/component.go Outdated
return V(l)
}

func noopDepth(_ int, _ ...any) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: You could ignore the blank identifiers in the parameter list because all of them are blank.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: 1b7e50f

Comment thread grpclog/component.go Outdated
"strings"
)

type severityLevel int
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole logic of parsing the newly added env var should be in grpclog/grpclog.go and not here. This file should only contain the implementation of the component logger.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: f566320

Comment thread grpclog/grpclog.go

func init() {
SetLoggerV2(newLoggerV2())
initLogger(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init --> initLogger --> setLoggerV2 etc. So many levels of function calls just makes this code very hard to read and understand and keep things in your mental model. Can we flatten this please. We can continue to have parseComponentLogLevels as a separate function since it deserves to be one. But the rest can all be inlined here into init.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easwars

OK, now I also think it should be flattened.

Before tidying up it, let me clarify about initLogger.
Its purpose is to make us to be able to replace the io.Writer easily for tests like this:

initLogger(&buf, tt.logSeverityLevel, "", "", tt.componentLogLevel)

Do you think we should flatten initLogger too?

Comment thread grpclog/loggerv2.go Outdated

func componentInfoDepth(depth int, args ...any) {
if internal.ComponentDepthLoggerV2Impl != nil {
internal.ComponentDepthLoggerV2Impl.InfoDepth(depth, args...)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have to do depth+1 here?

Copy link
Copy Markdown
Contributor Author

@110y 110y Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaves like what the current InfoDepth does:

grpc-go/grpclog/grpclog.go

Lines 213 to 219 in 619a19e

func InfoDepth(depth int, args ...any) {
if internal.DepthLoggerV2Impl != nil {
internal.DepthLoggerV2Impl.InfoDepth(depth, args...)
} else {
internal.LoggerV2Impl.Infoln(args...)
}
}

I checked that componentInfoDepth preserves the same behavior that the current InfoDepth does by following script and command:

package main

import (
	"flag"

	"google.golang.org/grpc/grpclog"
	_ "google.golang.org/grpc/grpclog/glogger"
)

var dnsLogger = grpclog.Component("dns")

func main() {
	flag.Parse()

	foo()
}

func foo() {
	bar()
}

func bar() {
	baz()
}

func baz() {
	dnsLogger.InfoDepth(1, "This is an info message from baz")
}
$ GRPC_GO_LOG_SEVERITY_LEVEL="INFO" go run main.go -logtostderr
... main.go:23] [dns]This is an info message from baz
$ GRPC_GO_COMPONENT_LOG_LEVEL="dns:INFO" go run main.go -logtostderr
... main.go:23] [dns]This is an info message from baz

Comment thread grpclog/loggerv2.go Outdated
Comment on lines +33 to +64
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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these functions are only called from the component logger, I think these should move to component.go unless you had a good reason to have them here, in which case, I would like to hear that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved: 1058fd9

Comment thread grpclog/loggerv2.go Outdated
Comment thread grpclog/component.go
Comment on lines +69 to +72
infoDepth func(depth int, args ...any)
warningDepth func(depth int, args ...any)
errorDepth func(depth int, args ...any)
fatalDepth func(depth int, args ...any)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need these function pointers? Can't we instead store the severityLevel for this component, and handle it directly in each of its methods?

func (c *componentData) InfoDepth(depth int, args ...any) {
    if c.level > severityInfo { return } // Fast-path exit
    // ... rest of logic
}

As an improvement, we could even store the computed prefix in the componentData struct. I'm talking about [" + string(c.name) + "] and not have to compute that for every log message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easwars
The reason why I use function pointers is to avoid checking severity every time a log function is called, by replacing loggers with the noopDepth function according to their levels.
I also think it's OK to handle it directly in each methods.
What do you think about this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the meantime, I updated the prefix computation logic: 619a19e

@110y
Copy link
Copy Markdown
Contributor Author

110y commented Mar 2, 2026

I saw that CI seems to fail randomly: https://github.com/grpc/grpc-go/actions/runs/22563149237/job/65353773787?pr=8885, https://github.com/grpc/grpc-go/actions/runs/22572272694/job/65382832623?pr=8885

Is this just a flaky test or do you think this might be caused by changes introduced in this PR...?

@easwars @eshitachandwani

@110y
Copy link
Copy Markdown
Contributor Author

110y commented Mar 2, 2026

@eshitachandwani @easwars

Thank you for your review!
I checked all of your review comments and updated this branch or left comments for further discussion.

Please take another look, thanks 🙏

@mbissa mbissa modified the milestones: 1.80 Release, 1.81 Release Mar 6, 2026
@easwars
Copy link
Copy Markdown
Contributor

easwars commented Mar 11, 2026

I apologize for the delay in the review process. I'm currently dealing with a few higher priority things and will get back to this asap. Thanks for your patience.

@110y
Copy link
Copy Markdown
Contributor Author

110y commented Mar 12, 2026

@easwars

No problem!
I know this change is not an urgent one. So, please feel free to review this when you are available for reviewing this kind of change, thank you for your work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Feature New features or improvements in behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow per-component logging configuration via environment variables

5 participants