Skip to content

Commit 94b186c

Browse files
authored
Check attribute/metric/event name format (open-telemetry#1302)
1 parent 40db676 commit 94b186c

7 files changed

+176
-13
lines changed

.chloggen/1302.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
change_type: enhancement
2+
component: docs
3+
note: Add note on tooling limitations for attribute names and enforce name format.
4+
issues: [1124]

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ check-policies:
226226
test-policies:
227227
docker run --rm -v $(PWD)/policies:/policies -v $(PWD)/policies_test:/policies_test \
228228
${OPA_CONTAINER} test \
229+
--var-values \
229230
--explain fails \
230231
/policies \
231232
/policies_test

docs/general/attribute-naming.md

+6
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ denote old attribute names in rename operations).
116116
the following Unicode code points: Latin alphabet, Numeric, Underscore, Dot
117117
(as namespace delimiter).
118118

119+
> Note:
120+
> Semantic Conventions tooling limits names to lowercase
121+
> Latin alphabet, Numeric, Underscore, Dot (as namespace delimiter).
122+
> Names must start with a letter, end with an alphanumeric character, and must not
123+
> contain two or more consecutive delimiters (Underscore or Dot).
124+
119125
## Recommendations for Application Developers
120126

121127
As an application developer when you need to record an attribute first consult

policies/attribute_name_collisions.rego

+18-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import rego.v1
44

55
# Data structures to make checking things faster.
66
attribute_names := { obj |
7-
group := input.groups[_];
8-
attr := group.attributes[_];
9-
obj := { "name": attr.name, "const_name": to_const_name(attr.name), "namespace_prefix": to_namespace_prefix(attr.name) }
7+
group := input.groups[_];
8+
attr := group.attributes[_];
9+
obj := { "name": attr.name, "const_name": to_const_name(attr.name), "namespace_prefix": to_namespace_prefix(attr.name) }
1010
}
1111

12-
12+
# check that attribute constant names do not collide
1313
deny contains attr_registry_collision(description, name) if {
1414
some i
1515
name := attribute_names[i].name
@@ -26,6 +26,7 @@ deny contains attr_registry_collision(description, name) if {
2626
description := sprintf("Attribute '%s' has the same constant name '%s' as '%s'.", [name, const_name, collisions])
2727
}
2828

29+
# check that attribute names do not collide with namespaces
2930
deny contains attr_registry_collision(description, name) if {
3031
some i
3132
name := attribute_names[i].name
@@ -38,7 +39,19 @@ deny contains attr_registry_collision(description, name) if {
3839
]
3940
count(collisions) > 0
4041
# TODO (https://github.com/open-telemetry/weaver/issues/279): provide other violation properties once weaver supports it.
41-
description := sprintf("Attribute '%s' is used as a namespace in '%s'.", [name, collisions])
42+
description := sprintf("Attribute '%s' name is used as a namespace in the following attributes '%s'.", [name, collisions])
43+
}
44+
45+
# check that attribute is not defined or referenced more than once within the same group
46+
deny contains attr_registry_collision(description, name) if {
47+
group := input.groups[_]
48+
attr := group.attributes[_]
49+
name := attr.name
50+
51+
collisions := [n | n := group.attributes[_].name; n == name ]
52+
count(collisions) > 1
53+
54+
description := sprintf("Attribute '%s' is already defined in the group '%s'. Attributes must be unique.", [name, group.id])
4255
}
4356

4457
attr_registry_collision(description, attr_name) = violation if {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package after_resolution
2+
import future.keywords
3+
4+
test_fails_on_const_name_collision if {
5+
collision := {"groups": [
6+
{"id": "test1", "attributes": [{"name": "foo.bar.baz"}]},
7+
{"id": "test2", "attributes": [{"name": "foo.bar_baz"}]}
8+
]}
9+
# each attribute counts as a collision, so there are 2 collisions
10+
count(deny) == 2 with input as collision
11+
}
12+
13+
test_fails_on_namespace_collision if {
14+
collision := {"groups": [
15+
{"id": "test1", "attributes": [{"name": "foo.bar.baz"}]},
16+
{"id": "test2", "attributes": [{"name": "foo.bar"}]}
17+
]}
18+
count(deny) == 1 with input as collision
19+
}

policies/yaml_schema.rego

+61-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,52 @@
11
package before_resolution
22

3-
yaml_schema_violation(description, group, attr) = violation {
4-
violation := {
5-
"id": description,
6-
"type": "semconv_attribute",
7-
"category": "yaml_schema_violation",
8-
"attr": attr,
9-
"group": group,
10-
}
3+
# checks attribute name format
4+
deny[yaml_schema_violation(description, group.id, name)] {
5+
group := input.groups[_]
6+
attr := group.attributes[_]
7+
name := attr.id
8+
9+
not regex.match(name_regex, name)
10+
11+
description := sprintf("Attribute name '%s' is invalid. Attribute name %s", [name, invalid_name_helper])
12+
}
13+
14+
# checks metric name format
15+
deny[yaml_schema_violation(description, group.id, name)] {
16+
group := input.groups[_]
17+
name := group.metric_name
18+
19+
name != null
20+
not regex.match(name_regex, name)
21+
22+
description := sprintf("Metric name '%s' is invalid. Metric name %s'", [name, invalid_name_helper])
23+
}
24+
25+
# checks event name format
26+
deny[yaml_schema_violation(description, group.id, name)] {
27+
group := input.groups[_]
28+
group.type == "event"
29+
name := group.name
30+
31+
name != null
32+
not regex.match(name_regex, name)
33+
34+
description := sprintf("Event name '%s' is invalid. Event name %s'", [name, invalid_name_helper])
35+
}
36+
37+
# checks attribute member id format
38+
deny[yaml_schema_violation(description, group.id, attr_name)] {
39+
group := input.groups[_]
40+
attr := group.attributes[_]
41+
attr_name := attr.id
42+
name := attr.type.members[_].id
43+
44+
not regex.match(name_regex, name)
45+
46+
description := sprintf("Member id '%s' on attribute '%s' is invalid. Member id %s'", [name, attr_name, invalid_name_helper])
1147
}
1248

49+
# check that attribute is fully qualified with their id, prefix is no longer supported
1350
deny[yaml_schema_violation(description, group.id, "")] {
1451
group := input.groups[_]
1552

@@ -19,3 +56,19 @@ deny[yaml_schema_violation(description, group.id, "")] {
1956
# TODO (https://github.com/open-telemetry/weaver/issues/279): provide other violation properties once weaver supports it.
2057
description := sprintf("Group '%s' uses prefix '%s'. All attribute should be fully qualified with their id, prefix is no longer supported.", [group.id, group.prefix])
2158
}
59+
60+
yaml_schema_violation(description, group, attr) = violation {
61+
violation := {
62+
"id": description,
63+
"type": "semconv_attribute",
64+
"category": "yaml_schema_violation",
65+
"attr": attr,
66+
"group": group,
67+
}
68+
}
69+
70+
# not valid: '1foo.bar', 'foo.bar.', 'foo.bar_', 'foo..bar', 'foo._bar' ...
71+
# valid: 'foo.bar', 'foo.1bar', 'foo.1_bar'
72+
name_regex := "^[a-z][a-z0-9]*([._][a-z0-9]+)*$"
73+
74+
invalid_name_helper := "must consist of lowercase alphanumeric characters separated by '_' and '.'"

policies/yaml_schema_test.rego

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package before_resolution
2+
3+
import future.keywords
4+
5+
test_fails_on_invalid_attribute_name if {
6+
every name in invalid_names {
7+
count(deny) == 1 with input as {"groups": create_attribute_group(name)}
8+
}
9+
}
10+
11+
test_fails_on_invalid_metric_name if {
12+
every name in invalid_names {
13+
count(deny) == 1 with input as {"groups": create_metric(name)}
14+
}
15+
}
16+
17+
test_fails_on_invalid_event_name if {
18+
every name in invalid_names {
19+
count(deny) == 1 with input as {"groups": create_event(name)}
20+
}
21+
}
22+
23+
test_passes_on_valid_names if {
24+
every name in valid_names {
25+
count(deny) == 0 with input as {"groups": create_attribute_group(name)}
26+
count(deny) == 0 with input as {"groups": create_metric(name)}
27+
count(deny) == 0 with input as {"groups": create_event(name)}
28+
}
29+
}
30+
31+
test_fails_if_prefix_is_present if {
32+
count(deny) == 1 with input as {"groups": [{"id": "test", "prefix": "foo"}]}
33+
}
34+
35+
create_attribute_group(attr) = json {
36+
json := [{"id": "yaml_schema.test", "attributes": [{"id": attr}]}]
37+
}
38+
39+
create_metric(name) = json {
40+
json := [{"id": "yaml_schema.test", "type": "metric", "metric_name": name}]
41+
}
42+
43+
create_event(name) = json {
44+
json := [{"id": "yaml_schema.test", "type": "event", "name": name}]
45+
}
46+
47+
invalid_names := [
48+
"1foo.bar",
49+
"_foo.bar",
50+
".foo.bar",
51+
"foo.bar_",
52+
"foo.bar.",
53+
"foo..bar",
54+
"foo._bar",
55+
"foo__bar",
56+
"foo_.bar",
57+
"foo,bar",
58+
"fü.bär",
59+
]
60+
61+
valid_names := [
62+
"foo.bar",
63+
"foo.1bar",
64+
"foo_1.bar",
65+
"foo.bar.baz",
66+
"foo.bar_baz",
67+
]

0 commit comments

Comments
 (0)