Skip to content

Commit

Permalink
[interceptors/validator] feat: add error logging in validator (#544)
Browse files Browse the repository at this point in the history
* 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
rohanraj7316 authored Mar 26, 2023
1 parent 85304c0 commit e41e6bd
Show file tree
Hide file tree
Showing 5 changed files with 365 additions and 207 deletions.
84 changes: 84 additions & 0 deletions interceptors/validator/interceptors.go
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
}
175 changes: 175 additions & 0 deletions interceptors/validator/interceptors_test.go
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)
}
55 changes: 55 additions & 0 deletions interceptors/validator/options.go
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
}
}
Loading

0 comments on commit e41e6bd

Please sign in to comment.