Skip to content

Commit e41e6bd

Browse files
authored
[interceptors/validator] feat: add error logging in validator (#544)
* feat: add error logging in validator used logging.Logger interface to add error logging in validator interceptor addition: #494 * feat: update interceptor implementation made fast fail and logger as optional args addition to that instead of providing values dynamically at the time of initialization made it more dynamic * fix: update options args updated args based on review * refactor: update validate func restructured if statement in-order to make code execution based on shouldFailFast flag more relevant. * refactor: shifted interceptors into new file restructured code in order to separate the concern. ie: in terms of code struct and testcases wise. * test: updated the testcases modified testcases based on the current modifications made in the code base. * fix: add copyright headers * fix: update comment and code updated code based on reviews
1 parent 85304c0 commit e41e6bd

File tree

5 files changed

+365
-207
lines changed

5 files changed

+365
-207
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) The go-grpc-middleware Authors.
2+
// Licensed under the Apache License 2.0.
3+
4+
package validator
5+
6+
import (
7+
"context"
8+
9+
"google.golang.org/grpc"
10+
)
11+
12+
// UnaryServerInterceptor returns a new unary server interceptor that validates incoming messages.
13+
//
14+
// Invalid messages will be rejected with `InvalidArgument` before reaching any userspace handlers.
15+
// If `WithFailFast` used it will interceptor and returns the first validation error. Otherwise, the interceptor
16+
// returns ALL validation error as a wrapped multi-error.
17+
// If `WithLogger` used it will log all the validation errors. Otherwise, no default logging.
18+
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation
19+
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored.
20+
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
21+
o := evaluateServerOpt(opts)
22+
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
23+
if err := validate(req, o.shouldFailFast, o.level, o.logger); err != nil {
24+
return nil, err
25+
}
26+
return handler(ctx, req)
27+
}
28+
}
29+
30+
// UnaryClientInterceptor returns a new unary client interceptor that validates outgoing messages.
31+
//
32+
// Invalid messages will be rejected with `InvalidArgument` before sending the request to server.
33+
// If `WithFailFast` used it will interceptor and returns the first validation error. Otherwise, the interceptor
34+
// returns ALL validation error as a wrapped multi-error.
35+
// If `WithLogger` used it will log all the validation errors. Otherwise, no default logging.
36+
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation
37+
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored.
38+
func UnaryClientInterceptor(opts ...Option) grpc.UnaryClientInterceptor {
39+
o := evaluateClientOpt(opts)
40+
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
41+
if err := validate(req, o.shouldFailFast, o.level, o.logger); err != nil {
42+
return err
43+
}
44+
return invoker(ctx, method, req, reply, cc, opts...)
45+
}
46+
}
47+
48+
// StreamServerInterceptor returns a new streaming server interceptor that validates incoming messages.
49+
//
50+
// If `WithFailFast` used it will interceptor and returns the first validation error. Otherwise, the interceptor
51+
// returns ALL validation error as a wrapped multi-error.
52+
// If `WithLogger` used it will log all the validation errors. Otherwise, no default logging.
53+
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation
54+
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored.
55+
// The stage at which invalid messages will be rejected with `InvalidArgument` varies based on the
56+
// type of the RPC. For `ServerStream` (1:m) requests, it will happen before reaching any userspace
57+
// handlers. For `ClientStream` (n:1) or `BidiStream` (n:m) RPCs, the messages will be rejected on
58+
// calls to `stream.Recv()`.
59+
func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor {
60+
o := evaluateServerOpt(opts)
61+
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
62+
wrapper := &recvWrapper{
63+
options: o,
64+
ServerStream: stream,
65+
}
66+
67+
return handler(srv, wrapper)
68+
}
69+
}
70+
71+
type recvWrapper struct {
72+
*options
73+
grpc.ServerStream
74+
}
75+
76+
func (s *recvWrapper) RecvMsg(m any) error {
77+
if err := s.ServerStream.RecvMsg(m); err != nil {
78+
return err
79+
}
80+
if err := validate(m, s.shouldFailFast, s.level, s.logger); err != nil {
81+
return err
82+
}
83+
return nil
84+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright (c) The go-grpc-middleware Authors.
2+
// Licensed under the Apache License 2.0.
3+
4+
package validator_test
5+
6+
import (
7+
"io"
8+
"testing"
9+
10+
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
11+
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/validator"
12+
"github.com/grpc-ecosystem/go-grpc-middleware/v2/testing/testpb"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
"github.com/stretchr/testify/suite"
16+
"google.golang.org/grpc"
17+
"google.golang.org/grpc/codes"
18+
"google.golang.org/grpc/status"
19+
)
20+
21+
type TestLogger struct{}
22+
23+
func (l *TestLogger) Log(lvl logging.Level, msg string) {}
24+
25+
func (l *TestLogger) With(fields ...string) logging.Logger {
26+
return &TestLogger{}
27+
}
28+
29+
type ValidatorTestSuite struct {
30+
*testpb.InterceptorTestSuite
31+
}
32+
33+
func (s *ValidatorTestSuite) TestValidPasses_Unary() {
34+
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing)
35+
assert.NoError(s.T(), err, "no error expected")
36+
}
37+
38+
func (s *ValidatorTestSuite) TestInvalidErrors_Unary() {
39+
_, err := s.Client.Ping(s.SimpleCtx(), testpb.BadPing)
40+
assert.Error(s.T(), err, "no error expected")
41+
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument")
42+
}
43+
44+
func (s *ValidatorTestSuite) TestValidPasses_ServerStream() {
45+
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList)
46+
require.NoError(s.T(), err, "no error on stream establishment expected")
47+
for {
48+
_, err := stream.Recv()
49+
if err == io.EOF {
50+
break
51+
}
52+
assert.NoError(s.T(), err, "no error on messages sent occurred")
53+
}
54+
}
55+
56+
type ClientValidatorTestSuite struct {
57+
*testpb.InterceptorTestSuite
58+
}
59+
60+
func (s *ClientValidatorTestSuite) TestValidPasses_Unary() {
61+
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing)
62+
assert.NoError(s.T(), err, "no error expected")
63+
}
64+
65+
func (s *ClientValidatorTestSuite) TestInvalidErrors_Unary() {
66+
_, err := s.Client.Ping(s.SimpleCtx(), testpb.BadPing)
67+
assert.Error(s.T(), err, "error expected")
68+
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument")
69+
}
70+
71+
func (s *ValidatorTestSuite) TestInvalidErrors_ServerStream() {
72+
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.BadPingList)
73+
require.NoError(s.T(), err, "no error on stream establishment expected")
74+
_, err = stream.Recv()
75+
assert.Error(s.T(), err, "error should be received on first message")
76+
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument")
77+
}
78+
79+
func (s *ValidatorTestSuite) TestInvalidErrors_BidiStream() {
80+
stream, err := s.Client.PingStream(s.SimpleCtx())
81+
require.NoError(s.T(), err, "no error on stream establishment expected")
82+
83+
require.NoError(s.T(), stream.Send(testpb.GoodPingStream))
84+
_, err = stream.Recv()
85+
assert.NoError(s.T(), err, "receiving a good ping should return a good pong")
86+
require.NoError(s.T(), stream.Send(testpb.GoodPingStream))
87+
_, err = stream.Recv()
88+
assert.NoError(s.T(), err, "receiving a good ping should return a good pong")
89+
90+
require.NoError(s.T(), stream.Send(testpb.BadPingStream))
91+
_, err = stream.Recv()
92+
assert.Error(s.T(), err, "receiving a bad ping should return a bad pong")
93+
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument")
94+
95+
err = stream.CloseSend()
96+
assert.NoError(s.T(), err, "there should be no error closing the stream on send")
97+
}
98+
99+
func TestValidatorTestSuite(t *testing.T) {
100+
sWithNoArgs := &ValidatorTestSuite{
101+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
102+
ServerOpts: []grpc.ServerOption{
103+
grpc.StreamInterceptor(validator.StreamServerInterceptor()),
104+
grpc.UnaryInterceptor(validator.UnaryServerInterceptor()),
105+
},
106+
},
107+
}
108+
suite.Run(t, sWithNoArgs)
109+
110+
sWithWithFailFastArgs := &ValidatorTestSuite{
111+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
112+
ServerOpts: []grpc.ServerOption{
113+
grpc.StreamInterceptor(validator.StreamServerInterceptor(validator.WithFailFast())),
114+
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithFailFast())),
115+
},
116+
},
117+
}
118+
suite.Run(t, sWithWithFailFastArgs)
119+
120+
sWithWithLoggerArgs := &ValidatorTestSuite{
121+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
122+
ServerOpts: []grpc.ServerOption{
123+
grpc.StreamInterceptor(validator.StreamServerInterceptor(validator.WithLogger(logging.DEBUG, &TestLogger{}))),
124+
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithLogger(logging.DEBUG, &TestLogger{}))),
125+
},
126+
},
127+
}
128+
suite.Run(t, sWithWithLoggerArgs)
129+
130+
sAll := &ValidatorTestSuite{
131+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
132+
ServerOpts: []grpc.ServerOption{
133+
grpc.StreamInterceptor(validator.StreamServerInterceptor(validator.WithFailFast(), validator.WithLogger(logging.DEBUG, &TestLogger{}))),
134+
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithFailFast(), validator.WithLogger(logging.DEBUG, &TestLogger{}))),
135+
},
136+
},
137+
}
138+
suite.Run(t, sAll)
139+
140+
csWithNoArgs := &ClientValidatorTestSuite{
141+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
142+
ClientOpts: []grpc.DialOption{
143+
grpc.WithUnaryInterceptor(validator.UnaryClientInterceptor()),
144+
},
145+
},
146+
}
147+
suite.Run(t, csWithNoArgs)
148+
149+
csWithWithFailFastArgs := &ClientValidatorTestSuite{
150+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
151+
ServerOpts: []grpc.ServerOption{
152+
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithFailFast())),
153+
},
154+
},
155+
}
156+
suite.Run(t, csWithWithFailFastArgs)
157+
158+
csWithWithLoggerArgs := &ClientValidatorTestSuite{
159+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
160+
ServerOpts: []grpc.ServerOption{
161+
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithLogger(logging.DEBUG, &TestLogger{}))),
162+
},
163+
},
164+
}
165+
suite.Run(t, csWithWithLoggerArgs)
166+
167+
csAll := &ClientValidatorTestSuite{
168+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
169+
ClientOpts: []grpc.DialOption{
170+
grpc.WithUnaryInterceptor(validator.UnaryClientInterceptor(validator.WithFailFast())),
171+
},
172+
},
173+
}
174+
suite.Run(t, csAll)
175+
}

interceptors/validator/options.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) The go-grpc-middleware Authors.
2+
// Licensed under the Apache License 2.0.
3+
4+
package validator
5+
6+
import "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
7+
8+
var (
9+
defaultOptions = &options{
10+
level: "",
11+
logger: nil,
12+
shouldFailFast: false,
13+
}
14+
)
15+
16+
type options struct {
17+
level logging.Level
18+
logger logging.Logger
19+
shouldFailFast bool
20+
}
21+
22+
type Option func(*options)
23+
24+
func evaluateServerOpt(opts []Option) *options {
25+
optCopy := &options{}
26+
*optCopy = *defaultOptions
27+
for _, o := range opts {
28+
o(optCopy)
29+
}
30+
return optCopy
31+
}
32+
33+
func evaluateClientOpt(opts []Option) *options {
34+
optCopy := &options{}
35+
*optCopy = *defaultOptions
36+
for _, o := range opts {
37+
o(optCopy)
38+
}
39+
return optCopy
40+
}
41+
42+
// WithLogger tells validator to log all the validation errors with the given log level.
43+
func WithLogger(level logging.Level, logger logging.Logger) Option {
44+
return func(o *options) {
45+
o.level = level
46+
o.logger = logger
47+
}
48+
}
49+
50+
// WithFailFast tells validator to immediately stop doing further validation after first validation error.
51+
func WithFailFast() Option {
52+
return func(o *options) {
53+
o.shouldFailFast = true
54+
}
55+
}

0 commit comments

Comments
 (0)