diff --git a/.chloggen/pprofile-refs.yaml b/.chloggen/pprofile-refs.yaml new file mode 100644 index 00000000000..60caec970bc --- /dev/null +++ b/.chloggen/pprofile-refs.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'enhancement' + +# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp) +component: pdata/pprofile + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Implement reference based attributes in Profiles + +# One or more tracking issues or pull requests related to the change +issues: [14546] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [api] diff --git a/consumer/consumererror/go.sum b/consumer/consumererror/go.sum index dd177e4dfb6..fb0e286bda7 100644 --- a/consumer/consumererror/go.sum +++ b/consumer/consumererror/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -29,6 +31,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= diff --git a/consumer/consumererror/xconsumererror/go.sum b/consumer/consumererror/xconsumererror/go.sum index 002e4ea530d..a38defe5d44 100644 --- a/consumer/consumererror/xconsumererror/go.sum +++ b/consumer/consumererror/xconsumererror/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,6 +26,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= diff --git a/consumer/consumertest/go.sum b/consumer/consumertest/go.sum index 002e4ea530d..a38defe5d44 100644 --- a/consumer/consumertest/go.sum +++ b/consumer/consumertest/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,6 +26,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= diff --git a/consumer/xconsumer/go.sum b/consumer/xconsumer/go.sum index 002e4ea530d..a38defe5d44 100644 --- a/consumer/xconsumer/go.sum +++ b/consumer/xconsumer/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,6 +26,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= diff --git a/internal/cmd/pdatagen/internal/pdata/pcommon_package.go b/internal/cmd/pdatagen/internal/pdata/pcommon_package.go index 0ef2c470c19..4d3ed758beb 100644 --- a/internal/cmd/pdatagen/internal/pdata/pcommon_package.go +++ b/internal/cmd/pdatagen/internal/pdata/pcommon_package.go @@ -109,6 +109,11 @@ var keyValueStruct = &messageStruct{ protoID: 2, returnMessage: anyValueClone, }, + &PrimitiveField{ + fieldName: "KeyStrindex", + protoID: 3, + protoType: proto.TypeInt32, + }, }, hasOnlyInternal: true, } @@ -171,6 +176,12 @@ var anyValueStruct = &messageStruct{ originFieldName: "BytesValue", protoType: proto.TypeBytes, }, + &OneOfPrimitiveValue{ + fieldName: "StringValueStrindex", + protoID: 8, + originFieldName: "StringValueStrindex", + protoType: proto.TypeInt32, + }, }, }, }, diff --git a/internal/fanoutconsumer/go.sum b/internal/fanoutconsumer/go.sum index 002e4ea530d..a38defe5d44 100644 --- a/internal/fanoutconsumer/go.sum +++ b/internal/fanoutconsumer/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,6 +26,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= diff --git a/pdata/internal/generated_proto_anyvalue.go b/pdata/internal/generated_proto_anyvalue.go index 4056181e391..404c61891b4 100644 --- a/pdata/internal/generated_proto_anyvalue.go +++ b/pdata/internal/generated_proto_anyvalue.go @@ -101,6 +101,17 @@ func (m *AnyValue) GetBytesValue() []byte { return nil } +type AnyValue_StringValueStrindex struct { + StringValueStrindex int32 +} + +func (m *AnyValue) GetStringValueStrindex() int32 { + if v, ok := m.GetValue().(*AnyValue_StringValueStrindex); ok { + return v.StringValueStrindex + } + return int32(0) +} + type AnyValue struct { Value any } @@ -153,6 +164,12 @@ var ( return &AnyValue_BytesValue{} }, } + + ProtoPoolAnyValue_StringValueStrindex = sync.Pool{ + New: func() any { + return &AnyValue_StringValueStrindex{} + }, + } ) func NewAnyValue() *AnyValue { @@ -205,6 +222,11 @@ func DeleteAnyValue(orig *AnyValue, nullable bool) { ov.BytesValue = nil ProtoPoolAnyValue_BytesValue.Put(ov) } + case *AnyValue_StringValueStrindex: + if metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() { + ov.StringValueStrindex = int32(0) + ProtoPoolAnyValue_StringValueStrindex.Put(ov) + } } orig.Reset() if nullable { @@ -298,6 +320,16 @@ func CopyAnyValue(dest, src *AnyValue) *AnyValue { ov.BytesValue = t.BytesValue dest.Value = ov + case *AnyValue_StringValueStrindex: + var ov *AnyValue_StringValueStrindex + if !metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() { + ov = &AnyValue_StringValueStrindex{} + } else { + ov = ProtoPoolAnyValue_StringValueStrindex.Get().(*AnyValue_StringValueStrindex) + } + ov.StringValueStrindex = t.StringValueStrindex + dest.Value = ov + default: dest.Value = nil } @@ -387,6 +419,9 @@ func (orig *AnyValue) MarshalJSON(dest *json.Stream) { dest.WriteObjectField("bytesValue") dest.WriteBytes(orig.BytesValue) + case *AnyValue_StringValueStrindex: + dest.WriteObjectField("stringValueStrindex") + dest.WriteInt32(orig.StringValueStrindex) } dest.WriteObjectEnd() } @@ -475,6 +510,17 @@ func (orig *AnyValue) UnmarshalJSON(iter *json.Iterator) { ov.BytesValue = iter.ReadBytes() orig.Value = ov } + case "stringValueStrindex", "string_value_strindex": + { + var ov *AnyValue_StringValueStrindex + if !metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() { + ov = &AnyValue_StringValueStrindex{} + } else { + ov = ProtoPoolAnyValue_StringValueStrindex.Get().(*AnyValue_StringValueStrindex) + } + ov.StringValueStrindex = iter.ReadInt32() + orig.Value = ov + } default: iter.Skip() @@ -515,6 +561,9 @@ func (orig *AnyValue) SizeProto() int { case *AnyValue_BytesValue: l = len(orig.BytesValue) n += 1 + proto.Sov(uint64(l)) + l + case *AnyValue_StringValueStrindex: + + n += 1 + proto.Sov(uint64(orig.StringValueStrindex)) } return n } @@ -577,6 +626,11 @@ func (orig *AnyValue) MarshalProto(buf []byte) int { pos-- buf[pos] = 0x3a + case *AnyValue_StringValueStrindex: + pos = proto.EncodeVarint(buf, pos, uint64(orig.StringValueStrindex)) + pos-- + buf[pos] = 0x40 + } return len(buf) - pos } @@ -737,6 +791,24 @@ func (orig *AnyValue) UnmarshalProto(buf []byte) error { } orig.Value = ov + case 8: + if wireType != proto.WireTypeVarint { + return fmt.Errorf("proto: wrong wireType = %d for field StringValueStrindex", wireType) + } + var num uint64 + num, pos, err = proto.ConsumeVarint(buf, pos) + if err != nil { + return err + } + var ov *AnyValue_StringValueStrindex + if !metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() { + ov = &AnyValue_StringValueStrindex{} + } else { + ov = ProtoPoolAnyValue_StringValueStrindex.Get().(*AnyValue_StringValueStrindex) + } + ov.StringValueStrindex = int32(num) + orig.Value = ov + default: pos, err = proto.ConsumeUnknown(buf, pos, wireType) if err != nil { diff --git a/pdata/internal/generated_proto_anyvalue_test.go b/pdata/internal/generated_proto_anyvalue_test.go index 7ddebddeea8..5f9fc0eb2e3 100644 --- a/pdata/internal/generated_proto_anyvalue_test.go +++ b/pdata/internal/generated_proto_anyvalue_test.go @@ -191,20 +191,22 @@ func genTestFailingUnmarshalProtoValuesAnyValue() map[string][]byte { return map[string][]byte{ "invalid_field": {0x02}, - "StringValue/wrong_wire_type": {0xc}, - "StringValue/missing_value": {0xa}, - "BoolValue/wrong_wire_type": {0x14}, - "BoolValue/missing_value": {0x10}, - "IntValue/wrong_wire_type": {0x1c}, - "IntValue/missing_value": {0x18}, - "DoubleValue/wrong_wire_type": {0x24}, - "DoubleValue/missing_value": {0x21}, - "ArrayValue/wrong_wire_type": {0x2c}, - "ArrayValue/missing_value": {0x2a}, - "KvlistValue/wrong_wire_type": {0x34}, - "KvlistValue/missing_value": {0x32}, - "BytesValue/wrong_wire_type": {0x3c}, - "BytesValue/missing_value": {0x3a}, + "StringValue/wrong_wire_type": {0xc}, + "StringValue/missing_value": {0xa}, + "BoolValue/wrong_wire_type": {0x14}, + "BoolValue/missing_value": {0x10}, + "IntValue/wrong_wire_type": {0x1c}, + "IntValue/missing_value": {0x18}, + "DoubleValue/wrong_wire_type": {0x24}, + "DoubleValue/missing_value": {0x21}, + "ArrayValue/wrong_wire_type": {0x2c}, + "ArrayValue/missing_value": {0x2a}, + "KvlistValue/wrong_wire_type": {0x34}, + "KvlistValue/missing_value": {0x32}, + "BytesValue/wrong_wire_type": {0x3c}, + "BytesValue/missing_value": {0x3a}, + "StringValueStrindex/wrong_wire_type": {0x44}, + "StringValueStrindex/missing_value": {0x40}, } } @@ -218,6 +220,7 @@ func genTestEncodingValuesAnyValue() map[string]*AnyValue { "DoubleValue/test": {Value: &AnyValue_DoubleValue{DoubleValue: float64(3.1415926)}}, "ArrayValue/default": {Value: &AnyValue_ArrayValue{ArrayValue: &ArrayValue{}}}, "ArrayValue/test": {Value: &AnyValue_ArrayValue{ArrayValue: GenTestArrayValue()}}, "KvlistValue/default": {Value: &AnyValue_KvlistValue{KvlistValue: &KeyValueList{}}}, "KvlistValue/test": {Value: &AnyValue_KvlistValue{KvlistValue: GenTestKeyValueList()}}, "BytesValue/default": {Value: &AnyValue_BytesValue{BytesValue: nil}}, - "BytesValue/test": {Value: &AnyValue_BytesValue{BytesValue: []byte{1, 2, 3}}}, + "BytesValue/test": {Value: &AnyValue_BytesValue{BytesValue: []byte{1, 2, 3}}}, "StringValueStrindex/default": {Value: &AnyValue_StringValueStrindex{StringValueStrindex: int32(0)}}, + "StringValueStrindex/test": {Value: &AnyValue_StringValueStrindex{StringValueStrindex: int32(13)}}, } } diff --git a/pdata/internal/generated_proto_keyvalue.go b/pdata/internal/generated_proto_keyvalue.go index 6f246a122e1..5f80a111ab3 100644 --- a/pdata/internal/generated_proto_keyvalue.go +++ b/pdata/internal/generated_proto_keyvalue.go @@ -16,8 +16,9 @@ import ( ) type KeyValue struct { - Value AnyValue - Key string + Value AnyValue + Key string + KeyStrindex int32 } var ( @@ -46,6 +47,7 @@ func DeleteKeyValue(orig *KeyValue, nullable bool) { } DeleteAnyValue(&orig.Value, false) + orig.Reset() if nullable { protoPoolKeyValue.Put(orig) @@ -68,6 +70,8 @@ func CopyKeyValue(dest, src *KeyValue) *KeyValue { dest.Key = src.Key CopyAnyValue(&dest.Value, &src.Value) + dest.KeyStrindex = src.KeyStrindex + return dest } @@ -132,6 +136,10 @@ func (orig *KeyValue) MarshalJSON(dest *json.Stream) { } dest.WriteObjectField("value") orig.Value.MarshalJSON(dest) + if orig.KeyStrindex != int32(0) { + dest.WriteObjectField("keyStrindex") + dest.WriteInt32(orig.KeyStrindex) + } dest.WriteObjectEnd() } @@ -144,6 +152,8 @@ func (orig *KeyValue) UnmarshalJSON(iter *json.Iterator) { case "value": orig.Value.UnmarshalJSON(iter) + case "keyStrindex", "key_strindex": + orig.KeyStrindex = iter.ReadInt32() default: iter.Skip() } @@ -161,6 +171,9 @@ func (orig *KeyValue) SizeProto() int { } l = orig.Value.SizeProto() n += 1 + proto.Sov(uint64(l)) + l + if orig.KeyStrindex != int32(0) { + n += 1 + proto.Sov(uint64(orig.KeyStrindex)) + } return n } @@ -182,6 +195,11 @@ func (orig *KeyValue) MarshalProto(buf []byte) int { pos-- buf[pos] = 0x12 + if orig.KeyStrindex != int32(0) { + pos = proto.EncodeVarint(buf, pos, uint64(orig.KeyStrindex)) + pos-- + buf[pos] = 0x18 + } return len(buf) - pos } @@ -227,6 +245,17 @@ func (orig *KeyValue) UnmarshalProto(buf []byte) error { if err != nil { return err } + + case 3: + if wireType != proto.WireTypeVarint { + return fmt.Errorf("proto: wrong wireType = %d for field KeyStrindex", wireType) + } + var num uint64 + num, pos, err = proto.ConsumeVarint(buf, pos) + if err != nil { + return err + } + orig.KeyStrindex = int32(num) default: pos, err = proto.ConsumeUnknown(buf, pos, wireType) if err != nil { @@ -241,6 +270,7 @@ func GenTestKeyValue() *KeyValue { orig := NewKeyValue() orig.Key = "test_key" orig.Value = *GenTestAnyValue() + orig.KeyStrindex = int32(13) return orig } diff --git a/pdata/internal/generated_proto_keyvalue_test.go b/pdata/internal/generated_proto_keyvalue_test.go index b8522b4800f..db495e18075 100644 --- a/pdata/internal/generated_proto_keyvalue_test.go +++ b/pdata/internal/generated_proto_keyvalue_test.go @@ -189,18 +189,21 @@ func TestMarshalAndUnmarshalProtoViaProtobufKeyValue(t *testing.T) { func genTestFailingUnmarshalProtoValuesKeyValue() map[string][]byte { return map[string][]byte{ - "invalid_field": {0x02}, - "Key/wrong_wire_type": {0xc}, - "Key/missing_value": {0xa}, - "Value/wrong_wire_type": {0x14}, - "Value/missing_value": {0x12}, + "invalid_field": {0x02}, + "Key/wrong_wire_type": {0xc}, + "Key/missing_value": {0xa}, + "Value/wrong_wire_type": {0x14}, + "Value/missing_value": {0x12}, + "KeyStrindex/wrong_wire_type": {0x1c}, + "KeyStrindex/missing_value": {0x18}, } } func genTestEncodingValuesKeyValue() map[string]*KeyValue { return map[string]*KeyValue{ - "empty": NewKeyValue(), - "Key/test": {Key: "test_key"}, - "Value/test": {Value: *GenTestAnyValue()}, + "empty": NewKeyValue(), + "Key/test": {Key: "test_key"}, + "Value/test": {Value: *GenTestAnyValue()}, + "KeyStrindex/test": {KeyStrindex: int32(13)}, } } diff --git a/pdata/pprofile/dictionary_helpers.go b/pdata/pprofile/dictionary_helpers.go new file mode 100644 index 00000000000..4f1fa43c2f0 --- /dev/null +++ b/pdata/pprofile/dictionary_helpers.go @@ -0,0 +1,160 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +import ( + "go.opentelemetry.io/collector/pdata/internal" + "go.opentelemetry.io/collector/pdata/internal/metadata" + "go.opentelemetry.io/collector/pdata/pcommon" +) + +// mapKeyValues returns the underlying KeyValue slice of a pcommon.Map. +func mapKeyValues(m pcommon.Map) []internal.KeyValue { + return *internal.GetMapOrig(internal.MapWrapper(m)) +} + +// resolveProfilesReferences walks through all profiles data after unmarshaling +// and resolves any string_value_ref and key_ref to their actual string values. +// This ensures the pdata API works transparently with referenced strings. +func resolveProfilesReferences(profiles Profiles) { + dict := profiles.Dictionary() + + // Resolve references in resource attributes + for i := 0; i < profiles.ResourceProfiles().Len(); i++ { + rp := profiles.ResourceProfiles().At(i) + resolveKeyValueReferences(dict, mapKeyValues(rp.Resource().Attributes())) + + // Resolve references in scope attributes + for j := 0; j < rp.ScopeProfiles().Len(); j++ { + sp := rp.ScopeProfiles().At(j) + resolveKeyValueReferences(dict, mapKeyValues(sp.Scope().Attributes())) + } + } +} + +// resolveKeyValueReferences resolves key_ref and string_value_ref in a KeyValue slice +func resolveKeyValueReferences(dict ProfilesDictionary, kvs []internal.KeyValue) { + for i := range kvs { + kv := &kvs[i] + // Resolve key_ref if set + if kv.KeyStrindex >= 0 { + idx := int(kv.KeyStrindex) + if idx < dict.StringTable().Len() { + kv.Key = dict.StringTable().At(idx) + // N.b. keep KeyStrindex set to optimize re-marshaling. This is + // technically a violation of the proto spec, but acceptable + // for the in-memory pdata API since keys are immutable. + } + } + // Resolve string_value_ref if set + resolveAnyValueReference(dict, &kv.Value) + } +} + +// resolveAnyValueReference resolves string_value_ref in an AnyValue +func resolveAnyValueReference(dict ProfilesDictionary, anyValue *internal.AnyValue) { + if ref, ok := anyValue.Value.(*internal.AnyValue_StringValueStrindex); ok && ref.StringValueStrindex != 0 { + idx := int(ref.StringValueStrindex) + if idx >= 0 && idx < dict.StringTable().Len() { + str := dict.StringTable().At(idx) + var ov *internal.AnyValue_StringValue + if !metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() { + ov = &internal.AnyValue_StringValue{} + } else { + ov = internal.ProtoPoolAnyValue_StringValue.Get().(*internal.AnyValue_StringValue) + } + ov.StringValue = str + anyValue.Value = ov + } + } else if kvList, ok := anyValue.Value.(*internal.AnyValue_KvlistValue); ok && kvList.KvlistValue != nil { + resolveKeyValueReferences(dict, kvList.KvlistValue.Values) + } else if arrVal, ok := anyValue.Value.(*internal.AnyValue_ArrayValue); ok && arrVal.ArrayValue != nil { + for i := 0; i < len(arrVal.ArrayValue.Values); i++ { + resolveAnyValueReference(dict, &arrVal.ArrayValue.Values[i]) + } + } +} + +// convertProfilesToReferences walks through all profiles data before marshaling +// and converts string values to references for efficient transmission. +// This builds up the string table in the dictionary and replaces strings with refs. +func convertProfilesToReferences(profiles Profiles) { + dict := profiles.Dictionary() + stringTable := dict.StringTable() + + // Map for quick string lookups - only allocate if needed + var stringIndex map[string]int32 + getStringIndex := func(s string) int32 { + if stringIndex == nil { + stringIndex = make(map[string]int32, stringTable.Len()) + for i := 0; i < stringTable.Len(); i++ { + stringIndex[stringTable.At(i)] = int32(i) + } + } + + if idx, ok := stringIndex[s]; ok { + return idx + } + idx := int32(stringTable.Len()) + stringTable.Append(s) + stringIndex[s] = idx + return idx + } + + // Convert strings in resource attributes + for i := 0; i < profiles.ResourceProfiles().Len(); i++ { + rp := profiles.ResourceProfiles().At(i) + convertKeyValueToReferences(getStringIndex, mapKeyValues(rp.Resource().Attributes())) + + // Convert strings in scope attributes + for j := 0; j < rp.ScopeProfiles().Len(); j++ { + sp := rp.ScopeProfiles().At(j) + convertKeyValueToReferences(getStringIndex, mapKeyValues(sp.Scope().Attributes())) + } + } +} + +// convertKeyValueToReferences converts string keys and values to references in a KeyValue slice +func convertKeyValueToReferences(getStringIndex func(string) int32, kvs []internal.KeyValue) { + for i := range kvs { + kv := &kvs[i] + + // Convert key to reference + if kv.Key != "" && kv.KeyStrindex == 0 { + kv.KeyStrindex = getStringIndex(kv.Key) + kv.Key = "" + } + + // Convert string values to references + convertAnyValueToReference(getStringIndex, &kv.Value) + } +} + +// convertAnyValueToReference converts string values to string_value_ref +func convertAnyValueToReference(getStringIndex func(string) int32, anyValue *internal.AnyValue) { + // Skip if already a reference + if _, ok := anyValue.Value.(*internal.AnyValue_StringValueStrindex); ok { + return + } + + if strVal, ok := anyValue.Value.(*internal.AnyValue_StringValue); ok && strVal.StringValue != "" { + // Convert to reference + idx := getStringIndex(strVal.StringValue) + var ov *internal.AnyValue_StringValueStrindex + if !metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() { + ov = &internal.AnyValue_StringValueStrindex{} + } else { + ov = internal.ProtoPoolAnyValue_StringValueStrindex.Get().(*internal.AnyValue_StringValueStrindex) + } + ov.StringValueStrindex = idx + anyValue.Value = ov + } else if kvList, ok := anyValue.Value.(*internal.AnyValue_KvlistValue); ok && kvList.KvlistValue != nil { + convertKeyValueToReferences(getStringIndex, kvList.KvlistValue.Values) + } else if arrVal, ok := anyValue.Value.(*internal.AnyValue_ArrayValue); ok && arrVal.ArrayValue != nil { + // Recursively convert arrays + for i := 0; i < len(arrVal.ArrayValue.Values); i++ { + convertAnyValueToReference(getStringIndex, &arrVal.ArrayValue.Values[i]) + } + } +} diff --git a/pdata/pprofile/dictionary_helpers_test.go b/pdata/pprofile/dictionary_helpers_test.go new file mode 100644 index 00000000000..e721a68f2e1 --- /dev/null +++ b/pdata/pprofile/dictionary_helpers_test.go @@ -0,0 +1,534 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/featuregate" + "go.opentelemetry.io/collector/pdata/internal" + "go.opentelemetry.io/collector/pdata/internal/metadata" +) + +func TestResolveProfilesReferencesEmpty(t *testing.T) { + profiles := NewProfiles() + // Should not panic on empty profiles + resolveProfilesReferences(profiles) + assert.Equal(t, 0, profiles.ResourceProfiles().Len()) +} + +func TestResolveProfilesReferencesWithKeyRef(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") // index 0 + dict.StringTable().Append("test-key") + dict.StringTable().Append("test-value") + + rp := profiles.ResourceProfiles().AppendEmpty() + attrs := rp.Resource().Attributes() + + // Manually create a KeyValue with key_ref + mapOrig := internal.GetMapOrig(internal.MapWrapper(attrs)) + *mapOrig = append(*mapOrig, internal.KeyValue{ + KeyStrindex: 1, // references "test-key" + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValueStrindex{ + StringValueStrindex: 2, // references "test-value" + }, + }, + }) + + resolveProfilesReferences(profiles) + + // Verify key_ref was resolved + kv := &(*mapOrig)[0] + assert.Equal(t, "test-key", kv.Key) + + // Verify string_value_ref was resolved + strVal, ok := kv.Value.Value.(*internal.AnyValue_StringValue) + assert.True(t, ok) + assert.Equal(t, "test-value", strVal.StringValue) +} + +func TestResolveProfilesReferencesInvalidIndices(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") // index 0 + dict.StringTable().Append("valid") + + rp := profiles.ResourceProfiles().AppendEmpty() + attrs := rp.Resource().Attributes() + + mapOrig := internal.GetMapOrig(internal.MapWrapper(attrs)) + *mapOrig = append(*mapOrig, internal.KeyValue{ + Key: "fallback-key", + KeyStrindex: 999, // invalid index + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValueStrindex{ + StringValueStrindex: 999, // invalid index + }, + }, + }) + + resolveProfilesReferences(profiles) + + // Key should remain unchanged since ref is invalid + kv := &(*mapOrig)[0] + assert.Equal(t, "fallback-key", kv.Key) + + // Value should remain as StringValueStrindex since index is invalid + _, ok := kv.Value.Value.(*internal.AnyValue_StringValueStrindex) + assert.True(t, ok) +} + +func TestResolveAnyValueReferenceWithPooling(t *testing.T) { + // Test with pooling enabled + prevPooling := metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() + require.NoError(t, featuregate.GlobalRegistry().Set(metadata.PdataUseProtoPoolingFeatureGate.ID(), true)) + defer func() { + require.NoError(t, featuregate.GlobalRegistry().Set(metadata.PdataUseProtoPoolingFeatureGate.ID(), prevPooling)) + }() + + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") + dict.StringTable().Append("pooled-value") + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_StringValueStrindex{ + StringValueStrindex: 1, + }, + } + + resolveAnyValueReference(dict, anyVal) + + strVal, ok := anyVal.Value.(*internal.AnyValue_StringValue) + assert.True(t, ok) + assert.Equal(t, "pooled-value", strVal.StringValue) +} + +func TestResolveAnyValueReferenceNestedKvList(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") + dict.StringTable().Append("nested-key") + dict.StringTable().Append("nested-value") + + kvList := &internal.KeyValueList{ + Values: []internal.KeyValue{ + { + KeyStrindex: 1, // references "nested-key" + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValueStrindex{ + StringValueStrindex: 2, // references "nested-value" + }, + }, + }, + }, + } + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_KvlistValue{ + KvlistValue: kvList, + }, + } + + resolveAnyValueReference(dict, anyVal) + + // Verify nested key_ref was resolved + assert.Equal(t, "nested-key", kvList.Values[0].Key) + + // Verify nested value was resolved + strVal, ok := kvList.Values[0].Value.Value.(*internal.AnyValue_StringValue) + assert.True(t, ok) + assert.Equal(t, "nested-value", strVal.StringValue) +} + +func TestResolveAnyValueReferenceNestedArray(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") + dict.StringTable().Append("array-item-1") + dict.StringTable().Append("array-item-2") + + arrVal := &internal.ArrayValue{ + Values: []internal.AnyValue{ + { + Value: &internal.AnyValue_StringValueStrindex{ + StringValueStrindex: 1, + }, + }, + { + Value: &internal.AnyValue_StringValueStrindex{ + StringValueStrindex: 2, + }, + }, + }, + } + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_ArrayValue{ + ArrayValue: arrVal, + }, + } + + resolveAnyValueReference(dict, anyVal) + + // Verify both array items were resolved + strVal1, ok := arrVal.Values[0].Value.(*internal.AnyValue_StringValue) + assert.True(t, ok) + assert.Equal(t, "array-item-1", strVal1.StringValue) + + strVal2, ok := arrVal.Values[1].Value.(*internal.AnyValue_StringValue) + assert.True(t, ok) + assert.Equal(t, "array-item-2", strVal2.StringValue) +} + +func TestConvertProfilesToReferencesEmpty(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") + + convertProfilesToReferences(profiles) + + // Should only have the initial empty string + assert.Equal(t, 1, dict.StringTable().Len()) +} + +func TestConvertProfilesToReferencesDeduplication(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") + + rp := profiles.ResourceProfiles().AppendEmpty() + rp.Resource().Attributes().PutStr("key1", "duplicated-value") + rp.Resource().Attributes().PutStr("key2", "duplicated-value") + rp.Resource().Attributes().PutStr("key3", "unique-value") + + convertProfilesToReferences(profiles) + + // Should have: "", "key1", "duplicated-value", "key2", "key3", "unique-value" + // But key1, key2, key3 might share indices if they're also deduplicated + // At minimum: "", "key1", "duplicated-value", "key2", "key3", "unique-value" = 6 + assert.GreaterOrEqual(t, dict.StringTable().Len(), 5) + + // Verify references were created + mapOrig := internal.GetMapOrig(internal.MapWrapper(rp.Resource().Attributes())) + for i := 0; i < len(*mapOrig); i++ { + kv := &(*mapOrig)[i] + assert.NotEqual(t, int32(0), kv.KeyStrindex, "Key should have a reference") + + // Values should be converted to StringValueStrindex + _, ok := kv.Value.Value.(*internal.AnyValue_StringValueStrindex) + assert.True(t, ok, "Value should be converted to StringValueStrindex") + } +} + +func TestConvertAnyValueToReferenceWithPooling(t *testing.T) { + prevPooling := metadata.PdataUseProtoPoolingFeatureGate.IsEnabled() + require.NoError(t, featuregate.GlobalRegistry().Set(metadata.PdataUseProtoPoolingFeatureGate.ID(), true)) + defer func() { + require.NoError(t, featuregate.GlobalRegistry().Set(metadata.PdataUseProtoPoolingFeatureGate.ID(), prevPooling)) + }() + + stringIndex := make(map[string]int32) + stringIndex["test-value"] = 5 + + getStringIndex := func(s string) int32 { + if idx, ok := stringIndex[s]; ok { + return idx + } + idx := int32(len(stringIndex)) + stringIndex[s] = idx + return idx + } + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_StringValue{ + StringValue: "test-value", + }, + } + + convertAnyValueToReference(getStringIndex, anyVal) + + refVal, ok := anyVal.Value.(*internal.AnyValue_StringValueStrindex) + assert.True(t, ok) + assert.Equal(t, int32(5), refVal.StringValueStrindex) +} + +func TestConvertAnyValueToReferenceEmptyString(t *testing.T) { + stringIndex := make(map[string]int32) + stringIndex[""] = 0 + + getStringIndex := func(s string) int32 { + if idx, ok := stringIndex[s]; ok { + return idx + } + idx := int32(len(stringIndex)) + stringIndex[s] = idx + return idx + } + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_StringValue{ + StringValue: "", // empty string should not be converted + }, + } + + convertAnyValueToReference(getStringIndex, anyVal) + + // Empty string should remain as StringValue, not converted to ref + _, ok := anyVal.Value.(*internal.AnyValue_StringValue) + assert.True(t, ok) +} + +func TestConvertAnyValueToReferenceNestedKvList(t *testing.T) { + stringIndex := make(map[string]int32) + stringIndex[""] = 0 + + counter := int32(1) + getStringIndex := func(s string) int32 { + if idx, ok := stringIndex[s]; ok { + return idx + } + idx := counter + counter++ + stringIndex[s] = idx + return idx + } + + kvList := &internal.KeyValueList{ + Values: []internal.KeyValue{ + { + Key: "nested-key", + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValue{ + StringValue: "nested-value", + }, + }, + }, + }, + } + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_KvlistValue{ + KvlistValue: kvList, + }, + } + + convertAnyValueToReference(getStringIndex, anyVal) + + // Verify nested key was converted + assert.NotEqual(t, int32(0), kvList.Values[0].KeyStrindex) + + // Verify nested value was converted + _, ok := kvList.Values[0].Value.Value.(*internal.AnyValue_StringValueStrindex) + assert.True(t, ok) +} + +func TestConvertAnyValueToReferenceNestedArray(t *testing.T) { + stringIndex := make(map[string]int32) + counter := int32(0) + + getStringIndex := func(s string) int32 { + if idx, ok := stringIndex[s]; ok { + return idx + } + idx := counter + counter++ + stringIndex[s] = idx + return idx + } + + arrVal := &internal.ArrayValue{ + Values: []internal.AnyValue{ + { + Value: &internal.AnyValue_StringValue{ + StringValue: "array-item", + }, + }, + }, + } + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_ArrayValue{ + ArrayValue: arrVal, + }, + } + + convertAnyValueToReference(getStringIndex, anyVal) + + // Verify array item was converted + _, ok := arrVal.Values[0].Value.(*internal.AnyValue_StringValueStrindex) + assert.True(t, ok) +} + +func TestConvertMapToReferencesEmptyKey(t *testing.T) { + profiles := NewProfiles() + rp := profiles.ResourceProfiles().AppendEmpty() + attrs := rp.Resource().Attributes() + + // Manually add a KeyValue with empty key + mapOrig := internal.GetMapOrig(internal.MapWrapper(attrs)) + *mapOrig = append(*mapOrig, internal.KeyValue{ + Key: "", // empty key should not be converted + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValue{ + StringValue: "value", + }, + }, + }) + + getStringIndex := func(_ string) int32 { + return 1 + } + + convertKeyValueToReferences(getStringIndex, mapKeyValues(attrs)) + + // Empty key should not have KeyStrindex set + kv := &(*mapOrig)[0] + assert.Equal(t, int32(0), kv.KeyStrindex) +} + +func TestConvertMapToReferencesExistingKeyRef(t *testing.T) { + profiles := NewProfiles() + rp := profiles.ResourceProfiles().AppendEmpty() + attrs := rp.Resource().Attributes() + + // Manually add a KeyValue with existing KeyStrindex + mapOrig := internal.GetMapOrig(internal.MapWrapper(attrs)) + *mapOrig = append(*mapOrig, internal.KeyValue{ + Key: "test-key", + KeyStrindex: 5, // already has a ref + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValue{ + StringValue: "value", + }, + }, + }) + + getStringIndex := func(_ string) int32 { + return 99 + } + + convertKeyValueToReferences(getStringIndex, mapKeyValues(attrs)) + + // KeyStrindex should remain unchanged + kv := &(*mapOrig)[0] + assert.Equal(t, int32(5), kv.KeyStrindex) +} + +func TestResolveAnyValueReferenceNonStringTypes(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") + + // Test with int value (should not be affected) + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_IntValue{ + IntValue: 42, + }, + } + + resolveAnyValueReference(dict, anyVal) + + // Should remain as IntValue + intVal, ok := anyVal.Value.(*internal.AnyValue_IntValue) + assert.True(t, ok) + assert.Equal(t, int64(42), intVal.IntValue) +} + +func TestConvertMapToReferencesClearsKey(t *testing.T) { + profiles := NewProfiles() + rp := profiles.ResourceProfiles().AppendEmpty() + attrs := rp.Resource().Attributes() + + mapOrig := internal.GetMapOrig(internal.MapWrapper(attrs)) + *mapOrig = append(*mapOrig, internal.KeyValue{ + Key: "my-key", + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValue{ + StringValue: "my-value", + }, + }, + }) + + getStringIndex := func(s string) int32 { + if s == "my-key" { + return 1 + } + return 2 + } + + convertKeyValueToReferences(getStringIndex, mapKeyValues(attrs)) + + kv := &(*mapOrig)[0] + // key_ref should be set + assert.Equal(t, int32(1), kv.KeyStrindex) + // key MUST NOT be set when key_ref is used (per proto spec) + assert.Empty(t, kv.Key, "Key must be cleared when KeyStrindex is set") +} + +func TestConvertAnyValueToReferenceNestedKvListClearsKey(t *testing.T) { + stringIndex := make(map[string]int32) + counter := int32(1) + getStringIndex := func(s string) int32 { + if idx, ok := stringIndex[s]; ok { + return idx + } + idx := counter + counter++ + stringIndex[s] = idx + return idx + } + + kvList := &internal.KeyValueList{ + Values: []internal.KeyValue{ + { + Key: "nested-key", + Value: internal.AnyValue{ + Value: &internal.AnyValue_StringValue{ + StringValue: "nested-value", + }, + }, + }, + }, + } + + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_KvlistValue{ + KvlistValue: kvList, + }, + } + + convertAnyValueToReference(getStringIndex, anyVal) + + // key_ref should be set + assert.NotEqual(t, int32(0), kvList.Values[0].KeyStrindex) + // key MUST NOT be set when key_ref is used (per proto spec) + assert.Empty(t, kvList.Values[0].Key, "Key must be cleared when KeyStrindex is set in nested kvlist") +} + +func TestConvertAnyValueToReferenceNonStringTypes(t *testing.T) { + getStringIndex := func(_ string) int32 { + return 0 + } + + // Test with bool value (should not be affected) + anyVal := &internal.AnyValue{ + Value: &internal.AnyValue_BoolValue{ + BoolValue: true, + }, + } + + convertAnyValueToReference(getStringIndex, anyVal) + + // Should remain as BoolValue + boolVal, ok := anyVal.Value.(*internal.AnyValue_BoolValue) + assert.True(t, ok) + assert.True(t, boolVal.BoolValue) +} diff --git a/pdata/pprofile/go.mod b/pdata/pprofile/go.mod index f673ff36739..1005bed519c 100644 --- a/pdata/pprofile/go.mod +++ b/pdata/pprofile/go.mod @@ -4,8 +4,10 @@ go 1.25.0 require ( github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/collector/featuregate v1.53.0 go.opentelemetry.io/collector/internal/testutil v0.147.0 go.opentelemetry.io/collector/pdata v1.53.0 + go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 go.uber.org/goleak v1.3.0 @@ -14,13 +16,13 @@ require ( ) require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/collector/featuregate v1.53.0 // indirect go.opentelemetry.io/proto/slim/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.51.0 // indirect diff --git a/pdata/pprofile/go.sum b/pdata/pprofile/go.sum index b82582e3bcf..2dbd849cc1a 100644 --- a/pdata/pprofile/go.sum +++ b/pdata/pprofile/go.sum @@ -38,16 +38,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= diff --git a/pdata/pprofile/json.go b/pdata/pprofile/json.go index 8b3fa3fac85..b691b81abc7 100644 --- a/pdata/pprofile/json.go +++ b/pdata/pprofile/json.go @@ -15,6 +15,9 @@ type JSONMarshaler struct{} // MarshalProfiles to the OTLP/JSON format. func (*JSONMarshaler) MarshalProfiles(pd Profiles) ([]byte, error) { + // Convert strings to references for efficient transmission + convertProfilesToReferences(pd) + dest := json.BorrowStream(nil) defer json.ReturnStream(dest) pd.getOrig().MarshalJSON(dest) @@ -37,5 +40,10 @@ func (*JSONUnmarshaler) UnmarshalProfiles(buf []byte) (Profiles, error) { return Profiles{}, iter.Error() } otlp.MigrateProfiles(pd.getOrig().ResourceProfiles) + + // Resolve all string_value_ref and key_ref to their actual strings + // so the pdata API works transparently + resolveProfilesReferences(pd) + return pd, nil } diff --git a/pdata/pprofile/json_references_test.go b/pdata/pprofile/json_references_test.go new file mode 100644 index 00000000000..191bbf0429b --- /dev/null +++ b/pdata/pprofile/json_references_test.go @@ -0,0 +1,87 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + stdjson "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newProfilesWithAttributes creates a Profiles with resource and scope +// attributes for testing reference conversion. +func newProfilesWithAttributes() Profiles { + profiles := NewProfiles() + profiles.Dictionary().StringTable().Append("") // index 0 + + rp := profiles.ResourceProfiles().AppendEmpty() + rp.Resource().Attributes().PutStr("service.name", "test-service") + rp.Resource().Attributes().PutStr("host.name", "test-host") + + sp := rp.ScopeProfiles().AppendEmpty() + sp.Scope().Attributes().PutStr("scope.attr", "scope-value") + + return profiles +} + +func TestJSONMarshalConvertsToReferences(t *testing.T) { + marshaler := JSONMarshaler{} + jsonBytes, err := marshaler.MarshalProfiles(newProfilesWithAttributes()) + require.NoError(t, err) + + // Parse the JSON output to verify references were used + var parsed map[string]any + require.NoError(t, stdjson.Unmarshal(jsonBytes, &parsed)) + + // The dictionary's stringTable should contain the attribute keys and values + dictionary, ok := parsed["dictionary"].(map[string]any) + require.True(t, ok, "JSON output should contain a dictionary object") + stringTable, ok := dictionary["stringTable"].([]any) + require.True(t, ok, "dictionary should contain a stringTable array") + + tableStrs := make([]string, len(stringTable)) + for i, v := range stringTable { + tableStrs[i], _ = v.(string) + } + assert.Contains(t, tableStrs, "service.name") + assert.Contains(t, tableStrs, "test-service") + assert.Contains(t, tableStrs, "host.name") + assert.Contains(t, tableStrs, "test-host") + assert.Contains(t, tableStrs, "scope.attr") + assert.Contains(t, tableStrs, "scope-value") +} + +func TestJSONUnmarshalResolvesReferences(t *testing.T) { + profiles := newProfilesWithAttributes() + + // Manually convert to references before marshaling, so the JSON output + // contains key_ref/string_value_ref regardless of whether the JSON + // marshaler itself calls convertProfilesToReferences. + convertProfilesToReferences(profiles) + + marshaler := JSONMarshaler{} + jsonBytes, err := marshaler.MarshalProfiles(profiles) + require.NoError(t, err) + + // Unmarshal and verify references were resolved + unmarshaler := JSONUnmarshaler{} + restored, err := unmarshaler.UnmarshalProfiles(jsonBytes) + require.NoError(t, err) + + rp := restored.ResourceProfiles().At(0) + serviceNameVal, ok := rp.Resource().Attributes().Get("service.name") + assert.True(t, ok, "service.name attribute should be accessible after JSON unmarshal") + assert.Equal(t, "test-service", serviceNameVal.Str()) + + hostNameVal, ok := rp.Resource().Attributes().Get("host.name") + assert.True(t, ok, "host.name attribute should be accessible after JSON unmarshal") + assert.Equal(t, "test-host", hostNameVal.Str()) + + sp := rp.ScopeProfiles().At(0) + scopeAttrVal, ok := sp.Scope().Attributes().Get("scope.attr") + assert.True(t, ok, "scope.attr should be accessible after JSON unmarshal") + assert.Equal(t, "scope-value", scopeAttrVal.Str()) +} diff --git a/pdata/pprofile/pb.go b/pdata/pprofile/pb.go index 58f4f179474..bfb25b04ed5 100644 --- a/pdata/pprofile/pb.go +++ b/pdata/pprofile/pb.go @@ -8,6 +8,9 @@ var _ MarshalSizer = (*ProtoMarshaler)(nil) type ProtoMarshaler struct{} func (e *ProtoMarshaler) MarshalProfiles(pd Profiles) ([]byte, error) { + // Convert strings to references for efficient transmission + convertProfilesToReferences(pd) + size := pd.getOrig().SizeProto() buf := make([]byte, size) _ = pd.getOrig().MarshalProto(buf) @@ -38,5 +41,10 @@ func (d *ProtoUnmarshaler) UnmarshalProfiles(buf []byte) (Profiles, error) { if err != nil { return Profiles{}, err } + + // Resolve all string_value_ref and key_ref to their actual strings + // so the pdata API works transparently + resolveProfilesReferences(pd) + return pd, nil } diff --git a/pdata/pprofile/pb_references_test.go b/pdata/pprofile/pb_references_test.go new file mode 100644 index 00000000000..ddbe43d4b5d --- /dev/null +++ b/pdata/pprofile/pb_references_test.go @@ -0,0 +1,196 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/pdata/internal" + "go.opentelemetry.io/collector/pdata/pcommon" +) + +func TestMarshalUnmarshalWithReferences(t *testing.T) { + profiles := NewProfiles() + + dict := profiles.Dictionary() + dict.StringTable().Append("") // index 0, required empty string + + rp := profiles.ResourceProfiles().AppendEmpty() + rp.Resource().Attributes().PutStr("service.name", "test-service") + rp.Resource().Attributes().PutStr("host.name", "test-host") + + sp := rp.ScopeProfiles().AppendEmpty() + sp.Scope().SetName("test-scope") + sp.Scope().Attributes().PutStr("scope.attr", "scope-value") + + profile := sp.Profiles().AppendEmpty() + profile.SetProfileID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + + // Marshal to proto bytes + marshaler := ProtoMarshaler{} + bytes, err := marshaler.MarshalProfiles(profiles) + require.NoError(t, err) + require.NotEmpty(t, bytes) + + // Verify that string table was populated (should have more than just the empty string) + assert.Greater(t, dict.StringTable().Len(), 1, "String table should be populated during marshal") + + // Verify references were created in the resource attributes + mapOrig := internal.GetMapOrig(internal.MapWrapper(rp.Resource().Attributes())) + foundRef := false + for i := 0; i < len(*mapOrig); i++ { + kv := (*mapOrig)[i] + if kv.KeyStrindex != 0 { + foundRef = true + break + } + // Check if value is a string reference + if ref, ok := kv.Value.Value.(*internal.AnyValue_StringValueStrindex); ok && ref.StringValueStrindex != 0 { + foundRef = true + break + } + } + assert.True(t, foundRef, "At least one reference should be created in attributes") + + // Unmarshal from proto bytes + unmarshaler := ProtoUnmarshaler{} + profiles2, err := unmarshaler.UnmarshalProfiles(bytes) + require.NoError(t, err) + + // Verify that the API works correctly - attributes should be accessible as strings + rp2 := profiles2.ResourceProfiles().At(0) + serviceNameVal, ok := rp2.Resource().Attributes().Get("service.name") + assert.True(t, ok, "service.name attribute should exist") + assert.Equal(t, "test-service", serviceNameVal.Str(), "service.name should be resolved to string") + + hostNameVal, ok := rp2.Resource().Attributes().Get("host.name") + assert.True(t, ok, "host.name attribute should exist") + assert.Equal(t, "test-host", hostNameVal.Str(), "host.name should be resolved to string") + + sp2 := rp2.ScopeProfiles().At(0) + scopeAttrVal, ok := sp2.Scope().Attributes().Get("scope.attr") + assert.True(t, ok, "scope.attr attribute should exist") + assert.Equal(t, "scope-value", scopeAttrVal.Str(), "scope.attr should be resolved to string") + + // Verify the string table is preserved + dict2 := profiles2.Dictionary() + assert.Greater(t, dict2.StringTable().Len(), 1, "String table should be preserved after unmarshal") +} + +func TestMarshalUnmarshalNestedValues(t *testing.T) { + profiles := NewProfiles() + dict := profiles.Dictionary() + dict.StringTable().Append("") // index 0 + + rp := profiles.ResourceProfiles().AppendEmpty() + attrs := rp.Resource().Attributes() + + kvlist := attrs.PutEmptyMap("nested.map") + kvlist.PutStr("inner.key1", "inner.value1") + kvlist.PutStr("inner.key2", "inner.value2") + + arr := attrs.PutEmptySlice("string.array") + arr.AppendEmpty().SetStr("string1") + arr.AppendEmpty().SetStr("string2") + arr.AppendEmpty().SetStr("string3") + + // Marshal and unmarshal + marshaler := ProtoMarshaler{} + bytes, err := marshaler.MarshalProfiles(profiles) + require.NoError(t, err) + + unmarshaler := ProtoUnmarshaler{} + profiles2, err := unmarshaler.UnmarshalProfiles(bytes) + require.NoError(t, err) + + // Verify nested map values are accessible + rp2 := profiles2.ResourceProfiles().At(0) + kvlist2, ok := rp2.Resource().Attributes().Get("nested.map") + assert.True(t, ok) + assert.Equal(t, pcommon.ValueTypeMap, kvlist2.Type()) + + innerMap := kvlist2.Map() + innerVal1, ok := innerMap.Get("inner.key1") + assert.True(t, ok) + assert.Equal(t, "inner.value1", innerVal1.Str()) + + innerVal2, ok := innerMap.Get("inner.key2") + assert.True(t, ok) + assert.Equal(t, "inner.value2", innerVal2.Str()) + + // Verify array values are accessible + arr2, ok := rp2.Resource().Attributes().Get("string.array") + assert.True(t, ok) + assert.Equal(t, pcommon.ValueTypeSlice, arr2.Type()) + + slice := arr2.Slice() + assert.Equal(t, 3, slice.Len()) + assert.Equal(t, "string1", slice.At(0).Str()) + assert.Equal(t, "string2", slice.At(1).Str()) + assert.Equal(t, "string3", slice.At(2).Str()) +} + +func TestRoundTripWithReferences(t *testing.T) { + original := NewProfiles() + dict := original.Dictionary() + dict.StringTable().Append("") + + for i := range 3 { + rp := original.ResourceProfiles().AppendEmpty() + rp.Resource().Attributes().PutStr("resource.id", "resource-"+string(rune('A'+i))) + + for j := range 2 { + sp := rp.ScopeProfiles().AppendEmpty() + sp.Scope().SetName("scope-" + string(rune('X'+j))) + sp.Scope().Attributes().PutStr("scope.version", "1.0.0") + + profile := sp.Profiles().AppendEmpty() + profile.SetProfileID([16]byte{byte(i), byte(j)}) + } + } + + // Marshal + marshaler := ProtoMarshaler{} + bytes, err := marshaler.MarshalProfiles(original) + require.NoError(t, err) + + // Unmarshal + unmarshaler := ProtoUnmarshaler{} + restored, err := unmarshaler.UnmarshalProfiles(bytes) + require.NoError(t, err) + + // Verify structure is preserved + assert.Equal(t, 3, restored.ResourceProfiles().Len()) + + for i := range 3 { + rp := restored.ResourceProfiles().At(i) + resourceID, ok := rp.Resource().Attributes().Get("resource.id") + assert.True(t, ok) + assert.Equal(t, "resource-"+string(rune('A'+i)), resourceID.Str()) + + assert.Equal(t, 2, rp.ScopeProfiles().Len()) + for j := range 2 { + sp := rp.ScopeProfiles().At(j) + assert.Equal(t, "scope-"+string(rune('X'+j)), sp.Scope().Name()) + + scopeVersion, ok := sp.Scope().Attributes().Get("scope.version") + assert.True(t, ok) + assert.Equal(t, "1.0.0", scopeVersion.Str()) + + assert.Equal(t, 1, sp.Profiles().Len()) + } + } + + // Verify the string table deduplication worked + // We should have fewer strings than if everything was duplicated + dictRestored := restored.Dictionary() + // At minimum we have: "", "resource.id", "resource-A", "resource-B", "resource-C", + // "scope.version", "1.0.0" = 7 entries + // May have more due to scope names + assert.LessOrEqual(t, dictRestored.StringTable().Len(), 7, + "String table should deduplicate strings efficiently") +} diff --git a/pdata/pprofile/pb_test.go b/pdata/pprofile/pb_test.go index c40dabc0fd2..7464db127b8 100644 --- a/pdata/pprofile/pb_test.go +++ b/pdata/pprofile/pb_test.go @@ -4,10 +4,13 @@ package pprofile import ( + "fmt" + "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + semconv "go.opentelemetry.io/otel/semconv/v1.38.0" gootlpprofiles "go.opentelemetry.io/proto/slim/otlp/profiles/v1development" goproto "google.golang.org/protobuf/proto" ) @@ -42,9 +45,18 @@ func TestProfilesProtoWireCompatibility(t *testing.T) { td2, err = unmarshaler.UnmarshalProfiles(wire2) require.NoError(t, err) - // Now compare that the original and final ProtoBuf messages are the same. - // This proves that goproto and gogoproto marshaling/unmarshaling are wire compatible. - assert.Equal(t, td, td2) + // After unmarshal, td2 will have resolved references (strings instead of string_value_ref/key_ref) + // while td may have references. Marshal td2 again to verify wire compatibility. + wire3, err := marshaler.MarshalProfiles(td2) + require.NoError(t, err) + + // Verify full round-trip fidelity: unmarshal both wire1 and wire3 into goproto + // messages and compare them semantically. This ensures all data (attributes, + // dictionary, profiles, etc.) survives the round-trip through both libraries. + var check1, check2 gootlpprofiles.ProfilesData + require.NoError(t, goproto.Unmarshal(wire1, &check1)) + require.NoError(t, goproto.Unmarshal(wire3, &check2)) + assert.True(t, goproto.Equal(&check1, &check2), "round-trip through goproto did not preserve profile data") } func TestProtoProfilesUnmarshalerError(t *testing.T) { @@ -75,6 +87,8 @@ func BenchmarkProfilesToProto(b *testing.B) { marshaler := &ProtoMarshaler{} profiles := generateBenchmarkProfiles(128) + b.ResetTimer() + b.ReportAllocs() for b.Loop() { buf, err := marshaler.MarshalProfiles(profiles) require.NoError(b, err) @@ -90,6 +104,7 @@ func BenchmarkProfilesFromProto(b *testing.B) { require.NoError(b, err) assert.NotEmpty(b, buf) + b.ResetTimer() b.ReportAllocs() for b.Loop() { profiles, err := unmarshaler.UnmarshalProfiles(buf) @@ -108,3 +123,213 @@ func generateBenchmarkProfiles(samplesCount int) Profiles { } return md } + +// generateProfiles creates a Profiles object with the specified number of resources, scopes, profiles, and samples. +func generateProfiles(b *testing.B, resourceCount, scopeCount, profileCount, sampleCount int) Profiles { + b.Helper() + + profiles := NewProfiles() + dict := profiles.Dictionary() + + // Pre-populate dictionary with common strings + dict.StringTable().Append("") // Index 0 is always empty string + dict.StringTable().Append("cpu") + dict.StringTable().Append("nanoseconds") + dict.StringTable().Append("samples") + dict.StringTable().Append("count") + + // Generate resource profiles + for r := range resourceCount { + rp := profiles.ResourceProfiles().AppendEmpty() + rp.SetSchemaUrl(semconv.SchemaURL) + resource := rp.Resource() + + // Add resource attributes + attrs := resource.Attributes() + attrs.PutStr(string(semconv.ServiceNameKey), fmt.Sprintf("service-%d", r)) + attrs.PutStr(string(semconv.ServiceVersionKey), fmt.Sprintf("version-%d", r)) + attrs.PutStr(string(semconv.ProcessPIDKey), strconv.Itoa(1000+r)) + attrs.PutStr(string(semconv.K8SPodNameKey), fmt.Sprintf("pod-%d", r%10)) + attrs.PutStr(string(semconv.K8SNamespaceNameKey), "default") + attrs.PutStr(string(semconv.TelemetrySDKNameKey), "opentelemetry") + + // Generate scope profiles + for s := range scopeCount { + sp := rp.ScopeProfiles().AppendEmpty() + sp.SetSchemaUrl(semconv.SchemaURL) + scope := sp.Scope() + scope.SetName(fmt.Sprintf("profiler-scope-%d", s)) + scope.SetVersion("1.0.0") + + // Generate profiles + for range profileCount { + profile := sp.Profiles().AppendEmpty() + + // Add sample types + sampleType := profile.SampleType() + sampleType.SetTypeStrindex(1) // "cpu" + sampleType.SetUnitStrindex(2) // "nanoseconds" + + // Add period type + periodType := profile.PeriodType() + periodType.SetTypeStrindex(1) // "cpu" + periodType.SetUnitStrindex(2) // "nanoseconds" + profile.SetPeriod(1000000) + + // Generate samples + samples := profile.Samples() + for i := range sampleCount { + sample := samples.AppendEmpty() + sample.SetStackIndex(int32(i % 100)) + + // Add attribute indices for samples + sample.AttributeIndices().Append(int32(i % 10)) + } + } + } + } + + return profiles +} + +func BenchmarkUnmarshalProfiles(b *testing.B) { + testCases := []struct { + name string + resourceCount int + scopeCount int + profileCount int + sampleCount int + }{ + { + name: "small", + resourceCount: 1, + scopeCount: 1, + profileCount: 1, + sampleCount: 100, + }, + { + name: "medium", + resourceCount: 5, + scopeCount: 2, + profileCount: 2, + sampleCount: 500, + }, + { + name: "large", + resourceCount: 20, + scopeCount: 3, + profileCount: 5, + sampleCount: 1000, + }, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + // Generate profile data and marshal it + profiles := generateProfiles(b, tc.resourceCount, tc.scopeCount, tc.profileCount, tc.sampleCount) + marshaler := &ProtoMarshaler{} + data, err := marshaler.MarshalProfiles(profiles) + if err != nil { + b.Fatalf("failed to marshal profiles: %v", err) + } + + unmarshaler := &ProtoUnmarshaler{} + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + profiles, err := unmarshaler.UnmarshalProfiles(data) + if err != nil { + b.Fatalf("failed to unmarshal: %v", err) + } + _ = profiles + } + }) + } +} + +func BenchmarkMarshalProfiles(b *testing.B) { + testCases := []struct { + name string + resourceCount int + scopeCount int + profileCount int + sampleCount int + }{ + { + name: "small", + resourceCount: 1, + scopeCount: 1, + profileCount: 1, + sampleCount: 100, + }, + { + name: "medium", + resourceCount: 5, + scopeCount: 2, + profileCount: 2, + sampleCount: 500, + }, + { + name: "large", + resourceCount: 20, + scopeCount: 3, + profileCount: 5, + sampleCount: 1000, + }, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + marshaler := &ProtoMarshaler{} + + // with_refs: simulate the normal ingest path where data was + // received on the wire (refs present), then unmarshaled (refs + // resolved but KeyRef kept), and is now being re-marshaled + // without any attribute modifications. + b.Run("with_refs", func(b *testing.B) { + profiles := generateProfiles(b, tc.resourceCount, tc.scopeCount, tc.profileCount, tc.sampleCount) + unmarshaler := &ProtoUnmarshaler{} + buf, err := marshaler.MarshalProfiles(profiles) + if err != nil { + b.Fatalf("failed to marshal: %v", err) + } + profiles, err = unmarshaler.UnmarshalProfiles(buf) + if err != nil { + b.Fatalf("failed to unmarshal: %v", err) + } + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + buf, err := marshaler.MarshalProfiles(profiles) + if err != nil { + b.Fatalf("failed to marshal: %v", err) + } + _ = buf + } + }) + + // without_refs: each iteration gets a fresh copy with no refs, + // simulating data that was constructed or had attributes modified. + b.Run("without_refs", func(b *testing.B) { + copies := make([]Profiles, b.N) + for i := range copies { + copies[i] = generateProfiles(b, tc.resourceCount, tc.scopeCount, tc.profileCount, tc.sampleCount) + } + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + buf, err := marshaler.MarshalProfiles(copies[i]) + if err != nil { + b.Fatalf("failed to marshal: %v", err) + } + _ = buf + } + }) + }) + } +} diff --git a/pdata/testdata/go.sum b/pdata/testdata/go.sum index 337f2f5d6b1..fb9006591be 100644 --- a/pdata/testdata/go.sum +++ b/pdata/testdata/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -18,6 +20,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ=