mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-01-26 03:52:03 +02:00
81b2a33e1b
Resolve https://github.com/open-telemetry/opentelemetry-go/issues/5249 ### Spec > exemplar_reservoir: A functional type that generates an exemplar reservoir a MeterProvider will use when storing exemplars. This functional type needs to be a factory or callback similar to aggregation selection functionality which allows different reservoirs to be chosen by the aggregation. > Users can provide an exemplar_reservoir, but it is up to their discretion. Therefore, the stream configuration parameter needs to be structured to accept an exemplar_reservoir, but MUST NOT obligate a user to provide one. If the user does not provide an exemplar_reservoir value, the MeterProvider MUST apply a [default exemplar reservoir](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplar-defaults). Also, > the reservoir MUST be given the Attributes associated with its timeseries point either at construction so that additional sampling performed by the reservoir has access to all attributes from a measurement in the "offer" method. ### Changes In sdk/metric/exemplar, add: * `exemplar.ReservoirProvider` * `exemplar.HistogramReservoirProvider` * `exemplar.FixedSizeReservoirProvider` In sdk/metric, add: * `metric.ExemplarReservoirProviderSelector` (func Aggregation -> ReservoirProvider) * `metric.DefaultExemplarReservoirProviderSelector` (our default implementation) * `ExemplarReservoirProviderSelector` added to `metric.Stream` Note: the only required public types are `metric.ExemplarReservoirProviderSelector` and `ExemplarReservoirProviderSelector` in `metric.Stream`. Others are for convenience and readability. ### Alternatives considered #### Add ExemplarReservoirProvider directly to metric.Stream, instead of ExemplarReservoirProviderSelector This would mean users can configure a `func() exemplar.Reservoir` instead of a `func(Aggregation) func() exemplar.Reservoir`. I don't think this complies with the statement: `This functional type needs to be a factory or callback similar to aggregation selection functionality which allows different reservoirs to be chosen by the aggregation.`. I'm interpreting "allows different reservoirs to be chosen by the aggregation" as meaning "allows different reservoirs to be chosen **based on the** aggregation", rather than meaning that the aggregation is somehow choosing the reservoir. ### Future work There is some refactoring I plan to do after this to simplify the interaction between the internal/aggregate and exemplar package. I've omitted that from this PR to keep the diff smaller. --------- Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> Co-authored-by: Robert Pająk <pellared@hotmail.com>
195 lines
4.9 KiB
Go
195 lines
4.9 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package aggregate // import "go.opentelemetry.io/otel/sdk/metric/internal/aggregate"
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
|
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
|
|
)
|
|
|
|
var (
|
|
keyUser = "user"
|
|
userAlice = attribute.String(keyUser, "Alice")
|
|
userBob = attribute.String(keyUser, "Bob")
|
|
userCarol = attribute.String(keyUser, "Carol")
|
|
userDave = attribute.String(keyUser, "Dave")
|
|
adminTrue = attribute.Bool("admin", true)
|
|
adminFalse = attribute.Bool("admin", false)
|
|
|
|
alice = attribute.NewSet(userAlice, adminTrue)
|
|
bob = attribute.NewSet(userBob, adminFalse)
|
|
carol = attribute.NewSet(userCarol, adminFalse)
|
|
dave = attribute.NewSet(userDave, adminFalse)
|
|
|
|
// Filtered.
|
|
attrFltr = func(kv attribute.KeyValue) bool {
|
|
return kv.Key == attribute.Key(keyUser)
|
|
}
|
|
fltrAlice = attribute.NewSet(userAlice)
|
|
fltrBob = attribute.NewSet(userBob)
|
|
|
|
// Sat Jan 01 2000 00:00:00 GMT+0000.
|
|
y2k = time.Unix(946684800, 0)
|
|
)
|
|
|
|
// y2kPlus returns the timestamp at n seconds past Sat Jan 01 2000 00:00:00 GMT+0000.
|
|
func y2kPlus(n int64) time.Time {
|
|
d := time.Duration(n) * time.Second
|
|
return y2k.Add(d)
|
|
}
|
|
|
|
// clock is a test clock. It provides a predictable value for now() that can be
|
|
// reset.
|
|
type clock struct {
|
|
ticks atomic.Int64
|
|
}
|
|
|
|
// Now returns the mocked time starting at y2kPlus(0). Each call to Now will
|
|
// increment the returned value by one second.
|
|
func (c *clock) Now() time.Time {
|
|
old := c.ticks.Add(1) - 1
|
|
return y2kPlus(old)
|
|
}
|
|
|
|
// Reset resets the clock c to tick from y2kPlus(0).
|
|
func (c *clock) Reset() { c.ticks.Store(0) }
|
|
|
|
// Register registers clock c's Now method as the now var. It returns an
|
|
// unregister func that should be called to restore the original now value.
|
|
func (c *clock) Register() (unregister func()) {
|
|
orig := now
|
|
now = c.Now
|
|
return func() { now = orig }
|
|
}
|
|
|
|
func dropExemplars[N int64 | float64](attr attribute.Set) FilteredExemplarReservoir[N] {
|
|
return dropReservoir[N](attr)
|
|
}
|
|
|
|
func TestBuilderFilter(t *testing.T) {
|
|
t.Run("Int64", testBuilderFilter[int64]())
|
|
t.Run("Float64", testBuilderFilter[float64]())
|
|
}
|
|
|
|
func testBuilderFilter[N int64 | float64]() func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
t.Helper()
|
|
|
|
value, attr := N(1), alice
|
|
run := func(b Builder[N], wantF attribute.Set, wantD []attribute.KeyValue) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
t.Helper()
|
|
|
|
meas := b.filter(func(_ context.Context, v N, f attribute.Set, d []attribute.KeyValue) {
|
|
assert.Equal(t, value, v, "measured incorrect value")
|
|
assert.Equal(t, wantF, f, "measured incorrect filtered attributes")
|
|
assert.ElementsMatch(t, wantD, d, "measured incorrect dropped attributes")
|
|
})
|
|
meas(context.Background(), value, attr)
|
|
}
|
|
}
|
|
|
|
t.Run("NoFilter", run(Builder[N]{}, attr, nil))
|
|
t.Run("Filter", run(Builder[N]{Filter: attrFltr}, fltrAlice, []attribute.KeyValue{adminTrue}))
|
|
}
|
|
}
|
|
|
|
type arg[N int64 | float64] struct {
|
|
ctx context.Context
|
|
|
|
value N
|
|
attr attribute.Set
|
|
}
|
|
|
|
type output struct {
|
|
n int
|
|
agg metricdata.Aggregation
|
|
}
|
|
|
|
type teststep[N int64 | float64] struct {
|
|
input []arg[N]
|
|
expect output
|
|
}
|
|
|
|
func test[N int64 | float64](meas Measure[N], comp ComputeAggregation, steps []teststep[N]) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
t.Helper()
|
|
|
|
got := new(metricdata.Aggregation)
|
|
for i, step := range steps {
|
|
for _, args := range step.input {
|
|
meas(args.ctx, args.value, args.attr)
|
|
}
|
|
|
|
t.Logf("step: %d", i)
|
|
assert.Equal(t, step.expect.n, comp(got), "incorrect data size")
|
|
metricdatatest.AssertAggregationsEqual(t, step.expect.agg, *got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func benchmarkAggregate[N int64 | float64](factory func() (Measure[N], ComputeAggregation)) func(*testing.B) {
|
|
counts := []int{1, 10, 100}
|
|
return func(b *testing.B) {
|
|
for _, n := range counts {
|
|
b.Run(strconv.Itoa(n), func(b *testing.B) {
|
|
benchmarkAggregateN(b, factory, n)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
var bmarkRes metricdata.Aggregation
|
|
|
|
func benchmarkAggregateN[N int64 | float64](b *testing.B, factory func() (Measure[N], ComputeAggregation), count int) {
|
|
ctx := context.Background()
|
|
attrs := make([]attribute.Set, count)
|
|
for i := range attrs {
|
|
attrs[i] = attribute.NewSet(attribute.Int("value", i))
|
|
}
|
|
|
|
b.Run("Measure", func(b *testing.B) {
|
|
got := &bmarkRes
|
|
meas, comp := factory()
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
for _, attr := range attrs {
|
|
meas(ctx, 1, attr)
|
|
}
|
|
}
|
|
|
|
comp(got)
|
|
})
|
|
|
|
b.Run("ComputeAggregation", func(b *testing.B) {
|
|
comps := make([]ComputeAggregation, b.N)
|
|
for n := range comps {
|
|
meas, comp := factory()
|
|
for _, attr := range attrs {
|
|
meas(ctx, 1, attr)
|
|
}
|
|
comps[n] = comp
|
|
}
|
|
|
|
got := &bmarkRes
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
comps[n](got)
|
|
}
|
|
})
|
|
}
|