1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2024-12-12 10:04:29 +02:00
opentelemetry-go/sdk/metric/meter_test.go
Tyler Yahn 15d6ba2921
Unify metric API into the one otel/metric package (#4018)
* Move instrument into metric

* Update metric docs to include instrument

* Update package names

* Update all imports of sdk/metric/instrument

* Rename Option to InstrumentOption

* Deprecate otel/metric/instrument

* Add changelog entry
2023-04-27 11:25:48 -07:00

1521 lines
41 KiB
Go

// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metric
import (
"context"
"fmt"
"strings"
"sync"
"testing"
"github.com/go-logr/logr"
"github.com/go-logr/logr/testr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/global"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric/aggregation"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
"go.opentelemetry.io/otel/sdk/resource"
)
// A meter should be able to make instruments concurrently.
func TestMeterInstrumentConcurrency(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(12)
m := NewMeterProvider().Meter("inst-concurrency")
go func() {
_, _ = m.Float64ObservableCounter("AFCounter")
wg.Done()
}()
go func() {
_, _ = m.Float64ObservableUpDownCounter("AFUpDownCounter")
wg.Done()
}()
go func() {
_, _ = m.Float64ObservableGauge("AFGauge")
wg.Done()
}()
go func() {
_, _ = m.Int64ObservableCounter("AICounter")
wg.Done()
}()
go func() {
_, _ = m.Int64ObservableUpDownCounter("AIUpDownCounter")
wg.Done()
}()
go func() {
_, _ = m.Int64ObservableGauge("AIGauge")
wg.Done()
}()
go func() {
_, _ = m.Float64Counter("SFCounter")
wg.Done()
}()
go func() {
_, _ = m.Float64UpDownCounter("SFUpDownCounter")
wg.Done()
}()
go func() {
_, _ = m.Float64Histogram("SFHistogram")
wg.Done()
}()
go func() {
_, _ = m.Int64Counter("SICounter")
wg.Done()
}()
go func() {
_, _ = m.Int64UpDownCounter("SIUpDownCounter")
wg.Done()
}()
go func() {
_, _ = m.Int64Histogram("SIHistogram")
wg.Done()
}()
wg.Wait()
}
var emptyCallback metric.Callback = func(context.Context, metric.Observer) error { return nil }
// A Meter Should be able register Callbacks Concurrently.
func TestMeterCallbackCreationConcurrency(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(2)
m := NewMeterProvider().Meter("callback-concurrency")
go func() {
_, _ = m.RegisterCallback(emptyCallback)
wg.Done()
}()
go func() {
_, _ = m.RegisterCallback(emptyCallback)
wg.Done()
}()
wg.Wait()
}
func TestNoopCallbackUnregisterConcurrency(t *testing.T) {
m := NewMeterProvider().Meter("noop-unregister-concurrency")
reg, err := m.RegisterCallback(emptyCallback)
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
_ = reg.Unregister()
wg.Done()
}()
go func() {
_ = reg.Unregister()
wg.Done()
}()
wg.Wait()
}
func TestCallbackUnregisterConcurrency(t *testing.T) {
reader := NewManualReader()
provider := NewMeterProvider(WithReader(reader))
meter := provider.Meter("unregister-concurrency")
actr, err := meter.Float64ObservableCounter("counter")
require.NoError(t, err)
ag, err := meter.Int64ObservableGauge("gauge")
require.NoError(t, err)
regCtr, err := meter.RegisterCallback(emptyCallback, actr)
require.NoError(t, err)
regG, err := meter.RegisterCallback(emptyCallback, ag)
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
_ = regCtr.Unregister()
_ = regG.Unregister()
wg.Done()
}()
go func() {
_ = regCtr.Unregister()
_ = regG.Unregister()
wg.Done()
}()
wg.Wait()
}
// Instruments should produce correct ResourceMetrics.
func TestMeterCreatesInstruments(t *testing.T) {
attrs := attribute.NewSet(attribute.String("name", "alice"))
opt := metric.WithAttributeSet(attrs)
testCases := []struct {
name string
fn func(*testing.T, metric.Meter)
want metricdata.Metrics
}{
{
name: "ObservableInt64Count",
fn: func(t *testing.T, m metric.Meter) {
cback := func(_ context.Context, o metric.Int64Observer) error {
o.Observe(4, opt)
return nil
}
ctr, err := m.Int64ObservableCounter("aint", metric.WithInt64Callback(cback))
assert.NoError(t, err)
_, err = m.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(ctr, 3)
return nil
}, ctr)
assert.NoError(t, err)
},
want: metricdata.Metrics{
Name: "aint",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: attrs, Value: 4},
{Value: 3},
},
},
},
},
{
name: "ObservableInt64UpDownCount",
fn: func(t *testing.T, m metric.Meter) {
cback := func(_ context.Context, o metric.Int64Observer) error {
o.Observe(4, opt)
return nil
}
ctr, err := m.Int64ObservableUpDownCounter("aint", metric.WithInt64Callback(cback))
assert.NoError(t, err)
_, err = m.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(ctr, 11)
return nil
}, ctr)
assert.NoError(t, err)
},
want: metricdata.Metrics{
Name: "aint",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: attrs, Value: 4},
{Value: 11},
},
},
},
},
{
name: "ObservableInt64Gauge",
fn: func(t *testing.T, m metric.Meter) {
cback := func(_ context.Context, o metric.Int64Observer) error {
o.Observe(4, opt)
return nil
}
gauge, err := m.Int64ObservableGauge("agauge", metric.WithInt64Callback(cback))
assert.NoError(t, err)
_, err = m.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(gauge, 11)
return nil
}, gauge)
assert.NoError(t, err)
},
want: metricdata.Metrics{
Name: "agauge",
Data: metricdata.Gauge[int64]{
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: attrs, Value: 4},
{Value: 11},
},
},
},
},
{
name: "ObservableFloat64Count",
fn: func(t *testing.T, m metric.Meter) {
cback := func(_ context.Context, o metric.Float64Observer) error {
o.Observe(4, opt)
return nil
}
ctr, err := m.Float64ObservableCounter("afloat", metric.WithFloat64Callback(cback))
assert.NoError(t, err)
_, err = m.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveFloat64(ctr, 3)
return nil
}, ctr)
assert.NoError(t, err)
},
want: metricdata.Metrics{
Name: "afloat",
Data: metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[float64]{
{Attributes: attrs, Value: 4},
{Value: 3},
},
},
},
},
{
name: "ObservableFloat64UpDownCount",
fn: func(t *testing.T, m metric.Meter) {
cback := func(_ context.Context, o metric.Float64Observer) error {
o.Observe(4, opt)
return nil
}
ctr, err := m.Float64ObservableUpDownCounter("afloat", metric.WithFloat64Callback(cback))
assert.NoError(t, err)
_, err = m.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveFloat64(ctr, 11)
return nil
}, ctr)
assert.NoError(t, err)
},
want: metricdata.Metrics{
Name: "afloat",
Data: metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[float64]{
{Attributes: attrs, Value: 4},
{Value: 11},
},
},
},
},
{
name: "ObservableFloat64Gauge",
fn: func(t *testing.T, m metric.Meter) {
cback := func(_ context.Context, o metric.Float64Observer) error {
o.Observe(4, opt)
return nil
}
gauge, err := m.Float64ObservableGauge("agauge", metric.WithFloat64Callback(cback))
assert.NoError(t, err)
_, err = m.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveFloat64(gauge, 11)
return nil
}, gauge)
assert.NoError(t, err)
},
want: metricdata.Metrics{
Name: "agauge",
Data: metricdata.Gauge[float64]{
DataPoints: []metricdata.DataPoint[float64]{
{Attributes: attrs, Value: 4},
{Value: 11},
},
},
},
},
{
name: "SyncInt64Count",
fn: func(t *testing.T, m metric.Meter) {
ctr, err := m.Int64Counter("sint")
assert.NoError(t, err)
ctr.Add(context.Background(), 3)
},
want: metricdata.Metrics{
Name: "sint",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{Value: 3},
},
},
},
},
{
name: "SyncInt64UpDownCount",
fn: func(t *testing.T, m metric.Meter) {
ctr, err := m.Int64UpDownCounter("sint")
assert.NoError(t, err)
ctr.Add(context.Background(), 11)
},
want: metricdata.Metrics{
Name: "sint",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[int64]{
{Value: 11},
},
},
},
},
{
name: "SyncInt64Histogram",
fn: func(t *testing.T, m metric.Meter) {
gauge, err := m.Int64Histogram("histogram")
assert.NoError(t, err)
gauge.Record(context.Background(), 7)
},
want: metricdata.Metrics{
Name: "histogram",
Data: metricdata.Histogram[int64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: []metricdata.HistogramDataPoint[int64]{
{
Attributes: attribute.Set{},
Count: 1,
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
BucketCounts: []uint64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Min: metricdata.NewExtrema[int64](7),
Max: metricdata.NewExtrema[int64](7),
Sum: 7,
},
},
},
},
},
{
name: "SyncFloat64Count",
fn: func(t *testing.T, m metric.Meter) {
ctr, err := m.Float64Counter("sfloat")
assert.NoError(t, err)
ctr.Add(context.Background(), 3)
},
want: metricdata.Metrics{
Name: "sfloat",
Data: metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[float64]{
{Value: 3},
},
},
},
},
{
name: "SyncFloat64UpDownCount",
fn: func(t *testing.T, m metric.Meter) {
ctr, err := m.Float64UpDownCounter("sfloat")
assert.NoError(t, err)
ctr.Add(context.Background(), 11)
},
want: metricdata.Metrics{
Name: "sfloat",
Data: metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[float64]{
{Value: 11},
},
},
},
},
{
name: "SyncFloat64Histogram",
fn: func(t *testing.T, m metric.Meter) {
gauge, err := m.Float64Histogram("histogram")
assert.NoError(t, err)
gauge.Record(context.Background(), 7)
},
want: metricdata.Metrics{
Name: "histogram",
Data: metricdata.Histogram[float64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: []metricdata.HistogramDataPoint[float64]{
{
Attributes: attribute.Set{},
Count: 1,
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
BucketCounts: []uint64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Min: metricdata.NewExtrema[float64](7.),
Max: metricdata.NewExtrema[float64](7.),
Sum: 7.0,
},
},
},
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
rdr := NewManualReader()
m := NewMeterProvider(WithReader(rdr)).Meter("testInstruments")
tt.fn(t, m)
rm := metricdata.ResourceMetrics{}
err := rdr.Collect(context.Background(), &rm)
assert.NoError(t, err)
require.Len(t, rm.ScopeMetrics, 1)
sm := rm.ScopeMetrics[0]
require.Len(t, sm.Metrics, 1)
got := sm.Metrics[0]
metricdatatest.AssertEqual(t, tt.want, got, metricdatatest.IgnoreTimestamp())
})
}
}
func TestRegisterNonSDKObserverErrors(t *testing.T) {
rdr := NewManualReader()
mp := NewMeterProvider(WithReader(rdr))
meter := mp.Meter("scope")
type obsrv struct{ metric.Observable }
o := obsrv{}
_, err := meter.RegisterCallback(
func(context.Context, metric.Observer) error { return nil },
o,
)
assert.ErrorContains(
t,
err,
"invalid observable: from different implementation",
"External instrument registered",
)
}
func TestMeterMixingOnRegisterErrors(t *testing.T) {
rdr := NewManualReader()
mp := NewMeterProvider(WithReader(rdr))
m1 := mp.Meter("scope1")
m2 := mp.Meter("scope2")
iCtr, err := m2.Int64ObservableCounter("int64 ctr")
require.NoError(t, err)
fCtr, err := m2.Float64ObservableCounter("float64 ctr")
require.NoError(t, err)
_, err = m1.RegisterCallback(
func(context.Context, metric.Observer) error { return nil },
iCtr, fCtr,
)
assert.ErrorContains(
t,
err,
`invalid registration: observable "int64 ctr" from Meter "scope2", registered with Meter "scope1"`,
"Instrument registered with non-creation Meter",
)
assert.ErrorContains(
t,
err,
`invalid registration: observable "float64 ctr" from Meter "scope2", registered with Meter "scope1"`,
"Instrument registered with non-creation Meter",
)
}
func TestCallbackObserverNonRegistered(t *testing.T) {
rdr := NewManualReader()
mp := NewMeterProvider(WithReader(rdr))
m1 := mp.Meter("scope1")
valid, err := m1.Int64ObservableCounter("ctr")
require.NoError(t, err)
m2 := mp.Meter("scope2")
iCtr, err := m2.Int64ObservableCounter("int64 ctr")
require.NoError(t, err)
fCtr, err := m2.Float64ObservableCounter("float64 ctr")
require.NoError(t, err)
type int64Obsrv struct{ metric.Int64Observable }
int64Foreign := int64Obsrv{}
type float64Obsrv struct{ metric.Float64Observable }
float64Foreign := float64Obsrv{}
_, err = m1.RegisterCallback(
func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(valid, 1)
o.ObserveInt64(iCtr, 1)
o.ObserveFloat64(fCtr, 1)
o.ObserveInt64(int64Foreign, 1)
o.ObserveFloat64(float64Foreign, 1)
return nil
},
valid,
)
require.NoError(t, err)
var got metricdata.ResourceMetrics
assert.NotPanics(t, func() {
err = rdr.Collect(context.Background(), &got)
})
assert.NoError(t, err)
want := metricdata.ResourceMetrics{
Resource: resource.Default(),
ScopeMetrics: []metricdata.ScopeMetrics{
{
Scope: instrumentation.Scope{
Name: "scope1",
},
Metrics: []metricdata.Metrics{
{
Name: "ctr",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{
Value: 1,
},
},
},
},
},
},
},
}
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
}
type logSink struct {
logr.LogSink
messages []string
}
func newLogSink(t *testing.T) *logSink {
return &logSink{LogSink: testr.New(t).GetSink()}
}
func (l *logSink) Info(level int, msg string, keysAndValues ...interface{}) {
l.messages = append(l.messages, msg)
l.LogSink.Info(level, msg, keysAndValues...)
}
func (l *logSink) Error(err error, msg string, keysAndValues ...interface{}) {
l.messages = append(l.messages, fmt.Sprintf("%s: %s", err, msg))
l.LogSink.Error(err, msg, keysAndValues...)
}
func (l *logSink) String() string {
out := make([]string, len(l.messages))
for i := range l.messages {
out[i] = "\t-" + l.messages[i]
}
return strings.Join(out, "\n")
}
func TestGlobalInstRegisterCallback(t *testing.T) {
l := newLogSink(t)
otel.SetLogger(logr.New(l))
const mtrName = "TestGlobalInstRegisterCallback"
preMtr := global.Meter(mtrName)
preInt64Ctr, err := preMtr.Int64ObservableCounter("pre.int64.counter")
require.NoError(t, err)
preFloat64Ctr, err := preMtr.Float64ObservableCounter("pre.float64.counter")
require.NoError(t, err)
rdr := NewManualReader()
mp := NewMeterProvider(WithReader(rdr), WithResource(resource.Empty()))
global.SetMeterProvider(mp)
postMtr := global.Meter(mtrName)
postInt64Ctr, err := postMtr.Int64ObservableCounter("post.int64.counter")
require.NoError(t, err)
postFloat64Ctr, err := postMtr.Float64ObservableCounter("post.float64.counter")
require.NoError(t, err)
cb := func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(preInt64Ctr, 1)
o.ObserveFloat64(preFloat64Ctr, 2)
o.ObserveInt64(postInt64Ctr, 3)
o.ObserveFloat64(postFloat64Ctr, 4)
return nil
}
_, err = preMtr.RegisterCallback(cb, preInt64Ctr, preFloat64Ctr, postInt64Ctr, postFloat64Ctr)
assert.NoError(t, err)
_, err = preMtr.RegisterCallback(cb, preInt64Ctr, preFloat64Ctr, postInt64Ctr, postFloat64Ctr)
assert.NoError(t, err)
got := metricdata.ResourceMetrics{}
err = rdr.Collect(context.Background(), &got)
assert.NoError(t, err)
assert.Lenf(t, l.messages, 0, "Warnings and errors logged:\n%s", l)
metricdatatest.AssertEqual(t, metricdata.ResourceMetrics{
ScopeMetrics: []metricdata.ScopeMetrics{
{
Scope: instrumentation.Scope{Name: "TestGlobalInstRegisterCallback"},
Metrics: []metricdata.Metrics{
{
Name: "pre.int64.counter",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{{Value: 1}},
},
},
{
Name: "pre.float64.counter",
Data: metricdata.Sum[float64]{
DataPoints: []metricdata.DataPoint[float64]{{Value: 2}},
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
},
},
{
Name: "post.int64.counter",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{{Value: 3}},
},
},
{
Name: "post.float64.counter",
Data: metricdata.Sum[float64]{
DataPoints: []metricdata.DataPoint[float64]{{Value: 4}},
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
},
},
},
},
},
}, got, metricdatatest.IgnoreTimestamp())
}
func TestMetersProvideScope(t *testing.T) {
rdr := NewManualReader()
mp := NewMeterProvider(WithReader(rdr))
m1 := mp.Meter("scope1")
ctr1, err := m1.Float64ObservableCounter("ctr1")
assert.NoError(t, err)
_, err = m1.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveFloat64(ctr1, 5)
return nil
}, ctr1)
assert.NoError(t, err)
m2 := mp.Meter("scope2")
ctr2, err := m2.Int64ObservableCounter("ctr2")
assert.NoError(t, err)
_, err = m2.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(ctr2, 7)
return nil
}, ctr2)
assert.NoError(t, err)
want := metricdata.ResourceMetrics{
Resource: resource.Default(),
ScopeMetrics: []metricdata.ScopeMetrics{
{
Scope: instrumentation.Scope{
Name: "scope1",
},
Metrics: []metricdata.Metrics{
{
Name: "ctr1",
Data: metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[float64]{
{
Value: 5,
},
},
},
},
},
},
{
Scope: instrumentation.Scope{
Name: "scope2",
},
Metrics: []metricdata.Metrics{
{
Name: "ctr2",
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{
Value: 7,
},
},
},
},
},
},
},
}
got := metricdata.ResourceMetrics{}
err = rdr.Collect(context.Background(), &got)
assert.NoError(t, err)
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
}
func TestUnregisterUnregisters(t *testing.T) {
r := NewManualReader()
mp := NewMeterProvider(WithReader(r))
m := mp.Meter("TestUnregisterUnregisters")
int64Counter, err := m.Int64ObservableCounter("int64.counter")
require.NoError(t, err)
int64UpDownCounter, err := m.Int64ObservableUpDownCounter("int64.up_down_counter")
require.NoError(t, err)
int64Gauge, err := m.Int64ObservableGauge("int64.gauge")
require.NoError(t, err)
floag64Counter, err := m.Float64ObservableCounter("floag64.counter")
require.NoError(t, err)
floag64UpDownCounter, err := m.Float64ObservableUpDownCounter("floag64.up_down_counter")
require.NoError(t, err)
floag64Gauge, err := m.Float64ObservableGauge("floag64.gauge")
require.NoError(t, err)
var called bool
reg, err := m.RegisterCallback(
func(context.Context, metric.Observer) error {
called = true
return nil
},
int64Counter,
int64UpDownCounter,
int64Gauge,
floag64Counter,
floag64UpDownCounter,
floag64Gauge,
)
require.NoError(t, err)
ctx := context.Background()
err = r.Collect(ctx, &metricdata.ResourceMetrics{})
require.NoError(t, err)
assert.True(t, called, "callback not called for registered callback")
called = false
require.NoError(t, reg.Unregister(), "unregister")
err = r.Collect(ctx, &metricdata.ResourceMetrics{})
require.NoError(t, err)
assert.False(t, called, "callback called for unregistered callback")
}
func TestRegisterCallbackDropAggregations(t *testing.T) {
aggFn := func(InstrumentKind) aggregation.Aggregation {
return aggregation.Drop{}
}
r := NewManualReader(WithAggregationSelector(aggFn))
mp := NewMeterProvider(WithReader(r))
m := mp.Meter("testRegisterCallbackDropAggregations")
int64Counter, err := m.Int64ObservableCounter("int64.counter")
require.NoError(t, err)
int64UpDownCounter, err := m.Int64ObservableUpDownCounter("int64.up_down_counter")
require.NoError(t, err)
int64Gauge, err := m.Int64ObservableGauge("int64.gauge")
require.NoError(t, err)
floag64Counter, err := m.Float64ObservableCounter("floag64.counter")
require.NoError(t, err)
floag64UpDownCounter, err := m.Float64ObservableUpDownCounter("floag64.up_down_counter")
require.NoError(t, err)
floag64Gauge, err := m.Float64ObservableGauge("floag64.gauge")
require.NoError(t, err)
var called bool
_, err = m.RegisterCallback(
func(context.Context, metric.Observer) error {
called = true
return nil
},
int64Counter,
int64UpDownCounter,
int64Gauge,
floag64Counter,
floag64UpDownCounter,
floag64Gauge,
)
require.NoError(t, err)
data := metricdata.ResourceMetrics{}
err = r.Collect(context.Background(), &data)
require.NoError(t, err)
assert.False(t, called, "callback called for all drop instruments")
assert.Len(t, data.ScopeMetrics, 0, "metrics exported for drop instruments")
}
func TestAttributeFilter(t *testing.T) {
t.Run("Delta", testAttributeFilter(metricdata.DeltaTemporality))
t.Run("Cumulative", testAttributeFilter(metricdata.CumulativeTemporality))
}
func testAttributeFilter(temporality metricdata.Temporality) func(*testing.T) {
fooBar := attribute.NewSet(attribute.String("foo", "bar"))
withFooBar := metric.WithAttributeSet(fooBar)
v1 := attribute.NewSet(attribute.String("foo", "bar"), attribute.Int("version", 1))
withV1 := metric.WithAttributeSet(v1)
v2 := attribute.NewSet(attribute.String("foo", "bar"), attribute.Int("version", 2))
withV2 := metric.WithAttributeSet(v2)
testcases := []struct {
name string
register func(t *testing.T, mtr metric.Meter) error
wantMetric metricdata.Metrics
}{
{
name: "ObservableFloat64Counter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Float64ObservableCounter("afcounter")
if err != nil {
return err
}
_, err = mtr.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveFloat64(ctr, 1.0, withV1)
o.ObserveFloat64(ctr, 2.0, withFooBar)
o.ObserveFloat64(ctr, 1.0, withV2)
return nil
}, ctr)
return err
},
wantMetric: metricdata.Metrics{
Name: "afcounter",
Data: metricdata.Sum[float64]{
DataPoints: []metricdata.DataPoint[float64]{
{Attributes: fooBar, Value: 4.0},
},
Temporality: temporality,
IsMonotonic: true,
},
},
},
{
name: "ObservableFloat64UpDownCounter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Float64ObservableUpDownCounter("afupdowncounter")
if err != nil {
return err
}
_, err = mtr.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveFloat64(ctr, 1.0, withV1)
o.ObserveFloat64(ctr, 2.0, withFooBar)
o.ObserveFloat64(ctr, 1.0, withV2)
return nil
}, ctr)
return err
},
wantMetric: metricdata.Metrics{
Name: "afupdowncounter",
Data: metricdata.Sum[float64]{
DataPoints: []metricdata.DataPoint[float64]{
{
Attributes: attribute.NewSet(attribute.String("foo", "bar")),
Value: 4.0,
},
},
Temporality: temporality,
IsMonotonic: false,
},
},
},
{
name: "ObservableFloat64Gauge",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Float64ObservableGauge("afgauge")
if err != nil {
return err
}
_, err = mtr.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveFloat64(ctr, 1.0, withV1)
o.ObserveFloat64(ctr, 2.0, withV2)
return nil
}, ctr)
return err
},
wantMetric: metricdata.Metrics{
Name: "afgauge",
Data: metricdata.Gauge[float64]{
DataPoints: []metricdata.DataPoint[float64]{
{Attributes: fooBar, Value: 2.0},
},
},
},
},
{
name: "ObservableInt64Counter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Int64ObservableCounter("aicounter")
if err != nil {
return err
}
_, err = mtr.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(ctr, 10, withV1)
o.ObserveInt64(ctr, 20, withFooBar)
o.ObserveInt64(ctr, 10, withV2)
return nil
}, ctr)
return err
},
wantMetric: metricdata.Metrics{
Name: "aicounter",
Data: metricdata.Sum[int64]{
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: fooBar, Value: 40},
},
Temporality: temporality,
IsMonotonic: true,
},
},
},
{
name: "ObservableInt64UpDownCounter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Int64ObservableUpDownCounter("aiupdowncounter")
if err != nil {
return err
}
_, err = mtr.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(ctr, 10, withV1)
o.ObserveInt64(ctr, 20, withFooBar)
o.ObserveInt64(ctr, 10, withV2)
return nil
}, ctr)
return err
},
wantMetric: metricdata.Metrics{
Name: "aiupdowncounter",
Data: metricdata.Sum[int64]{
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: fooBar, Value: 40},
},
Temporality: temporality,
IsMonotonic: false,
},
},
},
{
name: "ObservableInt64Gauge",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Int64ObservableGauge("aigauge")
if err != nil {
return err
}
_, err = mtr.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(ctr, 10, withV1)
o.ObserveInt64(ctr, 20, withV2)
return nil
}, ctr)
return err
},
wantMetric: metricdata.Metrics{
Name: "aigauge",
Data: metricdata.Gauge[int64]{
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: fooBar, Value: 20},
},
},
},
},
{
name: "SyncFloat64Counter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Float64Counter("sfcounter")
if err != nil {
return err
}
ctr.Add(context.Background(), 1.0, withV1)
ctr.Add(context.Background(), 2.0, withV2)
return nil
},
wantMetric: metricdata.Metrics{
Name: "sfcounter",
Data: metricdata.Sum[float64]{
DataPoints: []metricdata.DataPoint[float64]{
{Attributes: fooBar, Value: 3.0},
},
Temporality: temporality,
IsMonotonic: true,
},
},
},
{
name: "SyncFloat64UpDownCounter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Float64UpDownCounter("sfupdowncounter")
if err != nil {
return err
}
ctr.Add(context.Background(), 1.0, withV1)
ctr.Add(context.Background(), 2.0, withV2)
return nil
},
wantMetric: metricdata.Metrics{
Name: "sfupdowncounter",
Data: metricdata.Sum[float64]{
DataPoints: []metricdata.DataPoint[float64]{
{Attributes: fooBar, Value: 3.0},
},
Temporality: temporality,
IsMonotonic: false,
},
},
},
{
name: "SyncFloat64Histogram",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Float64Histogram("sfhistogram")
if err != nil {
return err
}
ctr.Record(context.Background(), 1.0, withV1)
ctr.Record(context.Background(), 2.0, withV2)
return nil
},
wantMetric: metricdata.Metrics{
Name: "sfhistogram",
Data: metricdata.Histogram[float64]{
DataPoints: []metricdata.HistogramDataPoint[float64]{
{
Attributes: fooBar,
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
BucketCounts: []uint64{0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Count: 2,
Min: metricdata.NewExtrema(1.),
Max: metricdata.NewExtrema(2.),
Sum: 3.0,
},
},
Temporality: temporality,
},
},
},
{
name: "SyncInt64Counter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Int64Counter("sicounter")
if err != nil {
return err
}
ctr.Add(context.Background(), 10, withV1)
ctr.Add(context.Background(), 20, withV2)
return nil
},
wantMetric: metricdata.Metrics{
Name: "sicounter",
Data: metricdata.Sum[int64]{
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: fooBar, Value: 30},
},
Temporality: temporality,
IsMonotonic: true,
},
},
},
{
name: "SyncInt64UpDownCounter",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Int64UpDownCounter("siupdowncounter")
if err != nil {
return err
}
ctr.Add(context.Background(), 10, withV1)
ctr.Add(context.Background(), 20, withV2)
return nil
},
wantMetric: metricdata.Metrics{
Name: "siupdowncounter",
Data: metricdata.Sum[int64]{
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: fooBar, Value: 30},
},
Temporality: temporality,
IsMonotonic: false,
},
},
},
{
name: "SyncInt64Histogram",
register: func(t *testing.T, mtr metric.Meter) error {
ctr, err := mtr.Int64Histogram("sihistogram")
if err != nil {
return err
}
ctr.Record(context.Background(), 1, withV1)
ctr.Record(context.Background(), 2, withV2)
return nil
},
wantMetric: metricdata.Metrics{
Name: "sihistogram",
Data: metricdata.Histogram[int64]{
DataPoints: []metricdata.HistogramDataPoint[int64]{
{
Attributes: fooBar,
Bounds: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
BucketCounts: []uint64{0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Count: 2,
Min: metricdata.NewExtrema[int64](1),
Max: metricdata.NewExtrema[int64](2),
Sum: 3.0,
},
},
Temporality: temporality,
},
},
},
}
return func(t *testing.T) {
for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
rdr := NewManualReader(WithTemporalitySelector(func(InstrumentKind) metricdata.Temporality {
return temporality
}))
mtr := NewMeterProvider(
WithReader(rdr),
WithView(NewView(
Instrument{Name: "*"},
Stream{AttributeFilter: func(kv attribute.KeyValue) bool {
return kv.Key == attribute.Key("foo")
}},
)),
).Meter("TestAttributeFilter")
require.NoError(t, tt.register(t, mtr))
m := metricdata.ResourceMetrics{}
err := rdr.Collect(context.Background(), &m)
assert.NoError(t, err)
require.Len(t, m.ScopeMetrics, 1)
require.Len(t, m.ScopeMetrics[0].Metrics, 1)
metricdatatest.AssertEqual(t, tt.wantMetric, m.ScopeMetrics[0].Metrics[0], metricdatatest.IgnoreTimestamp())
})
}
}
}
func TestObservableExample(t *testing.T) {
// This example can be found:
// https://github.com/open-telemetry/opentelemetry-specification/blob/8b91585e6175dd52b51e1d60bea105041225e35d/specification/metrics/supplementary-guidelines.md#asynchronous-example
var (
threadID1 = attribute.Int("tid", 1)
threadID2 = attribute.Int("tid", 2)
threadID3 = attribute.Int("tid", 3)
processID1001 = attribute.String("pid", "1001")
thread1 = attribute.NewSet(processID1001, threadID1)
thread2 = attribute.NewSet(processID1001, threadID2)
thread3 = attribute.NewSet(processID1001, threadID3)
process1001 = attribute.NewSet(processID1001)
)
setup := func(t *testing.T, temp metricdata.Temporality) (map[attribute.Set]int64, func(*testing.T), *metricdata.ScopeMetrics, *int64, *int64, *int64) {
t.Helper()
const (
instName = "pageFaults"
filteredStream = "filteredPageFaults"
scopeName = "ObservableExample"
)
selector := func(InstrumentKind) metricdata.Temporality { return temp }
reader := NewManualReader(WithTemporalitySelector(selector))
noopFilter := func(kv attribute.KeyValue) bool { return true }
noFiltered := NewView(Instrument{Name: instName}, Stream{Name: instName, AttributeFilter: noopFilter})
filter := func(kv attribute.KeyValue) bool { return kv.Key != "tid" }
filtered := NewView(Instrument{Name: instName}, Stream{Name: filteredStream, AttributeFilter: filter})
mp := NewMeterProvider(WithReader(reader), WithView(noFiltered, filtered))
meter := mp.Meter(scopeName)
observations := make(map[attribute.Set]int64)
_, err := meter.Int64ObservableCounter(instName, metric.WithInt64Callback(
func(_ context.Context, o metric.Int64Observer) error {
for attrSet, val := range observations {
o.Observe(val, metric.WithAttributeSet(attrSet))
}
return nil
},
))
require.NoError(t, err)
want := &metricdata.ScopeMetrics{
Scope: instrumentation.Scope{Name: scopeName},
Metrics: []metricdata.Metrics{
{
Name: filteredStream,
Data: metricdata.Sum[int64]{
Temporality: temp,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: process1001},
},
},
},
{
Name: instName,
Data: metricdata.Sum[int64]{
Temporality: temp,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: thread1},
{Attributes: thread2},
},
},
},
},
}
wantFiltered := &want.Metrics[0].Data.(metricdata.Sum[int64]).DataPoints[0].Value
wantThread1 := &want.Metrics[1].Data.(metricdata.Sum[int64]).DataPoints[0].Value
wantThread2 := &want.Metrics[1].Data.(metricdata.Sum[int64]).DataPoints[1].Value
collect := func(t *testing.T) {
t.Helper()
got := metricdata.ResourceMetrics{}
err := reader.Collect(context.Background(), &got)
require.NoError(t, err)
require.Len(t, got.ScopeMetrics, 1)
metricdatatest.AssertEqual(t, *want, got.ScopeMetrics[0], metricdatatest.IgnoreTimestamp())
}
return observations, collect, want, wantFiltered, wantThread1, wantThread2
}
t.Run("Cumulative", func(t *testing.T) {
temporality := metricdata.CumulativeTemporality
observations, verify, want, wantFiltered, wantThread1, wantThread2 := setup(t, temporality)
// During the time range (T0, T1]:
// pid = 1001, tid = 1, #PF = 50
// pid = 1001, tid = 2, #PF = 30
observations[thread1] = 50
observations[thread2] = 30
*wantFiltered = 80
*wantThread1 = 50
*wantThread2 = 30
verify(t)
// During the time range (T1, T2]:
// pid = 1001, tid = 1, #PF = 53
// pid = 1001, tid = 2, #PF = 38
observations[thread1] = 53
observations[thread2] = 38
*wantFiltered = 91
*wantThread1 = 53
*wantThread2 = 38
verify(t)
// During the time range (T2, T3]
// pid = 1001, tid = 1, #PF = 56
// pid = 1001, tid = 2, #PF = 42
observations[thread1] = 56
observations[thread2] = 42
*wantFiltered = 98
*wantThread1 = 56
*wantThread2 = 42
verify(t)
// During the time range (T3, T4]:
// pid = 1001, tid = 1, #PF = 60
// pid = 1001, tid = 2, #PF = 47
observations[thread1] = 60
observations[thread2] = 47
*wantFiltered = 107
*wantThread1 = 60
*wantThread2 = 47
verify(t)
// During the time range (T4, T5]:
// thread 1 died, thread 3 started
// pid = 1001, tid = 2, #PF = 53
// pid = 1001, tid = 3, #PF = 5
delete(observations, thread1)
observations[thread2] = 53
observations[thread3] = 5
*wantFiltered = 58
want.Metrics[1].Data = metricdata.Sum[int64]{
Temporality: temporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
// Thread 1 remains at last measured value.
{Attributes: thread1, Value: 60},
{Attributes: thread2, Value: 53},
{Attributes: thread3, Value: 5},
},
}
verify(t)
})
t.Run("Delta", func(t *testing.T) {
temporality := metricdata.DeltaTemporality
observations, verify, want, wantFiltered, wantThread1, wantThread2 := setup(t, temporality)
// During the time range (T0, T1]:
// pid = 1001, tid = 1, #PF = 50
// pid = 1001, tid = 2, #PF = 30
observations[thread1] = 50
observations[thread2] = 30
*wantFiltered = 80
*wantThread1 = 50
*wantThread2 = 30
verify(t)
// During the time range (T1, T2]:
// pid = 1001, tid = 1, #PF = 53
// pid = 1001, tid = 2, #PF = 38
observations[thread1] = 53
observations[thread2] = 38
*wantFiltered = 11
*wantThread1 = 3
*wantThread2 = 8
verify(t)
// During the time range (T2, T3]
// pid = 1001, tid = 1, #PF = 56
// pid = 1001, tid = 2, #PF = 42
observations[thread1] = 56
observations[thread2] = 42
*wantFiltered = 7
*wantThread1 = 3
*wantThread2 = 4
verify(t)
// During the time range (T3, T4]:
// pid = 1001, tid = 1, #PF = 60
// pid = 1001, tid = 2, #PF = 47
observations[thread1] = 60
observations[thread2] = 47
*wantFiltered = 9
*wantThread1 = 4
*wantThread2 = 5
verify(t)
// During the time range (T4, T5]:
// thread 1 died, thread 3 started
// pid = 1001, tid = 2, #PF = 53
// pid = 1001, tid = 3, #PF = 5
delete(observations, thread1)
observations[thread2] = 53
observations[thread3] = 5
*wantFiltered = -49
want.Metrics[1].Data = metricdata.Sum[int64]{
Temporality: temporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
// Thread 1 remains at last measured value.
{Attributes: thread1, Value: 0},
{Attributes: thread2, Value: 6},
{Attributes: thread3, Value: 5},
},
}
verify(t)
})
}
var (
aiCounter metric.Int64ObservableCounter
aiUpDownCounter metric.Int64ObservableUpDownCounter
aiGauge metric.Int64ObservableGauge
afCounter metric.Float64ObservableCounter
afUpDownCounter metric.Float64ObservableUpDownCounter
afGauge metric.Float64ObservableGauge
siCounter metric.Int64Counter
siUpDownCounter metric.Int64UpDownCounter
siHistogram metric.Int64Histogram
sfCounter metric.Float64Counter
sfUpDownCounter metric.Float64UpDownCounter
sfHistogram metric.Float64Histogram
)
func BenchmarkInstrumentCreation(b *testing.B) {
provider := NewMeterProvider(WithReader(NewManualReader()))
meter := provider.Meter("BenchmarkInstrumentCreation")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
aiCounter, _ = meter.Int64ObservableCounter("observable.int64.counter")
aiUpDownCounter, _ = meter.Int64ObservableUpDownCounter("observable.int64.up.down.counter")
aiGauge, _ = meter.Int64ObservableGauge("observable.int64.gauge")
afCounter, _ = meter.Float64ObservableCounter("observable.float64.counter")
afUpDownCounter, _ = meter.Float64ObservableUpDownCounter("observable.float64.up.down.counter")
afGauge, _ = meter.Float64ObservableGauge("observable.float64.gauge")
siCounter, _ = meter.Int64Counter("sync.int64.counter")
siUpDownCounter, _ = meter.Int64UpDownCounter("sync.int64.up.down.counter")
siHistogram, _ = meter.Int64Histogram("sync.int64.histogram")
sfCounter, _ = meter.Float64Counter("sync.float64.counter")
sfUpDownCounter, _ = meter.Float64UpDownCounter("sync.float64.up.down.counter")
sfHistogram, _ = meter.Float64Histogram("sync.float64.histogram")
}
}