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 global_test
16
17import (
18	"context"
19	"errors"
20	"testing"
21
22	"github.com/stretchr/testify/require"
23
24	"go.opentelemetry.io/otel/attribute"
25	"go.opentelemetry.io/otel/internal/global"
26	"go.opentelemetry.io/otel/metric"
27	metricglobal "go.opentelemetry.io/otel/metric/global"
28	"go.opentelemetry.io/otel/metric/number"
29	"go.opentelemetry.io/otel/oteltest"
30)
31
32var Must = metric.Must
33
34var asInt = number.NewInt64Number
35var asFloat = number.NewFloat64Number
36
37func TestDirect(t *testing.T) {
38	global.ResetForTest()
39
40	ctx := context.Background()
41	meter1 := metricglobal.Meter("test1", metric.WithInstrumentationVersion("semver:v1.0.0"))
42	meter2 := metricglobal.Meter("test2")
43	labels1 := []attribute.KeyValue{attribute.String("A", "B")}
44	labels2 := []attribute.KeyValue{attribute.String("C", "D")}
45	labels3 := []attribute.KeyValue{attribute.String("E", "F")}
46
47	counter := Must(meter1).NewInt64Counter("test.counter")
48	counter.Add(ctx, 1, labels1...)
49	counter.Add(ctx, 1, labels1...)
50
51	valuerecorder := Must(meter1).NewFloat64ValueRecorder("test.valuerecorder")
52	valuerecorder.Record(ctx, 1, labels1...)
53	valuerecorder.Record(ctx, 2, labels1...)
54
55	_ = Must(meter1).NewFloat64ValueObserver("test.valueobserver.float", func(_ context.Context, result metric.Float64ObserverResult) {
56		result.Observe(1., labels1...)
57		result.Observe(2., labels2...)
58	})
59
60	_ = Must(meter1).NewInt64ValueObserver("test.valueobserver.int", func(_ context.Context, result metric.Int64ObserverResult) {
61		result.Observe(1, labels1...)
62		result.Observe(2, labels2...)
63	})
64
65	second := Must(meter2).NewFloat64ValueRecorder("test.second")
66	second.Record(ctx, 1, labels3...)
67	second.Record(ctx, 2, labels3...)
68
69	mock, provider := oteltest.NewMeterProvider()
70	metricglobal.SetMeterProvider(provider)
71
72	counter.Add(ctx, 1, labels1...)
73	valuerecorder.Record(ctx, 3, labels1...)
74	second.Record(ctx, 3, labels3...)
75
76	mock.RunAsyncInstruments()
77
78	measurements := oteltest.AsStructs(mock.MeasurementBatches)
79
80	require.EqualValues(t,
81		[]oteltest.Measured{
82			{
83				Name:                   "test.counter",
84				InstrumentationName:    "test1",
85				InstrumentationVersion: "semver:v1.0.0",
86				Labels:                 oteltest.LabelsToMap(labels1...),
87				Number:                 asInt(1),
88			},
89			{
90				Name:                   "test.valuerecorder",
91				InstrumentationName:    "test1",
92				InstrumentationVersion: "semver:v1.0.0",
93				Labels:                 oteltest.LabelsToMap(labels1...),
94				Number:                 asFloat(3),
95			},
96			{
97				Name:                "test.second",
98				InstrumentationName: "test2",
99				Labels:              oteltest.LabelsToMap(labels3...),
100				Number:              asFloat(3),
101			},
102			{
103				Name:                   "test.valueobserver.float",
104				InstrumentationName:    "test1",
105				InstrumentationVersion: "semver:v1.0.0",
106				Labels:                 oteltest.LabelsToMap(labels1...),
107				Number:                 asFloat(1),
108			},
109			{
110				Name:                   "test.valueobserver.float",
111				InstrumentationName:    "test1",
112				InstrumentationVersion: "semver:v1.0.0",
113				Labels:                 oteltest.LabelsToMap(labels2...),
114				Number:                 asFloat(2),
115			},
116			{
117				Name:                   "test.valueobserver.int",
118				InstrumentationName:    "test1",
119				InstrumentationVersion: "semver:v1.0.0",
120				Labels:                 oteltest.LabelsToMap(labels1...),
121				Number:                 asInt(1),
122			},
123			{
124				Name:                   "test.valueobserver.int",
125				InstrumentationName:    "test1",
126				InstrumentationVersion: "semver:v1.0.0",
127				Labels:                 oteltest.LabelsToMap(labels2...),
128				Number:                 asInt(2),
129			},
130		},
131		measurements,
132	)
133}
134
135func TestBound(t *testing.T) {
136	global.ResetForTest()
137
138	// Note: this test uses opposite Float64/Int64 number kinds
139	// vs. the above, to cover all the instruments.
140	ctx := context.Background()
141	glob := metricglobal.Meter("test")
142	labels1 := []attribute.KeyValue{attribute.String("A", "B")}
143
144	counter := Must(glob).NewFloat64Counter("test.counter")
145	boundC := counter.Bind(labels1...)
146	boundC.Add(ctx, 1)
147	boundC.Add(ctx, 1)
148
149	valuerecorder := Must(glob).NewInt64ValueRecorder("test.valuerecorder")
150	boundM := valuerecorder.Bind(labels1...)
151	boundM.Record(ctx, 1)
152	boundM.Record(ctx, 2)
153
154	mock, provider := oteltest.NewMeterProvider()
155	metricglobal.SetMeterProvider(provider)
156
157	boundC.Add(ctx, 1)
158	boundM.Record(ctx, 3)
159
160	require.EqualValues(t,
161		[]oteltest.Measured{
162			{
163				Name:                "test.counter",
164				InstrumentationName: "test",
165				Labels:              oteltest.LabelsToMap(labels1...),
166				Number:              asFloat(1),
167			},
168			{
169				Name:                "test.valuerecorder",
170				InstrumentationName: "test",
171				Labels:              oteltest.LabelsToMap(labels1...),
172				Number:              asInt(3),
173			},
174		},
175		oteltest.AsStructs(mock.MeasurementBatches))
176
177	boundC.Unbind()
178	boundM.Unbind()
179}
180
181func TestUnbind(t *testing.T) {
182	// Tests Unbind with SDK never installed.
183	global.ResetForTest()
184
185	glob := metricglobal.Meter("test")
186	labels1 := []attribute.KeyValue{attribute.String("A", "B")}
187
188	counter := Must(glob).NewFloat64Counter("test.counter")
189	boundC := counter.Bind(labels1...)
190
191	valuerecorder := Must(glob).NewInt64ValueRecorder("test.valuerecorder")
192	boundM := valuerecorder.Bind(labels1...)
193
194	boundC.Unbind()
195	boundM.Unbind()
196}
197
198func TestUnbindThenRecordOne(t *testing.T) {
199	global.ResetForTest()
200
201	ctx := context.Background()
202	mock, provider := oteltest.NewMeterProvider()
203
204	meter := metricglobal.Meter("test")
205	counter := Must(meter).NewInt64Counter("test.counter")
206	boundC := counter.Bind()
207	metricglobal.SetMeterProvider(provider)
208	boundC.Unbind()
209
210	require.NotPanics(t, func() {
211		boundC.Add(ctx, 1)
212	})
213	require.Equal(t, 0, len(mock.MeasurementBatches))
214}
215
216type meterProviderWithConstructorError struct {
217	metric.MeterProvider
218}
219
220type meterWithConstructorError struct {
221	metric.MeterImpl
222}
223
224func (m *meterProviderWithConstructorError) Meter(iName string, opts ...metric.MeterOption) metric.Meter {
225	return metric.WrapMeterImpl(&meterWithConstructorError{m.MeterProvider.Meter(iName, opts...).MeterImpl()}, iName, opts...)
226}
227
228func (m *meterWithConstructorError) NewSyncInstrument(_ metric.Descriptor) (metric.SyncImpl, error) {
229	return metric.NoopSync{}, errors.New("constructor error")
230}
231
232func TestErrorInDeferredConstructor(t *testing.T) {
233	global.ResetForTest()
234
235	ctx := context.Background()
236	meter := metricglobal.GetMeterProvider().Meter("builtin")
237
238	c1 := Must(meter).NewInt64Counter("test")
239	c2 := Must(meter).NewInt64Counter("test")
240
241	_, provider := oteltest.NewMeterProvider()
242	sdk := &meterProviderWithConstructorError{provider}
243
244	require.Panics(t, func() {
245		metricglobal.SetMeterProvider(sdk)
246	})
247
248	c1.Add(ctx, 1)
249	c2.Add(ctx, 2)
250}
251
252func TestImplementationIndirection(t *testing.T) {
253	global.ResetForTest()
254
255	// Test that Implementation() does the proper indirection, i.e.,
256	// returns the implementation interface not the global, after
257	// registered.
258
259	meter1 := metricglobal.Meter("test1")
260
261	// Sync: no SDK yet
262	counter := Must(meter1).NewInt64Counter("interface.counter")
263
264	ival := counter.Measurement(1).SyncImpl().Implementation()
265	require.NotNil(t, ival)
266
267	_, ok := ival.(*oteltest.Sync)
268	require.False(t, ok)
269
270	// Async: no SDK yet
271	valueobserver := Must(meter1).NewFloat64ValueObserver(
272		"interface.valueobserver",
273		func(_ context.Context, result metric.Float64ObserverResult) {},
274	)
275
276	ival = valueobserver.AsyncImpl().Implementation()
277	require.NotNil(t, ival)
278
279	_, ok = ival.(*oteltest.Async)
280	require.False(t, ok)
281
282	// Register the SDK
283	_, provider := oteltest.NewMeterProvider()
284	metricglobal.SetMeterProvider(provider)
285
286	// Repeat the above tests
287
288	// Sync
289	ival = counter.Measurement(1).SyncImpl().Implementation()
290	require.NotNil(t, ival)
291
292	_, ok = ival.(*oteltest.Sync)
293	require.True(t, ok)
294
295	// Async
296	ival = valueobserver.AsyncImpl().Implementation()
297	require.NotNil(t, ival)
298
299	_, ok = ival.(*oteltest.Async)
300	require.True(t, ok)
301}
302
303func TestRecordBatchMock(t *testing.T) {
304	global.ResetForTest()
305
306	meter := metricglobal.GetMeterProvider().Meter("builtin")
307
308	counter := Must(meter).NewInt64Counter("test.counter")
309
310	meter.RecordBatch(context.Background(), nil, counter.Measurement(1))
311
312	mock, provider := oteltest.NewMeterProvider()
313	metricglobal.SetMeterProvider(provider)
314
315	meter.RecordBatch(context.Background(), nil, counter.Measurement(1))
316
317	require.EqualValues(t,
318		[]oteltest.Measured{
319			{
320				Name:                "test.counter",
321				InstrumentationName: "builtin",
322				Labels:              oteltest.LabelsToMap(),
323				Number:              asInt(1),
324			},
325		},
326		oteltest.AsStructs(mock.MeasurementBatches))
327}
328