diff --git a/semantic-conventions/CHANGELOG.md b/semantic-conventions/CHANGELOG.md index 0dc67ed4..a8b5810a 100644 --- a/semantic-conventions/CHANGELOG.md +++ b/semantic-conventions/CHANGELOG.md @@ -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) diff --git a/semantic-conventions/requirements.txt b/semantic-conventions/requirements.txt index e55c0210..6201369e 100644 --- a/semantic-conventions/requirements.txt +++ b/semantic-conventions/requirements.txt @@ -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 \ No newline at end of file +ipdb==0.13.4 +mistune==2.0.0a6 \ No newline at end of file diff --git a/semantic-conventions/src/opentelemetry/semconv/main.py b/semantic-conventions/src/opentelemetry/semconv/main.py index 5fa9f4b1..e40fd13d 100644 --- a/semantic-conventions/src/opentelemetry/semconv/main.py +++ b/semantic-conventions/src/opentelemetry/semconv/main.py @@ -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: @@ -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() @@ -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(): diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py index 6ea5de4a..5a2e11ae 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py @@ -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): @@ -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 @@ -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. """ @@ -101,6 +110,7 @@ def parse(prefix, yaml_attributes): "ref", "tag", "deprecated", + "stability", "required", "sampling_relevant", "note", @@ -112,12 +122,13 @@ 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() @@ -125,7 +136,6 @@ def parse(prefix, yaml_attributes): # 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") @@ -144,32 +154,39 @@ 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") @@ -177,23 +194,26 @@ def parse(prefix, yaml_attributes): ) 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 @@ -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: @@ -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"], @@ -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) diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py index ce7902ea..627a2af8 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py @@ -25,6 +25,7 @@ from opentelemetry.semconv.model.semantic_attribute import ( HasAttributes, SemanticAttribute, + StabilityLevel, Required, unique_attributes, ) @@ -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", ())) @@ -187,6 +194,7 @@ class ResourceSemanticConvention(HasAttributes, BaseSemanticConvention): "brief", "note", "prefix", + "stability", "extends", "attributes", "constraints", @@ -194,7 +202,7 @@ class ResourceSemanticConvention(HasAttributes, BaseSemanticConvention): 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): @@ -206,6 +214,7 @@ class SpanSemanticConvention(HasAttributes, BaseSemanticConvention): "brief", "note", "prefix", + "stability", "extends", "span_kind", "attributes", @@ -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"] diff --git a/semantic-conventions/src/opentelemetry/semconv/templating/code.py b/semantic-conventions/src/opentelemetry/semconv/templating/code.py index a8019a35..4d147783 100644 --- a/semantic-conventions/src/opentelemetry/semconv/templating/code.py +++ b/semantic-conventions/src/opentelemetry/semconv/templating/code.py @@ -16,13 +16,96 @@ import os.path import re import typing +import mistune from jinja2 import Environment, FileSystemLoader, select_autoescape +from opentelemetry.semconv.model.semantic_attribute import TextWithLinks from opentelemetry.semconv.model.semantic_convention import SemanticConventionSet from opentelemetry.semconv.model.utils import ID_RE +def render_markdown( + txt: str, + html=True, + link=None, + image=None, + emphasis=None, + strong=None, + inline_html=None, + paragraph=None, + heading=None, + block_code=None, + block_quote=None, + list=None, + list_item=None, + code=None, +): + class CustomRender(mistune.HTMLRenderer): + def link(self, url, text=None, title=None): + if link: + return link.format(url, text, title) + return super().link(url, text, title) if html else url + + def image(self, src, alt="", title=None): + if image: + return image.format(src, alt, title) + return super().image(src, alt, title) if html else src + + def emphasis(self, text): + if emphasis: + return emphasis.format(text) + return super().emphasis(text) if html else text + + def strong(self, text): + if strong: + return strong.format(text) + return super().strong(text) if html else text + + def inline_html(self, html_text): + if inline_html: + return inline_html.format(html_text) + return super().inline_html(html_text) if html else html_text + + def paragraph(self, text): + if paragraph: + return paragraph.format(text) + return super().paragraph(text) if html else text + + def heading(self, text, level): + if heading: + return heading.format(text, level) + return super().heading(text, level) if html else text + + def block_code(self, code, info=None): + if block_code: + return block_code.format(code) + return super().block_code(code, info) if html else code + + def block_quote(self, text): + if block_quote: + return block_quote.format(text) + return super().block_quote(text) + + def list(self, text, ordered, level, start=None): + if list: + return list.format(text) + return super().list(text, ordered, level, start) if html else text + + def list_item(self, text, level): + if list_item: + return list_item.format(text) + return super().list_item(text, level) if html else text + + def codespan(self, text): + if code: + return code.format(text) + return super().codespan(text) if html else text + + markdown = mistune.create_markdown(renderer=CustomRender()) + return markdown(txt) + + def to_doc_brief(doc_string: typing.Optional[str]) -> str: if doc_string is None: return "" @@ -32,6 +115,29 @@ def to_doc_brief(doc_string: typing.Optional[str]) -> str: return doc_string +def to_html_links(doc_string: typing.Optional[typing.Union[str, TextWithLinks]]) -> str: + if doc_string is None: + return "" + if isinstance(doc_string, TextWithLinks): + str_list = [] + for elm in doc_string.parts: + if isinstance(elm, str): + str_list.append(elm) + else: + str_list.append('{}'.format(elm.url, elm.text)) + doc_string = "".join(str_list) + doc_string = doc_string.strip() + if doc_string.endswith("."): + return doc_string[:-1] + return doc_string + + +def regex_replace(text: str, pattern: str, replace: str): + # convert standard dollar notation to python + replace = re.sub(r"\$", r"\\", replace) + return re.sub(pattern, replace, text, 0, re.U) + + def merge(list: typing.List, elm): return list.extend(elm) @@ -93,6 +199,9 @@ def setup_environment(env: Environment): env.filters["to_const_name"] = to_const_name env.filters["merge"] = merge env.filters["to_camelcase"] = to_camelcase + env.filters["to_html_links"] = to_html_links + env.filters["regex_replace"] = regex_replace + env.filters["render_markdown"] = render_markdown @staticmethod def prefix_output_file(file_name, pattern, semconv): diff --git a/semantic-conventions/src/opentelemetry/semconv/templating/markdown.py b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py similarity index 91% rename from semantic-conventions/src/opentelemetry/semconv/templating/markdown.py rename to semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py index 30aec6cb..2deda4f8 100644 --- a/semantic-conventions/src/opentelemetry/semconv/templating/markdown.py +++ b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py @@ -23,6 +23,7 @@ from opentelemetry.semconv.model.constraints import AnyOf, Include from opentelemetry.semconv.model.semantic_attribute import ( SemanticAttribute, + StabilityLevel, EnumAttributeType, Required, EnumMember, @@ -33,21 +34,14 @@ ) from opentelemetry.semconv.model.utils import ID_RE +from opentelemetry.semconv.templating.markdown.options import MarkdownOptions -class RenderContext: - is_full: bool - is_remove_constraint: bool - group_key: str - break_counter: int - enums: list - notes: list - current_md: str - def __init__(self, break_count): +class RenderContext: + def __init__(self): self.is_full = False self.is_remove_constraint = False self.group_key = "" - self.break_count = break_count self.enums = [] self.notes = [] self.units = [] @@ -78,24 +72,19 @@ class MarkdownRenderer: table_headers = "| Attribute | Type | Description | Examples | Required |\n|---|---|---|---|---|\n" def __init__( - self, - md_folder, - semconvset: SemanticConventionSet, - exclude: list = [], - break_count=default_break_conditional_labels, - check_only=False, + self, md_folder, semconvset: SemanticConventionSet, options=MarkdownOptions() ): - self.render_ctx = RenderContext(break_count) + self.options = options + self.render_ctx = RenderContext() self.semconvset = semconvset # We load all markdown files to render self.file_names = sorted( set(glob.glob("{}/**/*.md".format(md_folder), recursive=True)) - - set(exclude) + - set(options.exclude_files) ) # We build the dict that maps each attribute that has to be rendered to the latest visited file # that contains it self.filename_for_attr_fqn = self._create_attribute_location_dict() - self.check_only = check_only def to_markdown_attr( self, attribute: SemanticAttribute, output: io.StringIO, @@ -110,11 +99,27 @@ def to_markdown_attr( else attribute.attr_type ) description = "" - if attribute.deprecated: + if attribute.deprecated and self.options.enable_deprecated: if "deprecated" in attribute.deprecated.lower(): description = "**{}**
".format(attribute.deprecated) else: - description = "**Deprecated: {}**
".format(attribute.deprecated) + deprecated_msg = self.options.md_snippet_by_stability_level[ + StabilityLevel.DEPRECATED + ].format(attribute.deprecated) + description = "{}
".format(deprecated_msg) + elif ( + attribute.stability == StabilityLevel.STABLE and self.options.enable_stable + ): + description = "{}
".format( + self.options.md_snippet_by_stability_level[StabilityLevel.STABLE] + ) + elif ( + attribute.stability == StabilityLevel.EXPERIMENTAL + and self.options.enable_experimental + ): + description = "{}
".format( + self.options.md_snippet_by_stability_level[StabilityLevel.EXPERIMENTAL] + ) description += attribute.brief if attribute.note: self.render_ctx.add_note(attribute.note) @@ -138,13 +143,15 @@ def to_markdown_attr( example_list = attribute.examples if attribute.examples else [] # check for array types if attribute.attr_type.endswith("[]"): - examples = "`[" + ", ".join("{}".format(ex) for ex in example_list) + "]`" + examples = ( + "`[" + ", ".join("{}".format(ex) for ex in example_list) + "]`" + ) else: examples = "; ".join("`{}`".format(ex) for ex in example_list) if attribute.required == Required.ALWAYS: required = "Yes" elif attribute.required == Required.CONDITIONAL: - if len(attribute.required_msg) < self.render_ctx.break_count: + if len(attribute.required_msg) < self.options.break_count: required = attribute.required_msg else: # We put the condition in the notes after the table @@ -273,7 +280,7 @@ def render_md(self): content = md_file.read() output = io.StringIO() self._render_single_file(content, md_filename, output) - if self.check_only: + if self.options.check_only: if content != output.getvalue(): sys.exit( "File " @@ -283,7 +290,7 @@ def render_md(self): else: with open(md_filename, "w", encoding="utf-8") as md_file: md_file.write(output.getvalue()) - if self.check_only: + if self.options.check_only: print("{} files left unchanged.".format(len(self.file_names))) def _create_attribute_location_dict(self): diff --git a/semantic-conventions/src/opentelemetry/semconv/templating/markdown/options.py b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/options.py new file mode 100644 index 00000000..64dd6848 --- /dev/null +++ b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/options.py @@ -0,0 +1,48 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass, field +from typing import List + +from opentelemetry.semconv.model.semantic_attribute import StabilityLevel + + +@dataclass() +class MarkdownOptions: + + _badge_map = { + StabilityLevel.DEPRECATED: "![Deprecated](https://img.shields.io/badge/-deprecated-red)", + StabilityLevel.EXPERIMENTAL: "![Experimental](https://img.shields.io/badge/-experimental-blue)", + StabilityLevel.STABLE: "![Stable](https://img.shields.io/badge/-stable-lightgreen)", + } + + _label_map = { + StabilityLevel.DEPRECATED: "**Deprecated: {}**", + StabilityLevel.EXPERIMENTAL: "**Experimental**", + StabilityLevel.STABLE: "**Stable**", + } + + check_only: bool = False + enable_stable: bool = False + enable_experimental: bool = False + enable_deprecated: bool = True + use_badge: bool = False + break_count: int = 50 + exclude_files: List[str] = field(default_factory=list) + + @property + def md_snippet_by_stability_level(self): + if self.use_badge: + return self._badge_map + return self._label_map diff --git a/semantic-conventions/src/opentelemetry/semconv/version.py b/semantic-conventions/src/opentelemetry/semconv/version.py index 2555f9fe..17f4e2f3 100644 --- a/semantic-conventions/src/opentelemetry/semconv/version.py +++ b/semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.1" +__version__ = "0.4.0" diff --git a/semantic-conventions/src/tests/data/markdown/stability/badges_expected.md b/semantic-conventions/src/tests/data/markdown/stability/badges_expected.md new file mode 100644 index 00000000..7912b241 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/stability/badges_expected.md @@ -0,0 +1,10 @@ +# Common Attributes + + +| Attribute | Type | Description | Examples | Required | +|---|---|---|---|---| +| [`test.exp_attr`](labels_expected.md) | boolean | | | Yes | +| [`test.stable_attr`](labels_expected.md) | boolean | ![Stable](https://img.shields.io/badge/-stable-lightgreen)
| | Yes | +| [`test.deprecated_attr`](labels_expected.md) | boolean | | | Yes | +| [`test.def_stability`](labels_expected.md) | boolean | ![Stable](https://img.shields.io/badge/-stable-lightgreen)
| | Yes | + diff --git a/semantic-conventions/src/tests/data/markdown/stability/input.md b/semantic-conventions/src/tests/data/markdown/stability/input.md new file mode 100644 index 00000000..0109ba90 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/stability/input.md @@ -0,0 +1,5 @@ +# Common Attributes + + + + diff --git a/semantic-conventions/src/tests/data/markdown/stability/labels_expected.md b/semantic-conventions/src/tests/data/markdown/stability/labels_expected.md new file mode 100644 index 00000000..5d19fb6a --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/stability/labels_expected.md @@ -0,0 +1,10 @@ +# Common Attributes + + +| Attribute | Type | Description | Examples | Required | +|---|---|---|---|---| +| [`test.exp_attr`](labels_expected.md) | boolean | | | Yes | +| [`test.stable_attr`](labels_expected.md) | boolean | | | Yes | +| [`test.deprecated_attr`](labels_expected.md) | boolean | | | Yes | +| [`test.def_stability`](labels_expected.md) | boolean | | | Yes | + diff --git a/semantic-conventions/src/tests/data/markdown/stability/stability.yaml b/semantic-conventions/src/tests/data/markdown/stability/stability.yaml new file mode 100644 index 00000000..47f416f9 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/stability/stability.yaml @@ -0,0 +1,25 @@ +groups: + - id: test + type: span + brief: 'test' + prefix: test + attributes: + - id: exp_attr + type: boolean + required: always + stability: experimental + brief: "" + - id: stable_attr + type: boolean + required: always + stability: stable + brief: "" + - id: deprecated_attr + type: boolean + required: always + stability: deprecated + brief: "" + - id: def_stability + type: boolean + required: always + brief: "" diff --git a/semantic-conventions/src/tests/data/yaml/errors/stability/semconv_stability_deprecated.yaml b/semantic-conventions/src/tests/data/yaml/errors/stability/semconv_stability_deprecated.yaml new file mode 100644 index 00000000..80e447eb --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/errors/stability/semconv_stability_deprecated.yaml @@ -0,0 +1,13 @@ +groups: + - id: test + type: span + brief: 'test' + prefix: http + stability: deprecated + attributes: + - id: test_attr + type: boolean + required: always + stability: stable + brief: "" + diff --git a/semantic-conventions/src/tests/data/yaml/errors/stability/stability_deprecated.yaml b/semantic-conventions/src/tests/data/yaml/errors/stability/stability_deprecated.yaml new file mode 100644 index 00000000..ed0bbc60 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/errors/stability/stability_deprecated.yaml @@ -0,0 +1,13 @@ +groups: + - id: test + type: span + brief: 'test' + prefix: http + attributes: + - id: test_attr + type: boolean + required: always + stability: stable + deprecated: should fail. + brief: "" + diff --git a/semantic-conventions/src/tests/data/yaml/errors/stability/wrong_value.yaml b/semantic-conventions/src/tests/data/yaml/errors/stability/wrong_value.yaml new file mode 100644 index 00000000..0575474f --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/errors/stability/wrong_value.yaml @@ -0,0 +1,12 @@ +groups: + - id: test + type: span + brief: 'test' + prefix: http + attributes: + - id: test_attr + type: boolean + required: always + stability: will_fail + brief: "" + diff --git a/semantic-conventions/src/tests/data/yaml/errors/wrong_double_type.yaml b/semantic-conventions/src/tests/data/yaml/errors/wrong_double_type.yaml index 1660732f..2cd74fbf 100644 --- a/semantic-conventions/src/tests/data/yaml/errors/wrong_double_type.yaml +++ b/semantic-conventions/src/tests/data/yaml/errors/wrong_double_type.yaml @@ -8,4 +8,4 @@ groups: - id: one type: double brief: it contains a float number. - examples: [12, 1f, 1.0] + examples: [12, 1f, 1.0] \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/yaml/links.yaml b/semantic-conventions/src/tests/data/yaml/links.yaml new file mode 100644 index 00000000..2fc449e9 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/links.yaml @@ -0,0 +1,22 @@ +groups: + - id: http + type: span + brief: 'test' + prefix: http + note: test + attributes: + - id: none + type: boolean + brief: 'simple text' + - id: single + type: boolean + brief: 'text [eg](https://opentelemetry.io/)' + - id: double + type: boolean + brief: 'text1 [eg](https://opentelemetry.io/) text2 [eg](https://opentelemetry.io/) end' + - id: start + type: boolean + brief: '[eg](https://opentelemetry.io/) text' + - id: multiple_end + type: boolean + brief: 'text1 [eg](https://opentelemetry.io/) text2 [eg](https://opentelemetry.io/)' diff --git a/semantic-conventions/src/tests/data/yaml/numeric_attributes.yml b/semantic-conventions/src/tests/data/yaml/numeric_attributes.yml index 7d60a33d..5752370d 100644 --- a/semantic-conventions/src/tests/data/yaml/numeric_attributes.yml +++ b/semantic-conventions/src/tests/data/yaml/numeric_attributes.yml @@ -18,4 +18,4 @@ groups: brief: > this is the description of the second attribute. It's a number. - examples: [12.01, 1.0] + examples: [12.01, 1.0] \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/yaml/stability.yaml b/semantic-conventions/src/tests/data/yaml/stability.yaml new file mode 100644 index 00000000..280e8c34 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/stability.yaml @@ -0,0 +1,106 @@ +groups: + - id: test + type: span + brief: 'test' + prefix: http + attributes: + - id: exp_attr + type: boolean + required: always + stability: experimental + brief: "" + - id: stable_attr + type: boolean + required: always + stability: stable + brief: "" + - id: deprecated_attr + type: boolean + required: always + stability: deprecated + brief: "" + - id: def_stability + type: boolean + required: always + brief: "" + + - id: parent_default + type: span + brief: 'test' + prefix: http + stability: experimental + attributes: + - id: test_attr + type: boolean + required: always + brief: "" + - id: dep + type: boolean + required: always + deprecated: should not fail. + brief: "" + + - id: not_fail + type: span + brief: 'test' + prefix: http + stability: deprecated + attributes: + - id: test_attr + type: boolean + required: always + deprecated: should not fail. + brief: "" + + - id: resource_test + type: resource + brief: 'test' + prefix: http + attributes: + - id: exp_attr + type: boolean + required: always + stability: experimental + brief: "" + - id: stable_attr + type: boolean + required: always + stability: stable + brief: "" + - id: deprecated_attr + type: boolean + required: always + stability: deprecated + brief: "" + - id: def_stability + type: boolean + required: always + brief: "" + + - id: resource_parent_default + type: resource + brief: 'test' + prefix: http + stability: experimental + attributes: + - id: test_attr + type: boolean + required: always + brief: "" + - id: dep + type: boolean + required: always + deprecated: should not fail. + brief: "" + + - id: resource_not_fail + type: resource + brief: 'test' + prefix: http + stability: deprecated + attributes: + - id: test_attr + type: boolean + required: always + deprecated: should not fail. + brief: "" \ No newline at end of file diff --git a/semantic-conventions/src/tests/semconv/model/test_correct_parse.py b/semantic-conventions/src/tests/semconv/model/test_correct_parse.py index 576ba040..b906f1c4 100644 --- a/semantic-conventions/src/tests/semconv/model/test_correct_parse.py +++ b/semantic-conventions/src/tests/semconv/model/test_correct_parse.py @@ -16,7 +16,10 @@ import unittest from opentelemetry.semconv.model.constraints import Include -from opentelemetry.semconv.model.semantic_attribute import SemanticAttribute +from opentelemetry.semconv.model.semantic_attribute import ( + SemanticAttribute, + StabilityLevel, +) from opentelemetry.semconv.model.semantic_convention import ( parse_semantic_convention_groups, SemanticConventionSet, @@ -241,6 +244,16 @@ def test_rpc(self): } self.semantic_convention_check(list(semconv.models.values())[2], expected) + def test_markdown_link(self): + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("yaml/links.yaml")) + semconv.finish() + self.assertEqual(len(semconv.models), 1) + s = list(semconv.models.values())[0] + for attr in s.attributes: + brief = attr.brief + self.assertEqual(brief.raw_text, brief.__str__()) + # This fails until ONE-36916 is not addressed def test_ref(self): semconv = SemanticConventionSet(debug=False) @@ -369,6 +382,52 @@ def test_deprecation(self): ) self.assertIsNone(list(semconv.models.values())[0].attributes[3].deprecated) + def test_stability(self): + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("yaml/stability.yaml")) + semconv.finish() + self.assertEqual(len(semconv.models), 6) + + model = list(semconv.models.values())[0] + self.assertEqual(len(model.attributes), 4) + self.assertEqual(model.stability, None) + + attr = model.attributes[0] + self.assertEqual(attr.attr_id, "exp_attr") + self.assertEqual(attr.stability, StabilityLevel.EXPERIMENTAL) + + attr = model.attributes[1] + self.assertEqual(attr.attr_id, "stable_attr") + self.assertEqual(attr.stability, StabilityLevel.STABLE) + + attr = model.attributes[2] + self.assertEqual(attr.attr_id, "deprecated_attr") + self.assertEqual(attr.stability, StabilityLevel.DEPRECATED) + + attr = model.attributes[3] + self.assertEqual(attr.attr_id, "def_stability") + self.assertEqual(attr.stability, StabilityLevel.STABLE) + + model = list(semconv.models.values())[1] + self.assertEqual(len(model.attributes), 2) + self.assertEqual(model.stability, StabilityLevel.EXPERIMENTAL) + + attr = model.attributes[0] + self.assertEqual(attr.attr_id, "test_attr") + self.assertEqual(attr.stability, StabilityLevel.EXPERIMENTAL) + + attr = model.attributes[1] + self.assertEqual(attr.attr_id, "dep") + self.assertEqual(attr.stability, StabilityLevel.DEPRECATED) + + model = list(semconv.models.values())[2] + self.assertEqual(len(model.attributes), 1) + self.assertEqual(model.stability, StabilityLevel.DEPRECATED) + + attr = model.attributes[0] + self.assertEqual(attr.attr_id, "test_attr") + self.assertEqual(attr.stability, StabilityLevel.DEPRECATED) + def test_populate_other_attributes(self): semconv = SemanticConventionSet(debug=False) semconv.parse(self.load_file("yaml/http.yaml")) diff --git a/semantic-conventions/src/tests/semconv/model/test_error_detection.py b/semantic-conventions/src/tests/semconv/model/test_error_detection.py index bae466e6..36607707 100644 --- a/semantic-conventions/src/tests/semconv/model/test_error_detection.py +++ b/semantic-conventions/src/tests/semconv/model/test_error_detection.py @@ -130,6 +130,35 @@ def test_invalid_key_in_constraint(self): self.assertIn("myNewCnst", msg) self.assertEqual(e.line, 10) + def test_invalid_stability(self): + with self.assertRaises(ValidationError) as ex: + self.open_yaml("yaml/errors/stability/wrong_value.yaml") + self.fail() + e = ex.exception + msg = e.message.lower() + self.assertIn("is not allowed as a stability marker", msg) + self.assertEqual(e.line, 10) + + def test_invalid_stability_with_deprecated(self): + with self.assertRaises(ValidationError) as ex: + self.open_yaml("yaml/errors/stability/stability_deprecated.yaml") + self.fail() + e = ex.exception + msg = e.message.lower() + self.assertIn("there is a deprecation message but the stability is set to", msg) + self.assertEqual(e.line, 11) + + def test_invalid_semconv_stability_with_deprecated(self): + with self.assertRaises(ValidationError) as ex: + self.open_yaml("yaml/errors/stability/semconv_stability_deprecated.yaml") + self.fail() + e = ex.exception + msg = e.message.lower() + self.assertIn( + "semantic convention stability set to deprecated but attribute", msg + ) + self.assertEqual(e.line, 11) + def test_invalid_deprecated_empty_string(self): with self.assertRaises(ValidationError) as ex: self.open_yaml("yaml/errors/deprecated/deprecation_empty_string.yaml") @@ -137,7 +166,7 @@ def test_invalid_deprecated_empty_string(self): e = ex.exception msg = e.message.lower() self.assertIn( - "a string that specify why the attribute is deprecated and/or what to use instead!", + "a string that specifies why the attribute is deprecated and/or what to use instead!", msg, ) self.assertEqual(e.line, 10) @@ -149,7 +178,7 @@ def test_invalid_deprecated_boolean(self): e = ex.exception msg = e.message.lower() self.assertIn( - "a string that specify why the attribute is deprecated and/or what to use instead!", + "a string that specifies why the attribute is deprecated and/or what to use instead!", msg, ) self.assertEqual(e.line, 10) @@ -161,7 +190,7 @@ def test_invalid_deprecated_number(self): e = ex.exception msg = e.message.lower() self.assertIn( - "a string that specify why the attribute is deprecated and/or what to use instead!", + "a string that specifies why the attribute is deprecated and/or what to use instead!", msg, ) self.assertEqual(e.line, 10) diff --git a/semantic-conventions/src/tests/semconv/model/test_semantic_attribute.py b/semantic-conventions/src/tests/semconv/model/test_semantic_attribute.py index a5dcb34a..c493127d 100644 --- a/semantic-conventions/src/tests/semconv/model/test_semantic_attribute.py +++ b/semantic-conventions/src/tests/semconv/model/test_semantic_attribute.py @@ -17,7 +17,7 @@ def test_parse(load_yaml): yaml = load_yaml("semantic_attributes.yml") - attributes = SemanticAttribute.parse("prefix", yaml.get("attributes")) + attributes = SemanticAttribute.parse("prefix", "", yaml.get("attributes")) assert len(attributes) == 3 @@ -39,7 +39,7 @@ def test_parse(load_yaml): def test_parse_deprecated(load_yaml): yaml = load_yaml("semantic_attributes_deprecated.yml") - attributes = SemanticAttribute.parse("", yaml.get("attributes")) + attributes = SemanticAttribute.parse("", "", yaml.get("attributes")) assert len(attributes) == 1 assert list(attributes.keys()) == [".deprecated_attribute"] diff --git a/semantic-conventions/src/tests/semconv/templating/test_markdown.py b/semantic-conventions/src/tests/semconv/templating/test_markdown.py index ea897387..d4f47065 100644 --- a/semantic-conventions/src/tests/semconv/templating/test_markdown.py +++ b/semantic-conventions/src/tests/semconv/templating/test_markdown.py @@ -17,6 +17,7 @@ import unittest from opentelemetry.semconv.model.semantic_convention import SemanticConventionSet +from opentelemetry.semconv.templating.markdown.options import MarkdownOptions from opentelemetry.semconv.templating.markdown import MarkdownRenderer @@ -74,6 +75,40 @@ def testDeprecated(self): expected, ) + def testStability(self): + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("markdown/stability/stability.yaml")) + semconv.finish() + self.assertEqual(len(semconv.models), 1) + with open(self.load_file("markdown/stability/input.md"), "r") as markdown: + content = markdown.read() + # Labels + with open( + self.load_file("markdown/stability/labels_expected.md"), "r" + ) as markdown: + expected = markdown.read() + self.check_render( + semconv, + "markdown/stability/", + "markdown/stability/input.md", + content, + expected, + ) + # Badges + with open( + self.load_file("markdown/stability/badges_expected.md"), "r" + ) as markdown: + expected = markdown.read() + options = MarkdownOptions(enable_stable=True, use_badge=True) + self.check_render( + semconv, + "markdown/stability/", + "markdown/stability/input.md", + content, + expected, + options, + ) + def testSingle(self): semconv = SemanticConventionSet(debug=False) semconv.parse(self.load_file("markdown/single/http.yaml")) @@ -85,11 +120,7 @@ def testSingle(self): with open(self.load_file("markdown/single/expected.md"), "r") as markdown: expected = markdown.read() self.check_render( - semconv, - "markdown/single/", - "markdown/single/input.md", - content, - expected, + semconv, "markdown/single/", "markdown/single/input.md", content, expected, ) def testEmpty(self): @@ -113,10 +144,16 @@ def testExampleArray(self): self.assertEqual(len(semconv.models), 1) with open(self.load_file("markdown/example_array/input.md"), "r") as markdown: content = markdown.read() - with open(self.load_file("markdown/example_array/expected.md"), "r") as markdown: + with open( + self.load_file("markdown/example_array/expected.md"), "r" + ) as markdown: expected = markdown.read() self.check_render( - semconv, "markdown/example_array/", "markdown/example_array/input.md", content, expected + semconv, + "markdown/example_array/", + "markdown/example_array/input.md", + content, + expected, ) def testMultiple(self): @@ -160,9 +197,13 @@ def testExtendConstraint(self): semconv.parse(self.load_file("markdown/extend_constraint/general.yaml")) semconv.finish() self.assertEqual(len(semconv.models), 7) - with open(self.load_file("markdown/extend_constraint/input.md"), "r") as markdown: + with open( + self.load_file("markdown/extend_constraint/input.md"), "r" + ) as markdown: content = markdown.read() - with open(self.load_file("markdown/extend_constraint/expected.md"), "r") as markdown: + with open( + self.load_file("markdown/extend_constraint/expected.md"), "r" + ) as markdown: expected = markdown.read() self.check_render( semconv, @@ -181,7 +222,9 @@ def test_error_missing_end(self): with open(self.load_file("markdown/missing_end_tag/input.md"), "r") as markdown: content = markdown.read() with self.assertRaises(Exception) as ex: - renderer = MarkdownRenderer(self.load_file("markdown/missing_end_tag/"), semconv) + renderer = MarkdownRenderer( + self.load_file("markdown/missing_end_tag/"), semconv + ) renderer._render_single_file( content, "markdown/missing_end_tag/input.md", io.StringIO() ) @@ -193,10 +236,14 @@ def test_error_wrong_id(self): semconv.parse(self.load_file("markdown/wrong_semconv_id/general.yaml")) semconv.finish() self.assertEqual(len(semconv.models), 5) - with open(self.load_file("markdown/wrong_semconv_id/input.md"), "r") as markdown: + with open( + self.load_file("markdown/wrong_semconv_id/input.md"), "r" + ) as markdown: content = markdown.read() with self.assertRaises(Exception) as ex: - renderer = MarkdownRenderer(self.load_file("markdown/wrong_semconv_id/"), semconv) + renderer = MarkdownRenderer( + self.load_file("markdown/wrong_semconv_id/"), semconv + ) renderer._render_single_file( content, "markdown/wrong_semconv_id/input.md", io.StringIO() ) @@ -230,7 +277,9 @@ def test_parameter_full(self): self.assertEqual(len(semconv.models), 7) with open(self.load_file("markdown/parameter_full/input.md"), "r") as markdown: content = markdown.read() - with open(self.load_file("markdown/parameter_full/expected.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_full/expected.md"), "r" + ) as markdown: expected = markdown.read() self.check_render( semconv, @@ -248,7 +297,9 @@ def test_parameter_tag(self): self.assertEqual(len(semconv.models), 6) with open(self.load_file("markdown/parameter_tag/input.md"), "r") as markdown: content = markdown.read() - with open(self.load_file("markdown/parameter_tag/expected.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_tag/expected.md"), "r" + ) as markdown: expected = markdown.read() self.check_render( semconv, @@ -264,9 +315,13 @@ def test_parameter_tag_empty(self): semconv.parse(self.load_file("markdown/parameter_tag_empty/general.yaml")) semconv.finish() self.assertEqual(len(semconv.models), 6) - with open(self.load_file("markdown/parameter_tag_empty/input.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_tag_empty/input.md"), "r" + ) as markdown: content = markdown.read() - with open(self.load_file("markdown/parameter_tag_empty/expected.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_tag_empty/expected.md"), "r" + ) as markdown: expected = markdown.read() self.check_render( semconv, @@ -303,11 +358,17 @@ def test_parameter_tag_no_attr(self): def test_parameter_remove_constraint(self): semconv = SemanticConventionSet(debug=False) - semconv.parse(self.load_file("markdown/parameter_remove_constraint/database.yaml")) - semconv.parse(self.load_file("markdown/parameter_remove_constraint/general.yaml")) + semconv.parse( + self.load_file("markdown/parameter_remove_constraint/database.yaml") + ) + semconv.parse( + self.load_file("markdown/parameter_remove_constraint/general.yaml") + ) semconv.finish() self.assertEqual(len(semconv.models), 6) - with open(self.load_file("markdown/parameter_remove_constraint/input.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_remove_constraint/input.md"), "r" + ) as markdown: content = markdown.read() with open( self.load_file("markdown/parameter_remove_constraint/expected.md"), "r" @@ -330,7 +391,9 @@ def test_parameter_empty(self): self.assertEqual(len(semconv.models), 7) with open(self.load_file("markdown/parameter_empty/input.md"), "r") as markdown: content = markdown.read() - with open(self.load_file("markdown/parameter_empty/expected.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_empty/expected.md"), "r" + ) as markdown: expected = markdown.read() self.check_render( semconv, @@ -371,7 +434,9 @@ def test_wrong_syntax(self): semconv.parse(self.load_file("markdown/parameter_wrong_syntax/general.yaml")) semconv.finish() self.assertEqual(len(semconv.models), 7) - with open(self.load_file("markdown/parameter_wrong_syntax/input.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_wrong_syntax/input.md"), "r" + ) as markdown: content = markdown.read() expected = "" with self.assertRaises(ValueError) as ex: @@ -394,7 +459,9 @@ def test_wrong_duplicate(self): semconv.parse(self.load_file("markdown/parameter_wrong_duplicate/general.yaml")) semconv.finish() self.assertEqual(len(semconv.models), 7) - with open(self.load_file("markdown/parameter_wrong_duplicate/input.md"), "r") as markdown: + with open( + self.load_file("markdown/parameter_wrong_duplicate/input.md"), "r" + ) as markdown: content = markdown.read() expected = "" with self.assertRaises(ValueError) as ex: @@ -425,11 +492,19 @@ def test_units(self): "markdown/metrics/", "markdown/metrics/units_input.md", content, - expected - ) + expected, + ) - def check_render(self, semconv, folder, file_name, content: str, expected: str): - renderer = MarkdownRenderer(self.load_file(folder), semconv) + def check_render( + self, + semconv, + folder, + file_name, + content: str, + expected: str, + options=MarkdownOptions(), + ): + renderer = MarkdownRenderer(self.load_file(folder), semconv, options) output = io.StringIO() renderer._render_single_file(content, self.load_file(file_name), output) result = output.getvalue() @@ -439,7 +514,7 @@ def check_render(self, semconv, folder, file_name, content: str, expected: str): _TEST_DIR = os.path.dirname(__file__) def read_file(self, filename): - with open(self.load_file(filename), 'r') as test_file: + with open(self.load_file(filename), "r") as test_file: return test_file.read() def load_file(self, filename):