-
Notifications
You must be signed in to change notification settings - Fork 117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support instrumenting unary grpc #292
Conversation
original review: aws#264 (comment)
README.md
Outdated
|
||
```go | ||
grpcServer := grpc.NewServer( | ||
grpcmiddleware.WithUnaryServerChain( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably include below imports here to give clear idea about grpcmiddleware
package
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
grpc
had added similar chaining methods so we don't need to use grpc_middleware
now.
To apply the interceptors, we can use grpc.UnaryInterceptor
or grpc.WithUnaryInterceptor
directly. So I simplify the examples a bit and add comments to show how to apply multiple interceptors.
xray/grpc.go
Outdated
import ( | ||
"context" | ||
"errors" | ||
"github.com/golang/protobuf/proto" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we format this file to avoid review dog warnings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ran the go fmt
for the file, those warning should be fixed.
**grpc client** | ||
|
||
```go | ||
conn, err := grpc.Dial( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a little description here on how below grpc client and server instrumentation works ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a short description to point out the instrumentation is done by applying xray.UnaryServerInterceptor
or xray.UnaryClientInterceptor
.
format xray/grpc.go
add a description of unary interceptors usage. also, replace `grpc` by `gRPC` in the README.md to align with the name shows on gRPC official website. update examples to use grpc chain methods (remove the usage of `grpc_middleware`)
xray/grpc.go
Outdated
} | ||
|
||
// UnaryServerInterceptor provides gRPC unary server interceptor. | ||
func UnaryServerInterceptor(ctx context.Context, sn SegmentNamer) grpc.UnaryServerInterceptor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK, the ctx
here is only used to pass in the value of RecorderContextKey
, which should be a Config
.
Normally, it's a nil
so the recorder uses the global config.
I can see that for HTTP instrumentation, there's two types of wrapper:
Maybe we should break the interceptor into two signatures as well? One without context and another one with context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a fair call out. We can have two types of wrapper here as well with ctx
and without ctx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added UnaryServerInterceptorWithContext
.
Although grpc.WithInsecure is commonly used for development, it isn't necessary for using the xray interceptor.
refactor the original UnaryServerInterceptor into two signatures, one with ctx, another without ctx
xray/grpc.go
Outdated
seg.Unlock() | ||
|
||
resp, err = handler(ctx, req) | ||
recordContentLength(seg, resp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we also probably should include setting up response header and set _x_amzn_trace_id
value. Reference can be taken from here - https://github.com/tony84727/aws-xray-sdk-go/blob/support_grpc/xray/handler.go#L133. If we are not generating response header and add it as a value to the _x_amzn_trace_id
key then downstream services will keep on sampling even if we did sampling on upstream service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling grpc.SendHeader
to send the response trace header. grpc.SendHeader
might returns an error, I'm using multierr
to wrap multiple errors together. Not sure whether it's a good idea. Or should we just ignore the error and fail silently?
func UnaryClientInterceptor(host string) grpc.UnaryClientInterceptor { | ||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { | ||
return Capture(ctx, host, func(ctx context.Context) error { | ||
seg := GetSegment(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it is getting the server segment and is then mutated below. The unit test seems sane though, could you point out where the subsegment is being created for the client request?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, @anuraaga
I believe the subsegment is created by Capture
at line 26, which call BeginSubsegment
.
xray/grpc.go
Outdated
seg.Lock() | ||
seg.Namespace = "remote" | ||
seg.GetHTTP().GetRequest().URL = "grpc://" + host + method | ||
seg.GetHTTP().GetRequest().Method = method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be an HTTP method, not gRPC method. It's always POST
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
xray/grpc.go
Outdated
ctx, seg = NewSegmentFromHeader(ctx, name, &http.Request{ | ||
Host: host, | ||
URL: &requestURL, | ||
Method: info.FullMethod, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto this needs to be POST
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
xray/grpc.go
Outdated
seg.Lock() | ||
seg.GetHTTP().GetRequest().ClientIP, seg.GetHTTP().GetRequest().XForwardedFor = clientIPFromGrpcMetadata(md) | ||
seg.GetHTTP().GetRequest().URL = requestURL.String() | ||
seg.GetHTTP().GetRequest().Method = info.FullMethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
POST
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
xray/grpc.go
Outdated
} | ||
recordContentLength(seg, resp) | ||
if headerErr := addResponseTraceHeader(ctx, seg, traceHeader); headerErr != nil { | ||
err = multierr.Combine(err, headerErr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid adding this new library dependency on multierr
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As described here #292 (comment)
At this line there're two possible error sources:
- The error returned by the service handler itself
- The error returned by calling
grpc.SendHeader
I'm not sure how to report multiple errors within an interceptor. Maybe change this to print a line of error message when failling to send headers? What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can drop the error, it seems pretty rare. Alternatively a debug log message could be ok too. But yeah we shouldn't add it to the err
here. It's not quite as important if err
is actually set but imagine if its nil
- then it means our interceptor changes the status of the actual gRPC request to an error which will probably cause problems in user code. The interceptor should only record issues but not introduce errors to the business logic like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, using the internal logger to print the debug message.
README.md
Outdated
grpcServer := grpc.NewServer( | ||
// use grpc.ChainUnaryInterceptor instead to apply multiple interceptors | ||
grpc.UnaryInterceptor( | ||
xray.UnaryServerInterceptor(context.TODO(), xray.NewFixedSegmentNamer("myApp")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
xray.UnaryServerInterceptor(context.TODO(), xray.NewFixedSegmentNamer("myApp")), | |
xray.UnaryServerInterceptor(xray.NewFixedSegmentNamer("myApp")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. Thanks.
xray/grpc.go
Outdated
} | ||
|
||
// UnaryServerInterceptor provides gRPC unary server interceptor. | ||
func UnaryServerInterceptor(sn SegmentNamer) grpc.UnaryServerInterceptor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to have a default interceptor that uses the gRPC service name as the segment name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. So I guess there will be four variants of the server interceptor?
UnaryServerInterceptor()
UnaryServerInterceptorWithContext(ctx context.Context)
UnaryServerInterceptorWithNamer(sn SegmentNamer)
UnaryServerInterceptorWithContextAndNamer(ctx context.Context, sn SegmentNamer)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah - can you use the options pattern to have some symmetry with upstream gRPC and avoid the need for the explosion of factory methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
xray/grpc.go
Outdated
switch grpcStatus.Code() { | ||
case codes.ResourceExhausted: | ||
seg.Throttle = true | ||
case codes.Internal, codes.Unimplemented, codes.DataLoss: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error is like HTTP 4xx. This list is a good one to refer to for mapping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mapping doesn't classify all the possible grpc status codes here:
https://github.com/grpc/grpc-go/blob/dc77d7ffe311f78f2e577572d984af3c0a8df82b/codes/codes.go
So, I'm referencing the mapping here:
https://github.com/googleapis/googleapis/blob/b29fb6b9fcdf3bc67487fa22ab7783915023041e/google/rpc/code.proto#L35
mapping is referenced from https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
This reverts commit 5dbb380.
in addition, refactor UnaryServerInterceptor, apply options pattern to prevent multiple interceptor factory methods.
grpc-go seems to not using http.Client. So hooks of ClientTrace aren't invoked at all.
correct the example of UnaryServerInterceptor.
remove dep: multierr
correct the server interceptor example
README.md
Outdated
// use grpc.ChainUnaryInterceptor instead to apply multiple interceptors | ||
grpc.UnaryInterceptor( | ||
xray.UnaryServerInterceptor(), | ||
// or xray.UnaryServerInterceptor(xray.ServerInterceptorWithSegmentNamer(xray.NewFixedSegmentNamer("myApp"))) to use a custom segment namer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a bit wrong now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean there's a typo or I shouldn't use comments to mention the usage of options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think typo, it's xray.UnaryServerINterceptor(xray.NewFixedSegmentNamer(...))
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the options pattern and the current implementation, adding a segment namer to the unary server interceptor should look like this (at line 265):
aws-xray-sdk-go/xray/grpc_test.go
Lines 262 to 265 in 63d8684
grpc.UnaryInterceptor( | |
UnaryServerInterceptor( | |
ServerInterceptorWithContext(ctx), | |
ServerInterceptorWithSegmentNamer(NewFixedSegmentNamer("test")))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I don't think we need to be redundant with e.g. ServerInterceptor
. Just UnaryServerInterceptor(WithContext(ctx), WithSegmentNamer(...))
can be done I think, there's no difference between server / client versions of these options right? If we can simplify these names I think we're good to go
xray/grpc.go
Outdated
) | ||
|
||
// UnaryClientInterceptor provides gRPC unary client interceptor. | ||
func UnaryClientInterceptor(host string) grpc.UnaryClientInterceptor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One last point, how about we follow option pattern for client too and make host an option. By default, can use the service name here too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied the options pattern to UnaryClientInterceptor. The original host
here is used as the segment name and host of the recorded request URL. So I added two options:
- Segment Namer
- Host
Although I think it's a rare case to override the host?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah - it looks like you found host in the interceptor parameters, cc.Target
. If that's available than I agree there isn't much use case to override host. Having the segment namer option looks good, how about we just go ahead and remove the host option for now and add it if someone asks for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Removed the host option.
xray/grpc_test.go
Outdated
assert.NoError(t, err) | ||
segment, err := td.Recv() | ||
assert.NoError(t, err) | ||
assert.Equal(t, "bufnet/mwitkow.testproto.TestService/Ping", segment.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry also this should only be the service name by default, definitely not method and we don't need host in the default (a gRPC service name is semantically similar to a segment name, like a host would be on HTTP). So it should just be mwitkow.testproto.TestService
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. Fixed
instead of gRPC full method name
also, add some comments and allow setting a custom segment namer for ClientInterceptor
xray/grpc.go
Outdated
func ServerInterceptorWithSegmentNamer(sn SegmentNamer) ServerInterceptorOption { | ||
return newFuncServerInterceptorOption(func(option *serverInterceptorOption) { | ||
option.segmentNamer = sn | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should rename this into UnaryServerInterceptorWithSegmentNamer
? Or organize interceptors and options into sub-packages?
We might need to find a proper way to organize those options because interceptors of gRPC have four variants:
- Unary / Client
- Unary / Server
In the future, we might implement interceptors for gRPC streaming. Then we will have:
3. Stream / Client
4. Stream / Server
I can imagine, different types of interceptors will have different types of concerns and options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the extreme delay in getting back, just a comment about the option names, I think we're pretty close.
README.md
Outdated
// use grpc.ChainUnaryInterceptor instead to apply multiple interceptors | ||
grpc.UnaryInterceptor( | ||
xray.UnaryServerInterceptor(), | ||
// or xray.UnaryServerInterceptor(xray.ServerInterceptorWithSegmentNamer(xray.NewFixedSegmentNamer("myApp"))) to use a custom segment namer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I don't think we need to be redundant with e.g. ServerInterceptor
. Just UnaryServerInterceptor(WithContext(ctx), WithSegmentNamer(...))
can be done I think, there's no difference between server / client versions of these options right? If we can simplify these names I think we're good to go
Also it could be great reference to look through the OpenTelemetry instrumentation for good patterns re: naming, etc. They don't seem to have separate packages for unarystream / clientserver so I'm thinking it's relatively scalable. |
Merged Server/ClientInterceptorOption -> GrpcOption. I also add a refactor that changes the API a little bit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the patience! @bhautikpip or @srprash can you also give it a look over and we'll merge?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM :)
Issue #, if available:
#110
Description of changes:
Continue @sawadashota's effort (#264) to implement grpc support.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.