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) +}