1// Copyright The OpenTelemetry 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 15package metric_test 16 17import ( 18 "context" 19 "errors" 20 "testing" 21 22 "go.opentelemetry.io/otel/attribute" 23 "go.opentelemetry.io/otel/metric" 24 "go.opentelemetry.io/otel/metric/metrictest" 25 "go.opentelemetry.io/otel/metric/number" 26 "go.opentelemetry.io/otel/metric/sdkapi" 27 "go.opentelemetry.io/otel/metric/unit" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32) 33 34var Must = metric.Must 35 36var ( 37 syncKinds = []sdkapi.InstrumentKind{ 38 sdkapi.HistogramInstrumentKind, 39 sdkapi.CounterInstrumentKind, 40 sdkapi.UpDownCounterInstrumentKind, 41 } 42 asyncKinds = []sdkapi.InstrumentKind{ 43 sdkapi.GaugeObserverInstrumentKind, 44 sdkapi.CounterObserverInstrumentKind, 45 sdkapi.UpDownCounterObserverInstrumentKind, 46 } 47 addingKinds = []sdkapi.InstrumentKind{ 48 sdkapi.CounterInstrumentKind, 49 sdkapi.UpDownCounterInstrumentKind, 50 sdkapi.CounterObserverInstrumentKind, 51 sdkapi.UpDownCounterObserverInstrumentKind, 52 } 53 groupingKinds = []sdkapi.InstrumentKind{ 54 sdkapi.HistogramInstrumentKind, 55 sdkapi.GaugeObserverInstrumentKind, 56 } 57 58 monotonicKinds = []sdkapi.InstrumentKind{ 59 sdkapi.CounterInstrumentKind, 60 sdkapi.CounterObserverInstrumentKind, 61 } 62 63 nonMonotonicKinds = []sdkapi.InstrumentKind{ 64 sdkapi.UpDownCounterInstrumentKind, 65 sdkapi.UpDownCounterObserverInstrumentKind, 66 sdkapi.HistogramInstrumentKind, 67 sdkapi.GaugeObserverInstrumentKind, 68 } 69 70 precomputedSumKinds = []sdkapi.InstrumentKind{ 71 sdkapi.CounterObserverInstrumentKind, 72 sdkapi.UpDownCounterObserverInstrumentKind, 73 } 74 75 nonPrecomputedSumKinds = []sdkapi.InstrumentKind{ 76 sdkapi.CounterInstrumentKind, 77 sdkapi.UpDownCounterInstrumentKind, 78 sdkapi.HistogramInstrumentKind, 79 sdkapi.GaugeObserverInstrumentKind, 80 } 81) 82 83func TestSynchronous(t *testing.T) { 84 for _, k := range syncKinds { 85 require.True(t, k.Synchronous()) 86 require.False(t, k.Asynchronous()) 87 } 88 for _, k := range asyncKinds { 89 require.True(t, k.Asynchronous()) 90 require.False(t, k.Synchronous()) 91 } 92} 93 94func TestGrouping(t *testing.T) { 95 for _, k := range groupingKinds { 96 require.True(t, k.Grouping()) 97 require.False(t, k.Adding()) 98 } 99 for _, k := range addingKinds { 100 require.True(t, k.Adding()) 101 require.False(t, k.Grouping()) 102 } 103} 104 105func TestMonotonic(t *testing.T) { 106 for _, k := range monotonicKinds { 107 require.True(t, k.Monotonic()) 108 } 109 for _, k := range nonMonotonicKinds { 110 require.False(t, k.Monotonic()) 111 } 112} 113 114func TestPrecomputedSum(t *testing.T) { 115 for _, k := range precomputedSumKinds { 116 require.True(t, k.PrecomputedSum()) 117 } 118 for _, k := range nonPrecomputedSumKinds { 119 require.False(t, k.PrecomputedSum()) 120 } 121} 122 123func checkSyncBatches(ctx context.Context, t *testing.T, labels []attribute.KeyValue, provider *metrictest.MeterProvider, nkind number.Kind, mkind sdkapi.InstrumentKind, instrument sdkapi.InstrumentImpl, expected ...float64) { 124 t.Helper() 125 126 batchesCount := len(provider.MeasurementBatches) 127 if len(provider.MeasurementBatches) != len(expected) { 128 t.Errorf("Expected %d recorded measurement batches, got %d", batchesCount, len(provider.MeasurementBatches)) 129 } 130 recorded := metrictest.AsStructs(provider.MeasurementBatches) 131 132 for i, batch := range provider.MeasurementBatches { 133 if len(batch.Measurements) != 1 { 134 t.Errorf("Expected 1 measurement in batch %d, got %d", i, len(batch.Measurements)) 135 } 136 137 measurement := batch.Measurements[0] 138 descriptor := measurement.Instrument.Descriptor() 139 140 expected := metrictest.Measured{ 141 Name: descriptor.Name(), 142 Library: metrictest.Library{ 143 InstrumentationName: "apitest", 144 }, 145 Labels: metrictest.LabelsToMap(labels...), 146 Number: metrictest.ResolveNumberByKind(t, nkind, expected[i]), 147 } 148 require.Equal(t, expected, recorded[i]) 149 } 150} 151 152func TestOptions(t *testing.T) { 153 type testcase struct { 154 name string 155 opts []metric.InstrumentOption 156 desc string 157 unit unit.Unit 158 } 159 testcases := []testcase{ 160 { 161 name: "no opts", 162 opts: nil, 163 desc: "", 164 unit: "", 165 }, 166 { 167 name: "description", 168 opts: []metric.InstrumentOption{ 169 metric.WithDescription("stuff"), 170 }, 171 desc: "stuff", 172 unit: "", 173 }, 174 { 175 name: "description override", 176 opts: []metric.InstrumentOption{ 177 metric.WithDescription("stuff"), 178 metric.WithDescription("things"), 179 }, 180 desc: "things", 181 unit: "", 182 }, 183 { 184 name: "unit", 185 opts: []metric.InstrumentOption{ 186 metric.WithUnit("s"), 187 }, 188 desc: "", 189 unit: "s", 190 }, 191 { 192 name: "description override", 193 opts: []metric.InstrumentOption{ 194 metric.WithDescription("stuff"), 195 metric.WithDescription("things"), 196 }, 197 desc: "things", 198 unit: "", 199 }, 200 { 201 name: "unit", 202 opts: []metric.InstrumentOption{ 203 metric.WithUnit("s"), 204 }, 205 desc: "", 206 unit: "s", 207 }, 208 209 { 210 name: "unit override", 211 opts: []metric.InstrumentOption{ 212 metric.WithUnit("s"), 213 metric.WithUnit("h"), 214 }, 215 desc: "", 216 unit: "h", 217 }, 218 { 219 name: "all", 220 opts: []metric.InstrumentOption{ 221 metric.WithDescription("stuff"), 222 metric.WithUnit("s"), 223 }, 224 desc: "stuff", 225 unit: "s", 226 }, 227 } 228 for idx, tt := range testcases { 229 t.Logf("Testing counter case %s (%d)", tt.name, idx) 230 cfg := metric.NewInstrumentConfig(tt.opts...) 231 if diff := cmp.Diff(cfg.Description(), tt.desc); diff != "" { 232 t.Errorf("Compare Description: -got +want %s", diff) 233 } 234 if diff := cmp.Diff(cfg.Unit(), tt.unit); diff != "" { 235 t.Errorf("Compare Unit: -got +want %s", diff) 236 } 237 } 238} 239func testPair() (*metrictest.MeterProvider, metric.Meter) { 240 provider := metrictest.NewMeterProvider() 241 return provider, provider.Meter("apitest") 242} 243 244func TestCounter(t *testing.T) { 245 // N.B. the API does not check for negative 246 // values, that's the SDK's responsibility. 247 t.Run("float64 counter", func(t *testing.T) { 248 provider, meter := testPair() 249 c := Must(meter).NewFloat64Counter("test.counter.float") 250 ctx := context.Background() 251 labels := []attribute.KeyValue{attribute.String("A", "B")} 252 c.Add(ctx, 1994.1, labels...) 253 boundInstrument := c.Bind(labels...) 254 boundInstrument.Add(ctx, -742) 255 meter.RecordBatch(ctx, labels, c.Measurement(42)) 256 checkSyncBatches(ctx, t, labels, provider, number.Float64Kind, sdkapi.CounterInstrumentKind, c.SyncImpl(), 257 1994.1, -742, 42, 258 ) 259 }) 260 t.Run("int64 counter", func(t *testing.T) { 261 provider, meter := testPair() 262 c := Must(meter).NewInt64Counter("test.counter.int") 263 ctx := context.Background() 264 labels := []attribute.KeyValue{attribute.String("A", "B"), attribute.String("C", "D")} 265 c.Add(ctx, 42, labels...) 266 boundInstrument := c.Bind(labels...) 267 boundInstrument.Add(ctx, 4200) 268 meter.RecordBatch(ctx, labels, c.Measurement(420000)) 269 checkSyncBatches(ctx, t, labels, provider, number.Int64Kind, sdkapi.CounterInstrumentKind, c.SyncImpl(), 270 42, 4200, 420000, 271 ) 272 273 }) 274 t.Run("int64 updowncounter", func(t *testing.T) { 275 provider, meter := testPair() 276 c := Must(meter).NewInt64UpDownCounter("test.updowncounter.int") 277 ctx := context.Background() 278 labels := []attribute.KeyValue{attribute.String("A", "B"), attribute.String("C", "D")} 279 c.Add(ctx, 100, labels...) 280 boundInstrument := c.Bind(labels...) 281 boundInstrument.Add(ctx, -100) 282 meter.RecordBatch(ctx, labels, c.Measurement(42)) 283 checkSyncBatches(ctx, t, labels, provider, number.Int64Kind, sdkapi.UpDownCounterInstrumentKind, c.SyncImpl(), 284 100, -100, 42, 285 ) 286 }) 287 t.Run("float64 updowncounter", func(t *testing.T) { 288 provider, meter := testPair() 289 c := Must(meter).NewFloat64UpDownCounter("test.updowncounter.float") 290 ctx := context.Background() 291 labels := []attribute.KeyValue{attribute.String("A", "B"), attribute.String("C", "D")} 292 c.Add(ctx, 100.1, labels...) 293 boundInstrument := c.Bind(labels...) 294 boundInstrument.Add(ctx, -76) 295 meter.RecordBatch(ctx, labels, c.Measurement(-100.1)) 296 checkSyncBatches(ctx, t, labels, provider, number.Float64Kind, sdkapi.UpDownCounterInstrumentKind, c.SyncImpl(), 297 100.1, -76, -100.1, 298 ) 299 }) 300} 301 302func TestHistogram(t *testing.T) { 303 t.Run("float64 histogram", func(t *testing.T) { 304 provider, meter := testPair() 305 m := Must(meter).NewFloat64Histogram("test.histogram.float") 306 ctx := context.Background() 307 labels := []attribute.KeyValue{} 308 m.Record(ctx, 42, labels...) 309 boundInstrument := m.Bind(labels...) 310 boundInstrument.Record(ctx, 0) 311 meter.RecordBatch(ctx, labels, m.Measurement(-100.5)) 312 checkSyncBatches(ctx, t, labels, provider, number.Float64Kind, sdkapi.HistogramInstrumentKind, m.SyncImpl(), 313 42, 0, -100.5, 314 ) 315 }) 316 t.Run("int64 histogram", func(t *testing.T) { 317 provider, meter := testPair() 318 m := Must(meter).NewInt64Histogram("test.histogram.int") 319 ctx := context.Background() 320 labels := []attribute.KeyValue{attribute.Int("I", 1)} 321 m.Record(ctx, 173, labels...) 322 boundInstrument := m.Bind(labels...) 323 boundInstrument.Record(ctx, 80) 324 meter.RecordBatch(ctx, labels, m.Measurement(0)) 325 checkSyncBatches(ctx, t, labels, provider, number.Int64Kind, sdkapi.HistogramInstrumentKind, m.SyncImpl(), 326 173, 80, 0, 327 ) 328 }) 329} 330 331func TestObserverInstruments(t *testing.T) { 332 t.Run("float gauge", func(t *testing.T) { 333 labels := []attribute.KeyValue{attribute.String("O", "P")} 334 provider, meter := testPair() 335 o := Must(meter).NewFloat64GaugeObserver("test.gauge.float", func(_ context.Context, result metric.Float64ObserverResult) { 336 result.Observe(42.1, labels...) 337 }) 338 provider.RunAsyncInstruments() 339 checkObserverBatch(t, labels, provider, number.Float64Kind, sdkapi.GaugeObserverInstrumentKind, o.AsyncImpl(), 340 42.1, 341 ) 342 }) 343 t.Run("int gauge", func(t *testing.T) { 344 labels := []attribute.KeyValue{} 345 provider, meter := testPair() 346 o := Must(meter).NewInt64GaugeObserver("test.gauge.int", func(_ context.Context, result metric.Int64ObserverResult) { 347 result.Observe(-142, labels...) 348 }) 349 provider.RunAsyncInstruments() 350 checkObserverBatch(t, labels, provider, number.Int64Kind, sdkapi.GaugeObserverInstrumentKind, o.AsyncImpl(), 351 -142, 352 ) 353 }) 354 t.Run("float counterobserver", func(t *testing.T) { 355 labels := []attribute.KeyValue{attribute.String("O", "P")} 356 provider, meter := testPair() 357 o := Must(meter).NewFloat64CounterObserver("test.counter.float", func(_ context.Context, result metric.Float64ObserverResult) { 358 result.Observe(42.1, labels...) 359 }) 360 provider.RunAsyncInstruments() 361 checkObserverBatch(t, labels, provider, number.Float64Kind, sdkapi.CounterObserverInstrumentKind, o.AsyncImpl(), 362 42.1, 363 ) 364 }) 365 t.Run("int counterobserver", func(t *testing.T) { 366 labels := []attribute.KeyValue{} 367 provider, meter := testPair() 368 o := Must(meter).NewInt64CounterObserver("test.counter.int", func(_ context.Context, result metric.Int64ObserverResult) { 369 result.Observe(-142, labels...) 370 }) 371 provider.RunAsyncInstruments() 372 checkObserverBatch(t, labels, provider, number.Int64Kind, sdkapi.CounterObserverInstrumentKind, o.AsyncImpl(), 373 -142, 374 ) 375 }) 376 t.Run("float updowncounterobserver", func(t *testing.T) { 377 labels := []attribute.KeyValue{attribute.String("O", "P")} 378 provider, meter := testPair() 379 o := Must(meter).NewFloat64UpDownCounterObserver("test.updowncounter.float", func(_ context.Context, result metric.Float64ObserverResult) { 380 result.Observe(42.1, labels...) 381 }) 382 provider.RunAsyncInstruments() 383 checkObserverBatch(t, labels, provider, number.Float64Kind, sdkapi.UpDownCounterObserverInstrumentKind, o.AsyncImpl(), 384 42.1, 385 ) 386 }) 387 t.Run("int updowncounterobserver", func(t *testing.T) { 388 labels := []attribute.KeyValue{} 389 provider, meter := testPair() 390 o := Must(meter).NewInt64UpDownCounterObserver("test..int", func(_ context.Context, result metric.Int64ObserverResult) { 391 result.Observe(-142, labels...) 392 }) 393 provider.RunAsyncInstruments() 394 checkObserverBatch(t, labels, provider, number.Int64Kind, sdkapi.UpDownCounterObserverInstrumentKind, o.AsyncImpl(), 395 -142, 396 ) 397 }) 398} 399 400func TestBatchObserverInstruments(t *testing.T) { 401 provider, meter := testPair() 402 403 var obs1 metric.Int64GaugeObserver 404 var obs2 metric.Float64GaugeObserver 405 406 labels := []attribute.KeyValue{ 407 attribute.String("A", "B"), 408 attribute.String("C", "D"), 409 } 410 411 cb := Must(meter).NewBatchObserver( 412 func(_ context.Context, result metric.BatchObserverResult) { 413 result.Observe(labels, 414 obs1.Observation(42), 415 obs2.Observation(42.0), 416 ) 417 }, 418 ) 419 obs1 = cb.NewInt64GaugeObserver("test.gauge.int") 420 obs2 = cb.NewFloat64GaugeObserver("test.gauge.float") 421 422 provider.RunAsyncInstruments() 423 424 require.Len(t, provider.MeasurementBatches, 1) 425 426 impl1 := obs1.AsyncImpl().Implementation().(*metrictest.Async) 427 impl2 := obs2.AsyncImpl().Implementation().(*metrictest.Async) 428 429 require.NotNil(t, impl1) 430 require.NotNil(t, impl2) 431 432 got := provider.MeasurementBatches[0] 433 require.Equal(t, labels, got.Labels) 434 require.Len(t, got.Measurements, 2) 435 436 m1 := got.Measurements[0] 437 require.Equal(t, impl1, m1.Instrument.Implementation().(*metrictest.Async)) 438 require.Equal(t, 0, m1.Number.CompareNumber(number.Int64Kind, metrictest.ResolveNumberByKind(t, number.Int64Kind, 42))) 439 440 m2 := got.Measurements[1] 441 require.Equal(t, impl2, m2.Instrument.Implementation().(*metrictest.Async)) 442 require.Equal(t, 0, m2.Number.CompareNumber(number.Float64Kind, metrictest.ResolveNumberByKind(t, number.Float64Kind, 42))) 443} 444 445func checkObserverBatch(t *testing.T, labels []attribute.KeyValue, provider *metrictest.MeterProvider, nkind number.Kind, mkind sdkapi.InstrumentKind, observer sdkapi.AsyncImpl, expected float64) { 446 t.Helper() 447 assert.Len(t, provider.MeasurementBatches, 1) 448 if len(provider.MeasurementBatches) < 1 { 449 return 450 } 451 o := observer.Implementation().(*metrictest.Async) 452 if !assert.NotNil(t, o) { 453 return 454 } 455 got := provider.MeasurementBatches[0] 456 assert.Equal(t, labels, got.Labels) 457 assert.Len(t, got.Measurements, 1) 458 if len(got.Measurements) < 1 { 459 return 460 } 461 measurement := got.Measurements[0] 462 require.Equal(t, mkind, measurement.Instrument.Descriptor().InstrumentKind()) 463 assert.Equal(t, o, measurement.Instrument.Implementation().(*metrictest.Async)) 464 ft := metrictest.ResolveNumberByKind(t, nkind, expected) 465 assert.Equal(t, 0, measurement.Number.CompareNumber(nkind, ft)) 466} 467 468type testWrappedMeter struct { 469} 470 471var _ sdkapi.MeterImpl = testWrappedMeter{} 472 473func (testWrappedMeter) RecordBatch(context.Context, []attribute.KeyValue, ...sdkapi.Measurement) { 474} 475 476func (testWrappedMeter) NewSyncInstrument(_ sdkapi.Descriptor) (sdkapi.SyncImpl, error) { 477 return nil, nil 478} 479 480func (testWrappedMeter) NewAsyncInstrument(_ sdkapi.Descriptor, _ sdkapi.AsyncRunner) (sdkapi.AsyncImpl, error) { 481 return nil, errors.New("Test wrap error") 482} 483 484func TestWrappedInstrumentError(t *testing.T) { 485 impl := &testWrappedMeter{} 486 meter := metric.WrapMeterImpl(impl) 487 488 histogram, err := meter.NewInt64Histogram("test.histogram") 489 490 require.Equal(t, err, metric.ErrSDKReturnedNilImpl) 491 require.NotNil(t, histogram.SyncImpl()) 492 493 observer, err := meter.NewInt64GaugeObserver("test.observer", func(_ context.Context, result metric.Int64ObserverResult) {}) 494 495 require.NotNil(t, err) 496 require.NotNil(t, observer.AsyncImpl()) 497} 498 499func TestNilCallbackObserverNoop(t *testing.T) { 500 // Tests that a nil callback yields a no-op observer without error. 501 _, meter := testPair() 502 503 observer := Must(meter).NewInt64GaugeObserver("test.observer", nil) 504 505 impl := observer.AsyncImpl().Implementation() 506 desc := observer.AsyncImpl().Descriptor() 507 require.Equal(t, nil, impl) 508 require.Equal(t, "", desc.Name()) 509} 510