google.golang.org/grpc@v1.72.2/internal/testutils/stats/test_metrics_recorder.go (about) 1 /* 2 * 3 * Copyright 2024 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 // Package stats implements a TestMetricsRecorder utility. 20 package stats 21 22 import ( 23 "context" 24 "fmt" 25 "sync" 26 27 "github.com/google/go-cmp/cmp" 28 estats "google.golang.org/grpc/experimental/stats" 29 "google.golang.org/grpc/internal/testutils" 30 "google.golang.org/grpc/stats" 31 ) 32 33 // TestMetricsRecorder is a MetricsRecorder to be used in tests. It sends 34 // recording events on channels and provides helpers to check if certain events 35 // have taken place. It also persists metrics data keyed on the metrics 36 // descriptor. 37 type TestMetricsRecorder struct { 38 intCountCh *testutils.Channel 39 floatCountCh *testutils.Channel 40 intHistoCh *testutils.Channel 41 floatHistoCh *testutils.Channel 42 intGaugeCh *testutils.Channel 43 44 // mu protects data. 45 mu sync.Mutex 46 // data is the most recent update for each metric name. 47 data map[string]float64 48 } 49 50 // NewTestMetricsRecorder returns a new TestMetricsRecorder. 51 func NewTestMetricsRecorder() *TestMetricsRecorder { 52 return &TestMetricsRecorder{ 53 intCountCh: testutils.NewChannelWithSize(10), 54 floatCountCh: testutils.NewChannelWithSize(10), 55 intHistoCh: testutils.NewChannelWithSize(10), 56 floatHistoCh: testutils.NewChannelWithSize(10), 57 intGaugeCh: testutils.NewChannelWithSize(10), 58 59 data: make(map[string]float64), 60 } 61 } 62 63 // Metric returns the most recent data for a metric, and whether this recorder 64 // has received data for a metric. 65 func (r *TestMetricsRecorder) Metric(name string) (float64, bool) { 66 r.mu.Lock() 67 defer r.mu.Unlock() 68 data, ok := r.data[name] 69 return data, ok 70 } 71 72 // ClearMetrics clears the metrics data store of the test metrics recorder. 73 func (r *TestMetricsRecorder) ClearMetrics() { 74 r.mu.Lock() 75 defer r.mu.Unlock() 76 r.data = make(map[string]float64) 77 } 78 79 // MetricsData represents data associated with a metric. 80 type MetricsData struct { 81 Handle *estats.MetricDescriptor 82 83 // Only set based on the type of metric. So only one of IntIncr or FloatIncr 84 // is set. 85 IntIncr int64 86 FloatIncr float64 87 88 LabelKeys []string 89 LabelVals []string 90 } 91 92 // WaitForInt64Count waits for an int64 count metric to be recorded and verifies 93 // that the recorded metrics data matches the expected metricsDataWant. Returns 94 // an error if failed to wait or received wrong data. 95 func (r *TestMetricsRecorder) WaitForInt64Count(ctx context.Context, metricsDataWant MetricsData) error { 96 got, err := r.intCountCh.Receive(ctx) 97 if err != nil { 98 return fmt.Errorf("timeout waiting for int64Count") 99 } 100 metricsDataGot := got.(MetricsData) 101 if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { 102 return fmt.Errorf("int64count metricsData received unexpected value (-got, +want): %v", diff) 103 } 104 return nil 105 } 106 107 // WaitForInt64CountIncr waits for an int64 count metric to be recorded and 108 // verifies that the recorded metrics data incr matches the expected incr. 109 // Returns an error if failed to wait or received wrong data. 110 func (r *TestMetricsRecorder) WaitForInt64CountIncr(ctx context.Context, incrWant int64) error { 111 got, err := r.intCountCh.Receive(ctx) 112 if err != nil { 113 return fmt.Errorf("timeout waiting for int64Count") 114 } 115 metricsDataGot := got.(MetricsData) 116 if diff := cmp.Diff(metricsDataGot.IntIncr, incrWant); diff != "" { 117 return fmt.Errorf("int64count metricsData received unexpected value (-got, +want): %v", diff) 118 } 119 return nil 120 } 121 122 // RecordInt64Count sends the metrics data to the intCountCh channel and updates 123 // the internal data map with the recorded value. 124 func (r *TestMetricsRecorder) RecordInt64Count(handle *estats.Int64CountHandle, incr int64, labels ...string) { 125 r.intCountCh.ReceiveOrFail() 126 r.intCountCh.Send(MetricsData{ 127 Handle: handle.Descriptor(), 128 IntIncr: incr, 129 LabelKeys: append(handle.Labels, handle.OptionalLabels...), 130 LabelVals: labels, 131 }) 132 133 r.mu.Lock() 134 defer r.mu.Unlock() 135 r.data[handle.Name] = float64(incr) 136 } 137 138 // WaitForFloat64Count waits for a float count metric to be recorded and 139 // verifies that the recorded metrics data matches the expected metricsDataWant. 140 // Returns an error if failed to wait or received wrong data. 141 func (r *TestMetricsRecorder) WaitForFloat64Count(ctx context.Context, metricsDataWant MetricsData) error { 142 got, err := r.floatCountCh.Receive(ctx) 143 if err != nil { 144 return fmt.Errorf("timeout waiting for float64Count") 145 } 146 metricsDataGot := got.(MetricsData) 147 if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { 148 return fmt.Errorf("float64count metricsData received unexpected value (-got, +want): %v", diff) 149 } 150 return nil 151 } 152 153 // RecordFloat64Count sends the metrics data to the floatCountCh channel and 154 // updates the internal data map with the recorded value. 155 func (r *TestMetricsRecorder) RecordFloat64Count(handle *estats.Float64CountHandle, incr float64, labels ...string) { 156 r.floatCountCh.ReceiveOrFail() 157 r.floatCountCh.Send(MetricsData{ 158 Handle: handle.Descriptor(), 159 FloatIncr: incr, 160 LabelKeys: append(handle.Labels, handle.OptionalLabels...), 161 LabelVals: labels, 162 }) 163 164 r.mu.Lock() 165 defer r.mu.Unlock() 166 r.data[handle.Name] = incr 167 } 168 169 // WaitForInt64Histo waits for an int histo metric to be recorded and verifies 170 // that the recorded metrics data matches the expected metricsDataWant. Returns 171 // an error if failed to wait or received wrong data. 172 func (r *TestMetricsRecorder) WaitForInt64Histo(ctx context.Context, metricsDataWant MetricsData) error { 173 got, err := r.intHistoCh.Receive(ctx) 174 if err != nil { 175 return fmt.Errorf("timeout waiting for int64Histo") 176 } 177 metricsDataGot := got.(MetricsData) 178 if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { 179 return fmt.Errorf("int64Histo metricsData received unexpected value (-got, +want): %v", diff) 180 } 181 return nil 182 } 183 184 // RecordInt64Histo sends the metrics data to the intHistoCh channel and updates 185 // the internal data map with the recorded value. 186 func (r *TestMetricsRecorder) RecordInt64Histo(handle *estats.Int64HistoHandle, incr int64, labels ...string) { 187 r.intHistoCh.ReceiveOrFail() 188 r.intHistoCh.Send(MetricsData{ 189 Handle: handle.Descriptor(), 190 IntIncr: incr, 191 LabelKeys: append(handle.Labels, handle.OptionalLabels...), 192 LabelVals: labels, 193 }) 194 195 r.mu.Lock() 196 defer r.mu.Unlock() 197 r.data[handle.Name] = float64(incr) 198 } 199 200 // WaitForFloat64Histo waits for a float histo metric to be recorded and 201 // verifies that the recorded metrics data matches the expected metricsDataWant. 202 // Returns an error if failed to wait or received wrong data. 203 func (r *TestMetricsRecorder) WaitForFloat64Histo(ctx context.Context, metricsDataWant MetricsData) error { 204 got, err := r.floatHistoCh.Receive(ctx) 205 if err != nil { 206 return fmt.Errorf("timeout waiting for float64Histo") 207 } 208 metricsDataGot := got.(MetricsData) 209 if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { 210 return fmt.Errorf("float64Histo metricsData received unexpected value (-got, +want): %v", diff) 211 } 212 return nil 213 } 214 215 // RecordFloat64Histo sends the metrics data to the floatHistoCh channel and 216 // updates the internal data map with the recorded value. 217 func (r *TestMetricsRecorder) RecordFloat64Histo(handle *estats.Float64HistoHandle, incr float64, labels ...string) { 218 r.floatHistoCh.ReceiveOrFail() 219 r.floatHistoCh.Send(MetricsData{ 220 Handle: handle.Descriptor(), 221 FloatIncr: incr, 222 LabelKeys: append(handle.Labels, handle.OptionalLabels...), 223 LabelVals: labels, 224 }) 225 226 r.mu.Lock() 227 defer r.mu.Unlock() 228 r.data[handle.Name] = incr 229 } 230 231 // WaitForInt64Gauge waits for a int gauge metric to be recorded and verifies 232 // that the recorded metrics data matches the expected metricsDataWant. 233 func (r *TestMetricsRecorder) WaitForInt64Gauge(ctx context.Context, metricsDataWant MetricsData) error { 234 got, err := r.intGaugeCh.Receive(ctx) 235 if err != nil { 236 return fmt.Errorf("timeout waiting for int64Gauge") 237 } 238 metricsDataGot := got.(MetricsData) 239 if diff := cmp.Diff(metricsDataGot, metricsDataWant); diff != "" { 240 return fmt.Errorf("int64Gauge metricsData received unexpected value (-got, +want): %v", diff) 241 } 242 return nil 243 } 244 245 // RecordInt64Gauge sends the metrics data to the intGaugeCh channel and updates 246 // the internal data map with the recorded value. 247 func (r *TestMetricsRecorder) RecordInt64Gauge(handle *estats.Int64GaugeHandle, incr int64, labels ...string) { 248 r.intGaugeCh.ReceiveOrFail() 249 r.intGaugeCh.Send(MetricsData{ 250 Handle: handle.Descriptor(), 251 IntIncr: incr, 252 LabelKeys: append(handle.Labels, handle.OptionalLabels...), 253 LabelVals: labels, 254 }) 255 256 r.mu.Lock() 257 defer r.mu.Unlock() 258 r.data[handle.Name] = float64(incr) 259 } 260 261 // To implement a stats.Handler, which allows it to be set as a dial option: 262 263 // TagRPC is TestMetricsRecorder's implementation of TagRPC. 264 func (r *TestMetricsRecorder) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { 265 return ctx 266 } 267 268 // HandleRPC is TestMetricsRecorder's implementation of HandleRPC. 269 func (r *TestMetricsRecorder) HandleRPC(context.Context, stats.RPCStats) {} 270 271 // TagConn is TestMetricsRecorder's implementation of TagConn. 272 func (r *TestMetricsRecorder) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { 273 return ctx 274 } 275 276 // HandleConn is TestMetricsRecorder's implementation of HandleConn. 277 func (r *TestMetricsRecorder) HandleConn(context.Context, stats.ConnStats) {} 278 279 // NoopMetricsRecorder is a noop MetricsRecorder to be used in tests to prevent 280 // nil panics. 281 type NoopMetricsRecorder struct{} 282 283 // RecordInt64Count is a noop implementation of RecordInt64Count. 284 func (r *NoopMetricsRecorder) RecordInt64Count(*estats.Int64CountHandle, int64, ...string) {} 285 286 // RecordFloat64Count is a noop implementation of RecordFloat64Count. 287 func (r *NoopMetricsRecorder) RecordFloat64Count(*estats.Float64CountHandle, float64, ...string) {} 288 289 // RecordInt64Histo is a noop implementation of RecordInt64Histo. 290 func (r *NoopMetricsRecorder) RecordInt64Histo(*estats.Int64HistoHandle, int64, ...string) {} 291 292 // RecordFloat64Histo is a noop implementation of RecordFloat64Histo. 293 func (r *NoopMetricsRecorder) RecordFloat64Histo(*estats.Float64HistoHandle, float64, ...string) {} 294 295 // RecordInt64Gauge is a noop implementation of RecordInt64Gauge. 296 func (r *NoopMetricsRecorder) RecordInt64Gauge(*estats.Int64GaugeHandle, int64, ...string) {}