diff --git a/go.mod b/go.mod index b4316a1..fd2d73a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module go.unistack.org/micro-client-grpc/v3 +module go.unistack.org/micro-client-grpc/v4 -go 1.22.0 +go 1.23.0 require ( go.unistack.org/micro/v4 v4.1.2 @@ -13,10 +13,10 @@ require ( github.com/matoous/go-nanoid v1.5.1 // indirect github.com/spf13/cast v1.7.1 // indirect go.unistack.org/micro-proto/v4 v4.1.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect - google.golang.org/protobuf v1.36.3 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 513fc1a..5710689 100644 --- a/go.sum +++ b/go.sum @@ -38,18 +38,18 @@ go.unistack.org/micro-proto/v4 v4.1.0 h1:qPwL2n/oqh9RE3RTTDgt28XK3QzV597VugQPaw9 go.unistack.org/micro-proto/v4 v4.1.0/go.mod h1:ArmK7o+uFvxSY3dbJhKBBX4Pm1rhWdLEFf3LxBrMtec= go.unistack.org/micro/v4 v4.1.2 h1:9SOlPYyPNNFpg1A7BsvhDyQm3gysLH1AhWbDCp1hyoY= go.unistack.org/micro/v4 v4.1.2/go.mod h1:lr3oYED8Ay1vjK68QqRw30QOtdk/ffpZqMFDasOUhKw= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 h1:ZSlhAUqC4r8TPzqLXQ0m3upBNZeF+Y8jQ3c4CR3Ujms= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/grpc.go b/grpc.go index 1c26816..8297b82 100644 --- a/grpc.go +++ b/grpc.go @@ -5,8 +5,14 @@ import ( "context" "crypto/tls" "fmt" + "go.unistack.org/micro/v4/options" + "go.unistack.org/micro/v4/semconv" + "go.unistack.org/micro/v4/tracer" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "net" "reflect" + "strconv" "strings" "sync" "time" @@ -28,8 +34,10 @@ const ( ) type grpcClient struct { - pool *ConnPool - opts client.Options + funcCall client.FuncCall + funcStream client.FuncStream + pool *ConnPool + opts client.Options sync.RWMutex init bool } @@ -69,10 +77,7 @@ func (g *grpcClient) call(ctx context.Context, addr string, req client.Request, var header map[string][]string if md, ok := metadata.FromOutgoingContext(ctx); ok { - header = make(map[string][]string, len(md)) - for k, v := range md { - header[strings.ToLower(k)] = v - } + header = metadata.Copy(md) } else { header = make(map[string][]string, 2) } @@ -82,12 +87,11 @@ func (g *grpcClient) call(ctx context.Context, addr string, req client.Request, } } // set timeout in nanoseconds - header["Grpc-Timeout"] = append(header["Grpc-Timeout"], fmt.Sprintf("%dn", opts.RequestTimeout)) + header["grpc-timeout"] = append(header["grpc-timeout"], fmt.Sprintf("%dn", opts.RequestTimeout)) header["timeout"] = append(header["timeout"], fmt.Sprintf("%dn", opts.RequestTimeout)) header["content-type"] = append(header["content-type"], req.ContentType()) - md := gmetadata.MD(metadata.Copy(header)) - ctx = gmetadata.NewOutgoingContext(ctx, md) + ctx = gmetadata.NewOutgoingContext(ctx, header) cf, err := g.newCodec(req.ContentType()) if err != nil { @@ -170,7 +174,7 @@ func (g *grpcClient) call(ctx context.Context, addr string, req client.Request, if opts.ResponseMetadata != nil { *opts.ResponseMetadata = metadata.New(gmd.Len()) for k, v := range gmd { - opts.ResponseMetadata.Set(k, strings.Join(v, ",")) + opts.ResponseMetadata.Append(k, v...) } } @@ -181,24 +185,20 @@ func (g *grpcClient) stream(ctx context.Context, addr string, req client.Request var header map[string][]string if md, ok := metadata.FromOutgoingContext(ctx); ok { - header = make(map[string][]string, len(md)) - for k, v := range md { - header[k] = v - } + header = metadata.Copy(md) } else { header = make(map[string][]string) } // set timeout in nanoseconds if opts.StreamTimeout > time.Duration(0) { - header["Grpc-Timeout"] = append(header["Grpc-Timeout"], fmt.Sprintf("%dn", opts.RequestTimeout)) + header["grpc-timeout"] = append(header["grpc-timeout"], fmt.Sprintf("%dn", opts.RequestTimeout)) header["timeout"] = append(header["timeout"], fmt.Sprintf("%dn", opts.RequestTimeout)) } // set the content type for the request header["content-type"] = append(header["content-type"], req.ContentType()) - md := gmetadata.MD(metadata.Copy(header)) - ctx = gmetadata.NewOutgoingContext(ctx, md) + ctx = gmetadata.NewOutgoingContext(ctx, header) cf, err := g.newCodec(req.ContentType()) if err != nil { @@ -401,22 +401,17 @@ func (g *grpcClient) Init(opts ...client.Option) error { g.pool.Unlock() } - if err := g.opts.Broker.Init(); err != nil { - return err - } - if err := g.opts.Tracer.Init(); err != nil { - return err - } - if err := g.opts.Router.Init(); err != nil { - return err - } - if err := g.opts.Logger.Init(); err != nil { - return err - } - if err := g.opts.Meter.Init(); err != nil { - return err - } + g.funcCall = g.fnCall + g.funcStream = g.fnStream + g.opts.Hooks.EachPrev(func(hook options.Hook) { + switch h := hook.(type) { + case client.HookCall: + g.funcCall = h(g.funcCall) + case client.HookStream: + g.funcStream = h(g.funcStream) + } + }) g.init = true return nil @@ -430,12 +425,7 @@ func (g *grpcClient) NewRequest(service, method string, req interface{}, reqOpts return newGRPCRequest(service, method, req, g.opts.ContentType, reqOpts...) } -func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { - if req == nil { - return errors.InternalServerError("go.micro.client", "req is nil") - } else if rsp == nil { - return errors.InternalServerError("go.micro.client", "rsp is nil") - } +func (g *grpcClient) fnCall(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { // make a copy of call opts callOpts := g.opts.CallOptions @@ -565,7 +555,38 @@ func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface return gerr } -func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { +func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { + if req == nil { + return errors.InternalServerError("go.micro.client", "req is nil") + } else if rsp == nil { + return errors.InternalServerError("go.micro.client", "rsp is nil") + } + + ts := time.Now() + g.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc() + var sp tracer.Span + ctx, sp = g.opts.Tracer.Start(ctx, req.Endpoint()+" rpc-client", + tracer.WithSpanKind(tracer.SpanKindClient), + tracer.WithSpanLabels("endpoint", req.Endpoint()), + ) + err := g.funcCall(ctx, req, rsp, opts...) + g.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec() + te := time.Since(ts) + g.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds()) + g.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds()) + + if me := errors.FromError(err); me == nil { + sp.Finish() + g.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(200))).Inc() + } else { + sp.SetStatus(tracer.SpanStatusError, err.Error()) + g.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code))).Inc() + } + + return err +} + +func (g *grpcClient) fnStream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { // make a copy of call opts callOpts := g.opts.CallOptions for _, opt := range opts { @@ -693,6 +714,31 @@ func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...cli return nil, grr } +func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { + ts := time.Now() + g.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc() + var sp tracer.Span + ctx, sp = g.opts.Tracer.Start(ctx, req.Endpoint()+" rpc-client", + tracer.WithSpanKind(tracer.SpanKindClient), + tracer.WithSpanLabels("endpoint", req.Endpoint()), + ) + stream, err := g.funcStream(ctx, req, opts...) + g.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec() + te := time.Since(ts) + g.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds()) + g.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds()) + + if me := status.Convert(err); me == nil { + sp.Finish() + g.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(codes.OK))).Inc() + } else { + sp.SetStatus(tracer.SpanStatusError, err.Error()) + g.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code()))).Inc() + } + + return stream, err +} + func (g *grpcClient) String() string { return "grpc" }