diff --git a/internal/xattribute/pool.go b/internal/xattribute/pool.go new file mode 100644 index 00000000..ca607c4e --- /dev/null +++ b/internal/xattribute/pool.go @@ -0,0 +1,29 @@ +package xattribute + +import "sync" + +type stringSlice struct { + val []string +} + +func (s *stringSlice) Reset() { + s.val = s.val[:0] +} + +var stringSlicePool = &sync.Pool{ + New: func() any { + return &stringSlice{ + val: make([]string, 0, 16), + } + }, +} + +func getStringSlice() *stringSlice { + ss := stringSlicePool.Get().(*stringSlice) + ss.Reset() + return ss +} + +func putStringSlice(ss *stringSlice) { + stringSlicePool.Put(ss) +} diff --git a/internal/xattribute/xattribute.go b/internal/xattribute/xattribute.go new file mode 100644 index 00000000..dbad1468 --- /dev/null +++ b/internal/xattribute/xattribute.go @@ -0,0 +1,53 @@ +// Package xattribute provides some helpers to create OpenTelemetry attributes. +package xattribute + +import ( + "fmt" + "slices" + + "go.opentelemetry.io/otel/attribute" +) + +// StringerSlice creates a string slice attribute from slice of [fmt.Stringer] implementations. +func StringerSlice[S ~[]E, E fmt.Stringer](k string, v S) attribute.KeyValue { + if len(v) == 0 { + return attribute.StringSlice(k, nil) + } + + // Using pooled string slice is fine, since attribute package copies slice. + ss := getStringSlice() + defer putStringSlice(ss) + + ss.val = append(ss.val, make([]string, len(v))...) + for i, f := range v { + ss.val[i] = safeStringer(f) + } + return attribute.StringSlice(k, ss.val) +} + +func safeStringer[F fmt.Stringer](f F) (v string) { + defer func() { + if r := recover(); r != nil { + v = ":" + fmt.Sprint(r) + } + }() + return f.String() +} + +// StringMap creates a sorted string slice attribute from string map. +func StringMap(k string, m map[string]string) attribute.KeyValue { + if len(m) == 0 { + return attribute.StringSlice(k, nil) + } + + // Using pooled string slice is fine, since attribute package copies slice. + ss := getStringSlice() + defer putStringSlice(ss) + + for k, v := range m { + ss.val = append(ss.val, k+"="+v) + } + slices.Sort(ss.val) + + return attribute.StringSlice(k, ss.val) +} diff --git a/internal/xattribute/xattribute_test.go b/internal/xattribute/xattribute_test.go new file mode 100644 index 00000000..76d0afdf --- /dev/null +++ b/internal/xattribute/xattribute_test.go @@ -0,0 +1,97 @@ +package xattribute + +import ( + "fmt" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" +) + +type justStringer struct{} + +func (justStringer) String() string { + return "justStringer" +} + +type panicStringer struct{} + +func (panicStringer) String() string { + panic("bad stringer") +} + +func TestStringerSlice(t *testing.T) { + tests := []struct { + k string + v []fmt.Stringer + want attribute.KeyValue + }{ + { + "key", + nil, + attribute.StringSlice("key", nil), + }, + { + "key", + []fmt.Stringer{justStringer{}}, + attribute.StringSlice("key", []string{"justStringer"}), + }, + { + "key", + []fmt.Stringer{panicStringer{}}, + attribute.StringSlice("key", []string{":bad stringer"}), + }, + { + "key", + []fmt.Stringer{nil}, + attribute.StringSlice("key", []string{":runtime error: invalid memory address or nil pointer dereference"}), + }, + } + for i, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + require.Equal(t, tt.want, StringerSlice(tt.k, tt.v)) + }) + } +} + +func BenchmarkStringerSlice(b *testing.B) { + var ( + s [4]justStringer + sink attribute.KeyValue + ) + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + sink = StringerSlice("key", s[:]) + } + + runtime.KeepAlive(sink) +} + +func TestStringMap(t *testing.T) { + tests := []struct { + k string + m map[string]string + want attribute.KeyValue + }{ + { + "key", + nil, + attribute.StringSlice("key", nil), + }, + { + "key", + map[string]string{"a": "1", "b": "2"}, + attribute.StringSlice("key", []string{"a=1", "b=2"}), + }, + } + for i, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + require.Equal(t, tt.want, StringMap(tt.k, tt.m)) + }) + } +}