go.undefinedlabs.com/scopeagent@v0.4.2/instrumentation/grpc/client.go (about)

     1  package grpc
     2  
     3  import (
     4  	"github.com/opentracing/opentracing-go"
     5  	"github.com/opentracing/opentracing-go/ext"
     6  	"github.com/opentracing/opentracing-go/log"
     7  	"go.undefinedlabs.com/scopeagent/instrumentation"
     8  	scopetracer "go.undefinedlabs.com/scopeagent/tracer"
     9  	"golang.org/x/net/context"
    10  	"google.golang.org/grpc"
    11  	"google.golang.org/grpc/metadata"
    12  	"io"
    13  	"runtime"
    14  	"sync/atomic"
    15  )
    16  
    17  // OpenTracingClientInterceptor returns a grpc.UnaryClientInterceptor suitable
    18  // for use in a grpc.Dial call.
    19  //
    20  // For example:
    21  //
    22  //     conn, err := grpc.Dial(
    23  //         address,
    24  //         ...,  // (existing DialOptions)
    25  //         grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracer)))
    26  //
    27  // All gRPC client spans will inject the OpenTracing SpanContext into the gRPC
    28  // metadata; they will also look in the context.Context for an active
    29  // in-process parent Span and establish a ChildOf reference if such a parent
    30  // Span could be found.
    31  func OpenTracingClientInterceptor(tracer opentracing.Tracer, optFuncs ...Option) grpc.UnaryClientInterceptor {
    32  	otgrpcOpts := newOptions()
    33  	otgrpcOpts.apply(optFuncs...)
    34  	return func(
    35  		ctx context.Context,
    36  		method string,
    37  		req, resp interface{},
    38  		cc *grpc.ClientConn,
    39  		invoker grpc.UnaryInvoker,
    40  		opts ...grpc.CallOption,
    41  	) error {
    42  		var err error
    43  		var parentCtx opentracing.SpanContext
    44  		if parent := opentracing.SpanFromContext(ctx); parent != nil {
    45  			parentCtx = parent.Context()
    46  		}
    47  		if otgrpcOpts.inclusionFunc != nil &&
    48  			!otgrpcOpts.inclusionFunc(parentCtx, method, req, resp) {
    49  			return invoker(ctx, method, req, resp, cc, opts...)
    50  		}
    51  		if _, ok := tracer.(opentracing.NoopTracer); ok {
    52  			tracer = instrumentation.Tracer()
    53  		}
    54  		clientSpan := tracer.StartSpan(
    55  			method,
    56  			opentracing.ChildOf(parentCtx),
    57  			ext.SpanKindRPCClient,
    58  			gRPCComponentTag,
    59  			gRPCPeerServiceTag,
    60  		)
    61  		defer clientSpan.Finish()
    62  		clientSpan.SetTag(MethodName, method)
    63  		clientSpan.SetTag(MethodType, "UNITARY")
    64  		ext.PeerAddress.Set(clientSpan, cc.Target())
    65  		clientSpan.SetTag("grpc.target", cc.Target())
    66  
    67  		ctx = injectSpanContext(ctx, tracer, clientSpan)
    68  		if otgrpcOpts.logPayloads {
    69  			clientSpan.LogFields(log.Object("gRPC request", req))
    70  		}
    71  		err = invoker(ctx, method, req, resp, cc, opts...)
    72  		if err == nil {
    73  			if otgrpcOpts.logPayloads {
    74  				clientSpan.LogFields(log.Object("gRPC response", resp))
    75  			}
    76  			clientSpan.SetTag(Status, "OK")
    77  		} else {
    78  			SetSpanTags(clientSpan, err, true)
    79  			clientSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
    80  		}
    81  		if otgrpcOpts.decorator != nil {
    82  			otgrpcOpts.decorator(clientSpan, method, req, resp, err)
    83  		}
    84  		return err
    85  	}
    86  }
    87  
    88  // OpenTracingStreamClientInterceptor returns a grpc.StreamClientInterceptor suitable
    89  // for use in a grpc.Dial call. The interceptor instruments streaming RPCs by creating
    90  // a single span to correspond to the lifetime of the RPC's stream.
    91  //
    92  // For example:
    93  //
    94  //     conn, err := grpc.Dial(
    95  //         address,
    96  //         ...,  // (existing DialOptions)
    97  //         grpc.WithStreamInterceptor(otgrpc.OpenTracingStreamClientInterceptor(tracer)))
    98  //
    99  // All gRPC client spans will inject the OpenTracing SpanContext into the gRPC
   100  // metadata; they will also look in the context.Context for an active
   101  // in-process parent Span and establish a ChildOf reference if such a parent
   102  // Span could be found.
   103  func OpenTracingStreamClientInterceptor(tracer opentracing.Tracer, optFuncs ...Option) grpc.StreamClientInterceptor {
   104  	otgrpcOpts := newOptions()
   105  	otgrpcOpts.apply(optFuncs...)
   106  	return func(
   107  		ctx context.Context,
   108  		desc *grpc.StreamDesc,
   109  		cc *grpc.ClientConn,
   110  		method string,
   111  		streamer grpc.Streamer,
   112  		opts ...grpc.CallOption,
   113  	) (grpc.ClientStream, error) {
   114  		var err error
   115  		var parentCtx opentracing.SpanContext
   116  		if parent := opentracing.SpanFromContext(ctx); parent != nil {
   117  			parentCtx = parent.Context()
   118  		}
   119  		if otgrpcOpts.inclusionFunc != nil &&
   120  			!otgrpcOpts.inclusionFunc(parentCtx, method, nil, nil) {
   121  			return streamer(ctx, desc, cc, method, opts...)
   122  		}
   123  
   124  		if _, ok := tracer.(opentracing.NoopTracer); ok {
   125  			tracer = instrumentation.Tracer()
   126  		}
   127  		clientSpan := tracer.StartSpan(
   128  			method,
   129  			opentracing.ChildOf(parentCtx),
   130  			ext.SpanKindRPCClient,
   131  			gRPCComponentTag,
   132  			gRPCPeerServiceTag,
   133  		)
   134  		clientSpan.SetTag(MethodName, method)
   135  		clientSpan.SetTag(MethodType, "STREAMING")
   136  		clientSpan.SetTag("grpc.target", cc.Target())
   137  		ext.PeerAddress.Set(clientSpan, cc.Target())
   138  		clientSpan.SetTag("grpc.streamname", desc.StreamName)
   139  
   140  		ctx = injectSpanContext(ctx, tracer, clientSpan)
   141  		cs, err := streamer(ctx, desc, cc, method, opts...)
   142  		if err != nil {
   143  			clientSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
   144  			SetSpanTags(clientSpan, err, true)
   145  			clientSpan.Finish()
   146  			return cs, err
   147  		} else {
   148  			clientSpan.SetTag(Status, "OK")
   149  		}
   150  		return newOpenTracingClientStream(cs, method, desc, clientSpan, otgrpcOpts), nil
   151  	}
   152  }
   153  
   154  func newOpenTracingClientStream(cs grpc.ClientStream, method string, desc *grpc.StreamDesc, clientSpan opentracing.Span, otgrpcOpts *options) grpc.ClientStream {
   155  	finishChan := make(chan struct{})
   156  
   157  	isFinished := new(int32)
   158  	*isFinished = 0
   159  	finishFunc := func(err error) {
   160  		// The current OpenTracing specification forbids finishing a span more than
   161  		// once. Since we have multiple code paths that could concurrently call
   162  		// `finishFunc`, we need to add some sort of synchronization to guard against
   163  		// multiple finishing.
   164  		if !atomic.CompareAndSwapInt32(isFinished, 0, 1) {
   165  			return
   166  		}
   167  		close(finishChan)
   168  		defer clientSpan.Finish()
   169  		if err != nil {
   170  			clientSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
   171  			SetSpanTags(clientSpan, err, true)
   172  		} else {
   173  			clientSpan.SetTag(Status, "OK")
   174  		}
   175  		if otgrpcOpts.decorator != nil {
   176  			otgrpcOpts.decorator(clientSpan, method, nil, nil, err)
   177  		}
   178  	}
   179  	go func() {
   180  		select {
   181  		case <-finishChan:
   182  			// The client span is being finished by another code path; hence, no
   183  			// action is necessary.
   184  		case <-cs.Context().Done():
   185  			finishFunc(cs.Context().Err())
   186  		}
   187  	}()
   188  	otcs := &openTracingClientStream{
   189  		ClientStream: cs,
   190  		desc:         desc,
   191  		finishFunc:   finishFunc,
   192  		span:         clientSpan,
   193  	}
   194  
   195  	// The `ClientStream` interface allows one to omit calling `Recv` if it's
   196  	// known that the result will be `io.EOF`. See
   197  	// http://stackoverflow.com/q/42915337
   198  	// In such cases, there's nothing that triggers the span to finish. We,
   199  	// therefore, set a finalizer so that the span and the context goroutine will
   200  	// at least be cleaned up when the garbage collector is run.
   201  	runtime.SetFinalizer(otcs, func(otcs *openTracingClientStream) {
   202  		otcs.finishFunc(nil)
   203  	})
   204  	return otcs
   205  }
   206  
   207  type openTracingClientStream struct {
   208  	grpc.ClientStream
   209  	desc       *grpc.StreamDesc
   210  	finishFunc func(error)
   211  	span       opentracing.Span
   212  }
   213  
   214  func (cs *openTracingClientStream) Header() (metadata.MD, error) {
   215  	md, err := cs.ClientStream.Header()
   216  	if err != nil {
   217  		cs.finishFunc(err)
   218  	} else if span, ok := cs.span.(scopetracer.Span); ok {
   219  		span.UnsafeSetTag(Headers, md)
   220  	} else {
   221  		cs.span.SetTag(Headers, md)
   222  	}
   223  	return md, err
   224  }
   225  
   226  func (cs *openTracingClientStream) SendMsg(m interface{}) error {
   227  	err := cs.ClientStream.SendMsg(m)
   228  	if err != nil {
   229  		cs.finishFunc(err)
   230  	}
   231  	return err
   232  }
   233  
   234  func (cs *openTracingClientStream) RecvMsg(m interface{}) error {
   235  	err := cs.ClientStream.RecvMsg(m)
   236  	if err == io.EOF {
   237  		cs.finishFunc(nil)
   238  		return err
   239  	} else if err != nil {
   240  		cs.finishFunc(err)
   241  		return err
   242  	}
   243  	if !cs.desc.ServerStreams {
   244  		cs.finishFunc(nil)
   245  	}
   246  	return err
   247  }
   248  
   249  func (cs *openTracingClientStream) CloseSend() error {
   250  	err := cs.ClientStream.CloseSend()
   251  	if err != nil {
   252  		cs.finishFunc(err)
   253  	}
   254  	return err
   255  }
   256  
   257  func injectSpanContext(ctx context.Context, tracer opentracing.Tracer, clientSpan opentracing.Span) context.Context {
   258  	md, ok := metadata.FromOutgoingContext(ctx)
   259  	if !ok {
   260  		md = metadata.New(nil)
   261  	} else {
   262  		md = md.Copy()
   263  	}
   264  	mdWriter := metadataReaderWriter{md}
   265  	err := tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, mdWriter)
   266  	// We have no better place to record an error than the Span itself :-/
   267  	if err != nil {
   268  		clientSpan.LogFields(log.String("event", "Tracer.Inject() failed"), log.Error(err))
   269  	}
   270  	return metadata.NewOutgoingContext(ctx, md)
   271  }
   272  
   273  // Get client interceptors
   274  func GetClientInterceptors() []grpc.DialOption {
   275  	tracer := instrumentation.Tracer()
   276  	return []grpc.DialOption{
   277  		grpc.WithUnaryInterceptor(OpenTracingClientInterceptor(tracer)),
   278  		grpc.WithStreamInterceptor(OpenTracingStreamClientInterceptor(tracer)),
   279  	}
   280  }
   281  
   282  func Dial(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
   283  	opts = append(opts, GetClientInterceptors()...)
   284  	return grpc.Dial(target, opts...)
   285  }
   286  
   287  func DialContext(ctx context.Context, target string, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) {
   288  	opts = append(opts, GetClientInterceptors()...)
   289  	return grpc.DialContext(ctx, target, opts...)
   290  }