github.com/newrelic/go-agent@v3.26.0+incompatible/_integrations/nrgrpc/nrgrpc_client.go (about)

     1  // Copyright 2020 New Relic Corporation. All rights reserved.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package nrgrpc
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  	"net/url"
    10  	"strings"
    11  
    12  	newrelic "github.com/newrelic/go-agent"
    13  	"google.golang.org/grpc"
    14  	"google.golang.org/grpc/metadata"
    15  )
    16  
    17  func getURL(method, target string) *url.URL {
    18  	var host string
    19  	// target can be anything from
    20  	// https://github.com/grpc/grpc/blob/master/doc/naming.md
    21  	// see https://godoc.org/google.golang.org/grpc#DialContext
    22  	if strings.HasPrefix(target, "unix:") {
    23  		host = "localhost"
    24  	} else {
    25  		host = strings.TrimPrefix(target, "dns:///")
    26  	}
    27  	return &url.URL{
    28  		Scheme: "grpc",
    29  		Host:   host,
    30  		Path:   method,
    31  	}
    32  }
    33  
    34  // startClientSegment starts an ExternalSegment and adds Distributed Trace
    35  // headers to the outgoing grpc metadata in the context.
    36  func startClientSegment(ctx context.Context, method, target string) (*newrelic.ExternalSegment, context.Context) {
    37  	var seg *newrelic.ExternalSegment
    38  	if txn := newrelic.FromContext(ctx); nil != txn {
    39  		seg = newrelic.StartExternalSegment(txn, nil)
    40  
    41  		method = strings.TrimPrefix(method, "/")
    42  		seg.Host = getURL(method, target).Host
    43  		seg.Library = "gRPC"
    44  		seg.Procedure = method
    45  
    46  		payload := txn.CreateDistributedTracePayload()
    47  		if txt := payload.Text(); "" != txt {
    48  			md, ok := metadata.FromOutgoingContext(ctx)
    49  			if !ok {
    50  				md = metadata.New(nil)
    51  			}
    52  			md.Set(newrelic.DistributedTracePayloadHeader, txt)
    53  			ctx = metadata.NewOutgoingContext(ctx, md)
    54  		}
    55  	}
    56  
    57  	return seg, ctx
    58  }
    59  
    60  // UnaryClientInterceptor instruments client unary RPCs.  This interceptor
    61  // records each unary call with an external segment.  Using it requires two steps:
    62  //
    63  // 1. Use this function with grpc.WithChainUnaryInterceptor or
    64  // grpc.WithUnaryInterceptor when creating a grpc.ClientConn.  Example:
    65  //
    66  //	conn, err := grpc.Dial(
    67  //		"localhost:8080",
    68  //		grpc.WithUnaryInterceptor(nrgrpc.UnaryClientInterceptor),
    69  //		grpc.WithStreamInterceptor(nrgrpc.StreamClientInterceptor),
    70  //	)
    71  //
    72  // 2. Ensure that calls made with this grpc.ClientConn are done with a context
    73  // which contains a newrelic.Transaction.
    74  //
    75  // Full example:
    76  // https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/client/client.go
    77  //
    78  // This interceptor only instruments unary calls.  You must use both
    79  // UnaryClientInterceptor and StreamClientInterceptor to instrument unary and
    80  // streaming calls.  These interceptors add headers to the call metadata if
    81  // distributed tracing is enabled.
    82  func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    83  	seg, ctx := startClientSegment(ctx, method, cc.Target())
    84  	defer seg.End()
    85  	return invoker(ctx, method, req, reply, cc, opts...)
    86  }
    87  
    88  type wrappedClientStream struct {
    89  	grpc.ClientStream
    90  	segment       *newrelic.ExternalSegment
    91  	isUnaryServer bool
    92  }
    93  
    94  func (s wrappedClientStream) RecvMsg(m interface{}) error {
    95  	err := s.ClientStream.RecvMsg(m)
    96  	if err == io.EOF || s.isUnaryServer {
    97  		s.segment.End()
    98  	}
    99  	return err
   100  }
   101  
   102  // StreamClientInterceptor instruments client streaming RPCs.  This interceptor
   103  // records streaming each call with an external segment.  Using it requires two steps:
   104  //
   105  // 1. Use this function with grpc.WithChainStreamInterceptor or
   106  // grpc.WithStreamInterceptor when creating a grpc.ClientConn.  Example:
   107  //
   108  //	conn, err := grpc.Dial(
   109  //		"localhost:8080",
   110  //		grpc.WithUnaryInterceptor(nrgrpc.UnaryClientInterceptor),
   111  //		grpc.WithStreamInterceptor(nrgrpc.StreamClientInterceptor),
   112  //	)
   113  //
   114  // 2. Ensure that calls made with this grpc.ClientConn are done with a context
   115  // which contains a newrelic.Transaction.
   116  //
   117  // Full example:
   118  // https://github.com/newrelic/go-agent/blob/master/_integrations/nrgrpc/example/client/client.go
   119  //
   120  // This interceptor only instruments streaming calls.  You must use both
   121  // UnaryClientInterceptor and StreamClientInterceptor to instrument unary and
   122  // streaming calls.  These interceptors add headers to the call metadata if
   123  // distributed tracing is enabled.
   124  func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
   125  	seg, ctx := startClientSegment(ctx, method, cc.Target())
   126  	s, err := streamer(ctx, desc, cc, method, opts...)
   127  	if err != nil {
   128  		return s, err
   129  	}
   130  	return wrappedClientStream{
   131  		segment:       seg,
   132  		ClientStream:  s,
   133  		isUnaryServer: !desc.ServerStreams,
   134  	}, nil
   135  }