Skip to content

Commit a77ba4d

Browse files
authored
Settable grpc logger (#402)
* SettableLoggerV2 to dynamically change grpc logger. The go-grpc assumes that logger can be only configured once as the `SetLoggerV2` method is: ```Not mutex-protected, should be called before any gRPC functions.`` This is insufficient in case of e.g. "testing" scenarios where loggers might need to be supplied on per-test-case basis. * Integrate ZAP with SettableLoggerV2. The settable_test.go is a good example of integration of testing harness with grpclogging using zaptest, that is canonical usecase for this infrastructure.
1 parent be4c235 commit a77ba4d

File tree

8 files changed

+217
-3
lines changed

8 files changed

+217
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ myServer := grpc.NewServer(
5959
* [`grpc_zap`](logging/zap/) - integration of [zap](https://github.com/uber-go/zap) logging library into gRPC handlers.
6060
* [`grpc_logrus`](logging/logrus/) - integration of [logrus](https://github.com/sirupsen/logrus) logging library into gRPC handlers.
6161
* [`grpc_kit`](logging/kit/) - integration of [go-kit](https://github.com/go-kit/kit/tree/master/log) logging library into gRPC handlers.
62+
* [`grpc_grpc_logsettable`](logging/settable/) - a wrapper around `grpclog.LoggerV2` that allows to replace loggers in runtime (thread-safe).
6263

6364
#### Monitoring
6465
* [`grpc_prometheus`](https://github.com/grpc-ecosystem/go-grpc-prometheus) - Prometheus client-side and server-side monitoring middleware

logging/doc.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// Copyright 2017 Michal Witkowski. All Rights Reserved.
2-
// See LICENSE for licensing terms.
3-
41
//
52
/*
63
grpc_logging is a "parent" package for gRPC logging middlewares.

logging/settable/doc.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
/*
3+
grpc_logsettable contains a thread-safe wrapper around grpc-logging
4+
infrastructure.
5+
6+
The go-grpc assumes that logger can be only configured once as the `SetLoggerV2`
7+
method is:
8+
```Not mutex-protected, should be called before any gRPC functions.```
9+
10+
This package allows to supply parent logger once ("before any grpc"), but
11+
later change underlying implementation in thread-safe way when needed.
12+
13+
It's in particular useful for testing, where each testcase might need its own
14+
logger.
15+
*/
16+
package grpc_logsettable

logging/settable/logsettable.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package grpc_logsettable
2+
3+
import (
4+
"io/ioutil"
5+
"sync"
6+
7+
"google.golang.org/grpc/grpclog"
8+
)
9+
10+
// SettableLoggerV2 is thread-safe.
11+
type SettableLoggerV2 interface {
12+
grpclog.LoggerV2
13+
// Sets given logger as the underlying implementation.
14+
Set(loggerv2 grpclog.LoggerV2)
15+
// Sets `discard` logger as the underlying implementation.
16+
Reset()
17+
}
18+
19+
// ReplaceGrpcLoggerV2 creates and configures SettableLoggerV2 as grpc logger.
20+
func ReplaceGrpcLoggerV2() SettableLoggerV2 {
21+
settable := &settableLoggerV2{}
22+
settable.Reset()
23+
grpclog.SetLoggerV2(settable)
24+
return settable
25+
}
26+
27+
// SettableLoggerV2 implements SettableLoggerV2
28+
type settableLoggerV2 struct {
29+
log grpclog.LoggerV2
30+
mu sync.RWMutex
31+
}
32+
33+
func (s *settableLoggerV2) Set(log grpclog.LoggerV2) {
34+
s.mu.Lock()
35+
defer s.mu.Unlock()
36+
s.log = log
37+
}
38+
39+
func (s *settableLoggerV2) Reset() {
40+
s.Set(grpclog.NewLoggerV2(ioutil.Discard, ioutil.Discard, ioutil.Discard))
41+
}
42+
43+
func (s *settableLoggerV2) get() grpclog.LoggerV2 {
44+
s.mu.RLock()
45+
defer s.mu.RUnlock()
46+
return s.log
47+
}
48+
49+
func (s *settableLoggerV2) Info(args ...interface{}) {
50+
s.get().Info(args)
51+
}
52+
53+
func (s *settableLoggerV2) Infoln(args ...interface{}) {
54+
s.get().Infoln(args)
55+
}
56+
57+
func (s *settableLoggerV2) Infof(format string, args ...interface{}) {
58+
s.get().Infof(format, args)
59+
}
60+
61+
func (s *settableLoggerV2) Warning(args ...interface{}) {
62+
s.get().Warning(args)
63+
}
64+
65+
func (s *settableLoggerV2) Warningln(args ...interface{}) {
66+
s.get().Warningln(args)
67+
}
68+
69+
func (s *settableLoggerV2) Warningf(format string, args ...interface{}) {
70+
s.get().Warningf(format, args)
71+
}
72+
73+
func (s *settableLoggerV2) Error(args ...interface{}) {
74+
s.get().Error(args)
75+
}
76+
77+
func (s *settableLoggerV2) Errorln(args ...interface{}) {
78+
s.get().Errorln(args)
79+
}
80+
81+
func (s *settableLoggerV2) Errorf(format string, args ...interface{}) {
82+
s.get().Errorf(format, args)
83+
}
84+
85+
func (s *settableLoggerV2) Fatal(args ...interface{}) {
86+
s.get().Fatal(args)
87+
}
88+
89+
func (s *settableLoggerV2) Fatalln(args ...interface{}) {
90+
s.get().Fatalln(args)
91+
}
92+
93+
func (s *settableLoggerV2) Fatalf(format string, args ...interface{}) {
94+
s.get().Fatalf(format, args)
95+
}
96+
97+
func (s *settableLoggerV2) V(l int) bool {
98+
return s.get().V(l)
99+
}

logging/settable/logsettable_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package grpc_logsettable_test
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"os"
7+
"testing"
8+
9+
grpc_logsettable "github.com/grpc-ecosystem/go-grpc-middleware/logging/settable"
10+
"github.com/stretchr/testify/assert"
11+
"google.golang.org/grpc/grpclog"
12+
)
13+
14+
func ExampleSettableLoggerV2_init() {
15+
l1 := grpclog.NewLoggerV2(ioutil.Discard, ioutil.Discard, ioutil.Discard)
16+
l2 := grpclog.NewLoggerV2(os.Stdout, os.Stdout, os.Stdout)
17+
18+
settableLogger := grpc_logsettable.ReplaceGrpcLoggerV2()
19+
grpclog.Info("Discarded by default")
20+
21+
settableLogger.Set(l1)
22+
grpclog.Info("Discarded log by l1")
23+
24+
settableLogger.Set(l2)
25+
grpclog.Info("Emitted log by l2")
26+
// Expected output: INFO: 2021/03/15 12:59:54 [Emitted log by l2]
27+
}
28+
29+
func TestSettableLoggerV2_init(t *testing.T) {
30+
l1buffer := &bytes.Buffer{}
31+
l1 := grpclog.NewLoggerV2(l1buffer, l1buffer, l1buffer)
32+
33+
l2buffer := &bytes.Buffer{}
34+
l2 := grpclog.NewLoggerV2(l2buffer, l2buffer, l2buffer)
35+
36+
settableLogger := grpc_logsettable.ReplaceGrpcLoggerV2()
37+
grpclog.Info("Discarded by default")
38+
39+
settableLogger.Set(l1)
40+
grpclog.SetLoggerV2(settableLogger)
41+
grpclog.Info("Emitted log by l1")
42+
43+
settableLogger.Set(l2)
44+
grpclog.Info("Emitted log by l2")
45+
46+
assert.Contains(t, l1buffer.String(), "Emitted log by l1")
47+
assert.Contains(t, l2buffer.String(), "Emitted log by l2")
48+
}

logging/zap/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,6 @@ Note - due to implementation ZAP differs from Logrus in the "grpc.request.conten
7070
7171
7272
Please see examples and tests for examples of use.
73+
Please see settable_test.go for canonical integration through "zaptest" with golang testing infrastructure.
7374
*/
7475
package grpc_zap

logging/zap/grpclogger.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package grpc_zap
66
import (
77
"fmt"
88

9+
grpc_logsettable "github.com/grpc-ecosystem/go-grpc-middleware/logging/settable"
910
"go.uber.org/zap"
1011
"google.golang.org/grpc/grpclog"
1112
)
@@ -47,11 +48,13 @@ func (l *zapGrpcLogger) Println(args ...interface{}) {
4748
}
4849

4950
// ReplaceGrpcLoggerV2 replaces the grpc_log.LoggerV2 with the provided logger.
51+
// It should be called before any gRPC functions.
5052
func ReplaceGrpcLoggerV2(logger *zap.Logger) {
5153
ReplaceGrpcLoggerV2WithVerbosity(logger, 0)
5254
}
5355

5456
// ReplaceGrpcLoggerV2WithVerbosity replaces the grpc_.LoggerV2 with the provided logger and verbosity.
57+
// It should be called before any gRPC functions.
5558
func ReplaceGrpcLoggerV2WithVerbosity(logger *zap.Logger, verbosity int) {
5659
zgl := &zapGrpcLoggerV2{
5760
logger: logger.With(SystemField, zap.Bool("grpc_log", true)),
@@ -60,6 +63,22 @@ func ReplaceGrpcLoggerV2WithVerbosity(logger *zap.Logger, verbosity int) {
6063
grpclog.SetLoggerV2(zgl)
6164
}
6265

66+
// SetGrpcLoggerV2 replaces the grpc_log.LoggerV2 with the provided logger.
67+
// It can be used even when grpc infrastructure was initialized.
68+
func SetGrpcLoggerV2(settable grpc_logsettable.SettableLoggerV2, logger *zap.Logger) {
69+
SetGrpcLoggerV2WithVerbosity(settable, logger, 0)
70+
}
71+
72+
// SetGrpcLoggerV2WithVerbosity replaces the grpc_.LoggerV2 with the provided logger and verbosity.
73+
// It can be used even when grpc infrastructure was initialized.
74+
func SetGrpcLoggerV2WithVerbosity(settable grpc_logsettable.SettableLoggerV2, logger *zap.Logger, verbosity int) {
75+
zgl := &zapGrpcLoggerV2{
76+
logger: logger.With(SystemField, zap.Bool("grpc_log", true)),
77+
verbosity: verbosity,
78+
}
79+
settable.Set(zgl)
80+
}
81+
6382
type zapGrpcLoggerV2 struct {
6483
logger *zap.Logger
6584
verbosity int

logging/zap/settable_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package grpc_zap_test
2+
3+
import (
4+
"testing"
5+
6+
grpc_logsettable "github.com/grpc-ecosystem/go-grpc-middleware/logging/settable"
7+
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
8+
"go.uber.org/zap/zaptest"
9+
)
10+
11+
var grpc_logger grpc_logsettable.SettableLoggerV2
12+
13+
func init() {
14+
grpc_logger = grpc_logsettable.ReplaceGrpcLoggerV2()
15+
}
16+
17+
func beforeTest(t testing.TB) {
18+
grpc_zap.SetGrpcLoggerV2(grpc_logger, zaptest.NewLogger(t))
19+
20+
// Starting from go-1.15+ automated 'reset' can also be set:
21+
// t.Cleanup(func() {
22+
// grpc_logger.Reset()
23+
// })
24+
}
25+
26+
// This test illustrates setting up a testing harness that attributes
27+
// all grpc logs emitted during the test to the test-specific log.
28+
//
29+
// In case of test failure, only logs emitted by this testcase will be printed.
30+
func TestSpecificLogging(t *testing.T) {
31+
beforeTest(t)
32+
grpc_logger.Info("Test specific log-line")
33+
}

0 commit comments

Comments
 (0)