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 := &registryMetrics{
    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  )