Skip to content

Commit 665e053

Browse files
otelcol/grpclog: suppress benign gRPC client-disconnect warnings
When a gRPC client closes normally, the transport layer emits warnings like "HandleStreams failed to read frame: EOF" and "connection reset by peer". These are expected during graceful shutdown and create noise for operators monitoring production collector deployments. Add a benignFilterCore zapcore.Core wrapper that intercepts warn-level entries matching known client-disconnect patterns and drops them before they reach the output sink. Non-benign warnings are unaffected. Fixes #5169
1 parent a58f96f commit 665e053

2 files changed

Lines changed: 99 additions & 2 deletions

File tree

otelcol/internal/grpclog/logger.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,50 @@
44
package grpclog // import "go.opentelemetry.io/collector/otelcol/internal/grpclog"
55

66
import (
7+
"strings"
8+
79
"go.uber.org/zap"
810
"go.uber.org/zap/zapcore"
911
"go.uber.org/zap/zapgrpc"
1012
"google.golang.org/grpc/grpclog"
1113
)
1214

15+
// benignClientClosePatterns are gRPC transport-layer messages that occur during normal
16+
// client disconnection and should not be surfaced as warnings to operators.
17+
var benignClientClosePatterns = []string{
18+
"HandleStreams failed to read frame",
19+
"connection reset by peer",
20+
}
21+
22+
func isBenignClientCloseMessage(msg string) bool {
23+
for _, pattern := range benignClientClosePatterns {
24+
if strings.Contains(msg, pattern) {
25+
return true
26+
}
27+
}
28+
return false
29+
}
30+
31+
// benignFilterCore wraps a zapcore.Core to suppress known benign gRPC client-disconnect
32+
// warnings, reducing noise in production logs.
33+
type benignFilterCore struct {
34+
zapcore.Core
35+
}
36+
37+
func (c *benignFilterCore) With(fields []zapcore.Field) zapcore.Core {
38+
return &benignFilterCore{c.Core.With(fields)}
39+
}
40+
41+
func (c *benignFilterCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
42+
if entry.Level == zapcore.WarnLevel && isBenignClientCloseMessage(entry.Message) {
43+
return checked
44+
}
45+
return c.Core.Check(entry, checked)
46+
}
47+
1348
// SetLogger constructs a zapgrpc.Logger instance, and installs it as grpc logger, cloned from baseLogger with
1449
// exact configuration. The minimum level of gRPC logs is set to WARN should the loglevel of the collector is set to
15-
// INFO to avoid copious logging from grpc framework.
50+
// INFO to avoid copious logging from grpc framework. Benign client-disconnect warnings are suppressed.
1651
func SetLogger(baseLogger *zap.Logger) *zapgrpc.Logger {
1752
logger := zapgrpc.NewLogger(baseLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
1853
var c zapcore.Core
@@ -27,7 +62,7 @@ func SetLogger(baseLogger *zap.Logger) *zapgrpc.Logger {
2762
if err != nil {
2863
c = core
2964
}
30-
return c.With([]zapcore.Field{zap.Bool("grpc_log", true)})
65+
return &benignFilterCore{c.With([]zapcore.Field{zap.Bool("grpc_log", true)})}
3166
}), zap.AddCallerSkip(5)))
3267

3368
grpclog.SetLoggerV2(logger)

otelcol/internal/grpclog/logger_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,68 @@ func TestGRPCLogger(t *testing.T) {
8383
}
8484
}
8585

86+
func TestBenignClientCloseWarningsSuppressed(t *testing.T) {
87+
benignMessages := []string{
88+
"transport: http2Server.HandleStreams failed to read frame: EOF",
89+
"transport: http2Server.HandleStreams failed to read frame: read tcp: connection reset by peer",
90+
"[transport] connection reset by peer",
91+
}
92+
93+
for _, msg := range benignMessages {
94+
t.Run(msg, func(t *testing.T) {
95+
warnLogged := false
96+
hook := zap.Hooks(func(entry zapcore.Entry) error {
97+
if entry.Level == zapcore.WarnLevel {
98+
warnLogged = true
99+
}
100+
return nil
101+
})
102+
103+
cfg := zap.Config{
104+
Level: zap.NewAtomicLevelAt(zapcore.WarnLevel),
105+
Encoding: "console",
106+
}
107+
logger, err := cfg.Build(hook)
108+
require.NoError(t, err)
109+
110+
SetLogger(logger)
111+
component := &mockComponent{logger: grpclog.Component("transport")}
112+
component.Warning(msg)
113+
114+
assert.False(t, warnLogged, "benign client-close message should be suppressed: %s", msg)
115+
})
116+
}
117+
}
118+
119+
func TestNonBenignWarningsPreserved(t *testing.T) {
120+
warnLogged := false
121+
hook := zap.Hooks(func(entry zapcore.Entry) error {
122+
if entry.Level == zapcore.WarnLevel {
123+
warnLogged = true
124+
}
125+
return nil
126+
})
127+
128+
cfg := zap.Config{
129+
Level: zap.NewAtomicLevelAt(zapcore.WarnLevel),
130+
Encoding: "console",
131+
}
132+
logger, err := cfg.Build(hook)
133+
require.NoError(t, err)
134+
135+
SetLogger(logger)
136+
component := &mockComponent{logger: grpclog.Component("channelz")}
137+
component.Warning("some unexpected grpc error unrelated to client close")
138+
assert.True(t, warnLogged, "non-benign warning should still be logged at warn level")
139+
}
140+
141+
func TestIsBenignClientCloseMessage(t *testing.T) {
142+
assert.True(t, isBenignClientCloseMessage("HandleStreams failed to read frame: EOF"))
143+
assert.True(t, isBenignClientCloseMessage("read tcp: connection reset by peer"))
144+
assert.False(t, isBenignClientCloseMessage("unexpected internal error"))
145+
assert.False(t, isBenignClientCloseMessage("dial tcp: connection refused"))
146+
}
147+
86148
type mockComponent struct {
87149
logger grpclog.DepthLoggerV2
88150
}

0 commit comments

Comments
 (0)