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 }