mirror of
https://github.com/hanzoai/telemetry.git
synced 2026-10-30 20:01:49 +00:00
## Summary Fixes #15084 When profiles go through an **unmarshal → MergeTo → marshal → unmarshal** round-trip (e.g. batching in the exporter queue with a Kafka exporter), resource and scope attribute keys get corrupted. **Root cause:** `resolveProfilesReferences` (called during unmarshal) resolves `KeyStrindex` to the `Key` string but keeps `KeyStrindex` set as an optimization. `MergeTo` changes the dictionary without updating these `KeyStrindex` values on resource/scope attributes (since `switchDictionary` does not traverse them). On the next marshal, `convertKeyValueToReferences` skipped re-indexing when `KeyStrindex` was already non-zero (`kv.Key != "" && kv.KeyStrindex == 0`), writing stale indices. Upon final unmarshal, these stale indices resolved to wrong strings. **Fix:** Always re-index from the resolved `Key` string during marshal, regardless of the current `KeyStrindex` value. This is correct because after unmarshal the `Key` field always holds the authoritative string value. ## Changes - **`pdata/pprofile/dictionary_helpers.go`** — Change condition in `convertKeyValueToReferences` from `kv.Key != "" && kv.KeyStrindex == 0` to `kv.Key != ""` so stale indices are always overwritten. - **`pdata/pprofile/dictionary_helpers_test.go`** — Update `TestConvertMapToReferencesExistingKeyRef` expectations to match new behavior. - **`pdata/pprofile/profiles_merge_test.go`** — Add `TestProfilesMergeTo_ResourceAttributeRoundTrip` covering the full marshal→unmarshal→merge→marshal→unmarshal cycle for both resource and scope attributes. - **`.chloggen/fix-profiles-resource-attr-corruption.yaml`** — Changelog entry. ## Test plan - [x] New test `TestProfilesMergeTo_ResourceAttributeRoundTrip` reproduces the exact corruption scenario from #15084 and passes with the fix - [x] Updated test `TestConvertMapToReferencesExistingKeyRef` verifies the new always-re-index behavior - [x] All 405 existing tests in `pdata/pprofile` pass (`make test`) - [x] Linting passes (`make lint`) - [x] Formatting passes (`make fmt`) - [x] Changelog validates (`make chlog-validate`) --------- Co-authored-by: Pablo Baeyens <[email protected]>
537 lines
14 KiB
Go
537 lines
14 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/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))
|
|
|
|
// Key is set, so KeyStrindex must be updated to the new index
|
|
kv := &(*mapOrig)[0]
|
|
assert.Equal(t, int32(99), kv.KeyStrindex)
|
|
// Key must be cleared when KeyStrindex is set
|
|
assert.Empty(t, kv.Key)
|
|
}
|
|
|
|
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)
|
|
}
|