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 }