Skip to content

Commit ee0bf52

Browse files
authored
[cmd/mdatagen] Add lint/ordering validation for metadata.yaml (#13782)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Extend mdatagen to validate that keys in metadata.yaml files are alphabetically sorted (resource_attributes, attributes, metrics, events, and telemetry.metrics). The idea came from open-telemetry/opentelemetry-collector-contrib#42477 (comment) <!-- Issue number if applicable --> #### Link to tracking issue Fixes #13781 <!--Describe what testing was performed and which tests were added.--> #### Testing I added tests to validate unordered data and expect an error on it. ```sh ❯ mdatagen metadata.yaml Error: metadata.yaml ordering check failed: [telemetry metrics] keys are not sorted: [metric1 metric0] Error: metadata.yaml ordering check failed: [telemetry metrics] keys are not sorted: [metric1 metric0] ``` ```sh ❯ mdatagen metadata.yaml Error: metadata.yaml ordering check failed: [resource_attributes] keys are not sorted: [cloud.region cloud.availability_zone cloud.provider host.id host.name] Error: metadata.yaml ordering check failed: [resource_attributes] keys are not sorted: [cloud.region cloud.availability_zone cloud.provider host.id host.name] ``` --------- Signed-off-by: Paulo Dias <[email protected]>
1 parent 7eb966f commit ee0bf52

File tree

17 files changed

+653
-396
lines changed

17 files changed

+653
-396
lines changed

.chloggen/feat_13781.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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: "Add lint/ordering validation for metadata.yaml"
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [13781]
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+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: [user]

cmd/mdatagen/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ require (
3131
go.uber.org/goleak v1.3.0
3232
go.uber.org/zap v1.27.0
3333
golang.org/x/text v0.29.0
34+
gopkg.in/yaml.v3 v3.0.1
3435
)
3536

3637
require (
@@ -77,7 +78,6 @@ require (
7778
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
7879
google.golang.org/grpc v1.76.0 // indirect
7980
google.golang.org/protobuf v1.36.10 // indirect
80-
gopkg.in/yaml.v3 v3.0.1 // indirect
8181
)
8282

8383
replace go.opentelemetry.io/collector/component => ../../component

cmd/mdatagen/internal/command.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/spf13/cobra"
2222
"golang.org/x/text/cases"
2323
"golang.org/x/text/language"
24+
"gopkg.in/yaml.v3"
2425
)
2526

2627
const (
@@ -77,6 +78,15 @@ func run(ymlPath string) error {
7778
ymlDir := filepath.Dir(ymlPath)
7879
packageName := filepath.Base(ymlDir)
7980

81+
raw, readErr := os.ReadFile(ymlPath) //nolint:gosec // G304: abs path is cleaned/validated above; safe to read
82+
if readErr != nil {
83+
return fmt.Errorf("failed reading %v: %w", ymlPath, readErr)
84+
}
85+
86+
if err = validateYAMLKeyOrder(raw); err != nil {
87+
return fmt.Errorf("metadata.yaml ordering check failed: %w", err)
88+
}
89+
8090
md, err := LoadMetadata(ymlPath)
8191
if err != nil {
8292
return fmt.Errorf("failed loading %v: %w", ymlPath, err)
@@ -404,3 +414,62 @@ func generateFile(tmplFile, outputFile string, md Metadata, goPackage string) er
404414

405415
return formatErr
406416
}
417+
418+
func validateMappingKeysSorted(root *yaml.Node, path ...string) error {
419+
// unwrap doc
420+
n := root
421+
if n.Kind == yaml.DocumentNode && len(n.Content) > 0 {
422+
n = n.Content[0]
423+
}
424+
// follow path
425+
for _, seg := range path {
426+
if n.Kind != yaml.MappingNode {
427+
return nil
428+
}
429+
var next *yaml.Node
430+
for i := 0; i < len(n.Content); i += 2 {
431+
if n.Content[i].Value == seg {
432+
next = n.Content[i+1]
433+
break
434+
}
435+
}
436+
if next == nil {
437+
return nil
438+
}
439+
n = next
440+
}
441+
if n.Kind != yaml.MappingNode {
442+
return nil
443+
}
444+
445+
// collect keys
446+
keys := make([]string, 0, len(n.Content)/2)
447+
for i := 0; i < len(n.Content); i += 2 {
448+
keys = append(keys, n.Content[i].Value)
449+
}
450+
451+
if !slices.IsSorted(keys) {
452+
return fmt.Errorf("%v keys are not sorted: %v", path, keys)
453+
}
454+
return nil
455+
}
456+
457+
// ValidateYAMLKeyOrder checks the sections we care about.
458+
func validateYAMLKeyOrder(raw []byte) error {
459+
var doc yaml.Node
460+
if err := yaml.Unmarshal(raw, &doc); err != nil {
461+
return err
462+
}
463+
for _, p := range [][]string{
464+
{"resource_attributes"},
465+
{"attributes"},
466+
{"metrics"},
467+
{"events"},
468+
{"telemetry", "metrics"},
469+
} {
470+
if err := validateMappingKeysSorted(&doc, p...); err != nil {
471+
return err
472+
}
473+
}
474+
return nil
475+
}

cmd/mdatagen/internal/command_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,17 @@ func TestRunContents(t *testing.T) {
4848
wantGoleakSetup bool
4949
wantGoleakTeardown bool
5050
wantErr bool
51+
wantOrderErr bool
5152
wantAttributes []string
5253
}{
5354
{
5455
yml: "invalid.yaml",
5556
wantErr: true,
5657
},
58+
{
59+
yml: "unsorted_rattr.yaml",
60+
wantOrderErr: true,
61+
},
5762
{
5863
yml: "basic_connector.yaml",
5964
wantErr: false,
@@ -234,6 +239,11 @@ foo
234239
require.NoError(t, os.WriteFile(filepath.Join(tmpdir, generatedPackageDir, "generated_component_test.go"), []byte("test"), 0o600))
235240

236241
err = run(metadataFile)
242+
if tt.wantOrderErr {
243+
require.Error(t, err)
244+
require.Contains(t, err.Error(), "metadata.yaml ordering check failed")
245+
return
246+
}
237247
require.NoError(t, err)
238248

239249
var contents []byte

cmd/mdatagen/internal/sampleconnector/metadata.yaml

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,9 @@ status:
1818
- Any additional information that should be brought to the consumer's attention
1919

2020
resource_attributes:
21-
string.resource.attr:
22-
description: Resource attribute with any string value.
23-
type: string
24-
enabled: true
25-
26-
string.enum.resource.attr:
27-
description: Resource attribute with a known set of string values.
28-
type: string
29-
enum: [one, two]
21+
map.resource.attr:
22+
description: Resource attribute with a map value.
23+
type: map
3024
enabled: true
3125

3226
optional.resource.attr:
@@ -39,9 +33,15 @@ resource_attributes:
3933
type: slice
4034
enabled: true
4135

42-
map.resource.attr:
43-
description: Resource attribute with a map value.
44-
type: map
36+
string.enum.resource.attr:
37+
description: Resource attribute with a known set of string values.
38+
type: string
39+
enum: [one, two]
40+
enabled: true
41+
42+
string.resource.attr:
43+
description: Resource attribute with any string value.
44+
type: string
4545
enabled: true
4646

4747
string.resource.attr_disable_warning:
@@ -66,20 +66,6 @@ resource_attributes:
6666
if_enabled: This resource_attribute is deprecated and will be removed soon.
6767

6868
attributes:
69-
string_attr:
70-
description: Attribute with any string value.
71-
type: string
72-
73-
overridden_int_attr:
74-
name_override: state
75-
description: Integer attribute with overridden name.
76-
type: int
77-
78-
enum_attr:
79-
description: Attribute with a known set of string values.
80-
type: string
81-
enum: [red, green, blue]
82-
8369
boolean_attr:
8470
description: Attribute with a boolean value.
8571
type: bool
@@ -90,14 +76,28 @@ attributes:
9076
description: Another attribute with a boolean value.
9177
type: bool
9278

93-
slice_attr:
94-
description: Attribute with a slice value.
95-
type: slice
79+
enum_attr:
80+
description: Attribute with a known set of string values.
81+
type: string
82+
enum: [red, green, blue]
9683

9784
map_attr:
9885
description: Attribute with a map value.
9986
type: map
10087

88+
overridden_int_attr:
89+
name_override: state
90+
description: Integer attribute with overridden name.
91+
type: int
92+
93+
slice_attr:
94+
description: Attribute with a slice value.
95+
type: slice
96+
97+
string_attr:
98+
description: Attribute with any string value.
99+
type: string
100+
101101
metrics:
102102
default.metric:
103103
enabled: true
@@ -108,30 +108,11 @@ metrics:
108108
value_type: int
109109
monotonic: true
110110
aggregation_temporality: cumulative
111-
attributes: [string_attr, overridden_int_attr, enum_attr, slice_attr, map_attr]
111+
attributes:
112+
[string_attr, overridden_int_attr, enum_attr, slice_attr, map_attr]
112113
warnings:
113114
if_enabled_not_set: This metric will be disabled by default soon.
114115

115-
optional.metric:
116-
enabled: false
117-
description: "[DEPRECATED] Gauge double metric disabled by default."
118-
unit: "1"
119-
gauge:
120-
value_type: double
121-
attributes: [string_attr, boolean_attr, boolean_attr2]
122-
warnings:
123-
if_configured: This metric is deprecated and will be removed soon.
124-
125-
optional.metric.empty_unit:
126-
enabled: false
127-
description: "[DEPRECATED] Gauge double metric disabled by default."
128-
unit: ""
129-
gauge:
130-
value_type: double
131-
attributes: [string_attr, boolean_attr]
132-
warnings:
133-
if_configured: This metric is deprecated and will be removed soon.
134-
135116
default.metric.to_be_removed:
136117
enabled: true
137118
description: "[DEPRECATED] Non-monotonic delta sum double metric enabled by default."
@@ -153,4 +134,25 @@ metrics:
153134
input_type: string
154135
monotonic: true
155136
aggregation_temporality: cumulative
156-
attributes: [ string_attr, overridden_int_attr, enum_attr, slice_attr, map_attr ]
137+
attributes:
138+
[string_attr, overridden_int_attr, enum_attr, slice_attr, map_attr]
139+
140+
optional.metric:
141+
enabled: false
142+
description: "[DEPRECATED] Gauge double metric disabled by default."
143+
unit: "1"
144+
gauge:
145+
value_type: double
146+
attributes: [string_attr, boolean_attr, boolean_attr2]
147+
warnings:
148+
if_configured: This metric is deprecated and will be removed soon.
149+
150+
optional.metric.empty_unit:
151+
enabled: false
152+
description: "[DEPRECATED] Gauge double metric disabled by default."
153+
unit: ""
154+
gauge:
155+
value_type: double
156+
attributes: [string_attr, boolean_attr]
157+
warnings:
158+
if_configured: This metric is deprecated and will be removed soon.

cmd/mdatagen/internal/sampleprocessor/metadata.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,9 @@ status:
2121
- Any additional information that should be brought to the consumer's attention
2222

2323
resource_attributes:
24-
string.resource.attr:
25-
description: Resource attribute with any string value.
26-
type: string
27-
enabled: true
28-
29-
string.enum.resource.attr:
30-
description: Resource attribute with a known set of string values.
31-
type: string
32-
enum: [one, two]
24+
map.resource.attr:
25+
description: Resource attribute with a map value.
26+
type: map
3327
enabled: true
3428

3529
optional.resource.attr:
@@ -42,9 +36,15 @@ resource_attributes:
4236
type: slice
4337
enabled: true
4438

45-
map.resource.attr:
46-
description: Resource attribute with a map value.
47-
type: map
39+
string.enum.resource.attr:
40+
description: Resource attribute with a known set of string values.
41+
type: string
42+
enum: [one, two]
43+
enabled: true
44+
45+
string.resource.attr:
46+
description: Resource attribute with any string value.
47+
type: string
4848
enabled: true
4949

5050
string.resource.attr_disable_warning:

0 commit comments

Comments
 (0)