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

Support BYTESLICE attributes across trace and exporter paths (#8153)

Fixes https://github.com/open-telemetry/opentelemetry-go/issues/8164

Supersedes #8042 which does not address the `trace` or `zipkin`
packages, and has unrelated support changes.

Add end-to-end handling for `attribute.BYTESLICE` in the remaining trace
and exporter paths that still dropped, invalidated, or stringified byte
slice attributes.

This change:

- preserves byte slice attributes in `trace/auto`
- encodes byte slice attributes as OTLP `AnyValue_BytesValue` in trace,
log, and metric transforms
- serializes Zipkin byte slice attributes as JSON arrays of byte values
- adds regression tests for each updated path

## Problem

`attribute.BYTESLICE` is public, but several downstream conversions
still did not handle it correctly:

- `trace/auto` dropped byte slice attributes during conversion
- OTLP trace, log, and metric transforms fell through to their invalid
default handling
- Zipkin fell back to `Value.Emit()`, which produced a base64 string
rather than an explicit byte-array representation

That made `BYTESLICE` unusable or inconsistent depending on the export
path.

## Changes

### Trace

- Handle `attribute.BYTESLICE` in `trace/auto` by converting it to an
internal telemetry bytes value.
- Add a regression test covering byte slice conversion.

### OTLP

- Handle `attribute.BYTESLICE` in:
  - trace attribute transform
  - log gRPC attribute transform
  - log HTTP attribute transform
  - metric HTTP attribute transform
  - metric gRPC attribute transform
- Update the shared log and metric transform templates so generated
outputs stay aligned.
- Add regression tests for the trace transform, both log transform
outputs, and both metric transform outputs.

### Zipkin

- Handle `attribute.BYTESLICE` explicitly in Zipkin tag serialization.
- Serialize byte slices as JSON arrays of byte values instead of base64
text.
- Add a regression test for Zipkin byte slice serialization.

---------

Co-authored-by: Robert Pająk <pellared@hotmail.com>
This commit is contained in:
Tyler Yahn
2026-04-09 14:04:03 -07:00
committed by GitHub
parent 112bed7fc0
commit b1284dbfaa
19 changed files with 130 additions and 0 deletions
+5
View File
@@ -25,6 +25,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Fixed
- Fix gzipped request body replay on redirect in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8152)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/trace`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlptrace`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlplog`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8153)
<!-- Released section -->
<!-- Don't change this section unless doing release -->
@@ -25,6 +25,7 @@ var (
attrFloat64 = attribute.Float64("float64", 1)
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{
Key: attribute.Key("empty"),
@@ -53,6 +54,7 @@ var (
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valAttrBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -69,6 +71,7 @@ var (
kvFloat64 = &cpb.KeyValue{Key: "float64", Value: valDblOne}
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvAttrBytes = &cpb.KeyValue{Key: "bytes", Value: valAttrBytes}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -133,6 +136,11 @@ func TestAttrTransforms(t *testing.T) {
[]attribute.KeyValue{attrString},
[]*cpb.KeyValue{kvString},
},
{
"bytes",
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvAttrBytes},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -150,6 +158,7 @@ func TestAttrTransforms(t *testing.T) {
attrFloat64,
attrFloat64Slice,
attrString,
attrBytes,
attrStringSlice,
attrEmpty,
},
@@ -163,6 +172,7 @@ func TestAttrTransforms(t *testing.T) {
kvFloat64,
kvFloat64Slice,
kvString,
kvAttrBytes,
kvStringSlice,
kvEmpty,
},
@@ -195,6 +195,10 @@ func AttrValue(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_StringValue{
StringValue: v.AsString(),
}
case attribute.BYTESLICE:
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -25,6 +25,7 @@ var (
attrFloat64 = attribute.Float64("float64", 1)
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{
Key: attribute.Key("empty"),
@@ -53,6 +54,7 @@ var (
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valAttrBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -69,6 +71,7 @@ var (
kvFloat64 = &cpb.KeyValue{Key: "float64", Value: valDblOne}
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvAttrBytes = &cpb.KeyValue{Key: "bytes", Value: valAttrBytes}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -133,6 +136,11 @@ func TestAttrTransforms(t *testing.T) {
[]attribute.KeyValue{attrString},
[]*cpb.KeyValue{kvString},
},
{
"bytes",
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvAttrBytes},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -150,6 +158,7 @@ func TestAttrTransforms(t *testing.T) {
attrFloat64,
attrFloat64Slice,
attrString,
attrBytes,
attrStringSlice,
attrEmpty,
},
@@ -163,6 +172,7 @@ func TestAttrTransforms(t *testing.T) {
kvFloat64,
kvFloat64Slice,
kvString,
kvAttrBytes,
kvStringSlice,
kvEmpty,
},
@@ -195,6 +195,10 @@ func AttrValue(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_StringValue{
StringValue: v.AsString(),
}
case attribute.BYTESLICE:
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -81,6 +81,10 @@ func Value(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_StringValue{
StringValue: v.AsString(),
}
case attribute.BYTESLICE:
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -25,6 +25,7 @@ var (
attrFloat64 = attribute.Float64("float64", 1)
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{
Key: attribute.Key("empty"),
@@ -53,6 +54,7 @@ var (
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -69,6 +71,7 @@ var (
kvFloat64 = &cpb.KeyValue{Key: "float64", Value: valDblOne}
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvBytes = &cpb.KeyValue{Key: "bytes", Value: valBytes}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -133,6 +136,11 @@ func TestAttributeTransforms(t *testing.T) {
[]attribute.KeyValue{attrString},
[]*cpb.KeyValue{kvString},
},
{
"bytes",
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvBytes},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -150,6 +158,7 @@ func TestAttributeTransforms(t *testing.T) {
attrFloat64,
attrFloat64Slice,
attrString,
attrBytes,
attrStringSlice,
attrEmpty,
},
@@ -163,6 +172,7 @@ func TestAttributeTransforms(t *testing.T) {
kvFloat64,
kvFloat64Slice,
kvString,
kvBytes,
kvStringSlice,
kvEmpty,
},
@@ -81,6 +81,10 @@ func Value(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_StringValue{
StringValue: v.AsString(),
}
case attribute.BYTESLICE:
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -25,6 +25,7 @@ var (
attrFloat64 = attribute.Float64("float64", 1)
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{
Key: attribute.Key("empty"),
@@ -53,6 +54,7 @@ var (
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -69,6 +71,7 @@ var (
kvFloat64 = &cpb.KeyValue{Key: "float64", Value: valDblOne}
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvBytes = &cpb.KeyValue{Key: "bytes", Value: valBytes}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -133,6 +136,11 @@ func TestAttributeTransforms(t *testing.T) {
[]attribute.KeyValue{attrString},
[]*cpb.KeyValue{kvString},
},
{
"bytes",
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvBytes},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -150,6 +158,7 @@ func TestAttributeTransforms(t *testing.T) {
attrFloat64,
attrFloat64Slice,
attrString,
attrBytes,
attrStringSlice,
attrEmpty,
},
@@ -163,6 +172,7 @@ func TestAttributeTransforms(t *testing.T) {
kvFloat64,
kvFloat64Slice,
kvString,
kvBytes,
kvStringSlice,
kvEmpty,
},
@@ -87,6 +87,10 @@ func Value(v attribute.Value) *commonpb.AnyValue {
av.Value = &commonpb.AnyValue_StringValue{
StringValue: v.AsString(),
}
case attribute.BYTESLICE:
av.Value = &commonpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.STRINGSLICE:
av.Value = &commonpb.AnyValue_ArrayValue{
ArrayValue: &commonpb.ArrayValue{
@@ -26,6 +26,7 @@ func TestAttributes(t *testing.T) {
attribute.Int64("int64 to int64", 1234567),
attribute.Float64("float64 to double", 1.61),
attribute.String("string to string", "string"),
attribute.ByteSlice("bytes to bytes", []byte("bytes")),
attribute.Bool("bool to bool", true),
{Key: "empty to empty"},
},
@@ -62,6 +63,14 @@ func TestAttributes(t *testing.T) {
},
},
},
{
Key: "bytes to bytes",
Value: &commonpb.AnyValue{
Value: &commonpb.AnyValue_BytesValue{
BytesValue: []byte("bytes"),
},
},
},
{
Key: "bool to bool",
Value: &commonpb.AnyValue{
+8
View File
@@ -174,6 +174,14 @@ func attributeToStringPair(kv attribute.KeyValue) (string, string) {
case attribute.STRINGSLICE:
data, _ := json.Marshal(kv.Value.AsStringSlice())
return string(kv.Key), string(data)
case attribute.BYTESLICE:
raw := kv.Value.AsByteSlice()
data := make([]int, len(raw))
for i, b := range raw {
data[i] = int(b)
}
encoded, _ := json.Marshal(data)
return string(kv.Key), string(encoded)
default:
return string(kv.Key), kv.Value.Emit()
}
+9
View File
@@ -982,6 +982,15 @@ func TestModelConversion(t *testing.T) {
require.Equal(t, expectedOutputBatch, gottenOutputBatch)
}
func TestAttributeToStringPairByteSlice(t *testing.T) {
t.Parallel()
k, v := attributeToStringPair(attribute.ByteSlice("bytes", []byte{1, 2, 3}))
assert.Equal(t, "bytes", k)
assert.Equal(t, "[1,2,3]", v)
}
func zkmodelIDPtr(n uint64) *zkmodel.ID {
id := zkmodel.ID(n)
return &id
@@ -25,6 +25,7 @@ var (
attrFloat64 = attribute.Float64("float64", 1)
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{
Key: attribute.Key("empty"),
@@ -53,6 +54,7 @@ var (
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valAttrBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -69,6 +71,7 @@ var (
kvFloat64 = &cpb.KeyValue{Key: "float64", Value: valDblOne}
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvAttrBytes = &cpb.KeyValue{Key: "bytes", Value: valAttrBytes}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -133,6 +136,11 @@ func TestAttrTransforms(t *testing.T) {
[]attribute.KeyValue{attrString},
[]*cpb.KeyValue{kvString},
},
{
"bytes",
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvAttrBytes},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -150,6 +158,7 @@ func TestAttrTransforms(t *testing.T) {
attrFloat64,
attrFloat64Slice,
attrString,
attrBytes,
attrStringSlice,
attrEmpty,
},
@@ -163,6 +172,7 @@ func TestAttrTransforms(t *testing.T) {
kvFloat64,
kvFloat64Slice,
kvString,
kvAttrBytes,
kvStringSlice,
kvEmpty,
},
@@ -195,6 +195,10 @@ func AttrValue(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_StringValue{
StringValue: v.AsString(),
}
case attribute.BYTESLICE:
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -81,6 +81,10 @@ func Value(v attribute.Value) *cpb.AnyValue {
av.Value = &cpb.AnyValue_StringValue{
StringValue: v.AsString(),
}
case attribute.BYTESLICE:
av.Value = &cpb.AnyValue_BytesValue{
BytesValue: v.AsByteSlice(),
}
case attribute.STRINGSLICE:
av.Value = &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -25,6 +25,7 @@ var (
attrFloat64 = attribute.Float64("float64", 1)
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{
Key: attribute.Key("empty"),
@@ -53,6 +54,7 @@ var (
},
}}
valStrO = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "o"}}
valBytes = &cpb.AnyValue{Value: &cpb.AnyValue_BytesValue{BytesValue: []byte("otlp")}}
valStrN = &cpb.AnyValue{Value: &cpb.AnyValue_StringValue{StringValue: "n"}}
valStrSlice = &cpb.AnyValue{Value: &cpb.AnyValue_ArrayValue{
ArrayValue: &cpb.ArrayValue{
@@ -69,6 +71,7 @@ var (
kvFloat64 = &cpb.KeyValue{Key: "float64", Value: valDblOne}
kvFloat64Slice = &cpb.KeyValue{Key: "float64 slice", Value: valDblSlice}
kvString = &cpb.KeyValue{Key: "string", Value: valStrO}
kvBytes = &cpb.KeyValue{Key: "bytes", Value: valBytes}
kvStringSlice = &cpb.KeyValue{Key: "string slice", Value: valStrSlice}
kvEmpty = &cpb.KeyValue{Key: "empty", Value: &cpb.AnyValue{}}
)
@@ -133,6 +136,11 @@ func TestAttributeTransforms(t *testing.T) {
[]attribute.KeyValue{attrString},
[]*cpb.KeyValue{kvString},
},
{
"bytes",
[]attribute.KeyValue{attrBytes},
[]*cpb.KeyValue{kvBytes},
},
{
"string slice",
[]attribute.KeyValue{attrStringSlice},
@@ -150,6 +158,7 @@ func TestAttributeTransforms(t *testing.T) {
attrFloat64,
attrFloat64Slice,
attrString,
attrBytes,
attrStringSlice,
attrEmpty,
},
@@ -163,6 +172,7 @@ func TestAttributeTransforms(t *testing.T) {
kvFloat64,
kvFloat64Slice,
kvString,
kvBytes,
kvStringSlice,
kvEmpty,
},
+2
View File
@@ -314,6 +314,8 @@ func convAttrValue(value attribute.Value) telemetry.Value {
case attribute.STRING:
v := truncate(maxSpan.AttrValueLen, value.AsString())
return telemetry.StringValue(v)
case attribute.BYTESLICE:
return telemetry.BytesValue(value.AsByteSlice())
case attribute.BOOLSLICE:
slice := value.AsBoolSlice()
out := make([]telemetry.Value, 0, len(slice))
+9
View File
@@ -164,6 +164,15 @@ func TestSpanKindTransform(t *testing.T) {
}
}
func TestConvAttrValueBytes(t *testing.T) {
t.Parallel()
val := convAttrValue(attribute.ByteSliceValue([]byte("bytes")))
assert.Equal(t, telemetry.ValueKindBytes, val.Kind())
assert.Equal(t, []byte("bytes"), val.AsBytes())
}
func TestTracerStartPropagatesOrigCtx(t *testing.T) {
t.Parallel()