Files
telemetry/pdata/pprofile/pb_references_test.go
cdb3d3cd0d [pdata/pprofile] copy data if read-only (#15082)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description
When multiple exporters are used in a profiles pipeline, the same data
object is shared and marked as read-only, causing a panic when mutation
is attempted.

Fix this by only creating a copy when the input is marked as read-only.
This preserves the in-place mutation optimization for mutable data while
preventing panics in shared data scenarios.


<!-- Issue number if applicable -->
#### Link to tracking issue
Fixes #15080

<!--Describe what testing was performed and which tests were added.-->
#### Testing

<!--Describe the documentation added.-->
#### Documentation

<!--Please delete paragraphs that you did not use before submitting.-->

---------

Signed-off-by: Florian Lehner <[email protected]>
Co-authored-by: Damien Mathieu <[email protected]>
2026-04-09 10:44:50 +00:00

226 lines
7.4 KiB
Go

// 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")
}
func TestProtoMarshalReadOnlyProfiles(t *testing.T) {
profiles := NewProfiles()
dict := profiles.Dictionary()
dict.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")
marshaler := ProtoMarshaler{}
// Mark as read-only to simulate sharing across multiple exporters
profiles.MarkReadOnly()
// MarshalProfiles should not panic or fail when given read-only data
bytes, err := marshaler.MarshalProfiles(profiles)
require.NoError(t, err)
require.NotEmpty(t, bytes)
// Verify the original profiles is still usable and hasn't been mutated
rp = profiles.ResourceProfiles().At(0)
serviceNameVal, ok := rp.Resource().Attributes().Get("service.name")
assert.True(t, ok, "service.name should still be accessible")
assert.Equal(t, "test-service", serviceNameVal.Str())
}