google.golang.org/grpc@v1.72.2/stats/opentelemetry/metricsregistry_test.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  	"testing"
    22  	"time"
    23  
    24  	estats "google.golang.org/grpc/experimental/stats"
    25  	"google.golang.org/grpc/internal"
    26  	"google.golang.org/grpc/internal/grpctest"
    27  
    28  	"go.opentelemetry.io/otel/attribute"
    29  	otelmetric "go.opentelemetry.io/otel/sdk/metric"
    30  	"go.opentelemetry.io/otel/sdk/metric/metricdata"
    31  	"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
    32  )
    33  
    34  var defaultTestTimeout = 5 * time.Second
    35  
    36  type s struct {
    37  	grpctest.Tester
    38  }
    39  
    40  func Test(t *testing.T) {
    41  	grpctest.RunSubTests(t, s{})
    42  }
    43  
    44  type metricsRecorderForTest interface {
    45  	estats.MetricsRecorder
    46  	initializeMetrics()
    47  }
    48  
    49  func newClientStatsHandler(options MetricsOptions) metricsRecorderForTest {
    50  	return &clientStatsHandler{options: Options{MetricsOptions: options}}
    51  }
    52  
    53  func newServerStatsHandler(options MetricsOptions) metricsRecorderForTest {
    54  	return &serverStatsHandler{options: Options{MetricsOptions: options}}
    55  }
    56  
    57  // TestMetricsRegistryMetrics tests the OpenTelemetry behavior with respect to
    58  // registered metrics. It registers metrics in the metrics registry. It then
    59  // creates an OpenTelemetry client and server stats handler This test then makes
    60  // measurements on those instruments using one of the stats handlers, then tests
    61  // the expected metrics emissions, which includes default metrics and optional
    62  // label assertions.
    63  func (s) TestMetricsRegistryMetrics(t *testing.T) {
    64  	cleanup := internal.SnapshotMetricRegistryForTesting()
    65  	defer cleanup()
    66  
    67  	intCountHandle1 := estats.RegisterInt64Count(estats.MetricDescriptor{
    68  		Name:           "int-counter-1",
    69  		Description:    "Sum of calls from test",
    70  		Unit:           "int",
    71  		Labels:         []string{"int counter 1 label key"},
    72  		OptionalLabels: []string{"int counter 1 optional label key"},
    73  		Default:        true,
    74  	})
    75  	// A non default metric. If not specified in OpenTelemetry constructor, this
    76  	// will become a no-op, so measurements recorded on it won't show up in
    77  	// emitted metrics.
    78  	intCountHandle2 := estats.RegisterInt64Count(estats.MetricDescriptor{
    79  		Name:           "int-counter-2",
    80  		Description:    "Sum of calls from test",
    81  		Unit:           "int",
    82  		Labels:         []string{"int counter 2 label key"},
    83  		OptionalLabels: []string{"int counter 2 optional label key"},
    84  		Default:        false,
    85  	})
    86  	// Register another non default metric. This will get added to the default
    87  	// metrics set in the OpenTelemetry constructor options, so metrics recorded
    88  	// on this should show up in metrics emissions.
    89  	intCountHandle3 := estats.RegisterInt64Count(estats.MetricDescriptor{
    90  		Name:           "int-counter-3",
    91  		Description:    "sum of calls from test",
    92  		Unit:           "int",
    93  		Labels:         []string{"int counter 3 label key"},
    94  		OptionalLabels: []string{"int counter 3 optional label key"},
    95  		Default:        false,
    96  	})
    97  	floatCountHandle := estats.RegisterFloat64Count(estats.MetricDescriptor{
    98  		Name:           "float-counter",
    99  		Description:    "sum of calls from test",
   100  		Unit:           "float",
   101  		Labels:         []string{"float counter label key"},
   102  		OptionalLabels: []string{"float counter optional label key"},
   103  		Default:        true,
   104  	})
   105  	bounds := []float64{0, 5, 10}
   106  	intHistoHandle := estats.RegisterInt64Histo(estats.MetricDescriptor{
   107  		Name:           "int-histo",
   108  		Description:    "histogram of call values from tests",
   109  		Unit:           "int",
   110  		Labels:         []string{"int histo label key"},
   111  		OptionalLabels: []string{"int histo optional label key"},
   112  		Default:        true,
   113  		Bounds:         bounds,
   114  	})
   115  	floatHistoHandle := estats.RegisterFloat64Histo(estats.MetricDescriptor{
   116  		Name:           "float-histo",
   117  		Description:    "histogram of call values from tests",
   118  		Unit:           "float",
   119  		Labels:         []string{"float histo label key"},
   120  		OptionalLabels: []string{"float histo optional label key"},
   121  		Default:        true,
   122  		Bounds:         bounds,
   123  	})
   124  	intGaugeHandle := estats.RegisterInt64Gauge(estats.MetricDescriptor{
   125  		Name:           "simple-gauge",
   126  		Description:    "the most recent int emitted by test",
   127  		Unit:           "int",
   128  		Labels:         []string{"int gauge label key"},
   129  		OptionalLabels: []string{"int gauge optional label key"},
   130  		Default:        true,
   131  	})
   132  
   133  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   134  	defer cancel()
   135  
   136  	// Only float optional labels are configured, so only float optional labels should show up.
   137  	// All required labels should show up.
   138  	wantMetrics := []metricdata.Metrics{
   139  		{
   140  			Name:        "int-counter-1",
   141  			Description: "Sum of calls from test",
   142  			Unit:        "int",
   143  			Data: metricdata.Sum[int64]{
   144  				DataPoints: []metricdata.DataPoint[int64]{
   145  					{
   146  						Attributes: attribute.NewSet(attribute.String("int counter 1 label key", "int counter 1 label value")), // No optional label, not float.
   147  						Value:      1,
   148  					},
   149  				},
   150  				Temporality: metricdata.CumulativeTemporality,
   151  				IsMonotonic: true,
   152  			},
   153  		},
   154  		{
   155  			Name:        "int-counter-3",
   156  			Description: "sum of calls from test",
   157  			Unit:        "int",
   158  			Data: metricdata.Sum[int64]{
   159  				DataPoints: []metricdata.DataPoint[int64]{
   160  					{
   161  						Attributes: attribute.NewSet(attribute.String("int counter 3 label key", "int counter 3 label value")), // No optional label, not float.
   162  						Value:      4,
   163  					},
   164  				},
   165  				Temporality: metricdata.CumulativeTemporality,
   166  				IsMonotonic: true,
   167  			},
   168  		},
   169  		{
   170  			Name:        "float-counter",
   171  			Description: "sum of calls from test",
   172  			Unit:        "float",
   173  			Data: metricdata.Sum[float64]{
   174  				DataPoints: []metricdata.DataPoint[float64]{
   175  					{
   176  						Attributes: attribute.NewSet(attribute.String("float counter label key", "float counter label value"), attribute.String("float counter optional label key", "float counter optional label value")),
   177  						Value:      1.2,
   178  					},
   179  				},
   180  				Temporality: metricdata.CumulativeTemporality,
   181  				IsMonotonic: true,
   182  			},
   183  		},
   184  		{
   185  			Name:        "int-histo",
   186  			Description: "histogram of call values from tests",
   187  			Unit:        "int",
   188  			Data: metricdata.Histogram[int64]{
   189  				DataPoints: []metricdata.HistogramDataPoint[int64]{
   190  					{
   191  						Attributes:   attribute.NewSet(attribute.String("int histo label key", "int histo label value")), // No optional label, not float.
   192  						Count:        1,
   193  						Bounds:       bounds,
   194  						BucketCounts: []uint64{0, 1, 0, 0},
   195  						Min:          metricdata.NewExtrema(int64(3)),
   196  						Max:          metricdata.NewExtrema(int64(3)),
   197  						Sum:          3,
   198  					},
   199  				},
   200  				Temporality: metricdata.CumulativeTemporality,
   201  			},
   202  		},
   203  		{
   204  			Name:        "float-histo",
   205  			Description: "histogram of call values from tests",
   206  			Unit:        "float",
   207  			Data: metricdata.Histogram[float64]{
   208  				DataPoints: []metricdata.HistogramDataPoint[float64]{
   209  					{
   210  						Attributes:   attribute.NewSet(attribute.String("float histo label key", "float histo label value"), attribute.String("float histo optional label key", "float histo optional label value")),
   211  						Count:        1,
   212  						Bounds:       bounds,
   213  						BucketCounts: []uint64{0, 1, 0, 0},
   214  						Min:          metricdata.NewExtrema(float64(4.3)),
   215  						Max:          metricdata.NewExtrema(float64(4.3)),
   216  						Sum:          4.3,
   217  					},
   218  				},
   219  				Temporality: metricdata.CumulativeTemporality,
   220  			},
   221  		},
   222  		{
   223  			Name:        "simple-gauge",
   224  			Description: "the most recent int emitted by test",
   225  			Unit:        "int",
   226  			Data: metricdata.Gauge[int64]{
   227  				DataPoints: []metricdata.DataPoint[int64]{
   228  					{
   229  						Attributes: attribute.NewSet(attribute.String("int gauge label key", "int gauge label value")), // No optional label, not float.
   230  						Value:      8,
   231  					},
   232  				},
   233  			},
   234  		},
   235  	}
   236  
   237  	for _, test := range []struct {
   238  		name        string
   239  		constructor func(options MetricsOptions) metricsRecorderForTest
   240  	}{
   241  		{
   242  			name:        "client stats handler",
   243  			constructor: newClientStatsHandler,
   244  		},
   245  		{
   246  			name:        "server stats handler",
   247  			constructor: newServerStatsHandler,
   248  		},
   249  	} {
   250  		t.Run(test.name, func(t *testing.T) {
   251  			reader := otelmetric.NewManualReader()
   252  			provider := otelmetric.NewMeterProvider(otelmetric.WithReader(reader))
   253  
   254  			// This configures the defaults alongside int counter 3. All the instruments
   255  			// registered except int counter 2 and 3 are default, so all measurements
   256  			// recorded should show up in reader collected metrics except those for int
   257  			// counter 2.
   258  			// This also only toggles the float count and float histo optional labels,
   259  			// so only those should show up in metrics emissions. All the required
   260  			// labels should show up in metrics emissions.
   261  			mo := MetricsOptions{
   262  				Metrics:        DefaultMetrics().Add("int-counter-3"),
   263  				OptionalLabels: []string{"float counter optional label key", "float histo optional label key"},
   264  				MeterProvider:  provider,
   265  			}
   266  			mr := test.constructor(mo)
   267  			mr.initializeMetrics()
   268  			// These Record calls are guaranteed at a layer underneath OpenTelemetry for
   269  			// labels emitted to match the length of labels + optional labels.
   270  			intCountHandle1.Record(mr, 1, []string{"int counter 1 label value", "int counter 1 optional label value"}...)
   271  			// int-counter-2 is not part of metrics specified (not default), so this
   272  			// record call shouldn't show up in the reader.
   273  			intCountHandle2.Record(mr, 2, []string{"int counter 2 label value", "int counter 2 optional label value"}...)
   274  			// int-counter-3 is part of metrics specified, so this call should show up
   275  			// in the reader.
   276  			intCountHandle3.Record(mr, 4, []string{"int counter 3 label value", "int counter 3 optional label value"}...)
   277  
   278  			// All future recording points should show up in emissions as all of these are defaults.
   279  			floatCountHandle.Record(mr, 1.2, []string{"float counter label value", "float counter optional label value"}...)
   280  			intHistoHandle.Record(mr, 3, []string{"int histo label value", "int histo optional label value"}...)
   281  			floatHistoHandle.Record(mr, 4.3, []string{"float histo label value", "float histo optional label value"}...)
   282  			intGaugeHandle.Record(mr, 7, []string{"int gauge label value", "int gauge optional label value"}...)
   283  			// This second gauge call should take the place of the previous gauge call.
   284  			intGaugeHandle.Record(mr, 8, []string{"int gauge label value", "int gauge optional label value"}...)
   285  			rm := &metricdata.ResourceMetrics{}
   286  			reader.Collect(ctx, rm)
   287  			gotMetrics := map[string]metricdata.Metrics{}
   288  			for _, sm := range rm.ScopeMetrics {
   289  				for _, m := range sm.Metrics {
   290  					gotMetrics[m.Name] = m
   291  				}
   292  			}
   293  
   294  			for _, metric := range wantMetrics {
   295  				val, ok := gotMetrics[metric.Name]
   296  				if !ok {
   297  					t.Fatalf("Metric %v not present in recorded metrics", metric.Name)
   298  				}
   299  				if !metricdatatest.AssertEqual(t, metric, val, metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreExemplars()) {
   300  					t.Fatalf("Metrics data type not equal for metric: %v", metric.Name)
   301  				}
   302  			}
   303  
   304  			// int-counter-2 is not a default metric and wasn't specified in
   305  			// constructor, so emissions should not show up.
   306  			if _, ok := gotMetrics["int-counter-2"]; ok {
   307  				t.Fatalf("Metric int-counter-2 present in recorded metrics, was not configured")
   308  			}
   309  		})
   310  	}
   311  }