From 5300a788c23703eb46b6b8bc4bfba7eebc25cae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 07:40:49 +0100 Subject: [PATCH 1/8] pdata/pprofile: fix Key not cleared after conversion to KeyRef Per the proto spec [1], key and key_ref are mutually exclusive on the wire. Without clearing Key after setting KeyRef, both fields would be serialized, violating the MUST NOT constraint and wasting bytes. [1] https://github.com/open-telemetry/opentelemetry-proto/pull/733 --- pdata/pprofile/dictionary_helpers.go | 2 + pdata/pprofile/dictionary_helpers_test.go | 71 +++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/pdata/pprofile/dictionary_helpers.go b/pdata/pprofile/dictionary_helpers.go index cd3e6c3a59b..6d496036a7d 100644 --- a/pdata/pprofile/dictionary_helpers.go +++ b/pdata/pprofile/dictionary_helpers.go @@ -253,6 +253,7 @@ func convertMapToReferences(getStringIndex func(string) int32, m pcommon.Map) { // Convert key to reference if kv.Key != "" && kv.KeyRef == 0 { kv.KeyRef = getStringIndex(kv.Key) + kv.Key = "" } // Convert string values to references @@ -284,6 +285,7 @@ func convertAnyValueToReference(getStringIndex func(string) int32, anyValue *int kv := &kvList.KvlistValue.Values[i] if kv.Key != "" && kv.KeyRef == 0 { kv.KeyRef = getStringIndex(kv.Key) + kv.Key = "" } convertAnyValueToReference(getStringIndex, &kv.Value) } diff --git a/pdata/pprofile/dictionary_helpers_test.go b/pdata/pprofile/dictionary_helpers_test.go index b09a7235ba8..ce6318b385f 100644 --- a/pdata/pprofile/dictionary_helpers_test.go +++ b/pdata/pprofile/dictionary_helpers_test.go @@ -441,6 +441,77 @@ func TestResolveAnyValueReferenceNonStringTypes(t *testing.T) { 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 + } + + convertMapToReferences(getStringIndex, attrs) + + kv := &(*mapOrig)[0] + // key_ref should be set + assert.Equal(t, int32(1), kv.KeyRef) + // key MUST NOT be set when key_ref is used (per proto spec) + assert.Equal(t, "", kv.Key, "Key must be cleared when KeyRef 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].KeyRef) + // key MUST NOT be set when key_ref is used (per proto spec) + assert.Equal(t, "", kvList.Values[0].Key, "Key must be cleared when KeyRef is set in nested kvlist") +} + func TestConvertAnyValueToReferenceNonStringTypes(t *testing.T) { getStringIndex := func(_ string) int32 { return 0 From bad7c6803eae0c2db89fd15249efcd189ecd58bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 07:27:54 +0100 Subject: [PATCH 2/8] pdata/pprofile: refactor to make extra predicate redundant --- pdata/pprofile/dictionary_helpers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pdata/pprofile/dictionary_helpers.go b/pdata/pprofile/dictionary_helpers.go index 6d496036a7d..380b93a1dc6 100644 --- a/pdata/pprofile/dictionary_helpers.go +++ b/pdata/pprofile/dictionary_helpers.go @@ -98,9 +98,9 @@ func resolveMapReferences(dict ProfilesDictionary, m pcommon.Map) { kv := &(*mapOrig)[i] // Resolve key_ref if set - if kv.KeyRef != 0 { + if kv.KeyRef >= 0 { idx := int(kv.KeyRef) - if idx >= 0 && idx < dict.StringTable().Len() { + if idx < dict.StringTable().Len() { kv.Key = dict.StringTable().At(idx) // Keep ref set for potential re-marshaling } @@ -129,9 +129,9 @@ func resolveAnyValueReference(dict ProfilesDictionary, anyValue *internal.AnyVal } else if kvList, ok := anyValue.Value.(*internal.AnyValue_KvlistValue); ok && kvList.KvlistValue != nil { for i := 0; i < len(kvList.KvlistValue.Values); i++ { kv := &kvList.KvlistValue.Values[i] - if kv.KeyRef != 0 { + if kv.KeyRef >= 0 { idx := int(kv.KeyRef) - if idx >= 0 && idx < dict.StringTable().Len() { + if idx < dict.StringTable().Len() { kv.Key = dict.StringTable().At(idx) } } From 2a5504b9de4bed908b133ad1357741bc6cd4f442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 06:44:45 +0100 Subject: [PATCH 3/8] pdata/pprofile: remove flawed optimizations OTLP profiles received are expected to use dictionary encodings on the wire. Optimizing the unmarshal performance for payloads without dictionary encoding is therefor targetting a scenario that is unlikely to be encountered in the wild. More importantly, the optimization makes the implicit assumptions that payloads will always either use dictionary encoding for all resource attributes or regular encoding. This is currently currently not being proposed [1], and would seem overly pedantic to enforce. Meanwhile there is the risk of false positives, so completely remove the optimization. Remove the marshal optimization for now as well, but see follow-up commit for restoring it in a different way. [1] https://github.com/open-telemetry/opentelemetry-proto/pull/733 --- pdata/pprofile/dictionary_helpers.go | 126 ----- pdata/pprofile/dictionary_helpers_test.go | 587 ---------------------- 2 files changed, 713 deletions(-) diff --git a/pdata/pprofile/dictionary_helpers.go b/pdata/pprofile/dictionary_helpers.go index 380b93a1dc6..fb699b11dc8 100644 --- a/pdata/pprofile/dictionary_helpers.go +++ b/pdata/pprofile/dictionary_helpers.go @@ -14,25 +14,6 @@ import ( func resolveProfilesReferences(profiles Profiles) { dict := profiles.Dictionary() - // Quick check: if there are no resource profiles, nothing to do - if profiles.ResourceProfiles().Len() == 0 { - return - } - - // Check if resolution is needed by sampling first resource - rp := profiles.ResourceProfiles().At(0) - if !needsResolution(rp.Resource().Attributes()) { - // Check scope attributes too - if rp.ScopeProfiles().Len() == 0 { - return - } - sp := rp.ScopeProfiles().At(0) - if !needsResolution(sp.Scope().Attributes()) { - // Already resolved, skip - return - } - } - // Resolve references in resource attributes for i := 0; i < profiles.ResourceProfiles().Len(); i++ { rp := profiles.ResourceProfiles().At(i) @@ -46,50 +27,6 @@ func resolveProfilesReferences(profiles Profiles) { } } -// needsResolution checks if a map has any refs that need resolution -func needsResolution(m pcommon.Map) bool { - if m.Len() == 0 { - return false - } - mapOrig := internal.GetMapOrig(internal.MapWrapper(m)) - for i := 0; i < len(*mapOrig); i++ { - kv := &(*mapOrig)[i] - // If KeyRef is set, needs resolution - if kv.KeyRef != 0 { - return true - } - // Check if any values need resolution - if anyValueNeedsResolution(&kv.Value) { - return true - } - } - return false -} - -// anyValueNeedsResolution checks if an AnyValue has refs that need resolution -func anyValueNeedsResolution(anyValue *internal.AnyValue) bool { - if ref, ok := anyValue.Value.(*internal.AnyValue_StringValueRef); ok && ref.StringValueRef != 0 { - return true - } else if kvList, ok := anyValue.Value.(*internal.AnyValue_KvlistValue); ok && kvList.KvlistValue != nil { - for i := 0; i < len(kvList.KvlistValue.Values); i++ { - kv := &kvList.KvlistValue.Values[i] - if kv.KeyRef != 0 { - return true - } - if anyValueNeedsResolution(&kv.Value) { - return true - } - } - } else if arrVal, ok := anyValue.Value.(*internal.AnyValue_ArrayValue); ok && arrVal.ArrayValue != nil { - for i := 0; i < len(arrVal.ArrayValue.Values); i++ { - if anyValueNeedsResolution(&arrVal.ArrayValue.Values[i]) { - return true - } - } - } - return false -} - // resolveMapReferences resolves all string_value_ref and key_ref in a map func resolveMapReferences(dict ProfilesDictionary, m pcommon.Map) { mapOrig := internal.GetMapOrig(internal.MapWrapper(m)) @@ -151,25 +88,6 @@ func convertProfilesToReferences(profiles Profiles) { dict := profiles.Dictionary() stringTable := dict.StringTable() - // Quick check: if there are no resource profiles, nothing to do - if profiles.ResourceProfiles().Len() == 0 { - return - } - - // Check if conversion is needed by sampling first resource - rp := profiles.ResourceProfiles().At(0) - if !needsConversion(rp.Resource().Attributes()) { - // Check scope attributes too - if rp.ScopeProfiles().Len() == 0 { - return - } - sp := rp.ScopeProfiles().At(0) - if !needsConversion(sp.Scope().Attributes()) { - // Already converted, skip - return - } - } - // Map for quick string lookups - only allocate if needed stringIndex := make(map[string]int32, stringTable.Len()) for i := 0; i < stringTable.Len(); i++ { @@ -199,50 +117,6 @@ func convertProfilesToReferences(profiles Profiles) { } } -// needsConversion checks if a map has any string values that need conversion to refs -func needsConversion(m pcommon.Map) bool { - if m.Len() == 0 { - return false - } - mapOrig := internal.GetMapOrig(internal.MapWrapper(m)) - for i := 0; i < len(*mapOrig); i++ { - kv := &(*mapOrig)[i] - // If KeyRef is not set but Key is, needs conversion - if kv.Key != "" && kv.KeyRef == 0 { - return true - } - // Check if any string values need conversion - if anyValueNeedsConversion(&kv.Value) { - return true - } - } - return false -} - -// anyValueNeedsConversion checks if an AnyValue has string values that need conversion -func anyValueNeedsConversion(anyValue *internal.AnyValue) bool { - if strVal, ok := anyValue.Value.(*internal.AnyValue_StringValue); ok && strVal.StringValue != "" { - return true - } else if kvList, ok := anyValue.Value.(*internal.AnyValue_KvlistValue); ok && kvList.KvlistValue != nil { - for i := 0; i < len(kvList.KvlistValue.Values); i++ { - kv := &kvList.KvlistValue.Values[i] - if kv.Key != "" && kv.KeyRef == 0 { - return true - } - if anyValueNeedsConversion(&kv.Value) { - return true - } - } - } else if arrVal, ok := anyValue.Value.(*internal.AnyValue_ArrayValue); ok && arrVal.ArrayValue != nil { - for i := 0; i < len(arrVal.ArrayValue.Values); i++ { - if anyValueNeedsConversion(&arrVal.ArrayValue.Values[i]) { - return true - } - } - } - return false -} - // convertMapToReferences converts string keys and values to references func convertMapToReferences(getStringIndex func(string) int32, m pcommon.Map) { mapOrig := internal.GetMapOrig(internal.MapWrapper(m)) diff --git a/pdata/pprofile/dictionary_helpers_test.go b/pdata/pprofile/dictionary_helpers_test.go index ce6318b385f..49934513f15 100644 --- a/pdata/pprofile/dictionary_helpers_test.go +++ b/pdata/pprofile/dictionary_helpers_test.go @@ -531,590 +531,3 @@ func TestConvertAnyValueToReferenceNonStringTypes(t *testing.T) { assert.True(t, ok) assert.True(t, boolVal.BoolValue) } - -func TestNeedsResolution(t *testing.T) { - t.Run("empty map", func(t *testing.T) { - profiles := NewProfiles() - rp := profiles.ResourceProfiles().AppendEmpty() - attrs := rp.Resource().Attributes() - assert.False(t, needsResolution(attrs)) - }) - - t.Run("map with KeyRef set", func(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: "test-key", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "value", - }, - }, - }) - - assert.True(t, needsResolution(attrs)) - }) - - t.Run("map with StringValueRef in value", func(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: "test-key", - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }) - - assert.True(t, needsResolution(attrs)) - }) - - t.Run("map with no refs", func(t *testing.T) { - profiles := NewProfiles() - rp := profiles.ResourceProfiles().AppendEmpty() - attrs := rp.Resource().Attributes() - attrs.PutStr("key", "value") - - assert.False(t, needsResolution(attrs)) - }) - - t.Run("map with nested KvList with KeyRef", func(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: "test-key", - Value: internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "nested-key", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "nested-value", - }, - }, - }, - }, - }, - }, - }, - }) - - assert.True(t, needsResolution(attrs)) - }) - - t.Run("map with nested array with StringValueRef", func(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: "test-key", - Value: internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: &internal.ArrayValue{ - Values: []internal.AnyValue{ - { - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }, - }, - }, - }, - }) - - assert.True(t, needsResolution(attrs)) - }) -} - -func TestAnyValueNeedsResolution(t *testing.T) { - t.Run("StringValueRef with non-zero ref", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - } - assert.True(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("StringValueRef with zero ref", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 0, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("StringValue no ref", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "test", - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("IntValue", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_IntValue{ - IntValue: 42, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("BoolValue", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_BoolValue{ - BoolValue: true, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("DoubleValue", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_DoubleValue{ - DoubleValue: 3.14, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("BytesValue", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_BytesValue{ - BytesValue: []byte{1, 2, 3}, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("KvList with KeyRef", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "value", - }, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("KvList with StringValueRef in nested value", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("KvList with no refs", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "value", - }, - }, - }, - }, - }, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("KvList nil", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: nil, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("Array with StringValueRef", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: &internal.ArrayValue{ - Values: []internal.AnyValue{ - { - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("Array with no refs", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: &internal.ArrayValue{ - Values: []internal.AnyValue{ - { - Value: &internal.AnyValue_StringValue{ - StringValue: "test", - }, - }, - }, - }, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("Array nil", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: nil, - }, - } - assert.False(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("nested array within kvlist", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - Value: internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: &internal.ArrayValue{ - Values: []internal.AnyValue{ - { - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsResolution(anyVal)) - }) - - t.Run("deeply nested kvlist", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "level1", - Value: internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "level2", - KeyRef: 5, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "value", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsResolution(anyVal)) - }) -} - -func TestNeedsConversion(t *testing.T) { - t.Run("empty map", func(t *testing.T) { - profiles := NewProfiles() - rp := profiles.ResourceProfiles().AppendEmpty() - attrs := rp.Resource().Attributes() - assert.False(t, needsConversion(attrs)) - }) - - t.Run("map with key but no KeyRef", func(t *testing.T) { - profiles := NewProfiles() - rp := profiles.ResourceProfiles().AppendEmpty() - attrs := rp.Resource().Attributes() - attrs.PutStr("key", "value") - - assert.True(t, needsConversion(attrs)) - }) - - t.Run("map with KeyRef already set", func(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: "test-key", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }) - - assert.False(t, needsConversion(attrs)) - }) - - t.Run("map with StringValue needs conversion", func(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: "test-key", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "needs-conversion", - }, - }, - }) - - assert.True(t, needsConversion(attrs)) - }) -} - -func TestAnyValueNeedsConversion(t *testing.T) { - t.Run("StringValue with non-empty string", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "test", - }, - } - assert.True(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("StringValue with empty string", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "", - }, - } - assert.False(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("StringValueRef already converted", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - } - assert.False(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("IntValue", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_IntValue{ - IntValue: 42, - }, - } - assert.False(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("KvList with key but no KeyRef", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - KeyRef: 0, - Value: internal.AnyValue{ - Value: &internal.AnyValue_IntValue{ - IntValue: 1, - }, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("KvList with StringValue in nested value", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValue{ - StringValue: "needs-conversion", - }, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("KvList already converted", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }, - }, - }, - }, - } - assert.False(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("KvList nil", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: nil, - }, - } - assert.False(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("Array with StringValue", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: &internal.ArrayValue{ - Values: []internal.AnyValue{ - { - Value: &internal.AnyValue_StringValue{ - StringValue: "needs-conversion", - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("Array already converted", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: &internal.ArrayValue{ - Values: []internal.AnyValue{ - { - Value: &internal.AnyValue_StringValueRef{ - StringValueRef: 1, - }, - }, - }, - }, - }, - } - assert.False(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("Array nil", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: nil, - }, - } - assert.False(t, anyValueNeedsConversion(anyVal)) - }) - - t.Run("nested structures needing conversion", func(t *testing.T) { - anyVal := &internal.AnyValue{ - Value: &internal.AnyValue_KvlistValue{ - KvlistValue: &internal.KeyValueList{ - Values: []internal.KeyValue{ - { - Key: "test", - KeyRef: 1, - Value: internal.AnyValue{ - Value: &internal.AnyValue_ArrayValue{ - ArrayValue: &internal.ArrayValue{ - Values: []internal.AnyValue{ - { - Value: &internal.AnyValue_StringValue{ - StringValue: "deep-value", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - assert.True(t, anyValueNeedsConversion(anyVal)) - }) -} From 4cb2c3d08a4893cff619d96b2e4856d93ac49ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 07:49:37 +0100 Subject: [PATCH 4/8] pdata/pprofile: optimize MarshalProfiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimize MarshalProfiles for the case where a profile doesn't contain any non-dictionary references. This is the case for profiles that goos: darwin goarch: arm64 pkg: go.opentelemetry.io/collector/pdata/pprofile cpu: Apple M1 Max │ /tmp/before.txt │ /tmp/after.txt │ │ sec/op │ sec/op vs base │ MarshalProfiles/small-10 2.024µ ± 4% 1.686µ ± 6% -16.72% (p=0.002 n=6) MarshalProfiles/medium-10 142.3µ ± 5% 143.9µ ± 4% ~ (p=0.132 n=6) MarshalProfiles/large-10 4.212m ± 2% 4.237m ± 8% ~ (p=0.240 n=6) geomean 106.7µ 100.9µ -5.39% │ /tmp/before.txt │ /tmp/after.txt │ │ B/op │ B/op vs base │ MarshalProfiles/small-10 2.039Ki ± 0% 1.125Ki ± 0% -44.83% (p=0.002 n=6) MarshalProfiles/medium-10 73.79Ki ± 0% 72.00Ki ± 0% -2.42% (p=0.002 n=6) MarshalProfiles/large-10 2.019Mi ± 0% 2.016Mi ± 0% -0.16% (p=0.002 n=6) geomean 67.76Ki 55.09Ki -18.70% │ /tmp/before.txt │ /tmp/after.txt │ │ allocs/op │ allocs/op vs base │ MarshalProfiles/small-10 4.000 ± 0% 1.000 ± 0% -75.00% (p=0.002 n=6) MarshalProfiles/medium-10 4.000 ± 0% 1.000 ± 0% -75.00% (p=0.002 n=6) MarshalProfiles/large-10 4.000 ± 0% 1.000 ± 0% -75.00% (p=0.002 n=6) geomean 4.000 1.000 -75.00% --- pdata/pprofile/dictionary_helpers.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pdata/pprofile/dictionary_helpers.go b/pdata/pprofile/dictionary_helpers.go index fb699b11dc8..e7b60512503 100644 --- a/pdata/pprofile/dictionary_helpers.go +++ b/pdata/pprofile/dictionary_helpers.go @@ -89,12 +89,15 @@ func convertProfilesToReferences(profiles Profiles) { stringTable := dict.StringTable() // Map for quick string lookups - only allocate if needed - stringIndex := make(map[string]int32, stringTable.Len()) - for i := 0; i < stringTable.Len(); i++ { - stringIndex[stringTable.At(i)] = int32(i) - } - + 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 } From 3c02751a4bfc72ada67674a2a65fd52329f37dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 08:23:27 +0100 Subject: [PATCH 5/8] pdata/pprofile: refactor some code duplication --- pdata/pprofile/dictionary_helpers.go | 60 +++++++++-------------- pdata/pprofile/dictionary_helpers_test.go | 6 +-- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/pdata/pprofile/dictionary_helpers.go b/pdata/pprofile/dictionary_helpers.go index e7b60512503..0a59933c168 100644 --- a/pdata/pprofile/dictionary_helpers.go +++ b/pdata/pprofile/dictionary_helpers.go @@ -8,6 +8,11 @@ import ( "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. @@ -17,32 +22,30 @@ func resolveProfilesReferences(profiles Profiles) { // Resolve references in resource attributes for i := 0; i < profiles.ResourceProfiles().Len(); i++ { rp := profiles.ResourceProfiles().At(i) - resolveMapReferences(dict, rp.Resource().Attributes()) + resolveKeyValueReferences(dict, mapKeyValues(rp.Resource().Attributes())) // Resolve references in scope attributes for j := 0; j < rp.ScopeProfiles().Len(); j++ { sp := rp.ScopeProfiles().At(j) - resolveMapReferences(dict, sp.Scope().Attributes()) + resolveKeyValueReferences(dict, mapKeyValues(sp.Scope().Attributes())) } } } -// resolveMapReferences resolves all string_value_ref and key_ref in a map -func resolveMapReferences(dict ProfilesDictionary, m pcommon.Map) { - mapOrig := internal.GetMapOrig(internal.MapWrapper(m)) - - for i := 0; i < len(*mapOrig); i++ { - kv := &(*mapOrig)[i] - +// 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.KeyRef >= 0 { idx := int(kv.KeyRef) if idx < dict.StringTable().Len() { kv.Key = dict.StringTable().At(idx) - // Keep ref set for potential re-marshaling + // N.b. keep KeyRef 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) } @@ -64,16 +67,7 @@ func resolveAnyValueReference(dict ProfilesDictionary, anyValue *internal.AnyVal anyValue.Value = ov } } else if kvList, ok := anyValue.Value.(*internal.AnyValue_KvlistValue); ok && kvList.KvlistValue != nil { - for i := 0; i < len(kvList.KvlistValue.Values); i++ { - kv := &kvList.KvlistValue.Values[i] - if kv.KeyRef >= 0 { - idx := int(kv.KeyRef) - if idx < dict.StringTable().Len() { - kv.Key = dict.StringTable().At(idx) - } - } - resolveAnyValueReference(dict, &kv.Value) - } + 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]) @@ -110,22 +104,20 @@ func convertProfilesToReferences(profiles Profiles) { // Convert strings in resource attributes for i := 0; i < profiles.ResourceProfiles().Len(); i++ { rp := profiles.ResourceProfiles().At(i) - convertMapToReferences(getStringIndex, rp.Resource().Attributes()) + convertKeyValueToReferences(getStringIndex, mapKeyValues(rp.Resource().Attributes())) // Convert strings in scope attributes for j := 0; j < rp.ScopeProfiles().Len(); j++ { sp := rp.ScopeProfiles().At(j) - convertMapToReferences(getStringIndex, sp.Scope().Attributes()) + convertKeyValueToReferences(getStringIndex, mapKeyValues(sp.Scope().Attributes())) } } } -// convertMapToReferences converts string keys and values to references -func convertMapToReferences(getStringIndex func(string) int32, m pcommon.Map) { - mapOrig := internal.GetMapOrig(internal.MapWrapper(m)) - - for i := 0; i < len(*mapOrig); i++ { - kv := &(*mapOrig)[i] +// 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.KeyRef == 0 { @@ -157,15 +149,7 @@ func convertAnyValueToReference(getStringIndex func(string) int32, anyValue *int ov.StringValueRef = idx anyValue.Value = ov } else if kvList, ok := anyValue.Value.(*internal.AnyValue_KvlistValue); ok && kvList.KvlistValue != nil { - // Recursively convert nested key-value lists - for i := 0; i < len(kvList.KvlistValue.Values); i++ { - kv := &kvList.KvlistValue.Values[i] - if kv.Key != "" && kv.KeyRef == 0 { - kv.KeyRef = getStringIndex(kv.Key) - kv.Key = "" - } - convertAnyValueToReference(getStringIndex, &kv.Value) - } + 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++ { diff --git a/pdata/pprofile/dictionary_helpers_test.go b/pdata/pprofile/dictionary_helpers_test.go index 49934513f15..2e3cbfcd670 100644 --- a/pdata/pprofile/dictionary_helpers_test.go +++ b/pdata/pprofile/dictionary_helpers_test.go @@ -386,7 +386,7 @@ func TestConvertMapToReferencesEmptyKey(t *testing.T) { return 1 } - convertMapToReferences(getStringIndex, attrs) + convertKeyValueToReferences(getStringIndex, mapKeyValues(attrs)) // Empty key should not have KeyRef set kv := &(*mapOrig)[0] @@ -414,7 +414,7 @@ func TestConvertMapToReferencesExistingKeyRef(t *testing.T) { return 99 } - convertMapToReferences(getStringIndex, attrs) + convertKeyValueToReferences(getStringIndex, mapKeyValues(attrs)) // KeyRef should remain unchanged kv := &(*mapOrig)[0] @@ -463,7 +463,7 @@ func TestConvertMapToReferencesClearsKey(t *testing.T) { return 2 } - convertMapToReferences(getStringIndex, attrs) + convertKeyValueToReferences(getStringIndex, mapKeyValues(attrs)) kv := &(*mapOrig)[0] // key_ref should be set From a33a81e2c0073d1e571e091d9f44721964525563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 08:52:24 +0100 Subject: [PATCH 6/8] pdata/pprofile: split BenchmarkMarshalProfiles into with_refs and without_refs --- pdata/pprofile/pb_test.go | 51 +++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/pdata/pprofile/pb_test.go b/pdata/pprofile/pb_test.go index d0c79a965dd..0ca4ff14b2c 100644 --- a/pdata/pprofile/pb_test.go +++ b/pdata/pprofile/pb_test.go @@ -283,20 +283,55 @@ func BenchmarkMarshalProfiles(b *testing.B) { for _, tc := range testCases { b.Run(tc.name, func(b *testing.B) { - // Generate profile data once - profiles := generateProfiles(b, tc.resourceCount, tc.scopeCount, tc.profileCount, tc.sampleCount) - marshaler := &ProtoMarshaler{} - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { + // 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) } - _ = buf - } + 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 + } + }) }) } } From daf8673f563cfc9fc125504e07375774c73bd07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 12:29:20 +0100 Subject: [PATCH 7/8] pdata/pprofile: strengthen wire compatibility test with proto.Equal Replace weak assertions (NotNil + Len check on ResourceProfiles) with proto.Equal for full semantic comparison of round-tripped data. The previous check would pass even if attributes, dictionary entries, profile contents, or other fields were corrupted during the round-trip. --- pdata/pprofile/pb_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pdata/pprofile/pb_test.go b/pdata/pprofile/pb_test.go index 0ca4ff14b2c..7464db127b8 100644 --- a/pdata/pprofile/pb_test.go +++ b/pdata/pprofile/pb_test.go @@ -50,15 +50,13 @@ func TestProfilesProtoWireCompatibility(t *testing.T) { wire3, err := marshaler.MarshalProfiles(td2) require.NoError(t, err) - // Verify that wire1 and wire3 are compatible by unmarshaling both and checking the data + // 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)) - - // Both should unmarshal successfully, proving wire compatibility - assert.NotNil(t, check1.ResourceProfiles) - assert.NotNil(t, check2.ResourceProfiles) - assert.Len(t, check1.ResourceProfiles, len(check2.ResourceProfiles)) + assert.True(t, goproto.Equal(&check1, &check2), "round-trip through goproto did not preserve profile data") } func TestProtoProfilesUnmarshalerError(t *testing.T) { From 231f4fffda3218959314aad22fd71b34b1e58d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisend=C3=B6rfer?= Date: Sat, 21 Feb 2026 12:34:52 +0100 Subject: [PATCH 8/8] pdata/pprofile: convert references in JSON marshaler/unmarshaler Call convertProfilesToReferences before JSON marshaling so attribute keys and values are transmitted as string table references. Call resolveProfilesReferences after JSON unmarshaling so the pdata API works transparently with resolved strings. --- pdata/pprofile/json.go | 8 +++ pdata/pprofile/json_references_test.go | 87 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 pdata/pprofile/json_references_test.go 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()) +}