Skip to content

Commit

Permalink
Update semconv tool with stability fields and MD render. (#43)
Browse files Browse the repository at this point in the history
* Update semconv tool with stability fields and MD render.

* Bump version
  • Loading branch information
thisthat authored May 12, 2021
1 parent e72e0c2 commit 95d152e
Show file tree
Hide file tree
Showing 24 changed files with 785 additions and 91 deletions.
5 changes: 5 additions & 0 deletions semantic-conventions/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Please update the changelog as part of any significant pull request.

## v0.4.0

- Add stability fields. (#35)
- Add Markdown render for code generation.

## v0.3.1

- Fix markdown generator for int enums. (#36)
Expand Down
4 changes: 2 additions & 2 deletions semantic-conventions/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ ruamel.yaml.clib==0.2.0
typed-ast==1.4.1
typing-extensions==3.7.4.2
Jinja2==2.11.2

pytest==6.1.1
ipdb==0.13.4
ipdb==0.13.4
mistune==2.0.0a6
39 changes: 36 additions & 3 deletions semantic-conventions/src/opentelemetry/semconv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from opentelemetry.semconv.templating.code import CodeRenderer

from opentelemetry.semconv.templating.markdown import MarkdownRenderer
from opentelemetry.semconv.templating.markdown.options import MarkdownOptions


def parse_semconv(args, parser) -> SemanticConventionSet:
Expand Down Expand Up @@ -73,10 +74,16 @@ def main():


def process_markdown(semconv, args):
exclude = exclude_file_list(args.markdown_root, args.exclude)
md_renderer = MarkdownRenderer(
args.markdown_root, semconv, exclude, args.md_break_conditional, args.md_check
options = MarkdownOptions(
check_only=args.md_check,
enable_stable=args.md_stable,
enable_experimental=args.md_experimental,
enable_deprecated=args.md_enable_deprecated,
use_badge=args.md_use_badges,
break_count=args.md_break_conditional,
exclude_files=exclude_file_list(args.markdown_root, args.exclude),
)
md_renderer = MarkdownRenderer(args.markdown_root, semconv, options)
md_renderer.render_md()


Expand Down Expand Up @@ -155,6 +162,32 @@ def add_md_parser(subparsers):
required=False,
action="store_true",
)
parser.add_argument(
"--md-use-badges",
help="Use stability badges instead of labels for attributes.",
required=False,
action="store_true",
)
parser.add_argument(
"--md-stable",
help="Add labels to attributes marked as stable.",
required=False,
action="store_true",
)
parser.add_argument(
"--md-experimental",
help="Add labels to attributes marked as experimental.",
required=False,
action="store_true",
)
parser.add_argument(
"--md-disable-deprecated",
help="Removes deprecated notes of deprecated attributes.",
required=False,
default=True,
dest="md_enable_deprecated",
action="store_false",
)


def setup_parser():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@ class Required(Enum):
NO = 3


class StabilityLevel(Enum):
STABLE = 1
EXPERIMENTAL = 2
DEPRECATED = 3


class HasAttributes:
def _set_attributes(self, prefix, node):
self.attrs_by_name = SemanticAttribute.parse(prefix, node.get("attributes"))
def _set_attributes(self, prefix, stability, node):
self.attrs_by_name = SemanticAttribute.parse(
prefix, stability, node.get("attributes")
)

@property
def attributes(self):
Expand All @@ -64,6 +72,7 @@ class SemanticAttribute:
brief: str
examples: List[Union[str, int, bool]]
tag: str
stability: StabilityLevel
deprecated: str
required: Required
required_msg: str
Expand All @@ -88,7 +97,7 @@ def is_enum(self):
return isinstance(self.attr_type, EnumAttributeType)

@staticmethod
def parse(prefix, yaml_attributes):
def parse(prefix, semconv_stability, yaml_attributes):
""" This method parses the yaml representation for semantic attributes
creating the respective SemanticAttribute objects.
"""
Expand All @@ -101,6 +110,7 @@ def parse(prefix, yaml_attributes):
"ref",
"tag",
"deprecated",
"stability",
"required",
"sampling_relevant",
"note",
Expand All @@ -112,20 +122,20 @@ def parse(prefix, yaml_attributes):
validate_values(attribute, allowed_keys)
attr_id = attribute.get("id")
ref = attribute.get("ref")
position = attribute.lc.data[list(attribute)[0]]
position_data = attribute.lc.data
position = position_data[next(iter(attribute))]
if attr_id is None and ref is None:
msg = "At least one of id or ref is required."
raise ValidationError.from_yaml_pos(position, msg)
if attr_id is not None:
validate_id(attr_id, attribute.lc.data["id"])
validate_id(attr_id, position_data["id"])
attr_type, brief, examples = SemanticAttribute.parse_id(attribute)
fqn = "{}.{}".format(prefix, attr_id)
attr_id = attr_id.strip()
else:
# Ref
attr_type = None
if "type" in attribute:
position = attribute.lc.data[list(attribute)[0]]
msg = "Ref attribute '{}' must not declare a type".format(ref)
raise ValidationError.from_yaml_pos(position, msg)
brief = attribute.get("brief")
Expand All @@ -144,56 +154,66 @@ def parse(prefix, yaml_attributes):
required = Required.CONDITIONAL
required_msg = required_val.get("conditional", None)
if required_msg is None:
position = attribute.lc.data["required"]
position = position_data["required"]
msg = "Missing message for conditional required field!"
raise ValidationError.from_yaml_pos(position, msg)
else:
required = required_value_map.get(required_val)
if required == Required.CONDITIONAL:
position = attribute.lc.data["required"]
position = position_data["required"]
msg = "Missing message for conditional required field!"
raise ValidationError.from_yaml_pos(position, msg)
if required is None:
position = attribute.lc.data["required"]
position = position_data["required"]
msg = "Value '{}' for required field is not allowed".format(
required_val
)
raise ValidationError.from_yaml_pos(position, msg)
tag = attribute.get("tag", "").strip()
deprecated = attribute.get("deprecated")
if deprecated is not None:
if AttributeType.get_type(deprecated) != "string" or deprecated == "":
position = attribute.lc.data["deprecated"]
msg = (
"Deprecated field expects a string that specify why the attribute is deprecated and/or what"
" to use instead! "
)
raise ValidationError.from_yaml_pos(position, msg)
deprecated = deprecated.strip()
stability, deprecated = SemanticAttribute.parse_stability_deprecated(
attribute.get("stability"), attribute.get("deprecated"), position_data
)
if (
semconv_stability == StabilityLevel.DEPRECATED
and stability is not StabilityLevel.DEPRECATED
):
position = (
position_data["stability"]
if "stability" in position_data
else position_data["deprecated"]
)
msg = "Semantic convention stability set to deprecated but attribute '{}' is {}".format(
attr_id, stability
)
raise ValidationError.from_yaml_pos(position, msg)
stability = stability or semconv_stability or StabilityLevel.STABLE
sampling_relevant = (
AttributeType.to_bool("sampling_relevant", attribute)
if attribute.get("sampling_relevant")
else False
)
note = attribute.get("note", "")
fqn = fqn.strip()
parsed_brief = TextWithLinks(brief.strip() if brief else "")
parsed_note = TextWithLinks(note.strip())
attr = SemanticAttribute(
fqn=fqn,
attr_id=attr_id,
ref=ref,
attr_type=attr_type,
brief=brief.strip() if brief else "",
brief=parsed_brief,
examples=examples,
tag=tag,
deprecated=deprecated,
stability=stability,
required=required,
required_msg=str(required_msg).strip(),
sampling_relevant=sampling_relevant,
note=note.strip(),
note=parsed_note,
position=position,
)
if attr.fqn in attributes:
position = attribute.lc.data[list(attribute)[0]]
position = position_data[list(attribute)[0]]
msg = (
"Attribute id "
+ fqn
Expand Down Expand Up @@ -253,6 +273,48 @@ def parse_id(attribute):
AttributeType.check_examples_type(attr_type, examples, zlass)
return attr_type, str(brief), examples

@staticmethod
def parse_stability_deprecated(stability, deprecated, position_data):
if deprecated is not None and stability is None:
stability = "deprecated"
if deprecated is not None:
if stability is not None and stability != "deprecated":
position = position_data["deprecated"]
msg = "There is a deprecation message but the stability is set to '{}'".format(
stability
)
raise ValidationError.from_yaml_pos(position, msg)
if AttributeType.get_type(deprecated) != "string" or deprecated == "":
position = position_data["deprecated"]
msg = (
"Deprecated field expects a string that specifies why the attribute is deprecated and/or what"
" to use instead! "
)
raise ValidationError.from_yaml_pos(position, msg)
deprecated = deprecated.strip()
if stability is not None:
stability = SemanticAttribute.check_stability(
stability,
position_data["stability"]
if "stability" in position_data
else position_data["deprecated"],
)
return stability, deprecated

@staticmethod
def check_stability(stability_value, position):

stability_value_map = {
"deprecated": StabilityLevel.DEPRECATED,
"experimental": StabilityLevel.EXPERIMENTAL,
"stable": StabilityLevel.STABLE,
}
val = stability_value_map.get(stability_value)
if val is not None:
return val
msg = "Value '{}' is not allowed as a stability marker".format(stability_value)
raise ValidationError.from_yaml_pos(position, msg)

def equivalent_to(self, other: "SemanticAttribute"):
if self.attr_id is not None:
if self.fqn == other.fqn:
Expand Down Expand Up @@ -395,7 +457,9 @@ def parse(attribute_type):
for member in attribute_type["members"]:
validate_values(member, allowed_keys, mandatory_keys)
if not EnumAttributeType.is_valid_enum_value(member["value"]):
raise ValidationError(0, 0, "Invalid value used in enum: <{}>".format(member["value"]))
raise ValidationError(
0, 0, "Invalid value used in enum: <{}>".format(member["value"])
)
members.append(
EnumMember(
member_id=member["id"],
Expand All @@ -419,3 +483,43 @@ class EnumMember:
value: str
brief: str
note: str


class MdLink:
text: str
url: str

def __init__(self, text, url):
self.text = text
self.url = url

def __str__(self):
return "[{}]({})".format(self.text, self.url)


class TextWithLinks(str):
parts: List[Union[str, MdLink]]
raw_text: str
md_link = re.compile("\[([^\[\]]+)\]\(([^)]+)")

def __init__(self, text):
super().__init__()
self.raw_text = text
self.parts = []
last_position = 0
for match in self.md_link.finditer(text):
prev_text = text[last_position : match.start()]
link = MdLink(match.group(1), match.group(2))
if prev_text:
self.parts.append(prev_text)
self.parts.append(link)
last_position = match.end() + 1
last_part = text[last_position:]
if last_part:
self.parts.append(last_part)

def __str__(self):
str_list = []
for elm in self.parts:
str_list.append(elm.__str__())
return "".join(str_list)
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from opentelemetry.semconv.model.semantic_attribute import (
HasAttributes,
SemanticAttribute,
StabilityLevel,
Required,
unique_attributes,
)
Expand Down Expand Up @@ -116,6 +117,12 @@ def __init__(self, group):
self.semconv_id = self.id
self.note = group.get("note", "").strip()
self.prefix = group.get("prefix", "").strip()
stability = group.get("stability")
deprecated = group.get("deprecated")
position_data = group.lc.data
self.stability, self.deprecated = SemanticAttribute.parse_stability_deprecated(
stability, deprecated, position_data
)
self.extends = group.get("extends", "").strip()
self.constraints = parse_constraints(group.get("constraints", ()))

Expand Down Expand Up @@ -187,14 +194,15 @@ class ResourceSemanticConvention(HasAttributes, BaseSemanticConvention):
"brief",
"note",
"prefix",
"stability",
"extends",
"attributes",
"constraints",
)

def __init__(self, group):
super().__init__(group)
self._set_attributes(self.prefix, group)
self._set_attributes(self.prefix, self.stability, group)


class SpanSemanticConvention(HasAttributes, BaseSemanticConvention):
Expand All @@ -206,6 +214,7 @@ class SpanSemanticConvention(HasAttributes, BaseSemanticConvention):
"brief",
"note",
"prefix",
"stability",
"extends",
"span_kind",
"attributes",
Expand All @@ -214,7 +223,7 @@ class SpanSemanticConvention(HasAttributes, BaseSemanticConvention):

def __init__(self, group):
super().__init__(group)
self._set_attributes(self.prefix, group)
self._set_attributes(self.prefix, self.stability, group)
self.span_kind = SpanKind.parse(group.get("span_kind"))
if self.span_kind is None:
position = group.lc.data["span_kind"]
Expand Down
Loading

0 comments on commit 95d152e

Please sign in to comment.