Skip to content

Commit

Permalink
Enable rendering of "template" attributes in table generation (open-t…
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexanderWert authored Aug 28, 2023
1 parent 6f3e715 commit 0546bb1
Show file tree
Hide file tree
Showing 17 changed files with 423 additions and 41 deletions.
3 changes: 3 additions & 0 deletions semantic-conventions/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Please update the changelog as part of any significant pull request.

## Unreleased

- Render template-type attributes from yaml files
([#186](https://github.com/open-telemetry/build-tools/pull/186))

## v0.20.0

- Change default stability level to experimental
Expand Down
10 changes: 9 additions & 1 deletion semantic-conventions/semconv.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,15 @@
"string[]",
"int[]",
"double[]",
"boolean[]"
"boolean[]",
"template[string]",
"template[int]",
"template[double]",
"template[boolean]",
"template[string[]]",
"template[int[]]",
"template[double[]]",
"template[boolean[]]"
],
"description": "literal denoting the type"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
validate_values,
)

TEMPLATE_PREFIX = "template["
TEMPLATE_SUFFIX = "]"


class RequirementLevel(Enum):
REQUIRED = 1
Expand Down Expand Up @@ -74,6 +77,10 @@ def import_attribute(self):
def inherit_attribute(self):
return replace(self, inherited=True)

@property
def instantiated_type(self):
return AttributeType.get_instantiated_type(self.attr_type)

@property
def is_local(self):
return not self.imported and not self.inherited
Expand Down Expand Up @@ -117,7 +124,9 @@ def parse(
raise ValidationError.from_yaml_pos(position, msg)
if attr_id is not None:
validate_id(attr_id, position_data["id"])
attr_type, brief, examples = SemanticAttribute.parse_id(attribute)
attr_type, brief, examples = SemanticAttribute.parse_attribute(
attribute
)
if prefix:
fqn = f"{prefix}.{attr_id}"
else:
Expand Down Expand Up @@ -231,7 +240,7 @@ def parse(
return attributes

@staticmethod
def parse_id(attribute):
def parse_attribute(attribute):
check_no_missing_keys(attribute, ["type", "brief"])
attr_val = attribute["type"]
try:
Expand All @@ -242,14 +251,9 @@ def parse_id(attribute):
position = attribute.lc.data["type"]
raise ValidationError.from_yaml_pos(position, e.message) from e
brief = attribute["brief"]
zlass = (
AttributeType.type_mapper(attr_type)
if isinstance(attr_type, str)
else "enum"
)

examples = attribute.get("examples")
is_simple_type = AttributeType.is_simple_type(attr_type)
is_template_type = AttributeType.is_template_type(attr_type)
# if we are an array, examples must already be an array
if (
is_simple_type
Expand All @@ -263,19 +267,32 @@ def parse_id(attribute):
# TODO: If validation fails later, this will crash when trying to access position data
# since a list, contrary to a CommentedSeq, does not have position data
examples = [examples]
if is_simple_type and attr_type not in (
"boolean",
"boolean[]",
"int",
"int[]",
"double",
"double[]",
if is_template_type or (
is_simple_type
and attr_type
not in (
"boolean",
"boolean[]",
"int",
"int[]",
"double",
"double[]",
)
):
if not examples:
position = attribute.lc.data[list(attribute)[0]]
msg = f"Empty examples for {attr_type} are not allowed"
raise ValidationError.from_yaml_pos(position, msg)

if is_template_type:
return attr_type, str(brief), examples

zlass = (
AttributeType.type_mapper(attr_type)
if isinstance(attr_type, str)
else "enum"
)

# TODO: Implement type check for enum examples or forbid them
if examples is not None and is_simple_type:
AttributeType.check_examples_type(attr_type, examples, zlass)
Expand Down Expand Up @@ -358,6 +375,29 @@ def is_simple_type(attr_type: str):
"boolean[]",
)

@staticmethod
def is_template_type(attr_type: str):
if not isinstance(attr_type, str):
return False

return (
attr_type.startswith(TEMPLATE_PREFIX)
and attr_type.endswith(TEMPLATE_SUFFIX)
and AttributeType.is_simple_type(
attr_type[len(TEMPLATE_PREFIX) : len(attr_type) - len(TEMPLATE_SUFFIX)]
)
)

@staticmethod
def get_instantiated_type(attr_type: str):
if AttributeType.is_template_type(attr_type):
return attr_type[
len(TEMPLATE_PREFIX) : len(attr_type) - len(TEMPLATE_SUFFIX)
]
if AttributeType.is_simple_type(attr_type):
return attr_type
return "enum"

@staticmethod
def type_mapper(attr_type: str):
type_mapper = {
Expand Down Expand Up @@ -432,7 +472,9 @@ def parse(attribute_type):
otherwise it returns the basic type as string.
"""
if isinstance(attribute_type, str):
if AttributeType.is_simple_type(attribute_type):
if AttributeType.is_simple_type(
attribute_type
) or AttributeType.is_template_type(attribute_type):
return attribute_type
# Wrong type used - raise the exception and fill the missing data in the parent
raise ValidationError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
import typing
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Tuple, Union
from typing import Dict, Optional, Tuple, Union

from ruamel.yaml import YAML

from opentelemetry.semconv.model.constraints import AnyOf, Include, parse_constraints
from opentelemetry.semconv.model.exceptions import ValidationError
from opentelemetry.semconv.model.semantic_attribute import (
AttributeType,
RequirementLevel,
SemanticAttribute,
unique_attributes,
Expand Down Expand Up @@ -111,10 +112,26 @@ class BaseSemanticConvention(ValidatableYamlNode):

@property
def attributes(self):
return self._get_attributes(False)

@property
def attribute_templates(self):
return self._get_attributes(True)

@property
def attributes_and_templates(self):
return self._get_attributes(None)

def _get_attributes(self, templates: Optional[bool]):
if not hasattr(self, "attrs_by_name"):
return []

return list(self.attrs_by_name.values())
return [
attr
for attr in self.attrs_by_name.values()
if templates is None
or templates == AttributeType.is_template_type(attr.attr_type)
]

def __init__(self, group):
super().__init__(group)
Expand Down Expand Up @@ -565,6 +582,12 @@ def attributes(self):
output.extend(semconv.attributes)
return output

def attribute_templates(self):
output = []
for semconv in self.models.values():
output.extend(semconv.attribute_templates)
return output


CONVENTION_CLS_BY_GROUP_TYPE = {
cls.GROUP_TYPE_NAME: cls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def get_data_single_file(
"template": template_path,
"semconvs": semconvset.models,
"attributes": semconvset.attributes(),
"attribute_templates": semconvset.attribute_templates(),
}
data.update(self.parameters)
return data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from opentelemetry.semconv.model.constraints import AnyOf, Include
from opentelemetry.semconv.model.semantic_attribute import (
AttributeType,
EnumAttributeType,
EnumMember,
RequirementLevel,
Expand Down Expand Up @@ -97,11 +98,11 @@ def to_markdown_attr(
"""
This method renders attributes as markdown table entry
"""
name = self.render_attribute_id(attribute.fqn)
name = self.render_fqn_for_attribute(attribute)
attr_type = (
"enum"
if isinstance(attribute.attr_type, EnumAttributeType)
else attribute.attr_type
else AttributeType.get_instantiated_type(attribute.attr_type)
)
description = ""
if attribute.deprecated and self.options.enable_deprecated:
Expand Down Expand Up @@ -184,14 +185,16 @@ def to_markdown_attribute_table(
):
attr_to_print = []
for attr in sorted(
semconv.attributes, key=lambda a: "" if a.ref is None else a.ref
semconv.attributes_and_templates,
key=lambda a: "" if a.ref is None else a.ref,
):
if self.render_ctx.group_key is not None:
if attr.tag == self.render_ctx.group_key:
attr_to_print.append(attr)
continue
if self.render_ctx.is_full or attr.is_local:
attr_to_print.append(attr)

if self.render_ctx.group_key is not None and not attr_to_print:
raise ValueError(
f"No attributes retained for '{semconv.semconv_id}' filtering by '{self.render_ctx.group_key}'"
Expand Down Expand Up @@ -275,7 +278,7 @@ def to_creation_time_attributes(
)

for attr in sampling_relevant_attrs:
output.write("* " + self.render_attribute_id(attr.fqn) + "\n")
output.write("* " + self.render_fqn_for_attribute(attr) + "\n")

@staticmethod
def to_markdown_unit_table(members, output: io.StringIO):
Expand Down Expand Up @@ -325,19 +328,37 @@ def to_markdown_enum(self, output: io.StringIO):
if notes:
output.write("\n")

def render_fqn_for_attribute(self, attribute):
rel_path = self.get_attr_reference_relative_path(attribute.fqn)
name = attribute.fqn
if AttributeType.is_template_type(attribute.attr_type):
name = f"{attribute.fqn}.<key>"

if rel_path is not None:
return f"[`{name}`]({rel_path})"
return f"`{name}`"

def render_attribute_id(self, attribute_id):
"""
Method to render in markdown an attribute id. If the id points to an attribute in another rendered table, a
markdown link is introduced.
"""
rel_path = self.get_attr_reference_relative_path(attribute_id)
if rel_path is not None:
return f"[`{attribute_id}`]({rel_path})"
return f"`{attribute_id}`"

def get_attr_reference_relative_path(self, attribute_id):
md_file = self.filename_for_attr_fqn.get(attribute_id)
if md_file:
path = PurePath(self.render_ctx.current_md)
if path.as_posix() != PurePath(md_file).as_posix():
diff = PurePath(os.path.relpath(md_file, start=path.parent)).as_posix()
if diff != ".":
return f"[`{attribute_id}`]({diff})"
return f"`{attribute_id}`"
rel_path = PurePath(
os.path.relpath(md_file, start=path.parent)
).as_posix()
if rel_path != ".":
return rel_path
return None

def to_markdown_constraint(
self,
Expand Down Expand Up @@ -390,7 +411,9 @@ def _create_attribute_location_dict(self):
)
a: SemanticAttribute
valid_attr = (
a for a in semconv.attributes if a.is_local and not a.ref
a
for a in semconv.attributes_and_templates
if a.is_local and not a.ref
)
for attr in valid_attr:
m[attr.fqn] = md
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

package io.opentelemetry.instrumentation.api.attributetemplates;

class AttributesTemplate {

/**
* this is the description of the first attribute template
*/
public static final AttributeKey<String> ATTRIBUTE_TEMPLATE_ONE = stringKey("attribute_template_one");

/**
* this is the description of the second attribute template. It's a number.
*/
public static final AttributeKey<Long> ATTRIBUTE_TEMPLATE_TWO = longKey("attribute_template_two");

/**
* this is the description of the third attribute template. It's a boolean.
*/
public static final AttributeKey<Boolean> ATTRIBUTE_THREE = booleanKey("attribute_three");
}
Loading

0 comments on commit 0546bb1

Please sign in to comment.