Skip to content

Commit 3dbc211

Browse files
authored
[cmd/mdatagen] Add basic support for entities to metadata.yaml schema (#14053)
When entities are defined, mdatagen generates `AssociateWith{EntityType}()` methods on ResourceBuilder that associate resources with entity types using the entity refs API. The `entities` section is backward compatible - existing metadata.yaml files without entities continue to work as before. This change is fully additive and conditionally executed only if metadata.yaml has `entities` defined. The generated Go API is experimental and will change to entities builder. However, for now, it allows introducing concept of entities in mdatagen and emit entities attached to the resource. The new metadata.yaml syntax for defining entities adopts [Weaver v2](https://github.com/open-telemetry/weaver/blob/main/schemas/semconv-syntax.v2.md#entities-definition) Updates #14051
1 parent eecd9fc commit 3dbc211

20 files changed

+510
-1
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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: cmd/mdatagen
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "`metadata.yaml` now supports an optional `entities` section to organize resource attributes into logical entities with identity and description attributes"
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [14051]
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+
When entities are defined, mdatagen generates `AssociateWith{EntityType}()` methods on ResourceBuilder
20+
that associate resources with entity types using the entity refs API. The entities section is backward
21+
compatible - existing metadata.yaml files without entities continue to work as before.
22+
23+
# Optional: The change log or logs in which this entry should be included.
24+
# e.g. '[user]' or '[user, api]'
25+
# Include 'user' if the change is relevant to end users.
26+
# Include 'api' if there is a change to a library API.
27+
# Default: '[user]'
28+
change_logs: [user]

cmd/mdatagen/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
go.opentelemetry.io/collector/consumer/consumertest v0.139.0
1717
go.opentelemetry.io/collector/filter v0.139.0
1818
go.opentelemetry.io/collector/pdata v1.45.0
19+
go.opentelemetry.io/collector/pdata/xpdata v0.139.0
1920
go.opentelemetry.io/collector/pipeline v1.45.0
2021
go.opentelemetry.io/collector/processor v1.45.0
2122
go.opentelemetry.io/collector/processor/processortest v0.139.0

cmd/mdatagen/internal/command.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ func validateYAMLKeyOrder(raw []byte) error {
462462
}
463463
for _, p := range [][]string{
464464
{"resource_attributes"},
465+
{"entities"},
465466
{"attributes"},
466467
{"metrics"},
467468
{"events"},

cmd/mdatagen/internal/metadata.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Metadata struct {
3030
SemConvVersion string `mapstructure:"sem_conv_version"`
3131
// ResourceAttributes that can be emitted by the component.
3232
ResourceAttributes map[AttributeName]Attribute `mapstructure:"resource_attributes"`
33+
// Entities organizes resource attributes into logical entities.
34+
Entities []Entity `mapstructure:"entities"`
3335
// Attributes emitted by one or more metrics.
3436
Attributes map[AttributeName]Attribute `mapstructure:"attributes"`
3537
// Metrics that can be emitted by the component.
@@ -56,6 +58,10 @@ func (md Metadata) GetCodeCovComponentID() string {
5658
return strings.ReplaceAll(md.Status.Class+"_"+md.Type, "/", "_")
5759
}
5860

61+
func (md Metadata) HasEntities() bool {
62+
return len(md.Entities) > 0
63+
}
64+
5965
func (md *Metadata) Validate() error {
6066
var errs error
6167
if err := md.validateType(); err != nil {
@@ -75,6 +81,10 @@ func (md *Metadata) Validate() error {
7581
errs = errors.Join(errs, err)
7682
}
7783

84+
if err := md.validateEntities(); err != nil {
85+
errs = errors.Join(errs, err)
86+
}
87+
7888
if err := md.validateMetricsAndEvents(); err != nil {
7989
errs = errors.Join(errs, err)
8090
}
@@ -122,6 +132,51 @@ func (md *Metadata) validateResourceAttributes() error {
122132
return errs
123133
}
124134

135+
func (md *Metadata) validateEntities() error {
136+
var errs error
137+
usedAttrs := make(map[AttributeName]string)
138+
seenTypes := make(map[string]bool)
139+
140+
for _, entity := range md.Entities {
141+
if entity.Type == "" {
142+
errs = errors.Join(errs, errors.New("entity type cannot be empty"))
143+
continue
144+
}
145+
if seenTypes[entity.Type] {
146+
errs = errors.Join(errs, fmt.Errorf(`duplicate entity type: %v`, entity.Type))
147+
}
148+
seenTypes[entity.Type] = true
149+
150+
if entity.Brief == "" {
151+
errs = errors.Join(errs, fmt.Errorf(`entity "%v": brief is required`, entity.Type))
152+
}
153+
if len(entity.Identity) == 0 {
154+
errs = errors.Join(errs, fmt.Errorf(`entity "%v": identity is required`, entity.Type))
155+
}
156+
for _, ref := range entity.Identity {
157+
if _, ok := md.ResourceAttributes[ref.Ref]; !ok {
158+
errs = errors.Join(errs, fmt.Errorf(`entity "%v": identity refers to undefined resource attribute: %v`, entity.Type, ref.Ref))
159+
}
160+
if otherEntity, used := usedAttrs[ref.Ref]; used {
161+
errs = errors.Join(errs, fmt.Errorf(`entity "%v": attribute %v is already used by entity "%v"`, entity.Type, ref.Ref, otherEntity))
162+
} else {
163+
usedAttrs[ref.Ref] = entity.Type
164+
}
165+
}
166+
for _, ref := range entity.Description {
167+
if _, ok := md.ResourceAttributes[ref.Ref]; !ok {
168+
errs = errors.Join(errs, fmt.Errorf(`entity "%v": description refers to undefined resource attribute: %v`, entity.Type, ref.Ref))
169+
}
170+
if otherEntity, used := usedAttrs[ref.Ref]; used {
171+
errs = errors.Join(errs, fmt.Errorf(`entity "%v": attribute %v is already used by entity "%v"`, entity.Type, ref.Ref, otherEntity))
172+
} else {
173+
usedAttrs[ref.Ref] = entity.Type
174+
}
175+
}
176+
}
177+
return errs
178+
}
179+
125180
func (md *Metadata) validateMetricsAndEvents() error {
126181
var errs error
127182
usedAttrs := map[AttributeName]bool{}
@@ -441,3 +496,21 @@ func (s Signal) HasConditionalAttributes(attrs map[AttributeName]Attribute) bool
441496
}
442497
return false
443498
}
499+
500+
type Entity struct {
501+
// Type is the type of the entity.
502+
Type string `mapstructure:"type"`
503+
// Brief is a brief description of the entity.
504+
Brief string `mapstructure:"brief"`
505+
// Stability is the stability level of the entity.
506+
Stability string `mapstructure:"stability"`
507+
// Identity contains references to resource attributes that uniquely identify the entity.
508+
Identity []EntityAttributeRef `mapstructure:"identity"`
509+
// Description contains references to resource attributes that describe the entity.
510+
Description []EntityAttributeRef `mapstructure:"description"`
511+
}
512+
513+
type EntityAttributeRef struct {
514+
// Ref is the reference to a resource attribute.
515+
Ref AttributeName `mapstructure:"ref"`
516+
}

cmd/mdatagen/internal/metadata_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,32 @@ func TestValidate(t *testing.T) {
120120
name: "testdata/no_type_attr.yaml",
121121
wantErr: "empty type for attribute: used_attr",
122122
},
123+
{
124+
name: "testdata/entity_undefined_id_attribute.yaml",
125+
wantErr: `entity "host": identity refers to undefined resource attribute: host.missing`,
126+
},
127+
{
128+
name: "testdata/entity_undefined_description_attribute.yaml",
129+
wantErr: `entity "host": description refers to undefined resource attribute: host.missing`,
130+
},
131+
{
132+
name: "testdata/entity_empty_id_attributes.yaml",
133+
wantErr: `entity "host": identity is required`,
134+
},
135+
{
136+
name: "testdata/entity_duplicate_attributes.yaml",
137+
wantErr: `attribute host.name is already used by entity`,
138+
},
139+
{
140+
name: "testdata/entity_duplicate_types.yaml",
141+
wantErr: `duplicate entity type: host`,
142+
},
123143
}
124144
for _, tt := range tests {
125145
t.Run(tt.name, func(t *testing.T) {
126146
_, err := LoadMetadata(tt.name)
127147
require.Error(t, err)
128-
require.EqualError(t, err, tt.wantErr)
148+
require.ErrorContains(t, err, tt.wantErr)
129149
})
130150
}
131151
}

cmd/mdatagen/internal/sampleconnector/documentation.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,19 @@ metrics:
113113
| string.resource.attr_disable_warning | Resource attribute with any string value. | Any Str | true |
114114
| string.resource.attr_remove_warning | Resource attribute with any string value. | Any Str | false |
115115
| string.resource.attr_to_be_removed | Resource attribute with any string value. | Any Str | true |
116+
117+
## Entities
118+
119+
The following entities are defined for this component:
120+
121+
### test.entity
122+
123+
A test entity.
124+
125+
**Stability:** stable
126+
127+
**Identity Attributes:**
128+
- `string.resource.attr`
129+
130+
**Description Attributes:**
131+
- `map.resource.attr`

cmd/mdatagen/internal/sampleconnector/internal/metadata/generated_resource.go

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/mdatagen/internal/sampleconnector/metadata.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ resource_attributes:
6565
warnings:
6666
if_enabled: This resource_attribute is deprecated and will be removed soon.
6767

68+
entities:
69+
- type: test.entity
70+
brief: A test entity.
71+
stability: stable
72+
identity:
73+
- ref: string.resource.attr
74+
description:
75+
- ref: map.resource.attr
76+
6877
attributes:
6978
boolean_attr:
7079
description: Attribute with a boolean value.

cmd/mdatagen/internal/templates/documentation.md.tmpl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,43 @@ events:
212212

213213
{{- end }}
214214

215+
{{- if .Entities }}
216+
217+
## Entities
218+
219+
The following entities are defined for this component:
220+
221+
{{- range $entity := .Entities }}
222+
223+
### {{ $entity.Type }}
224+
225+
{{ $entity.Brief }}
226+
227+
{{- if $entity.Stability }}
228+
229+
**Stability:** {{ $entity.Stability }}
230+
{{- end }}
231+
232+
{{- if $entity.Identity }}
233+
234+
**Identity Attributes:**
235+
{{- range $entity.Identity }}
236+
- `{{ .Ref }}`
237+
{{- end }}
238+
{{- end }}
239+
240+
{{- if $entity.Description }}
241+
242+
**Description Attributes:**
243+
{{- range $entity.Description }}
244+
- `{{ .Ref }}`
245+
{{- end }}
246+
{{- end }}
247+
248+
{{- end }}
249+
250+
{{- end }}
251+
215252
{{- if .Telemetry.Metrics }}
216253

217254
## Internal Telemetry

cmd/mdatagen/internal/templates/resource.go.tmpl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ package {{ .Package }}
44

55
import (
66
"go.opentelemetry.io/collector/pdata/pcommon"
7+
{{- if .HasEntities }}
8+
"go.opentelemetry.io/collector/pdata/xpdata/entity"
9+
{{- end }}
710
)
811

912
// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml.
@@ -43,6 +46,31 @@ func (rb *ResourceBuilder) Set{{ $name.Render }}(val {{ $attr.Type.Primitive }})
4346
{{- end }}
4447
{{ end }}
4548

49+
{{- if .HasEntities }}
50+
51+
{{ range $entity := .Entities }}
52+
// AssociateWith{{ $entity.Type | publicVar }} associates the resource with entity type "{{ $entity.Type }}".
53+
// This method is experimental and will be replaced with an entity builder pattern in the future.
54+
// However, for now, it allows associating resources with entities and producing correct entity references.
55+
func (rb *ResourceBuilder) AssociateWith{{ $entity.Type | publicVar }}() {
56+
entityRef := entity.ResourceEntityRefs(rb.res).AppendEmpty()
57+
entityRef.SetType("{{ $entity.Type }}")
58+
{{- if $entity.Identity }}
59+
idKeys := entityRef.IdKeys()
60+
{{- range $entity.Identity }}
61+
idKeys.Append("{{ .Ref }}")
62+
{{- end }}
63+
{{- end }}
64+
{{- if $entity.Description }}
65+
descKeys := entityRef.DescriptionKeys()
66+
{{- range $entity.Description }}
67+
descKeys.Append("{{ .Ref }}")
68+
{{- end }}
69+
{{- end }}
70+
}
71+
{{ end }}
72+
{{- end }}
73+
4674
// Emit returns the built resource and resets the internal builder state.
4775
func (rb *ResourceBuilder) Emit() pcommon.Resource {
4876
r := rb.res

0 commit comments

Comments
 (0)