google.golang.org/grpc@v1.74.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 otelattribute "go.opentelemetry.io/otel/attribute" 25 otelmetric "go.opentelemetry.io/otel/metric" 26 27 "google.golang.org/grpc" 28 estats "google.golang.org/grpc/experimental/stats" 29 "google.golang.org/grpc/internal" 30 "google.golang.org/grpc/metadata" 31 "google.golang.org/grpc/stats" 32 "google.golang.org/grpc/status" 33 ) 34 35 type serverMetricsHandler struct { 36 estats.MetricsRecorder 37 options Options 38 serverMetrics serverMetrics 39 } 40 41 func (h *serverMetricsHandler) 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 *serverMetricsHandler) 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 *serverMetricsHandler) 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 *serverMetricsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { 175 return ctx 176 } 177 178 // HandleConn exists to satisfy stats.Handler. 179 func (h *serverMetricsHandler) HandleConn(context.Context, stats.ConnStats) {} 180 181 // TagRPC implements per RPC context management for metrics. 182 func (h *serverMetricsHandler) 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 ctx, ai := getOrCreateRPCAttemptInfo(ctx) 200 ai.startTime = time.Now() 201 ai.method = removeLeadingSlash(method) 202 203 return setRPCInfo(ctx, &rpcInfo{ai: ai}) 204 } 205 206 // HandleRPC handles per RPC stats implementation. 207 func (h *serverMetricsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { 208 ri := getRPCInfo(ctx) 209 if ri == nil { 210 logger.Error("ctx passed into server side stats handler metrics event handling has no server call data present") 211 return 212 } 213 h.processRPCData(ctx, rs, ri.ai) 214 } 215 216 func (h *serverMetricsHandler) processRPCData(ctx context.Context, s stats.RPCStats, ai *attemptInfo) { 217 switch st := s.(type) { 218 case *stats.InHeader: 219 if ai.pluginOptionLabels == nil && h.options.MetricsOptions.pluginOption != nil { 220 labels := h.options.MetricsOptions.pluginOption.GetLabels(st.Header) 221 if labels == nil { 222 labels = map[string]string{} // Shouldn't return a nil map. Make it empty if so to ignore future Get Calls for this Attempt. 223 } 224 ai.pluginOptionLabels = labels 225 } 226 attrs := otelmetric.WithAttributeSet(otelattribute.NewSet( 227 otelattribute.String("grpc.method", ai.method), 228 )) 229 h.serverMetrics.callStarted.Add(ctx, 1, attrs) 230 case *stats.OutPayload: 231 atomic.AddInt64(&ai.sentCompressedBytes, int64(st.CompressedLength)) 232 case *stats.InPayload: 233 atomic.AddInt64(&ai.recvCompressedBytes, int64(st.CompressedLength)) 234 case *stats.End: 235 h.processRPCEnd(ctx, ai, st) 236 default: 237 } 238 } 239 240 func (h *serverMetricsHandler) processRPCEnd(ctx context.Context, ai *attemptInfo, e *stats.End) { 241 latency := float64(time.Since(ai.startTime)) / float64(time.Second) 242 st := "OK" 243 if e.Error != nil { 244 s, _ := status.FromError(e.Error) 245 st = canonicalString(s.Code()) 246 } 247 attributes := []otelattribute.KeyValue{ 248 otelattribute.String("grpc.method", ai.method), 249 otelattribute.String("grpc.status", st), 250 } 251 for k, v := range ai.pluginOptionLabels { 252 attributes = append(attributes, otelattribute.String(k, v)) 253 } 254 255 // Allocate vararg slice once. 256 opts := []otelmetric.RecordOption{otelmetric.WithAttributeSet(otelattribute.NewSet(attributes...))} 257 h.serverMetrics.callDuration.Record(ctx, latency, opts...) 258 h.serverMetrics.callSentTotalCompressedMessageSize.Record(ctx, atomic.LoadInt64(&ai.sentCompressedBytes), opts...) 259 h.serverMetrics.callRcvdTotalCompressedMessageSize.Record(ctx, atomic.LoadInt64(&ai.recvCompressedBytes), opts...) 260 } 261 262 const ( 263 // ServerCallStartedMetricName is the number of server calls started. 264 ServerCallStartedMetricName string = "grpc.server.call.started" 265 // ServerCallSentCompressedTotalMessageSizeMetricName is the compressed 266 // message bytes sent per server call. 267 ServerCallSentCompressedTotalMessageSizeMetricName string = "grpc.server.call.sent_total_compressed_message_size" 268 // ServerCallRcvdCompressedTotalMessageSizeMetricName is the compressed 269 // message bytes received per server call. 270 ServerCallRcvdCompressedTotalMessageSizeMetricName string = "grpc.server.call.rcvd_total_compressed_message_size" 271 // ServerCallDurationMetricName is the end-to-end time taken to complete a 272 // call from server transport's perspective. 273 ServerCallDurationMetricName string = "grpc.server.call.duration" 274 )