google.golang.org/grpc@v1.74.2/stats/opentelemetry/client_tracing.go (about) 1 /* 2 * Copyright 2024 gRPC authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package opentelemetry 18 19 import ( 20 "context" 21 "log" 22 "strings" 23 24 otelcodes "go.opentelemetry.io/otel/codes" 25 "go.opentelemetry.io/otel/trace" 26 "google.golang.org/grpc" 27 grpccodes "google.golang.org/grpc/codes" 28 "google.golang.org/grpc/stats" 29 otelinternaltracing "google.golang.org/grpc/stats/opentelemetry/internal/tracing" 30 "google.golang.org/grpc/status" 31 ) 32 33 const ( 34 delayedResolutionEventName = "Delayed name resolution complete" 35 tracerName = "grpc-go" 36 ) 37 38 type clientTracingHandler struct { 39 options Options 40 } 41 42 func (h *clientTracingHandler) initializeTraces() { 43 if h.options.TraceOptions.TracerProvider == nil { 44 log.Printf("TracerProvider is not provided in client TraceOptions") 45 return 46 } 47 } 48 49 func (h *clientTracingHandler) unaryInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 50 ctx, _ = getOrCreateCallInfo(ctx, cc, method, opts...) 51 52 var span trace.Span 53 ctx, span = h.createCallTraceSpan(ctx, method) 54 err := invoker(ctx, method, req, reply, cc, opts...) 55 h.finishTrace(err, span) 56 return err 57 } 58 59 func (h *clientTracingHandler) streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { 60 ctx, _ = getOrCreateCallInfo(ctx, cc, method, opts...) 61 62 var span trace.Span 63 ctx, span = h.createCallTraceSpan(ctx, method) 64 callback := func(err error) { h.finishTrace(err, span) } 65 opts = append([]grpc.CallOption{grpc.OnFinish(callback)}, opts...) 66 return streamer(ctx, desc, cc, method, opts...) 67 } 68 69 // finishTrace sets the span status based on the RPC result and ends the span. 70 // It is used to finalize tracing for both unary and streaming calls. 71 func (h *clientTracingHandler) finishTrace(err error, ts trace.Span) { 72 s := status.Convert(err) 73 if s.Code() == grpccodes.OK { 74 ts.SetStatus(otelcodes.Ok, s.Message()) 75 } else { 76 ts.SetStatus(otelcodes.Error, s.Message()) 77 } 78 ts.End() 79 } 80 81 // traceTagRPC populates provided context with a new span using the 82 // TextMapPropagator supplied in trace options and internal itracing.carrier. 83 // It creates a new outgoing carrier which serializes information about this 84 // span into gRPC Metadata, if TextMapPropagator is provided in the trace 85 // options. if TextMapPropagator is not provided, it returns the context as is. 86 func (h *clientTracingHandler) traceTagRPC(ctx context.Context, ai *attemptInfo, nameResolutionDelayed bool) (context.Context, *attemptInfo) { 87 // Add a "Delayed name resolution complete" event to the call span 88 // if there was name resolution delay. In case of multiple retry attempts, 89 // ensure that event is added only once. 90 callSpan := trace.SpanFromContext(ctx) 91 ci := getCallInfo(ctx) 92 if nameResolutionDelayed && !ci.nameResolutionEventAdded.Swap(true) && callSpan.SpanContext().IsValid() { 93 callSpan.AddEvent(delayedResolutionEventName) 94 } 95 mn := "Attempt." + strings.Replace(ai.method, "/", ".", -1) 96 tracer := h.options.TraceOptions.TracerProvider.Tracer(tracerName, trace.WithInstrumentationVersion(grpc.Version)) 97 ctx, span := tracer.Start(ctx, mn) 98 carrier := otelinternaltracing.NewOutgoingCarrier(ctx) 99 h.options.TraceOptions.TextMapPropagator.Inject(ctx, carrier) 100 ai.traceSpan = span 101 return carrier.Context(), ai 102 } 103 104 // createCallTraceSpan creates a call span to put in the provided context using 105 // provided TraceProvider. If TraceProvider is nil, it returns context as is. 106 func (h *clientTracingHandler) createCallTraceSpan(ctx context.Context, method string) (context.Context, trace.Span) { 107 mn := "Sent." + strings.Replace(removeLeadingSlash(method), "/", ".", -1) 108 tracer := h.options.TraceOptions.TracerProvider.Tracer(tracerName, trace.WithInstrumentationVersion(grpc.Version)) 109 ctx, span := tracer.Start(ctx, mn, trace.WithSpanKind(trace.SpanKindClient)) 110 return ctx, span 111 } 112 113 // TagConn exists to satisfy stats.Handler for tracing. 114 func (h *clientTracingHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { 115 return ctx 116 } 117 118 // HandleConn exists to satisfy stats.Handler for tracing. 119 func (h *clientTracingHandler) HandleConn(context.Context, stats.ConnStats) {} 120 121 // TagRPC implements per RPC attempt context management for traces. 122 func (h *clientTracingHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { 123 ctx, ai := getOrCreateRPCAttemptInfo(ctx) 124 ctx, ai = h.traceTagRPC(ctx, ai, info.NameResolutionDelay) 125 return setRPCInfo(ctx, &rpcInfo{ai: ai}) 126 } 127 128 // HandleRPC handles per RPC tracing implementation. 129 func (h *clientTracingHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { 130 ri := getRPCInfo(ctx) 131 if ri == nil { 132 logger.Error("ctx passed into client side tracing handler trace event handling has no client attempt data present") 133 return 134 } 135 populateSpan(rs, ri.ai) 136 }