go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/grpcmon/client.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package grpcmon
    16  
    17  import (
    18  	"context"
    19  	"math"
    20  	"time"
    21  
    22  	codepb "google.golang.org/genproto/googleapis/rpc/code"
    23  	"google.golang.org/grpc/stats"
    24  	"google.golang.org/grpc/status"
    25  
    26  	"go.chromium.org/luci/common/tsmon/distribution"
    27  	"go.chromium.org/luci/common/tsmon/field"
    28  	"go.chromium.org/luci/common/tsmon/metric"
    29  	"go.chromium.org/luci/common/tsmon/types"
    30  )
    31  
    32  var (
    33  	// sizeBucket covers range of 1..4GiB
    34  	//
    35  	// This is used in the metrics of sent/received message sizes.
    36  	sizeBucket = distribution.GeometricBucketer(math.Pow(32, 0.064), 100)
    37  
    38  	grpcClientCount = metric.NewCounter(
    39  		"grpc/client/count",
    40  		"Total number of RPCs.",
    41  		nil,
    42  		field.String("method"),         // full name of the grpc method
    43  		field.String("canonical_code")) // status.Code of the result in string
    44  
    45  	grpcClientDuration = metric.NewCumulativeDistribution(
    46  		"grpc/client/duration",
    47  		"Distribution of client-side RPC duration (in milliseconds).",
    48  		&types.MetricMetadata{Units: types.Milliseconds},
    49  		distribution.DefaultBucketer,
    50  		field.String("method"),
    51  		field.String("canonical_code"))
    52  
    53  	grpcClientSentMsg = metric.NewCumulativeDistribution(
    54  		"grpc/client/sent_messages",
    55  		"Count Distribution of sent messages per client-side RPC.",
    56  		nil,
    57  		// TODO(ddoman): tune bucket.
    58  		distribution.DefaultBucketer,
    59  		field.String("method"))
    60  
    61  	grpcClientRecvMsg = metric.NewCumulativeDistribution(
    62  		"grpc/client/received_messages",
    63  		"Count distribution of received messages per client-side RPC.",
    64  		nil,
    65  		// TODO(ddoman): tune bucket.
    66  		distribution.DefaultBucketer,
    67  		field.String("method"))
    68  
    69  	grpcClientSentByte = metric.NewCumulativeDistribution(
    70  		"grpc/client/sent_bytes",
    71  		"Size distribution of request protocol messages. Size is the actual number "+
    72  			"of bytes sent on the wire, which may have been subject to compressions.",
    73  		&types.MetricMetadata{Units: types.Bytes},
    74  		sizeBucket,
    75  		field.String("method"))
    76  
    77  	grpcClientRecvByte = metric.NewCumulativeDistribution(
    78  		"grpc/client/received_bytes",
    79  		"Size distribution of response protocol messages. Size is the actual number "+
    80  			"of bytes received on the wire, which may have been subject to compressions.",
    81  		&types.MetricMetadata{Units: types.Bytes},
    82  		sizeBucket,
    83  		field.String("method"))
    84  
    85  	rtKey = "Holds the current rpc tag"
    86  )
    87  
    88  // reportClientRPCMetrics sends metrics after RPC call has finished.
    89  func reportClientRPCMetrics(ctx context.Context, method string, err error, dur time.Duration) {
    90  	code := status.Code(err)
    91  	canon, ok := codepb.Code_name[int32(code)]
    92  	if !ok {
    93  		canon = code.String() // Code(%d)
    94  	}
    95  	grpcClientCount.Add(ctx, 1, method, canon)
    96  	grpcClientDuration.Add(ctx, float64(dur.Milliseconds()), method, canon)
    97  }
    98  
    99  // ClientRPCStatsMonitor implements stats.Handler to update tsmon metrics with
   100  // RPC stats.
   101  //
   102  // Can be passed to a gRPC client via WithStatsHandler(...) dial option.
   103  // To chain this with other stats handler, use WithMultiStatsHandler.
   104  type ClientRPCStatsMonitor struct{}
   105  
   106  // TagRPC creates a context for the RPC.
   107  //
   108  // The context used for the rest lifetime of the RPC will be derived
   109  // from the returned context.
   110  func (m *ClientRPCStatsMonitor) TagRPC(ctx context.Context, tag *stats.RPCTagInfo) context.Context {
   111  	return context.WithValue(ctx, &rtKey, tag)
   112  }
   113  
   114  func methodNameFromTag(ctx context.Context) string {
   115  	rt, ok := ctx.Value(&rtKey).(*stats.RPCTagInfo)
   116  	if !ok {
   117  		// This should never happen.
   118  		panic("handleRPCEnd: missing rpc-tag")
   119  	}
   120  	return rt.FullMethodName
   121  }
   122  
   123  // handleRPCEnd updates the metrics for an RPC completion.
   124  func (m *ClientRPCStatsMonitor) handleRPCEnd(ctx context.Context, e *stats.End) {
   125  	reportClientRPCMetrics(ctx, methodNameFromTag(ctx), e.Error, e.EndTime.Sub(e.BeginTime))
   126  }
   127  
   128  // handleRPC updates the metrics with the information for an incoming payload.
   129  func (m *ClientRPCStatsMonitor) handleRPCInPayload(ctx context.Context, p *stats.InPayload) {
   130  	n := methodNameFromTag(ctx)
   131  	grpcClientRecvMsg.Add(ctx, 1, n)
   132  	grpcClientRecvByte.Add(ctx, float64(p.WireLength), n)
   133  }
   134  
   135  // handleRPC updates the metrics with the information for an outgoing payload.
   136  func (m *ClientRPCStatsMonitor) handleRPCOutPayload(ctx context.Context, p *stats.OutPayload) {
   137  	n := methodNameFromTag(ctx)
   138  	grpcClientSentMsg.Add(ctx, 1, n)
   139  	grpcClientSentByte.Add(ctx, float64(p.WireLength), n)
   140  }
   141  
   142  // HandleRPC processes the RPC stats.
   143  func (m *ClientRPCStatsMonitor) HandleRPC(ctx context.Context, s stats.RPCStats) {
   144  	switch event := s.(type) {
   145  	case *stats.End:
   146  		m.handleRPCEnd(ctx, event)
   147  	case *stats.InPayload:
   148  		m.handleRPCInPayload(ctx, event)
   149  	case *stats.OutPayload:
   150  		m.handleRPCOutPayload(ctx, event)
   151  	default:
   152  		// do nothing.
   153  	}
   154  }
   155  
   156  // TagConn creates a context for the connection.
   157  //
   158  // The context passed to HandleConn will be derived from the returned context.
   159  // The context passed to HandleRPC will NOT be derived from the returned context.
   160  func (m *ClientRPCStatsMonitor) TagConn(ctx context.Context, t *stats.ConnTagInfo) context.Context {
   161  	// do nothing
   162  	return ctx
   163  }
   164  
   165  // HandleConn processes the Conn stats.
   166  func (m *ClientRPCStatsMonitor) HandleConn(context.Context, stats.ConnStats) {
   167  	// do nothing
   168  }