1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-03 18:35:08 +02:00

exporters: support SLICE attributes (#8216)

Fixes #8162

Follow-up to #8153 for `attribute.SLICE`.

Add end-to-end exporter handling for `attribute.SLICE` in the remaining
paths that still treated it as invalid or relied on fallback formatting.

Changes:

- encode `attribute.SLICE` as OTLP `AnyValue_ArrayValue` for trace, log,
and metric transforms
- serialize Zipkin `SLICE` attributes using the non-OTLP AnyValue string
representation
- add trace-side coverage for recursive `convAttrValue` slice conversion
This commit is contained in:
Robert Pająk
2026-04-20 11:03:34 +02:00
committed by GitHub
parent 3384d39f6b
commit fa9276b15e
21 changed files with 637 additions and 42 deletions
@@ -10,6 +10,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"go.opentelemetry.io/otel/attribute"
cpb "go.opentelemetry.io/proto/otlp/common/v1"
@@ -26,8 +28,13 @@ var (
attrFloat64Slice = attribute.Float64Slice("float64 slice", []float64{-1, 1})
attrString = attribute.String("string", "o")
attrBytes = attribute.ByteSlice("bytes", []byte("otlp"))
attrStringSlice = attribute.StringSlice("string slice", []string{"o", "n"})
attrEmpty = attribute.KeyValue{
attrSlice = attribute.Slice("slice",
attribute.BoolValue(true),
attribute.ByteSliceValue([]byte("otlp")),
attribute.SliceValue(attribute.IntValue(2), attribute.Value{}),
)
attrStringSlice = attribute.StringSlice("string slice", []string{"o", "n"})
attrEmpty = attribute.KeyValue{
Key: attribute.Key("empty"),
Value: attribute.Value{},
}
@@ -40,6 +47,7 @@ var (
},
}}
valIntOne = &cpb.AnyValue{Value: &cpb.AnyValue_IntValue{IntValue: 1}}
valIntTwo = &cpb.AnyValue{Value: &cpb.AnyValue_IntValue{IntValue: 2}}
valIntNOne = &cpb.AnyValue{Value: &cpb.AnyValue_IntValue{IntValue: -1}}
valIntSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -53,8 +61,21 @@ var (
Values: []*cpb.AnyValue{valDblNOne, valDblOne},
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valAttrBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
Values: []*cpb.AnyValue{
valBoolTrue,
valAttrBytes,
{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
Values: []*cpb.AnyValue{valIntTwo, {}},
},
}},
},
},
}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -72,6 +93,7 @@ var (
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvAttrBytes = &cpb.KeyValue{Key: "bytes", Value: valAttrBytes}
kvAttrSlice = &cpb.KeyValue{Key: "slice", Value: valSlice}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -141,6 +163,11 @@ func TestAttrTransforms(t *testing.T) {
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvAttrBytes},
},
{
"slice",
[]attribute.KeyValue{attrSlice},
[]*cpb.KeyValue{kvAttrSlice},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -159,6 +186,7 @@ func TestAttrTransforms(t *testing.T) {
attrFloat64Slice,
attrString,
attrBytes,
attrSlice,
attrStringSlice,
attrEmpty,
},
@@ -173,6 +201,7 @@ func TestAttrTransforms(t *testing.T) {
kvFloat64Slice,
kvString,
kvAttrBytes,
kvAttrSlice,
kvStringSlice,
kvEmpty,
},
@@ -180,12 +209,45 @@ func TestAttrTransforms(t *testing.T) {
} {
t.Run(test.name, func(t *testing.T) {
t.Run("Attrs", func(t *testing.T) {
assert.ElementsMatch(t, test.want, Attrs(test.in))
assertKeyValueSlicesEqual(t, test.want, Attrs(test.in))
})
t.Run("AttrIter", func(t *testing.T) {
s := attribute.NewSet(test.in...)
assert.ElementsMatch(t, test.want, AttrIter(s.Iter()))
assertKeyValueSlicesEqual(t, test.want, AttrIter(s.Iter()))
})
})
}
}
func TestAttrsPreserveDuplicateKeys(t *testing.T) {
want := []*cpb.KeyValue{
{Key: "dup", Value: valBoolTrue},
{Key: "dup", Value: valStrO},
}
assertKeyValueSlicesEqual(t, want, Attrs([]attribute.KeyValue{
attribute.Bool("dup", true),
attribute.String("dup", "o"),
}))
}
func assertKeyValueSlicesEqual(t *testing.T, want, got []*cpb.KeyValue) {
t.Helper()
require.Len(t, got, len(want))
used := make([]bool, len(got))
for i, wantKV := range want {
matched := false
for j, gotKV := range got {
if used[j] {
continue
}
if proto.Equal(wantKV, gotKV) {
used[j] = true
matched = true
break
}
}
assert.Truef(t, matched, "missing match for want[%d] = %#v in got = %#v", i, wantKV, got)
}
}
@@ -199,6 +199,12 @@ func AttrValue(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.SLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
Values: attrValues(v.AsSlice()),
},
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -262,6 +268,14 @@ func stringSliceValues(vals []string) []*cpb.AnyValue {
return converted
}
func attrValues(vals []attribute.Value) []*cpb.AnyValue {
converted := make([]*cpb.AnyValue, len(vals))
for i, v := range vals {
converted[i] = AttrValue(v)
}
return converted
}
// LogAttrs transforms a slice of [api.KeyValue] into OTLP key-values.
func LogAttrs(attrs []api.KeyValue) []*cpb.KeyValue {
if len(attrs) == 0 {
@@ -9,8 +9,6 @@ package transform
import (
"testing"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/log"
cpb "go.opentelemetry.io/proto/otlp/common/v1"
)
@@ -138,7 +136,19 @@ func TestLogAttrs(t *testing.T) {
},
} {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.want, LogAttrs(test.in))
assertKeyValueSlicesEqual(t, test.want, LogAttrs(test.in))
})
}
}
func TestLogAttrsPreserveDuplicateKeys(t *testing.T) {
want := []*cpb.KeyValue{
{Key: "dup", Value: valBoolTrue},
{Key: "dup", Value: valStrO},
}
assertKeyValueSlicesEqual(t, want, LogAttrs([]log.KeyValue{
log.Bool("dup", true),
log.String("dup", "o"),
}))
}
@@ -85,6 +85,12 @@ func Value(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.SLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
Values: attrValues(v.AsSlice()),
},
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -147,3 +153,11 @@ func stringSliceValues(vals []string) []*cpb.AnyValue {
}
return converted
}
func attrValues(vals []attribute.Value) []*cpb.AnyValue {
converted := make([]*cpb.AnyValue, len(vals))
for i, v := range vals {
converted[i] = Value(v)
}
return converted
}
@@ -10,6 +10,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"go.opentelemetry.io/otel/attribute"
cpb "go.opentelemetry.io/proto/otlp/common/v1"
@@ -26,8 +28,13 @@ var (
attrFloat64Slice = attribute.Float64Slice("float64 slice", []float64{-1, 1})
attrString = attribute.String("string", "o")
attrBytes = attribute.ByteSlice("bytes", []byte("otlp"))
attrStringSlice = attribute.StringSlice("string slice", []string{"o", "n"})
attrEmpty = attribute.KeyValue{
attrSlice = attribute.Slice("slice",
attribute.BoolValue(true),
attribute.ByteSliceValue([]byte("otlp")),
attribute.SliceValue(attribute.IntValue(2), attribute.Value{}),
)
attrStringSlice = attribute.StringSlice("string slice", []string{"o", "n"})
attrEmpty = attribute.KeyValue{
Key: attribute.Key("empty"),
Value: attribute.Value{},
}
@@ -40,6 +47,7 @@ var (
},
}}
valIntOne = &cpb.AnyValue{Value: &cpb.AnyValue_IntValue{IntValue: 1}}
valIntTwo = &cpb.AnyValue{Value: &cpb.AnyValue_IntValue{IntValue: 2}}
valIntNOne = &cpb.AnyValue{Value: &cpb.AnyValue_IntValue{IntValue: -1}}
valIntSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -53,8 +61,21 @@ var (
Values: []*cpb.AnyValue{valDblNOne, valDblOne},
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
Values: []*cpb.AnyValue{
valBoolTrue,
valBytes,
{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
Values: []*cpb.AnyValue{valIntTwo, {}},
},
}},
},
},
}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -72,6 +93,7 @@ var (
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvBytes = &cpb.KeyValue{Key: "bytes", Value: valBytes}
kvSlice = &cpb.KeyValue{Key: "slice", Value: valSlice}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -141,6 +163,11 @@ func TestAttributeTransforms(t *testing.T) {
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvBytes},
},
{
"slice",
[]attribute.KeyValue{attrSlice},
[]*cpb.KeyValue{kvSlice},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -159,6 +186,7 @@ func TestAttributeTransforms(t *testing.T) {
attrFloat64Slice,
attrString,
attrBytes,
attrSlice,
attrStringSlice,
attrEmpty,
},
@@ -173,6 +201,7 @@ func TestAttributeTransforms(t *testing.T) {
kvFloat64Slice,
kvString,
kvBytes,
kvSlice,
kvStringSlice,
kvEmpty,
},
@@ -180,12 +209,45 @@ func TestAttributeTransforms(t *testing.T) {
} {
t.Run(test.name, func(t *testing.T) {
t.Run("KeyValues", func(t *testing.T) {
assert.ElementsMatch(t, test.want, KeyValues(test.in))
assertKeyValueSlicesEqual(t, test.want, KeyValues(test.in))
})
t.Run("AttrIter", func(t *testing.T) {
s := attribute.NewSet(test.in...)
assert.ElementsMatch(t, test.want, AttrIter(s.Iter()))
assertKeyValueSlicesEqual(t, test.want, AttrIter(s.Iter()))
})
})
}
}
func TestKeyValuesPreserveDuplicateKeys(t *testing.T) {
want := []*cpb.KeyValue{
{Key: "dup", Value: valBoolTrue},
{Key: "dup", Value: valStrO},
}
assertKeyValueSlicesEqual(t, want, KeyValues([]attribute.KeyValue{
attribute.Bool("dup", true),
attribute.String("dup", "o"),
}))
}
func assertKeyValueSlicesEqual(t *testing.T, want, got []*cpb.KeyValue) {
t.Helper()
require.Len(t, got, len(want))
used := make([]bool, len(got))
for i, wantKV := range want {
matched := false
for j, gotKV := range got {
if used[j] {
continue
}
if proto.Equal(wantKV, gotKV) {
used[j] = true
matched = true
break
}
}
assert.Truef(t, matched, "missing match for want[%d] = %#v in got = %#v", i, wantKV, got)
}
}