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