Skip to content

Commit 8038c69

Browse files
authored
[xpdata] Add high-level API for managing entities attached to resources (#14039)
Introduce `Entity`, `EntitySlice`, and `EntityAttributeMap` types that provide a user-friendly interface for working with resource entities. The new API ensures consistency between entity and resource attributes by sharing the underlying attribute map, and prevents attribute conflicts between entities. This API should eventually replace the generated protobuf-based API for better usability. Resolves #14042
1 parent 89327db commit 8038c69

File tree

10 files changed

+788
-18
lines changed

10 files changed

+788
-18
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: pdata/xpdata
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add high-level Entity API for managing entities attached to resources
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [14042]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: |
19+
Introduces `Entity`, `EntitySlice`, and `EntityAttributeMap` types that provide a user-friendly interface
20+
for working with resource entities. The new API ensures consistency between entity and resource attributes
21+
by sharing the underlying attribute map, and prevents attribute conflicts between entities. This API may
22+
eventually replace the generated protobuf-based API for better usability.
23+
24+
# Optional: The change log or logs in which this entry should be included.
25+
# e.g. '[user]' or '[user, api]'
26+
# Include 'user' if the change is relevant to end users.
27+
# Include 'api' if there is a change to a library API.
28+
# Default: '[user]'
29+
change_logs: [api]

pdata/xpdata/entity/entity.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package entity // import "go.opentelemetry.io/collector/pdata/xpdata/entity"
5+
6+
import (
7+
"go.opentelemetry.io/collector/pdata/pcommon"
8+
)
9+
10+
// Entity is a helper struct that represents an entity in a more user-friendly way than the underlying
11+
// EntityRef protobuf message. After adding an entity to a resource, the entity shares the resource's
12+
// attributes map, so modifications to the entity's attributes are immediately reflected in the resource.
13+
// To create an Entity, use the EntityMap's PutEmpty method.
14+
type Entity struct {
15+
ref EntityRef
16+
attributes pcommon.Map
17+
}
18+
19+
func (e Entity) Type() string {
20+
return e.ref.Type()
21+
}
22+
23+
func (e Entity) SchemaURL() string {
24+
return e.ref.SchemaUrl()
25+
}
26+
27+
func (e Entity) SetSchemaURL(schemaURL string) {
28+
e.ref.SetSchemaUrl(schemaURL)
29+
}
30+
31+
// IDAttributes returns an EntityAttributeMap for managing the entity's id attributes.
32+
func (e Entity) IDAttributes() EntityAttributeMap {
33+
return EntityAttributeMap{
34+
keys: e.ref.IdKeys(),
35+
attributes: e.attributes,
36+
}
37+
}
38+
39+
// DescriptionAttributes returns an EntityAttributeMap for managing the entity's description attributes.
40+
func (e Entity) DescriptionAttributes() EntityAttributeMap {
41+
return EntityAttributeMap{
42+
keys: e.ref.DescriptionKeys(),
43+
attributes: e.attributes,
44+
}
45+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package entity // import "go.opentelemetry.io/collector/pdata/xpdata/entity"
5+
6+
import "go.opentelemetry.io/collector/pdata/pcommon"
7+
8+
// EntityAttributeMap is a wrapper around pcommon.Map that restricts operations to only the keys
9+
// that belong to a specific set of entity attributes (either ID or Description attributes).
10+
type EntityAttributeMap struct {
11+
keys pcommon.StringSlice
12+
attributes pcommon.Map
13+
}
14+
15+
// Get returns the Value associated with the key and true. Returned
16+
// Value is not a copy, it is a reference to the value stored in this map. It is
17+
// allowed to modify the returned value using Value.Set* functions.
18+
//
19+
// If the key does not exist in the entity's key list or in the underlying map,
20+
// returns an invalid instance and false. Calling any functions on the returned
21+
// invalid instance will cause a panic.
22+
func (m EntityAttributeMap) Get(key string) (pcommon.Value, bool) {
23+
if !m.containsKey(key) {
24+
return pcommon.Value{}, false
25+
}
26+
return m.attributes.Get(key)
27+
}
28+
29+
// CanPut returns true if it's safe to call Put methods on the given key.
30+
// Returns true if:
31+
// - The key is already owned by this entity (in the entity's key list), OR
32+
// - The key doesn't exist in the shared attributes map (available to claim)
33+
//
34+
// Returns false if the key exists in the shared map but belongs to another entity.
35+
//
36+
// Use this method before calling Put* methods to avoid conflicts:
37+
//
38+
// if entity.IDAttributes().CanPut("service.name") {
39+
// entity.IDAttributes().PutStr("service.name", "my-service")
40+
// }
41+
func (m EntityAttributeMap) CanPut(key string) bool {
42+
if m.containsKey(key) {
43+
return true
44+
}
45+
_, exists := m.attributes.Get(key)
46+
return !exists
47+
}
48+
49+
// PutEmpty inserts or updates an empty value to the map under given key
50+
// and returns the updated/inserted value.
51+
// The key is also added to the entity's key list if not already present.
52+
//
53+
// WARNING: This method is destructive and will overwrite any existing value in the shared
54+
// attributes map, even if it belongs to another entity. Use CanPut() to check safety first
55+
// if you need to avoid conflicts with other entities.
56+
func (m EntityAttributeMap) PutEmpty(k string) pcommon.Value {
57+
if !m.containsKey(k) {
58+
m.keys.Append(k)
59+
}
60+
return m.attributes.PutEmpty(k)
61+
}
62+
63+
// PutStr performs the Insert or Update action. The Value is
64+
// inserted to the map that did not originally have the key. The key/value is
65+
// updated to the map where the key already existed.
66+
// The key is also added to the entity's key list if not already present.
67+
//
68+
// WARNING: This method is destructive and will overwrite any existing value in the shared
69+
// attributes map, even if it belongs to another entity. Use CanPut() to check safety first
70+
// if you need to avoid conflicts with other entities.
71+
func (m EntityAttributeMap) PutStr(k, v string) {
72+
if !m.containsKey(k) {
73+
m.keys.Append(k)
74+
}
75+
m.attributes.PutStr(k, v)
76+
}
77+
78+
// Remove removes the entry associated with the key and returns true if the key existed.
79+
// The key is also removed from the entity's key list.
80+
func (m EntityAttributeMap) Remove(key string) bool {
81+
var keyFound bool
82+
m.keys.RemoveIf(func(k string) bool {
83+
if k == key {
84+
keyFound = true
85+
return true
86+
}
87+
return false
88+
})
89+
if !keyFound {
90+
return false
91+
}
92+
m.attributes.Remove(key)
93+
return true
94+
}
95+
96+
func (m EntityAttributeMap) containsKey(key string) bool {
97+
for _, k := range m.keys.All() {
98+
if k == key {
99+
return true
100+
}
101+
}
102+
return false
103+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package entity
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
11+
"go.opentelemetry.io/collector/pdata/pcommon"
12+
)
13+
14+
func TestEntityAttributeMap(t *testing.T) {
15+
m := newTestEntityAttributeMap()
16+
17+
val, ok := m.Get("non-existent")
18+
assert.False(t, ok)
19+
assert.Equal(t, pcommon.Value{}, val)
20+
21+
m.PutStr("k1", "v1")
22+
val, ok = m.Get("k1")
23+
assert.True(t, ok)
24+
assert.Equal(t, "v1", val.Str())
25+
26+
assert.True(t, m.Remove("k1"))
27+
_, ok = m.Get("k1")
28+
assert.False(t, ok)
29+
30+
assert.False(t, m.Remove("non-existent"))
31+
}
32+
33+
func TestEntityAttributeMap_Get(t *testing.T) {
34+
m := newTestEntityAttributeMap()
35+
m.attributes.PutStr("owned", "value1")
36+
m.attributes.PutStr("not-owned", "value2")
37+
m.keys.Append("owned")
38+
39+
val, ok := m.Get("owned")
40+
assert.True(t, ok)
41+
assert.Equal(t, "value1", val.Str())
42+
43+
_, ok = m.Get("not-owned")
44+
assert.False(t, ok)
45+
46+
_, ok = m.Get("non-existent")
47+
assert.False(t, ok)
48+
}
49+
50+
func TestEntityAttributeMap_PutStr(t *testing.T) {
51+
m := newTestEntityAttributeMap()
52+
53+
m.PutStr("k1", "v1")
54+
assert.Equal(t, 1, m.keys.Len())
55+
assert.Equal(t, "k1", m.keys.At(0))
56+
val, ok := m.attributes.Get("k1")
57+
assert.True(t, ok)
58+
assert.Equal(t, "v1", val.Str())
59+
60+
m.PutStr("k1", "v2")
61+
assert.Equal(t, 1, m.keys.Len())
62+
val, ok = m.attributes.Get("k1")
63+
assert.True(t, ok)
64+
assert.Equal(t, "v2", val.Str())
65+
}
66+
67+
func TestEntityAttributeMap_PutStr_Overwrites(t *testing.T) {
68+
m := newTestEntityAttributeMap()
69+
m.attributes.PutStr("existing", "old-value")
70+
71+
m.PutStr("existing", "new-value")
72+
73+
assert.Equal(t, 1, m.keys.Len())
74+
assert.Equal(t, "existing", m.keys.At(0))
75+
val, ok := m.attributes.Get("existing")
76+
assert.True(t, ok)
77+
assert.Equal(t, "new-value", val.Str())
78+
}
79+
80+
func TestEntityAttributeMap_PutEmpty(t *testing.T) {
81+
m := newTestEntityAttributeMap()
82+
83+
val := m.PutEmpty("k1")
84+
val.SetStr("v1")
85+
86+
assert.Equal(t, 1, m.keys.Len())
87+
assert.Equal(t, "k1", m.keys.At(0))
88+
resVal, ok := m.attributes.Get("k1")
89+
assert.True(t, ok)
90+
assert.Equal(t, "v1", resVal.Str())
91+
}
92+
93+
func TestEntityAttributeMap_PutEmpty_Overwrites(t *testing.T) {
94+
m := newTestEntityAttributeMap()
95+
m.attributes.PutStr("existing", "old-value")
96+
97+
val := m.PutEmpty("existing")
98+
val.SetStr("new-value")
99+
100+
assert.Equal(t, 1, m.keys.Len())
101+
assert.Equal(t, "existing", m.keys.At(0))
102+
resVal, ok := m.attributes.Get("existing")
103+
assert.True(t, ok)
104+
assert.Equal(t, "new-value", resVal.Str())
105+
}
106+
107+
func TestEntityAttributeMap_Remove(t *testing.T) {
108+
m := newTestEntityAttributeMap()
109+
m.keys.Append("k1")
110+
m.keys.Append("k2")
111+
m.attributes.PutStr("k1", "v1")
112+
m.attributes.PutStr("k2", "v2")
113+
114+
assert.True(t, m.Remove("k1"))
115+
assert.Equal(t, 1, m.keys.Len())
116+
assert.Equal(t, "k2", m.keys.At(0))
117+
_, ok := m.attributes.Get("k1")
118+
assert.False(t, ok)
119+
120+
val, ok := m.attributes.Get("k2")
121+
assert.True(t, ok)
122+
assert.Equal(t, "v2", val.Str())
123+
}
124+
125+
func TestEntityAttributeMap_Remove_NotOwned(t *testing.T) {
126+
m := newTestEntityAttributeMap()
127+
m.attributes.PutStr("not-owned", "value")
128+
129+
assert.False(t, m.Remove("not-owned"))
130+
131+
val, ok := m.attributes.Get("not-owned")
132+
assert.True(t, ok)
133+
assert.Equal(t, "value", val.Str())
134+
}
135+
136+
func TestEntityAttributeMap_Remove_NonExistent(t *testing.T) {
137+
m := newTestEntityAttributeMap()
138+
139+
assert.False(t, m.Remove("non-existent"))
140+
}
141+
142+
func TestEntityAttributeMap_CanPut(t *testing.T) {
143+
m := newTestEntityAttributeMap()
144+
m.keys.Append("owned")
145+
m.attributes.PutStr("owned", "value")
146+
m.attributes.PutStr("other", "value")
147+
148+
assert.True(t, m.CanPut("owned"))
149+
assert.False(t, m.CanPut("other"))
150+
assert.True(t, m.CanPut("new-key"))
151+
}
152+
153+
func newTestEntityAttributeMap() EntityAttributeMap {
154+
return EntityAttributeMap{
155+
keys: pcommon.NewStringSlice(),
156+
attributes: pcommon.NewMap(),
157+
}
158+
}

0 commit comments

Comments
 (0)