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 }