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 }