Skip to content
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

Merged
merged 38 commits into from
Jun 7, 2021
Merged

Conversation

tony84727
Copy link
Contributor

@tony84727 tony84727 commented Apr 1, 2021

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.

@tony84727 tony84727 changed the title Support grpc Support instrumenting unary grpc Apr 2, 2021
@tony84727 tony84727 marked this pull request as ready for review April 5, 2021 09:48
@bhautikpip bhautikpip self-requested a review April 6, 2021 00:36
README.md Outdated

```go
grpcServer := grpc.NewServer(
grpcmiddleware.WithUnaryServerChain(
Copy link
Contributor

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"

Copy link
Contributor Author

@tony84727 tony84727 Apr 27, 2021

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"
Copy link
Contributor

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?

Copy link
Contributor Author

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(
Copy link
Contributor

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 ?

Copy link
Contributor Author

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 {
Copy link
Contributor Author

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:

  1. xray.Handler
  2. xray.HandlerWithContext

Maybe we should break the interceptor into two signatures as well? One without context and another one with context.

Copy link
Contributor

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

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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?

@willarmiros willarmiros requested a review from anuraaga May 11, 2021 00:19
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)
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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,
Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST

Copy link
Contributor Author

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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:

  1. The error returned by the service handler itself
  2. 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?

Copy link
Contributor

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.

Copy link
Contributor Author

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")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
xray.UnaryServerInterceptor(context.TODO(), xray.NewFixedSegmentNamer("myApp")),
xray.UnaryServerInterceptor(xray.NewFixedSegmentNamer("myApp")),

Copy link
Contributor Author

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 {
Copy link
Contributor

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

Copy link
Contributor Author

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?

  1. UnaryServerInterceptor()
  2. UnaryServerInterceptorWithContext(ctx context.Context)
  3. UnaryServerInterceptorWithNamer(sn SegmentNamer)
  4. UnaryServerInterceptorWithContextAndNamer(ctx context.Context, sn SegmentNamer)

Copy link
Contributor

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?

https://github.com/grpc/grpc-go/blob/master/server.go#L118

Copy link
Contributor Author

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
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
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor Author

@tony84727 tony84727 May 15, 2021

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):

grpc.UnaryInterceptor(
UnaryServerInterceptor(
ServerInterceptorWithContext(ctx),
ServerInterceptorWithSegmentNamer(NewFixedSegmentNamer("test")))),

Copy link
Contributor

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 {
Copy link
Contributor

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

Copy link
Contributor Author

@tony84727 tony84727 May 14, 2021

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:

  1. Segment Namer
  2. Host

Although I think it's a rare case to override the host?

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Removed the host option.

assert.NoError(t, err)
segment, err := td.Recv()
assert.NoError(t, err)
assert.Equal(t, "bufnet/mwitkow.testproto.TestService/Ping", segment.Name)
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Fixed

xray/grpc.go Outdated
Comment on lines 224 to 228
func ServerInterceptorWithSegmentNamer(sn SegmentNamer) ServerInterceptorOption {
return newFuncServerInterceptorOption(func(option *serverInterceptorOption) {
option.segmentNamer = sn
})
}
Copy link
Contributor Author

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:

  1. Unary / Client
  2. 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.

@tony84727 tony84727 requested a review from anuraaga May 27, 2021 02:43
Copy link
Contributor

@anuraaga anuraaga left a 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
Copy link
Contributor

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

@anuraaga
Copy link
Contributor

anuraaga commented Jun 1, 2021

Also it could be great reference to look through the OpenTelemetry instrumentation for good patterns re: naming, etc.

https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go

They don't seem to have separate packages for unarystream / clientserver so I'm thinking it's relatively scalable.

@tony84727
Copy link
Contributor Author

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

Merged Server/ClientInterceptorOption -> GrpcOption.

I also add a refactor that changes the API a little bit:
Changed the option WithContext -> withRecorder because the option was used to provide a custom xray.Config to the server handling context. We only propagate the xray.Config value of the given context. We're not propagating other values associated with the context. This should make the option's intent more clear. (Also, it seems to be a code smell to store a context inside a struct)

Copy link
Contributor

@anuraaga anuraaga left a 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?

Copy link
Contributor

@bhautikpip bhautikpip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants