diff --git a/README.md b/README.md index 9ef3dd9..feb66b3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Features: - [ ] mdns - [ ] kubernetes - [ ] default interceptors implementation: + - [ ] default - [ ] validation - [ ] health - [ ] context logger @@ -24,8 +25,8 @@ Features: - [ ] rate-limiting - [ ] auth claim in context - [ ] recovery - - [ ] tracing (open-tracing) - - [ ] metrics (prometheus) + - [x] tracing (open-tracing) + - [x] metrics (prometheus) - [ ] retries - [ ] context DB / transaction - ... diff --git a/example/example.go b/example/example.go index 1b05f4b..87ab274 100644 --- a/example/example.go +++ b/example/example.go @@ -14,6 +14,8 @@ import ( "github.com/sirupsen/logrus" "go.linka.cloud/grpc/client" + metrics2 "go.linka.cloud/grpc/interceptors/metrics" + validation2 "go.linka.cloud/grpc/interceptors/validation" "go.linka.cloud/grpc/logger" "go.linka.cloud/grpc/registry/mdns" "go.linka.cloud/grpc/service" @@ -32,6 +34,7 @@ func (g *GreeterHandler) SayHello(ctx context.Context, req *HelloRequest) (*Hell } func (g *GreeterHandler) SayHelloStream(req *HelloStreamRequest, s Greeter_SayHelloStreamServer) error { + for i := int64(0); i < req.Count; i++ { if err := s.Send(&HelloReply{Message: fmt.Sprintf("Hello %s (%d)!", req.Name, i+1)}); err != nil { return err @@ -64,6 +67,8 @@ func main() { defer cancel() var svc service.Service var err error + metrics := metrics2.NewInterceptors() + validation := validation2.NewInterceptors(true) address := "0.0.0.0:9991" svc, err = service.New( service.WithContext(ctx), @@ -88,6 +93,7 @@ func main() { service.WithGRPCWeb(true), service.WithGRPCWebPrefix("/grpc"), service.WithMiddlewares(httpLogger), + service.WithInterceptors(validation, metrics), ) if err != nil { panic(err) @@ -115,6 +121,10 @@ func main() { logrus.Fatal(err) } logrus.Infof("received message: %s", res.Message) + res, err = g.SayHello(context.Background(), &HelloRequest{}) + if err == nil { + logrus.Fatal("expected validation error") + } stream, err := g.SayHelloStream(context.Background(), &HelloStreamRequest{Name: "test", Count: 10}) if err != nil { logrus.Fatal(err) diff --git a/example/example.pb.go b/example/example.pb.go index 41e7fa0..70a797f 100644 --- a/example/example.pb.go +++ b/example/example.pb.go @@ -10,6 +10,7 @@ import ( reflect "reflect" sync "sync" + _ "github.com/envoyproxy/protoc-gen-validate/validate" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -178,30 +179,33 @@ var file_example_example_proto_rawDesc = []byte{ 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, - 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x3e, 0x0a, - 0x12, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x32, 0xb7, 0x01, - 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x08, 0x53, 0x61, 0x79, - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, - 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, - 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, - 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, - 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x3a, 0x01, 0x2a, 0x12, 0x4c, 0x0a, 0x0e, 0x53, 0x61, 0x79, - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1e, 0x2e, 0x68, 0x65, - 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, - 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, - 0x70, 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x42, 0x22, 0x5a, 0x20, 0x67, 0x6f, 0x2e, 0x6c, 0x69, - 0x6e, 0x6b, 0x61, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x65, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x3b, 0x6d, 0x61, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2d, 0x0a, 0x0c, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x02, 0x18, 0x28, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, + 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x49, 0x0a, 0x12, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x02, 0x18, 0x28, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x32, 0xb7, 0x01, 0x0a, + 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, + 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, + 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x2f, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x3a, 0x01, 0x2a, 0x12, 0x4c, 0x0a, 0x0e, 0x53, 0x61, 0x79, 0x48, + 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x42, 0x22, 0x5a, 0x20, 0x67, 0x6f, 0x2e, 0x6c, 0x69, 0x6e, + 0x6b, 0x61, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x3b, 0x6d, 0x61, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/example/example.pb.validate.go b/example/example.pb.validate.go index bc3cad3..980331c 100644 --- a/example/example.pb.validate.go +++ b/example/example.pb.validate.go @@ -46,7 +46,16 @@ func (m *HelloRequest) Validate(all bool) error { var errors []error - // no validation rules for Name + if l := utf8.RuneCountInString(m.GetName()); l < 2 || l > 40 { + err := HelloRequestValidationError{ + field: "Name", + reason: "value length must be between 2 and 40 runes, inclusive", + } + if !all { + return err + } + errors = append(errors, err) + } if len(errors) > 0 { return HelloRequestMultiError(errors) @@ -229,7 +238,16 @@ func (m *HelloStreamRequest) Validate(all bool) error { var errors []error - // no validation rules for Name + if l := utf8.RuneCountInString(m.GetName()); l < 2 || l > 40 { + err := HelloStreamRequestValidationError{ + field: "Name", + reason: "value length must be between 2 and 40 runes, inclusive", + } + if !all { + return err + } + errors = append(errors, err) + } // no validation rules for Count diff --git a/example/example.proto b/example/example.proto index 11399a8..9f860d6 100644 --- a/example/example.proto +++ b/example/example.proto @@ -5,6 +5,7 @@ package helloworld; option go_package = "go.linka.cloud/grpc/example;main"; import "google/api/annotations.proto"; +import "validate/validate.proto"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) { @@ -17,7 +18,7 @@ service Greeter { } message HelloRequest { - string name = 1; + string name = 1 [(validate.rules).string = {min_len: 2, max_len: 40}]; } message HelloReply { @@ -25,6 +26,6 @@ message HelloReply { } message HelloStreamRequest { - string name = 1; + string name = 1 [(validate.rules).string = {min_len: 2, max_len: 40}]; int64 count = 2; } diff --git a/go.mod b/go.mod index 2bb1fa7..af5fcbc 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,9 @@ require ( github.com/golang/protobuf v1.5.2 github.com/google/uuid v1.1.2 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 github.com/iancoleman/strcase v0.2.0 // indirect github.com/improbable-eng/grpc-web v0.14.1 github.com/jinzhu/gorm v1.9.12 @@ -19,6 +21,7 @@ require ( github.com/lyft/protoc-gen-star v0.6.0 // indirect github.com/miekg/dns v1.1.35 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/opentracing/opentracing-go v1.1.0 github.com/planetscale/vtprotobuf v0.2.0 github.com/rs/cors v1.7.0 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index b7974ee..d247823 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,7 @@ github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZw github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= @@ -102,7 +103,9 @@ github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oD github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -299,6 +302,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -306,6 +310,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -429,6 +435,7 @@ github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lL github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -475,6 +482,7 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWEr github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= @@ -504,12 +512,14 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -528,6 +538,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= diff --git a/interceptors/interceptors.go b/interceptors/interceptors.go new file mode 100644 index 0000000..72ae9cb --- /dev/null +++ b/interceptors/interceptors.go @@ -0,0 +1,20 @@ +package interceptors + +import ( + "google.golang.org/grpc" +) + +type ServerInterceptors interface { + UnaryServerInterceptor() grpc.UnaryServerInterceptor + StreamServerInterceptor() grpc.StreamServerInterceptor +} + +type ClientInterceptors interface { + UnaryClientInterceptor() grpc.UnaryClientInterceptor + StreamClientInterceptor() grpc.StreamClientInterceptor +} + +type Interceptors interface { + ServerInterceptors + ClientInterceptors +} diff --git a/interceptors/metrics/interceptors.go b/interceptors/metrics/interceptors.go new file mode 100644 index 0000000..da98cc9 --- /dev/null +++ b/interceptors/metrics/interceptors.go @@ -0,0 +1,57 @@ +package metrics + +import ( + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "google.golang.org/grpc" + + "go.linka.cloud/grpc/interceptors" +) + +type metrics struct { + s *grpc_prometheus.ServerMetrics + c *grpc_prometheus.ClientMetrics +} + +func NewInterceptors(opts ...grpc_prometheus.CounterOption) interceptors.Interceptors { + s := grpc_prometheus.NewServerMetrics(opts...) + c := grpc_prometheus.NewClientMetrics(opts...) + return &metrics{s: s, c: c} +} + +func NewServerInterceptors(opts ...grpc_prometheus.CounterOption) interceptors.ServerInterceptors { + s := grpc_prometheus.NewServerMetrics(opts...) + return &metrics{s: s} +} + +func NewClientInterceptors(opts ...grpc_prometheus.CounterOption) interceptors.ClientInterceptors { + c := grpc_prometheus.NewClientMetrics(opts...) + return &metrics{c: c} +} + +func DefaultInterceptors() interceptors.Interceptors { + return &metrics{s: grpc_prometheus.DefaultServerMetrics, c: grpc_prometheus.DefaultClientMetrics} +} + +func DefaultServerInterceptors() interceptors.ServerInterceptors { + return &metrics{s: grpc_prometheus.DefaultServerMetrics} +} + +func DefaultClientInterceptors() interceptors.ClientInterceptors { + return &metrics{c: grpc_prometheus.DefaultClientMetrics} +} + +func (m *metrics) UnaryServerInterceptor() grpc.UnaryServerInterceptor { + return m.s.UnaryServerInterceptor() +} + +func (m *metrics) StreamServerInterceptor() grpc.StreamServerInterceptor { + return m.s.StreamServerInterceptor() +} + +func (m *metrics) UnaryClientInterceptor() grpc.UnaryClientInterceptor { + return m.c.UnaryClientInterceptor() +} + +func (m *metrics) StreamClientInterceptor() grpc.StreamClientInterceptor { + return m.c.StreamClientInterceptor() +} diff --git a/interceptors/tracing/interceptors.go b/interceptors/tracing/interceptors.go new file mode 100644 index 0000000..5c44c4c --- /dev/null +++ b/interceptors/tracing/interceptors.go @@ -0,0 +1,41 @@ +package tracing + +import ( + "github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc" + "github.com/opentracing/opentracing-go" + "google.golang.org/grpc" + + "go.linka.cloud/grpc/interceptors" +) + +type tracing struct { + opts []otgrpc.Option +} + +func NewInterceptors(opts ...otgrpc.Option) interceptors.Interceptors { + return tracing{opts: opts} +} + +func NewClientInterceptors(opts ...otgrpc.Option) interceptors.ClientInterceptors { + return tracing{opts: opts} +} + +func (t tracing) UnaryClientInterceptor() grpc.UnaryClientInterceptor { + return otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer(), t.opts...) +} + +func (t tracing) StreamClientInterceptor() grpc.StreamClientInterceptor { + return otgrpc.OpenTracingStreamClientInterceptor(opentracing.GlobalTracer(), t.opts...) +} + +func NewServerInterceptors(opts ...otgrpc.Option) interceptors.ServerInterceptors { + return tracing{opts: opts} +} + +func (t tracing) UnaryServerInterceptor() grpc.UnaryServerInterceptor { + return otgrpc.OpenTracingServerInterceptor(opentracing.GlobalTracer(), t.opts...) +} + +func (t tracing) StreamServerInterceptor() grpc.StreamServerInterceptor { + return otgrpc.OpenTracingStreamServerInterceptor(opentracing.GlobalTracer(), t.opts...) +} diff --git a/interceptors/validation/interceptors.go b/interceptors/validation/interceptors.go new file mode 100644 index 0000000..958ae1a --- /dev/null +++ b/interceptors/validation/interceptors.go @@ -0,0 +1 @@ +package validation diff --git a/interceptors/validation/validation.go b/interceptors/validation/validation.go new file mode 100644 index 0000000..24196da --- /dev/null +++ b/interceptors/validation/validation.go @@ -0,0 +1,161 @@ +package validation + +import ( + "context" + + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + + "go.linka.cloud/grpc/errors" + "go.linka.cloud/grpc/interceptors" +) + +// The validate interface starting with protoc-gen-validate v0.6.0. +// See https://github.com/envoyproxy/protoc-gen-validate/pull/455. +type validator interface { + Validate(all bool) error +} + +// The validate interface prior to protoc-gen-validate v0.6.0. +type validatorLegacy interface { + Validate() error +} + +type validatorMultiError interface { + AllErrors() []error +} + +type validatorError interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} + +func validatorErrorToGrpc(e validatorError) *errdetails.BadRequest_FieldViolation { + return &errdetails.BadRequest_FieldViolation{ + Field: e.Field(), + Description: e.Reason(), + } +} + +func errToStatus(err error) error { + switch v := err.(type) { + case validatorError: + return errors.InvalidArgumentD(err, validatorErrorToGrpc(v)) + case validatorMultiError: + var details []proto.Message + for _, v := range v.AllErrors() { + if d, ok := v.(validatorError); ok { + details = append(details, validatorErrorToGrpc(d)) + } + } + return errors.InvalidArgumentd(err, details...) + default: + return errors.InvalidArgument(err) + } +} + +func (i interceptor) validate(req interface{}) error { + switch v := req.(type) { + case validatorLegacy: + if err := v.Validate(); err != nil { + return errToStatus(err) + } + case validator: + if err := v.Validate(i.all); err != nil { + return errToStatus(err) + } + } + return nil +} + +type interceptor struct { + all bool +} + +func NewInterceptors(validateAll bool) interceptors.Interceptors { + return &interceptor{all: validateAll} +} + +// UnaryServerInterceptor returns a new unary server interceptor that validates incoming messages. +// +// Invalid messages will be rejected with `InvalidArgument` before reaching any userspace handlers. +func (i interceptor) UnaryServerInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if err := i.validate(req); 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. +func (i interceptor) UnaryClientInterceptor() grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + if err := i.validate(req); err != nil { + return err + } + return invoker(ctx, method, req, reply, cc, opts...) + } +} + +// StreamServerInterceptor returns a new streaming server interceptor that validates incoming messages. +// +// 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 (i interceptor) StreamServerInterceptor() grpc.StreamServerInterceptor { + return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + wrapper := &recvWrapper{ServerStream: stream, i: i} + return handler(srv, wrapper) + } +} + +func (i interceptor) StreamClientInterceptor() grpc.StreamClientInterceptor { + return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + desc.Handler = (&sendWrapper{handler: desc.Handler, i: i}).Handler() + return streamer(ctx, desc, cc, method) + } +} + +type recvWrapper struct { + i interceptor + grpc.ServerStream +} + +func (s *recvWrapper) RecvMsg(m interface{}) error { + if err := s.ServerStream.RecvMsg(m); err != nil { + return err + } + + if err := s.i.validate(m); err != nil { + return err + } + + return nil +} + +type sendWrapper struct { + i interceptor + grpc.ServerStream + handler grpc.StreamHandler +} + +func (s *sendWrapper) Handler() grpc.StreamHandler { + return func(srv interface{}, stream grpc.ServerStream) error { + return s.handler(srv, s) + } +} + +func (s *sendWrapper) SendMsg(m interface{}) error { + if err := s.i.validate(m); err != nil { + return err + } + return s.ServerStream.SendMsg(m) +} diff --git a/service/options.go b/service/options.go index 193de65..43cb3ad 100644 --- a/service/options.go +++ b/service/options.go @@ -17,6 +17,7 @@ import ( "google.golang.org/grpc" "go.linka.cloud/grpc/certs" + "go.linka.cloud/grpc/interceptors" "go.linka.cloud/grpc/registry" "go.linka.cloud/grpc/transport" "go.linka.cloud/grpc/utils/addr" @@ -228,16 +229,45 @@ func WithAfterStop(fn ...func() error) Option { } } +func WithInterceptors(i ...interceptors.Interceptors) Option { + return func(o *options) { + for _, v := range i { + o.unaryServerInterceptors = append(o.unaryServerInterceptors, v.UnaryServerInterceptor()) + o.streamServerInterceptors = append(o.streamServerInterceptors, v.StreamServerInterceptor()) + o.unaryClientInterceptors = append(o.unaryClientInterceptors, v.UnaryClientInterceptor()) + o.streamClientInterceptors = append(o.streamClientInterceptors, v.StreamClientInterceptor()) + } + } +} + +func WithServerInterceptors(i ...interceptors.ServerInterceptors) Option { + return func(o *options) { + for _, v := range i { + o.unaryServerInterceptors = append(o.unaryServerInterceptors, v.UnaryServerInterceptor()) + o.streamServerInterceptors = append(o.streamServerInterceptors, v.StreamServerInterceptor()) + } + } +} + +func WithClientInterceptors(i ...interceptors.ClientInterceptors) Option { + return func(o *options) { + for _, v := range i { + o.unaryClientInterceptors = append(o.unaryClientInterceptors, v.UnaryClientInterceptor()) + o.streamClientInterceptors = append(o.streamClientInterceptors, v.StreamClientInterceptor()) + } + } +} + func WithUnaryClientInterceptor(i ...grpc.UnaryClientInterceptor) Option { return func(o *options) { - o.clientInterceptors = append(o.clientInterceptors, i...) + o.unaryClientInterceptors = append(o.unaryClientInterceptors, i...) } } // WithUnaryServerInterceptor adds unary Wrapper interceptors to the options passed into the server func WithUnaryServerInterceptor(i ...grpc.UnaryServerInterceptor) Option { return func(o *options) { - o.serverInterceptors = append(o.serverInterceptors, i...) + o.unaryServerInterceptors = append(o.unaryServerInterceptors, i...) } } @@ -340,10 +370,11 @@ type options struct { serverOpts []grpc.ServerOption - serverInterceptors []grpc.UnaryServerInterceptor + + unaryServerInterceptors []grpc.UnaryServerInterceptor streamServerInterceptors []grpc.StreamServerInterceptor - clientInterceptors []grpc.UnaryClientInterceptor + unaryClientInterceptors []grpc.UnaryClientInterceptor streamClientInterceptors []grpc.StreamClientInterceptor mux ServeMux @@ -428,7 +459,7 @@ func (o *options) ServerOpts() []grpc.ServerOption { } func (o *options) ServerInterceptors() []grpc.UnaryServerInterceptor { - return o.serverInterceptors + return o.unaryServerInterceptors } func (o *options) StreamServerInterceptors() []grpc.StreamServerInterceptor { @@ -436,7 +467,7 @@ func (o *options) StreamServerInterceptors() []grpc.StreamServerInterceptor { } func (o *options) ClientInterceptors() []grpc.UnaryClientInterceptor { - return o.clientInterceptors + return o.unaryClientInterceptors } func (o *options) StreamClientInterceptors() []grpc.StreamClientInterceptor { diff --git a/service/service.go b/service/service.go index 7445137..fe939bc 100644 --- a/service/service.go +++ b/service/service.go @@ -107,10 +107,10 @@ func newService(opts ...Option) (*service, error) { } return s.run() } - ui := grpcmiddleware.ChainUnaryServer(s.opts.serverInterceptors...) + ui := grpcmiddleware.ChainUnaryServer(s.opts.unaryServerInterceptors...) s.inproc = s.inproc.WithServerUnaryInterceptor(ui) - si := grpcmiddleware.ChainStreamServer(/*TODO(adphi): add to options*/) + si := grpcmiddleware.ChainStreamServer( /*TODO(adphi): add to options*/ ) s.inproc = s.inproc.WithServerStreamInterceptor(si) gopts := []grpc.ServerOption{ @@ -181,8 +181,8 @@ func (s *service) run() error { if reflect.DeepEqual(s.opts.cors, cors.Options{}) { s.opts.cors = cors.Options{ - AllowedHeaders: []string{"*"}, - AllowedMethods: []string{ + AllowedHeaders: []string{"*"}, + AllowedMethods: []string{ http.MethodGet, http.MethodPost, http.MethodPut, @@ -235,7 +235,7 @@ func (s *service) run() error { logrus.Warnf("received %v", sig) return s.Close() case err := <-errs: - if err != nil && !ignoreMuxError(err){ + if err != nil && !ignoreMuxError(err) { logrus.Error(err) return err } diff --git a/signals/signal_posix.go b/signals/signal_posix.go index 9bdb4e7..a0f00a7 100644 --- a/signals/signal_posix.go +++ b/signals/signal_posix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows /*