-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(xattribute): add utility package for building attributes
- Loading branch information
Showing
3 changed files
with
179 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = "<stringer panic>:" + 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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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{"<stringer panic>:bad stringer"}), | ||
}, | ||
{ | ||
"key", | ||
[]fmt.Stringer{nil}, | ||
attribute.StringSlice("key", []string{"<stringer panic>: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)) | ||
}) | ||
} | ||
} |