-
Notifications
You must be signed in to change notification settings - Fork 691
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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
- Loading branch information
1 parent
85304c0
commit e41e6bd
Showing
5 changed files
with
365 additions
and
207 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// Copyright (c) The go-grpc-middleware Authors. | ||
// Licensed under the Apache License 2.0. | ||
|
||
package validator | ||
|
||
import ( | ||
"context" | ||
|
||
"google.golang.org/grpc" | ||
) | ||
|
||
// UnaryServerInterceptor returns a new unary server interceptor that validates incoming messages. | ||
// | ||
// Invalid messages will be rejected with `InvalidArgument` before reaching any userspace handlers. | ||
// If `WithFailFast` used it will interceptor and returns the first validation error. Otherwise, the interceptor | ||
// returns ALL validation error as a wrapped multi-error. | ||
// If `WithLogger` used it will log all the validation errors. Otherwise, no default logging. | ||
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation | ||
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored. | ||
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor { | ||
o := evaluateServerOpt(opts) | ||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { | ||
if err := validate(req, o.shouldFailFast, o.level, o.logger); err != nil { | ||
return nil, err | ||
} | ||
return handler(ctx, req) | ||
} | ||
} | ||
|
||
// UnaryClientInterceptor returns a new unary client interceptor that validates outgoing messages. | ||
// | ||
// Invalid messages will be rejected with `InvalidArgument` before sending the request to server. | ||
// If `WithFailFast` used it will interceptor and returns the first validation error. Otherwise, the interceptor | ||
// returns ALL validation error as a wrapped multi-error. | ||
// If `WithLogger` used it will log all the validation errors. Otherwise, no default logging. | ||
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation | ||
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored. | ||
func UnaryClientInterceptor(opts ...Option) grpc.UnaryClientInterceptor { | ||
o := evaluateClientOpt(opts) | ||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { | ||
if err := validate(req, o.shouldFailFast, o.level, o.logger); err != nil { | ||
return err | ||
} | ||
return invoker(ctx, method, req, reply, cc, opts...) | ||
} | ||
} | ||
|
||
// StreamServerInterceptor returns a new streaming server interceptor that validates incoming messages. | ||
// | ||
// If `WithFailFast` used it will interceptor and returns the first validation error. Otherwise, the interceptor | ||
// returns ALL validation error as a wrapped multi-error. | ||
// If `WithLogger` used it will log all the validation errors. Otherwise, no default logging. | ||
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation | ||
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored. | ||
// The stage at which invalid messages will be rejected with `InvalidArgument` varies based on the | ||
// type of the RPC. For `ServerStream` (1:m) requests, it will happen before reaching any userspace | ||
// handlers. For `ClientStream` (n:1) or `BidiStream` (n:m) RPCs, the messages will be rejected on | ||
// calls to `stream.Recv()`. | ||
func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor { | ||
o := evaluateServerOpt(opts) | ||
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { | ||
wrapper := &recvWrapper{ | ||
options: o, | ||
ServerStream: stream, | ||
} | ||
|
||
return handler(srv, wrapper) | ||
} | ||
} | ||
|
||
type recvWrapper struct { | ||
*options | ||
grpc.ServerStream | ||
} | ||
|
||
func (s *recvWrapper) RecvMsg(m any) error { | ||
if err := s.ServerStream.RecvMsg(m); err != nil { | ||
return err | ||
} | ||
if err := validate(m, s.shouldFailFast, s.level, s.logger); err != nil { | ||
return err | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// Copyright (c) The go-grpc-middleware Authors. | ||
// Licensed under the Apache License 2.0. | ||
|
||
package validator_test | ||
|
||
import ( | ||
"io" | ||
"testing" | ||
|
||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" | ||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/validator" | ||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/testing/testpb" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"github.com/stretchr/testify/suite" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
type TestLogger struct{} | ||
|
||
func (l *TestLogger) Log(lvl logging.Level, msg string) {} | ||
|
||
func (l *TestLogger) With(fields ...string) logging.Logger { | ||
return &TestLogger{} | ||
} | ||
|
||
type ValidatorTestSuite struct { | ||
*testpb.InterceptorTestSuite | ||
} | ||
|
||
func (s *ValidatorTestSuite) TestValidPasses_Unary() { | ||
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing) | ||
assert.NoError(s.T(), err, "no error expected") | ||
} | ||
|
||
func (s *ValidatorTestSuite) TestInvalidErrors_Unary() { | ||
_, err := s.Client.Ping(s.SimpleCtx(), testpb.BadPing) | ||
assert.Error(s.T(), err, "no error expected") | ||
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument") | ||
} | ||
|
||
func (s *ValidatorTestSuite) TestValidPasses_ServerStream() { | ||
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList) | ||
require.NoError(s.T(), err, "no error on stream establishment expected") | ||
for { | ||
_, err := stream.Recv() | ||
if err == io.EOF { | ||
break | ||
} | ||
assert.NoError(s.T(), err, "no error on messages sent occurred") | ||
} | ||
} | ||
|
||
type ClientValidatorTestSuite struct { | ||
*testpb.InterceptorTestSuite | ||
} | ||
|
||
func (s *ClientValidatorTestSuite) TestValidPasses_Unary() { | ||
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing) | ||
assert.NoError(s.T(), err, "no error expected") | ||
} | ||
|
||
func (s *ClientValidatorTestSuite) TestInvalidErrors_Unary() { | ||
_, err := s.Client.Ping(s.SimpleCtx(), testpb.BadPing) | ||
assert.Error(s.T(), err, "error expected") | ||
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument") | ||
} | ||
|
||
func (s *ValidatorTestSuite) TestInvalidErrors_ServerStream() { | ||
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.BadPingList) | ||
require.NoError(s.T(), err, "no error on stream establishment expected") | ||
_, err = stream.Recv() | ||
assert.Error(s.T(), err, "error should be received on first message") | ||
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument") | ||
} | ||
|
||
func (s *ValidatorTestSuite) TestInvalidErrors_BidiStream() { | ||
stream, err := s.Client.PingStream(s.SimpleCtx()) | ||
require.NoError(s.T(), err, "no error on stream establishment expected") | ||
|
||
require.NoError(s.T(), stream.Send(testpb.GoodPingStream)) | ||
_, err = stream.Recv() | ||
assert.NoError(s.T(), err, "receiving a good ping should return a good pong") | ||
require.NoError(s.T(), stream.Send(testpb.GoodPingStream)) | ||
_, err = stream.Recv() | ||
assert.NoError(s.T(), err, "receiving a good ping should return a good pong") | ||
|
||
require.NoError(s.T(), stream.Send(testpb.BadPingStream)) | ||
_, err = stream.Recv() | ||
assert.Error(s.T(), err, "receiving a bad ping should return a bad pong") | ||
assert.Equal(s.T(), codes.InvalidArgument, status.Code(err), "gRPC status must be InvalidArgument") | ||
|
||
err = stream.CloseSend() | ||
assert.NoError(s.T(), err, "there should be no error closing the stream on send") | ||
} | ||
|
||
func TestValidatorTestSuite(t *testing.T) { | ||
sWithNoArgs := &ValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ServerOpts: []grpc.ServerOption{ | ||
grpc.StreamInterceptor(validator.StreamServerInterceptor()), | ||
grpc.UnaryInterceptor(validator.UnaryServerInterceptor()), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, sWithNoArgs) | ||
|
||
sWithWithFailFastArgs := &ValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ServerOpts: []grpc.ServerOption{ | ||
grpc.StreamInterceptor(validator.StreamServerInterceptor(validator.WithFailFast())), | ||
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithFailFast())), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, sWithWithFailFastArgs) | ||
|
||
sWithWithLoggerArgs := &ValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ServerOpts: []grpc.ServerOption{ | ||
grpc.StreamInterceptor(validator.StreamServerInterceptor(validator.WithLogger(logging.DEBUG, &TestLogger{}))), | ||
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithLogger(logging.DEBUG, &TestLogger{}))), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, sWithWithLoggerArgs) | ||
|
||
sAll := &ValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ServerOpts: []grpc.ServerOption{ | ||
grpc.StreamInterceptor(validator.StreamServerInterceptor(validator.WithFailFast(), validator.WithLogger(logging.DEBUG, &TestLogger{}))), | ||
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithFailFast(), validator.WithLogger(logging.DEBUG, &TestLogger{}))), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, sAll) | ||
|
||
csWithNoArgs := &ClientValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ClientOpts: []grpc.DialOption{ | ||
grpc.WithUnaryInterceptor(validator.UnaryClientInterceptor()), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, csWithNoArgs) | ||
|
||
csWithWithFailFastArgs := &ClientValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ServerOpts: []grpc.ServerOption{ | ||
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithFailFast())), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, csWithWithFailFastArgs) | ||
|
||
csWithWithLoggerArgs := &ClientValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ServerOpts: []grpc.ServerOption{ | ||
grpc.UnaryInterceptor(validator.UnaryServerInterceptor(validator.WithLogger(logging.DEBUG, &TestLogger{}))), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, csWithWithLoggerArgs) | ||
|
||
csAll := &ClientValidatorTestSuite{ | ||
InterceptorTestSuite: &testpb.InterceptorTestSuite{ | ||
ClientOpts: []grpc.DialOption{ | ||
grpc.WithUnaryInterceptor(validator.UnaryClientInterceptor(validator.WithFailFast())), | ||
}, | ||
}, | ||
} | ||
suite.Run(t, csAll) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright (c) The go-grpc-middleware Authors. | ||
// Licensed under the Apache License 2.0. | ||
|
||
package validator | ||
|
||
import "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" | ||
|
||
var ( | ||
defaultOptions = &options{ | ||
level: "", | ||
logger: nil, | ||
shouldFailFast: false, | ||
} | ||
) | ||
|
||
type options struct { | ||
level logging.Level | ||
logger logging.Logger | ||
shouldFailFast bool | ||
} | ||
|
||
type Option func(*options) | ||
|
||
func evaluateServerOpt(opts []Option) *options { | ||
optCopy := &options{} | ||
*optCopy = *defaultOptions | ||
for _, o := range opts { | ||
o(optCopy) | ||
} | ||
return optCopy | ||
} | ||
|
||
func evaluateClientOpt(opts []Option) *options { | ||
optCopy := &options{} | ||
*optCopy = *defaultOptions | ||
for _, o := range opts { | ||
o(optCopy) | ||
} | ||
return optCopy | ||
} | ||
|
||
// WithLogger tells validator to log all the validation errors with the given log level. | ||
func WithLogger(level logging.Level, logger logging.Logger) Option { | ||
return func(o *options) { | ||
o.level = level | ||
o.logger = logger | ||
} | ||
} | ||
|
||
// WithFailFast tells validator to immediately stop doing further validation after first validation error. | ||
func WithFailFast() Option { | ||
return func(o *options) { | ||
o.shouldFailFast = true | ||
} | ||
} |
Oops, something went wrong.