You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-03 18:35:08 +02:00
trace,metric,log: add WithInstrumentationAttributeSet option (#7287)
Per https://github.com/open-telemetry/opentelemetry-go/pull/7266#issuecomment-3237027300 Related to https://github.com/open-telemetry/opentelemetry-go/issues/7217 ## What This PR adds `WithInstrumentationAttributeSet` option functions to the `log`, `metric`, and `trace` packages as suggested in https://github.com/open-telemetry/opentelemetry-go/pull/7266#issuecomment-3237027300. These new functions provide a more concurrent-safe alternative to the existing `WithInstrumentationAttributes` functions by accepting a pre-constructed `attribute.Set` instead of variadic `attribute.KeyValue` parameters. ## Why As discussed in #7266, the existing `WithInstrumentationAttributes` functions can lead to data races when used concurrently because `attribute.NewSet()` may mutate the passed slice in-place. While the issue was partially addressed by moving the `attribute.NewSet()` call outside the closure, the best long-term solution is to provide an alternative that accepts an immutable `attribute.Set`. **Benefits:** 1. **Concurrent Safety**: Since `attribute.Set` is immutable, these functions are inherently safe for concurrent use 2. **Performance**: Avoids repeated calls to `attribute.NewSet()` when the same attributes are used multiple times 3. **Consistency**: Matches the existing pattern used in `metric.WithAttributeSet()` 4. **Flexibility**: Allows users to pre-compute attribute sets and reuse them Deprecating `WithInstrumentationAttributes` is out of scope. See https://github.com/open-telemetry/opentelemetry-go/pull/7287#issuecomment-3245820459. ## Benchmarks ``` goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/trace cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkNewTracerConfig/with_no_options-20 280298306 4.268 ns/op 0 B/op 0 allocs/op BenchmarkNewTracerConfig/with_an_instrumentation_version-20 33389427 30.84 ns/op 0 B/op 0 allocs/op BenchmarkNewTracerConfig/with_a_schema_url-20 35441077 30.46 ns/op 0 B/op 0 allocs/op BenchmarkNewTracerConfig/with_instrumentation_attribute-20 17607649 88.23 ns/op 64 B/op 1 allocs/op BenchmarkNewTracerConfig/with_instrumentation_attribute_set-20 38336211 31.30 ns/op 0 B/op 0 allocs/op ``` ``` goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/metric cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkNewMeterConfig/with_no_options-20 262998199 4.525 ns/op 0 B/op 0 allocs/op BenchmarkNewMeterConfig/with_an_instrumentation_version-20 40483780 29.31 ns/op 0 B/op 0 allocs/op BenchmarkNewMeterConfig/with_a_schema_url-20 39162420 30.58 ns/op 0 B/op 0 allocs/op BenchmarkNewMeterConfig/with_instrumentation_attribute-20 19900275 77.50 ns/op 64 B/op 1 allocs/op BenchmarkNewMeterConfig/with_instrumentation_attribute_set-20 37519020 31.93 ns/op 0 B/op 0 allocs/op ``` ``` goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/log cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkNewLoggerConfig/with_no_options-20 271100760 4.322 ns/op 0 B/op 0 allocs/op BenchmarkNewLoggerConfig/with_an_instrumentation_version-20 38392390 30.77 ns/op 0 B/op 0 allocs/op BenchmarkNewLoggerConfig/with_a_schema_url-20 39615074 30.25 ns/op 0 B/op 0 allocs/op BenchmarkNewLoggerConfig/with_instrumentation_attribute-20 17108463 82.51 ns/op 64 B/op 1 allocs/op BenchmarkNewLoggerConfig/with_instrumentation_attribute_set-20 37746534 31.70 ns/op 0 B/op 0 allocs/op ```
This commit is contained in:
+29
-3
@@ -131,9 +131,12 @@ func mergeSets(a, b attribute.Set) attribute.Set {
|
||||
//
|
||||
// The passed attributes will be de-duplicated.
|
||||
//
|
||||
// If multiple WithInstrumentationAttributes 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.
|
||||
// Note that [WithInstrumentationAttributeSet] is recommended as
|
||||
// it is more efficient and also allows safely reusing the passed argument.
|
||||
//
|
||||
// If multiple [WithInstrumentationAttributes] or [WithInstrumentationAttributeSet]
|
||||
// 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.
|
||||
func WithInstrumentationAttributes(attr ...attribute.KeyValue) LoggerOption {
|
||||
if len(attr) == 0 {
|
||||
return loggerOptionFunc(func(config LoggerConfig) LoggerConfig {
|
||||
@@ -152,6 +155,29 @@ func WithInstrumentationAttributes(attr ...attribute.KeyValue) LoggerOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithInstrumentationAttributeSet returns a [LoggerOption] that adds the
|
||||
// instrumentation attributes of a [Logger].
|
||||
//
|
||||
// If multiple [WithInstrumentationAttributes] or [WithInstrumentationAttributeSet]
|
||||
// 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.
|
||||
func WithInstrumentationAttributeSet(set attribute.Set) LoggerOption {
|
||||
if set.Len() == 0 {
|
||||
return loggerOptionFunc(func(config LoggerConfig) LoggerConfig {
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
return loggerOptionFunc(func(config LoggerConfig) LoggerConfig {
|
||||
if config.attrs.Len() == 0 {
|
||||
config.attrs = set
|
||||
} else {
|
||||
config.attrs = mergeSets(config.attrs, set)
|
||||
}
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
// WithSchemaURL returns a [LoggerOption] that sets the schema URL for a
|
||||
// [Logger].
|
||||
func WithSchemaURL(schemaURL string) LoggerOption {
|
||||
|
||||
@@ -31,6 +31,19 @@ func TestNewLoggerConfig(t *testing.T) {
|
||||
assert.Equal(t, attr, c.InstrumentationAttributes(), "instrumentation attributes")
|
||||
}
|
||||
|
||||
func TestWithInstrumentationAttributeSet(t *testing.T) {
|
||||
attrs := attribute.NewSet(
|
||||
attribute.String("service", "test"),
|
||||
attribute.Int("three", 3),
|
||||
)
|
||||
|
||||
c := log.NewLoggerConfig(
|
||||
log.WithInstrumentationAttributeSet(attrs),
|
||||
)
|
||||
|
||||
assert.Equal(t, attrs, c.InstrumentationAttributes(), "instrumentation attributes")
|
||||
}
|
||||
|
||||
func TestWithInstrumentationAttributesMerge(t *testing.T) {
|
||||
aliceAttr := attribute.String("user", "Alice")
|
||||
bobAttr := attribute.String("user", "Bob")
|
||||
@@ -76,4 +89,93 @@ func TestWithInstrumentationAttributesMerge(t *testing.T) {
|
||||
assert.Equal(t, alice, c.InstrumentationAttributes(),
|
||||
"Empty attributes should not affect existing ones.")
|
||||
})
|
||||
|
||||
t.Run("SameKeyWithSet", func(t *testing.T) {
|
||||
c := log.NewLoggerConfig(
|
||||
log.WithInstrumentationAttributeSet(alice),
|
||||
log.WithInstrumentationAttributeSet(bob),
|
||||
)
|
||||
assert.Equal(t, bob, c.InstrumentationAttributes(),
|
||||
"Later values for the same key should overwrite earlier ones.")
|
||||
})
|
||||
|
||||
t.Run("DifferentKeysWithSet", func(t *testing.T) {
|
||||
c := log.NewLoggerConfig(
|
||||
log.WithInstrumentationAttributeSet(alice),
|
||||
log.WithInstrumentationAttributeSet(attribute.NewSet(adminAttr)),
|
||||
)
|
||||
assert.Equal(t, aliceAdmin, c.InstrumentationAttributes(),
|
||||
"Different keys should be merged.")
|
||||
})
|
||||
|
||||
t.Run("MixedWithSet", func(t *testing.T) {
|
||||
c := log.NewLoggerConfig(
|
||||
log.WithInstrumentationAttributeSet(aliceAdmin),
|
||||
log.WithInstrumentationAttributeSet(bob),
|
||||
)
|
||||
assert.Equal(t, bobAdmin, c.InstrumentationAttributes(),
|
||||
"Combination of same and different keys should be merged.")
|
||||
})
|
||||
|
||||
t.Run("MergedEmptyWithSet", func(t *testing.T) {
|
||||
c := log.NewLoggerConfig(
|
||||
log.WithInstrumentationAttributeSet(alice),
|
||||
log.WithInstrumentationAttributeSet(attribute.NewSet()),
|
||||
)
|
||||
assert.Equal(t, alice, c.InstrumentationAttributes(),
|
||||
"Empty attribute set should not affect existing ones.")
|
||||
})
|
||||
|
||||
t.Run("MixedAttributesAndSet", func(t *testing.T) {
|
||||
c := log.NewLoggerConfig(
|
||||
log.WithInstrumentationAttributes(aliceAttr),
|
||||
log.WithInstrumentationAttributeSet(attribute.NewSet(bobAttr, adminAttr)),
|
||||
)
|
||||
assert.Equal(t, bobAdmin, c.InstrumentationAttributes(),
|
||||
"Attributes and attribute sets should be merged together.")
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkNewLoggerConfig(b *testing.B) {
|
||||
for _, bb := range []struct {
|
||||
name string
|
||||
options []log.LoggerOption
|
||||
}{
|
||||
{
|
||||
name: "with no options",
|
||||
},
|
||||
{
|
||||
name: "with an instrumentation version",
|
||||
options: []log.LoggerOption{
|
||||
log.WithInstrumentationVersion("testing version"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with a schema url",
|
||||
options: []log.LoggerOption{
|
||||
log.WithSchemaURL("testing URL"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with instrumentation attribute",
|
||||
options: []log.LoggerOption{
|
||||
log.WithInstrumentationAttributes(attribute.String("foo", "value")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with instrumentation attribute set",
|
||||
options: []log.LoggerOption{
|
||||
log.WithInstrumentationAttributeSet(attribute.NewSet(attribute.String("bar", "value"))),
|
||||
},
|
||||
},
|
||||
} {
|
||||
b.Run(bb.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
log.NewLoggerConfig(bb.options...)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user