From 48dd8b1a0975e14a09d615f8ee80212b20f1e107 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Mon, 13 Apr 2026 12:47:46 -0400 Subject: [PATCH] Add x.Settable to allow reusing attribute options (#8178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7851 This adopts this proposal from @MrAlias: https://github.com/open-telemetry/opentelemetry-go/issues/7851#issuecomment-3837399805 This adds a Set function to attrOpt, and changes attrOpt functions to use a pointer receiver. Users can use this by checking if the attribute option implements the `Settable` interface in `metric/x`. There are no public API changes to the metric package, as this is still an experiment. See the benchmark change and the interface documentation for how it can be used to avoid calling metric.WithAttributeSet. I updated the benchmark in https://github.com/open-telemetry/opentelemetry-go/commit/448394b549375f4f6742201e199e0339d0b78524, but didn't commit it here to avoid a dependency between the SDK and the `metric/x` package. As expected, this removes one allocation from the Dynamic case! The results are: ``` │ main.txt │ resettable.txt │ │ sec/op │ sec/op vs base │ EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet-24 238.2n ± 5% 220.7n ± 9% -7.39% (p=0.002 n=6) EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet-24 611.1n ± 4% 620.3n ± 2% ~ (p=0.394 n=6) EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet-24 1.147µ ± 7% 1.171µ ± 5% ~ (p=0.258 n=6) EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24 363.8n ± 6% 363.5n ± 6% ~ (p=1.000 n=6) EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24 1.464µ ± 5% 1.475µ ± 10% ~ (p=0.727 n=6) EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24 2.924µ ± 7% 2.589µ ± 6% -11.47% (p=0.002 n=6) │ main.txt │ resettable.txt │ │ B/op │ B/op vs base │ EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet-24 88.00 ± 0% 64.00 ± 0% -27.27% (p=0.002 n=6) EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet-24 344.0 ± 0% 321.0 ± 0% -6.69% (p=0.002 n=6) EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet-24 729.0 ± 0% 706.0 ± 0% -3.16% (p=0.002 n=6) EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24 152.0 ± 0% 128.0 ± 0% -15.79% (p=0.002 n=6) EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24 921.0 ± 0% 899.0 ± 0% -2.39% (p=0.002 n=6) EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24 2.026Ki ± 0% 2.006Ki ± 0% -1.01% (p=0.002 n=6) │ main.txt │ resettable.txt │ │ allocs/op │ allocs/op vs base │ EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet-24 2.000 ± 0% 1.000 ± 0% -50.00% (p=0.002 n=6) EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet-24 2.000 ± 0% 1.000 ± 0% -50.00% (p=0.002 n=6) EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet-24 2.000 ± 0% 1.000 ± 0% -50.00% (p=0.002 n=6) EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24 3.000 ± 0% 2.000 ± 0% -33.33% (p=0.002 n=6) EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24 4.000 ± 0% 3.000 ± 0% -25.00% (p=0.002 n=6) EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24 4.000 ± 0% 3.000 ± 0% -25.00% (p=0.002 n=6) ``` --------- Co-authored-by: Tyler Yahn --- CHANGELOG.md | 1 + metric/instrument.go | 26 +++++++++++++++++++++----- metric/instrument_test.go | 32 ++++++++++++++++++++++++++++++++ metric/x/options.go | 29 +++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac30439e5..ff45c6985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm Set `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=` to enable for all periodic readers. See `go.opentelemetry.io/otel/sdk/metric/internal/x` for feature documentation. (#8071) - Add `WithDefaultAttributes` to `go.opentelemetry.io/otel/metric/x` to support setting default attributes on instruments. (#8135) +- Add `Settable` to `go.opentelemetry.io/otel/metric/x` to allow reusing attribute options. (#8178) ### Changed diff --git a/metric/instrument.go b/metric/instrument.go index eb767b760..2e79ab568 100644 --- a/metric/instrument.go +++ b/metric/instrument.go @@ -310,6 +310,10 @@ type attrOpt struct { set attribute.Set } +func (o *attrOpt) Set(set attribute.Set) { + o.set = set +} + // mergeSets returns the union of keys between a and b. Any duplicate keys will // use the value associated with b. func mergeSets(a, b attribute.Set) attribute.Set { @@ -322,7 +326,7 @@ func mergeSets(a, b attribute.Set) attribute.Set { return attribute.NewSet(merged...) } -func (o attrOpt) applyAdd(c AddConfig) AddConfig { +func (o *attrOpt) applyAdd(c AddConfig) AddConfig { switch { case o.set.Len() == 0: case c.attrs.Len() == 0: @@ -333,7 +337,7 @@ func (o attrOpt) applyAdd(c AddConfig) AddConfig { return c } -func (o attrOpt) applyRecord(c RecordConfig) RecordConfig { +func (o *attrOpt) applyRecord(c RecordConfig) RecordConfig { switch { case o.set.Len() == 0: case c.attrs.Len() == 0: @@ -344,7 +348,7 @@ func (o attrOpt) applyRecord(c RecordConfig) RecordConfig { return c } -func (o attrOpt) applyObserve(c ObserveConfig) ObserveConfig { +func (o *attrOpt) applyObserve(c ObserveConfig) ObserveConfig { switch { case o.set.Len() == 0: case c.attrs.Len() == 0: @@ -361,8 +365,14 @@ func (o attrOpt) applyObserve(c ObserveConfig) ObserveConfig { // If multiple WithAttributeSet or WithAttributes options are passed the // attributes will be merged together in the order they are passed. Attributes // with duplicate keys will use the last value passed. +// +// Experimental: The returned option may implement +// [go.opentelemetry.io/otel/metric/x.Settable][attribute.Set], which can be +// used to replace the option's attribute set and reuse the option without +// additional allocations. This behavior is experimental and may be changed or +// removed in a future release without notice. func WithAttributeSet(attributes attribute.Set) MeasurementOption { - return attrOpt{set: attributes} + return &attrOpt{set: attributes} } // WithAttributes converts attributes into an attribute Set and sets the Set to @@ -380,8 +390,14 @@ func WithAttributeSet(attributes attribute.Set) MeasurementOption { // // See [WithAttributeSet] for information about how multiple WithAttributes are // merged. +// +// Experimental: The returned option may implement +// [go.opentelemetry.io/otel/metric/x.Settable][[]attribute.KeyValue], which can be +// used to replace the option's attributes and reuse the option without +// additional allocations. This behavior is experimental and may be changed or +// removed in a future release without notice. func WithAttributes(attributes ...attribute.KeyValue) MeasurementOption { cp := make([]attribute.KeyValue, len(attributes)) copy(cp, attributes) - return attrOpt{set: attribute.NewSet(cp...)} + return &attrOpt{set: attribute.NewSet(cp...)} } diff --git a/metric/instrument_test.go b/metric/instrument_test.go index 34e09383e..f71e334c4 100644 --- a/metric/instrument_test.go +++ b/metric/instrument_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" ) @@ -127,3 +128,34 @@ func TestWithAttributesConcurrentSafe(*testing.T) { wg.Wait() } + +func TestSettableOptions(t *testing.T) { + type settable interface { + Set(attribute.Set) + } + + aliceAttr := attribute.String("user", "Alice") + alice := attribute.NewSet(aliceAttr) + bobAttr := attribute.String("user", "Bob") + bob := attribute.NewSet(bobAttr) + + t.Run("WithAttributeSet", func(t *testing.T) { + opt := WithAttributeSet(alice) + r, ok := opt.(settable) + require.True(t, ok, "WithAttributeSet option does not implement settable") + + r.Set(bob) + c := NewAddConfig([]AddOption{opt.(AddOption)}) + assert.Equal(t, bob, c.Attributes()) + }) + + t.Run("WithAttributes", func(t *testing.T) { + opt := WithAttributes(aliceAttr) + r, ok := opt.(settable) + require.True(t, ok, "WithAttributes option does not implement settable") + + r.Set(bob) + c := NewAddConfig([]AddOption{opt.(AddOption)}) + assert.Equal(t, bob, c.Attributes()) + }) +} diff --git a/metric/x/options.go b/metric/x/options.go index bfb662045..7be062046 100644 --- a/metric/x/options.go +++ b/metric/x/options.go @@ -27,3 +27,32 @@ func (o defaultAttributesOption) AllowedKeys() []attribute.Key { func WithDefaultAttributes(keys ...attribute.Key) metric.InstrumentOption { return defaultAttributesOption{keys: keys} } + +// Settable is an optional interface that Options can implement +// to allow reuse without additional allocations. +// +// Example usage with sync.Pool: +// +// var optionPool = sync.Pool{ +// New: func() any { +// return metric.WithAttributeSet(*attribute.EmptySet()) +// }, +// } +// +// func record(ctx context.Context, counter metric.Int64Counter, set attribute.Set) { +// opt := optionPool.Get().(metric.MeasurementOption) +// defer optionPool.Put(opt) +// +// if r, ok := opt.(x.Settable[attribute.Set]); ok { +// r.Set(set) +// } else { +// opt = metric.WithAttributeSet(set) +// } +// counter.Add(ctx, 1, opt) +// } +// +// WARNING: It is the user's responsibility to ensure that the option is not +// concurrently set while being passed to the API or used by another goroutine. +type Settable[T any] interface { + Set(T) +}