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) {}