google.golang.org/grpc@v1.72.2/stats/opentelemetry/server_metrics.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 "sync/atomic" 22 "time" 23 24 "google.golang.org/grpc" 25 estats "google.golang.org/grpc/experimental/stats" 26 "google.golang.org/grpc/internal" 27 "google.golang.org/grpc/metadata" 28 "google.golang.org/grpc/stats" 29 "google.golang.org/grpc/status" 30 31 otelattribute "go.opentelemetry.io/otel/attribute" 32 otelmetric "go.opentelemetry.io/otel/metric" 33 ) 34 35 type serverStatsHandler struct { 36 estats.MetricsRecorder 37 options Options 38 serverMetrics serverMetrics 39 } 40 41 func (h *serverStatsHandler) initializeMetrics() { 42 // Will set no metrics to record, logically making this stats handler a 43 // no-op. 44 if h.options.MetricsOptions.MeterProvider == nil { 45 return 46 } 47 48 meter := h.options.MetricsOptions.MeterProvider.Meter("grpc-go", otelmetric.WithInstrumentationVersion(grpc.Version)) 49 if meter == nil { 50 return 51 } 52 metrics := h.options.MetricsOptions.Metrics 53 if metrics == nil { 54 metrics = DefaultMetrics() 55 } 56 57 h.serverMetrics.callStarted = createInt64Counter(metrics.Metrics(), "grpc.server.call.started", meter, otelmetric.WithUnit("call"), otelmetric.WithDescription("Number of server calls started.")) 58 h.serverMetrics.callSentTotalCompressedMessageSize = createInt64Histogram(metrics.Metrics(), "grpc.server.call.sent_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes sent per server call."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) 59 h.serverMetrics.callRcvdTotalCompressedMessageSize = createInt64Histogram(metrics.Metrics(), "grpc.server.call.rcvd_total_compressed_message_size", meter, otelmetric.WithUnit("By"), otelmetric.WithDescription("Compressed message bytes received per server call."), otelmetric.WithExplicitBucketBoundaries(DefaultSizeBounds...)) 60 h.serverMetrics.callDuration = createFloat64Histogram(metrics.Metrics(), "grpc.server.call.duration", meter, otelmetric.WithUnit("s"), otelmetric.WithDescription("End-to-end time taken to complete a call from server transport's perspective."), otelmetric.WithExplicitBucketBoundaries(DefaultLatencyBounds...)) 61 62 rm := ®istryMetrics{ 63 optionalLabels: h.options.MetricsOptions.OptionalLabels, 64 } 65 h.MetricsRecorder = rm 66 rm.registerMetrics(metrics, meter) 67 } 68 69 // attachLabelsTransportStream intercepts SetHeader and SendHeader calls of the 70 // underlying ServerTransportStream to attach metadataExchangeLabels. 71 type attachLabelsTransportStream struct { 72 grpc.ServerTransportStream 73 74 attachedLabels atomic.Bool 75 metadataExchangeLabels metadata.MD 76 } 77 78 func (s *attachLabelsTransportStream) SetHeader(md metadata.MD) error { 79 if !s.attachedLabels.Swap(true) { 80 s.ServerTransportStream.SetHeader(s.metadataExchangeLabels) 81 } 82 return s.ServerTransportStream.SetHeader(md) 83 } 84 85 func (s *attachLabelsTransportStream) SendHeader(md metadata.MD) error { 86 if !s.attachedLabels.Swap(true) { 87 s.ServerTransportStream.SetHeader(s.metadataExchangeLabels) 88 } 89 90 return s.ServerTransportStream.SendHeader(md) 91 } 92 93 func (h *serverStatsHandler) unaryInterceptor(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { 94 var metadataExchangeLabels metadata.MD 95 if h.options.MetricsOptions.pluginOption != nil { 96 metadataExchangeLabels = h.options.MetricsOptions.pluginOption.GetMetadata() 97 } 98 99 sts := grpc.ServerTransportStreamFromContext(ctx) 100 101 alts := &attachLabelsTransportStream{ 102 ServerTransportStream: sts, 103 metadataExchangeLabels: metadataExchangeLabels, 104 } 105 ctx = grpc.NewContextWithServerTransportStream(ctx, alts) 106 107 res, err := handler(ctx, req) 108 if err != nil { // maybe trailers-only if headers haven't already been sent 109 if !alts.attachedLabels.Swap(true) { 110 alts.SetTrailer(alts.metadataExchangeLabels) 111 } 112 } else { // headers will be written; a message was sent 113 if !alts.attachedLabels.Swap(true) { 114 alts.SetHeader(alts.metadataExchangeLabels) 115 } 116 } 117 118 return res, err 119 } 120 121 // attachLabelsStream embeds a grpc.ServerStream, and intercepts the 122 // SetHeader/SendHeader/SendMsg/SendTrailer call to attach metadata exchange 123 // labels. 124 type attachLabelsStream struct { 125 grpc.ServerStream 126 127 attachedLabels atomic.Bool 128 metadataExchangeLabels metadata.MD 129 } 130 131 func (s *attachLabelsStream) SetHeader(md metadata.MD) error { 132 if !s.attachedLabels.Swap(true) { 133 s.ServerStream.SetHeader(s.metadataExchangeLabels) 134 } 135 136 return s.ServerStream.SetHeader(md) 137 } 138 139 func (s *attachLabelsStream) SendHeader(md metadata.MD) error { 140 if !s.attachedLabels.Swap(true) { 141 s.ServerStream.SetHeader(s.metadataExchangeLabels) 142 } 143 144 return s.ServerStream.SendHeader(md) 145 } 146 147 func (s *attachLabelsStream) SendMsg(m any) error { 148 if !s.attachedLabels.Swap(true) { 149 s.ServerStream.SetHeader(s.metadataExchangeLabels) 150 } 151 return s.ServerStream.SendMsg(m) 152 } 153 154 func (h *serverStatsHandler) streamInterceptor(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 155 var metadataExchangeLabels metadata.MD 156 if h.options.MetricsOptions.pluginOption != nil { 157 metadataExchangeLabels = h.options.MetricsOptions.pluginOption.GetMetadata() 158 } 159 als := &attachLabelsStream{ 160 ServerStream: ss, 161 metadataExchangeLabels: metadataExchangeLabels, 162 } 163 err := handler(srv, als) 164 165 // Add metadata exchange labels to trailers if never sent in headers, 166 // irrespective of whether or not RPC failed. 167 if !als.attachedLabels.Load() { 168 als.SetTrailer(als.metadataExchangeLabels) 169 } 170 return err 171 } 172 173 // TagConn exists to satisfy stats.Handler. 174 func (h *serverStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { 175 return ctx 176 } 177 178 // HandleConn exists to satisfy stats.Handler. 179 func (h *serverStatsHandler) HandleConn(context.Context, stats.ConnStats) {} 180 181 // TagRPC implements per RPC context management. 182 func (h *serverStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { 183 method := info.FullMethodName 184 if h.options.MetricsOptions.MethodAttributeFilter != nil { 185 if !h.options.MetricsOptions.MethodAttributeFilter(method) { 186 method = "other" 187 } 188 } 189 server := internal.ServerFromContext.(func(context.Context) *grpc.Server)(ctx) 190 if server == nil { // Shouldn't happen, defensive programming. 191 logger.Error("ctx passed into server side stats handler has no grpc server ref") 192 method = "other" 193 } else { 194 isRegisteredMethod := internal.IsRegisteredMethod.(func(*grpc.Server, string) bool) 195 if !isRegisteredMethod(server, method) { 196 method = "other" 197 } 198 } 199 200 ai := &attemptInfo{ 201 startTime: time.Now(), 202 method: removeLeadingSlash(method), 203 } 204 if h.options.isTracingEnabled() { 205 ctx, ai = h.traceTagRPC(ctx, ai) 206 } 207 return setRPCInfo(ctx, &rpcInfo{ 208 ai: ai, 209 }) 210 } 211 212 // HandleRPC implements per RPC tracing and stats implementation. 213 func (h *serverStatsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { 214 ri := getRPCInfo(ctx) 215 if ri == nil { 216 logger.Error("ctx passed into server side stats handler metrics event handling has no server call data present") 217 return 218 } 219 if h.options.isTracingEnabled() { 220 populateSpan(rs, ri.ai) 221 } 222 if h.options.isMetricsEnabled() { 223 h.processRPCData(ctx, rs, ri.ai) 224 } 225 } 226 227 func (h *serverStatsHandler) processRPCData(ctx context.Context, s stats.RPCStats, ai *attemptInfo) { 228 switch st := s.(type) { 229 case *stats.InHeader: 230 if ai.pluginOptionLabels == nil && h.options.MetricsOptions.pluginOption != nil { 231 labels := h.options.MetricsOptions.pluginOption.GetLabels(st.Header) 232 if labels == nil { 233 labels = map[string]string{} // Shouldn't return a nil map. Make it empty if so to ignore future Get Calls for this Attempt. 234 } 235 ai.pluginOptionLabels = labels 236 } 237 attrs := otelmetric.WithAttributeSet(otelattribute.NewSet( 238 otelattribute.String("grpc.method", ai.method), 239 )) 240 h.serverMetrics.callStarted.Add(ctx, 1, attrs) 241 case *stats.OutPayload: 242 atomic.AddInt64(&ai.sentCompressedBytes, int64(st.CompressedLength)) 243 case *stats.InPayload: 244 atomic.AddInt64(&ai.recvCompressedBytes, int64(st.CompressedLength)) 245 case *stats.End: 246 h.processRPCEnd(ctx, ai, st) 247 default: 248 } 249 } 250 251 func (h *serverStatsHandler) processRPCEnd(ctx context.Context, ai *attemptInfo, e *stats.End) { 252 latency := float64(time.Since(ai.startTime)) / float64(time.Second) 253 st := "OK" 254 if e.Error != nil { 255 s, _ := status.FromError(e.Error) 256 st = canonicalString(s.Code()) 257 } 258 attributes := []otelattribute.KeyValue{ 259 otelattribute.String("grpc.method", ai.method), 260 otelattribute.String("grpc.status", st), 261 } 262 for k, v := range ai.pluginOptionLabels { 263 attributes = append(attributes, otelattribute.String(k, v)) 264 } 265 266 // Allocate vararg slice once. 267 opts := []otelmetric.RecordOption{otelmetric.WithAttributeSet(otelattribute.NewSet(attributes...))} 268 h.serverMetrics.callDuration.Record(ctx, latency, opts...) 269 h.serverMetrics.callSentTotalCompressedMessageSize.Record(ctx, atomic.LoadInt64(&ai.sentCompressedBytes), opts...) 270 h.serverMetrics.callRcvdTotalCompressedMessageSize.Record(ctx, atomic.LoadInt64(&ai.recvCompressedBytes), opts...) 271 } 272 273 const ( 274 // ServerCallStartedMetricName is the number of server calls started. 275 ServerCallStartedMetricName string = "grpc.server.call.started" 276 // ServerCallSentCompressedTotalMessageSizeMetricName is the compressed 277 // message bytes sent per server call. 278 ServerCallSentCompressedTotalMessageSizeMetricName string = "grpc.server.call.sent_total_compressed_message_size" 279 // ServerCallRcvdCompressedTotalMessageSizeMetricName is the compressed 280 // message bytes received per server call. 281 ServerCallRcvdCompressedTotalMessageSizeMetricName string = "grpc.server.call.rcvd_total_compressed_message_size" 282 // ServerCallDurationMetricName is the end-to-end time taken to complete a 283 // call from server transport's perspective. 284 ServerCallDurationMetricName string = "grpc.server.call.duration" 285 )