diff --git a/.github/workflows/semconvgen.yml b/.github/workflows/semconvgen.yml index 28a42a8f..16ede1c5 100644 --- a/.github/workflows/semconvgen.yml +++ b/.github/workflows/semconvgen.yml @@ -13,7 +13,7 @@ jobs: tests: runs-on: ubuntu-latest defaults: - run: + run: working-directory: semantic-conventions/ steps: - uses: actions/checkout@v3 @@ -39,7 +39,7 @@ jobs: run: pylint *.py src/ - name: Type checking (mypy) run: mypy src/ - + build-and-publish-docker: runs-on: ubuntu-latest @@ -68,3 +68,13 @@ jobs: else tag_and_push "${GITHUB_REF#"refs/tags/"}" fi + - name: Push the Dev Docker image + if: startsWith(github.ref, 'refs/heads/feature/') + run: | + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + function tag_and_push { + docker tag semconvgen "otel/semconvgen:${1}" && docker push "otel/semconvgen:${1}" + } + TAG="${GITHUB_REF#"refs/heads/"}" + TAG="${TAG/"/"/"-"}" + tag_and_push "${TAG}" diff --git a/semantic-conventions/CHANGELOG.md b/semantic-conventions/CHANGELOG.md index 153d7587..e608dace 100644 --- a/semantic-conventions/CHANGELOG.md +++ b/semantic-conventions/CHANGELOG.md @@ -4,6 +4,11 @@ Please update the changelog as part of any significant pull request. ## Unreleased +- Added code-generation mode that groups attributes by the root namespace and ability to write each group into individual file. + [BREAKING] The `--file-per-group ` that used to create multiple directories (like `output//file`) now generates + multiple files (`output/file`) instead. + ([#243](https://github.com/open-telemetry/build-tools/pull/243)) + ## v0.23.0 - Rephrase and relax sampling-relevant description @@ -17,7 +22,7 @@ Please update the changelog as part of any significant pull request. ([#205](https://github.com/open-telemetry/build-tools/pull/205)) - Fix referencing template attributes ([#206](https://github.com/open-telemetry/build-tools/pull/206)) - + ## v0.21.0 - Render template-type attributes from yaml files diff --git a/semantic-conventions/README.md b/semantic-conventions/README.md index 9d1bc5a7..5ebdba88 100644 --- a/semantic-conventions/README.md +++ b/semantic-conventions/README.md @@ -85,8 +85,8 @@ convention that have the tag `network`. `` will print the constraints and attributes of both `http` and `http.server` semantic conventions that have the tag `network`. -`` will print a table describing a single metric -`http.server.active_requests`. +`` will print a table describing a single metric +`http.server.active_requests`. ## Code Generator @@ -125,12 +125,70 @@ This way, multiple files are generated. The value of `pattern` can be one of the - `semconv_id`: The id of the semantic convention. - `prefix`: The prefix with which all attributes starts with. - `extends`: The id of the parent semantic convention. +- `root_namespace`: The root namespace of attribute to group by. Finally, additional value can be passed to the template in form of `key=value` pairs separated by comma using the `--parameters [{key=value},]+` or `-D` flag. ### Customizing Jinja's Whitespace Control -The image also supports customising +The image also supports customizing [Whitespace Control in Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/#whitespace-control) via the additional flag `--trim-whitespace`. Providing the flag will enable both `lstrip_blocks` and `trim_blocks`. + +### Accessing Semantic Conventions in the template + +When template is processed, it has access to a set of variables that depends on the `--file-per-group` value (or lack of it). +You can access properties of these variables and call Jinja or Python functions defined on them. + +#### Single file (no `--file-per-group` pattern is provided) + +Processes all parsed semantic conventions + +- `semconvs` - the dictionary containing parsed `BaseSemanticConvention` instances with semconv `id` as a key +- `attributes_and_templates` - the dictionary containing all attributes (including template ones) grouped by their root namespace. + Each element in the dictionary is a list of attributes that share the same root namespace. Attributes that don't have a namespace + appear under `""` key. +- `attributes` - the list of all attributes from all parsed semantic conventions. Does not include template attributes. +- `attribute_templates` - the list of all attribute templates from all parsed semantic conventions. + +#### The `root_namespace` pattern + +Processes a single namespace and is called for each namespace detected. + +- `attributes_and_templates` - the list containing all attributes (including template ones) in the given root namespace. +- `root_namespace` - the root namespace being processed. + +#### Other patterns + +Processes a single pattern value and is called for each distinct value. + +- `semconv` - the instance of parsed `BaseSemanticConvention` being processed. + +### Filtering and mapping + +Jinja templates has a notion of [filters](https://jinja.palletsprojects.com/en/2.11.x/templates/#list-of-builtin-filters) allowing to transform objects or filter lists. + +Semconvgen supports following additional filters to simplify common operations in templates. + +#### `SemanticAttribute` operations + +1. `is_definition` - Checks if the attribute is the original definition of the attribute and not a reference. +2. `is_deprecated` - Checks if the attribute is deprecated. The same check can also be done with `(attribute.stability | string()) == "StabilityLevel.DEPRECATED"` +3. `is_experimental` - Checks if the attribute is experimental. The same check can also be done with `(attribute.stability | string()) == "StabilityLevel.EXPERIMENTAL"` +4. `is_stable` - Checks if the attribute is experimental. The same check can also be done with `(attribute.stability | string()) == "StabilityLevel.STABLE"` +5. `is_template` - Checks if the attribute is a template attribute. + +#### String operations + +1. `first_up` - Upper-cases the first character in the string. Does not modify anything else +2. `regex_replace(text, pattern, replace)` - Makes regex-based replace in `text` string using `pattern`` +3. `to_camelcase` - Converts a string to camel case (using `.` and `_` as words delimiter in the original string). + The first character of every word is upper-cased, other characters are lower-cased. E.g. `foo.bAR_baz` becomes `fooBarBaz` +4. `to_const_name` - Converts a string to Python or Java constant name (SNAKE_CASE) replacing `.` or `-` with `_`. E.g. + `foo.bAR-baz` becomes `FOO_BAR_BAZ`. +5. `to_doc_brief` - Trims whitespace and removes dot at the end. E.g. ` Hello world.\t` becomes `Hello world` + +#### `BaseSemanticConvention` operations + +1. `is_metric` - Checks if semantic convention describes a metric. diff --git a/semantic-conventions/src/opentelemetry/semconv/main.py b/semantic-conventions/src/opentelemetry/semconv/main.py index ce144d94..e6331814 100644 --- a/semantic-conventions/src/opentelemetry/semconv/main.py +++ b/semantic-conventions/src/opentelemetry/semconv/main.py @@ -129,7 +129,8 @@ def add_code_parser(subparsers): parser.add_argument( "--output", "-o", - help="Specify the output file for the code generation.", + help="Specify the output file name for the code generation. " + "See also `--file-per-group` on how to generate multiple files.", type=str, required=True, ) @@ -143,8 +144,10 @@ def add_code_parser(subparsers): parser.add_argument( "--file-per-group", dest="pattern", - help="Each Semantic Convention is processed by the template and store in a different file. PATTERN is expected " - "to be the name of a SemanticConvention field and is prepended as a prefix to the output argument", + help="Semantic conventions are processed by the template and stored in a different file. " + "File names start with a 'pattern' and end with the name specified in the 'output' argument. " + "The 'pattern' can either match 'root_namespace' to group attributes by the root namespace or " + "match a name of Semantic Convention property which value will be used as a file name prefix.", type=str, ) parser.add_argument( diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py index f6df923d..f22a9bbd 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py @@ -60,6 +60,7 @@ class SemanticAttribute: sampling_relevant: bool note: str position: List[int] + root_namespace: str inherited: bool = False imported: bool = False @@ -203,6 +204,10 @@ def parse( fqn = fqn.strip() parsed_brief = TextWithLinks(brief.strip() if brief else "") parsed_note = TextWithLinks(note.strip()) + + namespaces = fqn.split(".") + root_namespace = namespaces[0] if len(namespaces) > 1 else "" + attr = SemanticAttribute( fqn=fqn, attr_id=attr_id, @@ -218,6 +223,7 @@ def parse( sampling_relevant=sampling_relevant, note=parsed_note, position=position, + root_namespace=root_namespace, ) if attr.fqn in attributes: position = position_data[list(attribute)[0]] @@ -318,7 +324,6 @@ def parse_stability_deprecated(stability, deprecated, position_data): @staticmethod def check_stability(stability_value, position): - stability_value_map = { "deprecated": StabilityLevel.DEPRECATED, "experimental": StabilityLevel.EXPERIMENTAL, diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py index 13ade651..9aafb1c8 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py @@ -264,6 +264,9 @@ def __init__(self, group): self.metric_name = group.get("metric_name") self.unit = group.get("unit") self.instrument = group.get("instrument") + + namespaces = self.metric_name.split(".") + self.root_namespace = namespaces[0] if len(namespaces) > 1 else "" self.validate() def validate(self): diff --git a/semantic-conventions/src/opentelemetry/semconv/templating/code.py b/semantic-conventions/src/opentelemetry/semconv/templating/code.py index b84a39fd..1685d967 100644 --- a/semantic-conventions/src/opentelemetry/semconv/templating/code.py +++ b/semantic-conventions/src/opentelemetry/semconv/templating/code.py @@ -21,10 +21,17 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape from opentelemetry.semconv.model.semantic_attribute import ( + AttributeType, RequirementLevel, + SemanticAttribute, + StabilityLevel, TextWithLinks, ) -from opentelemetry.semconv.model.semantic_convention import SemanticConventionSet +from opentelemetry.semconv.model.semantic_convention import ( + BaseSemanticConvention, + MetricSemanticConvention, + SemanticConventionSet, +) from opentelemetry.semconv.model.utils import ID_RE @@ -156,6 +163,36 @@ def to_camelcase(name: str, first_upper=False) -> str: return first + "".join(word.capitalize() for word in rest) +def first_up(name: str) -> str: + return name[0].upper() + name[1:] + + +def is_stable(obj: typing.Union[SemanticAttribute, BaseSemanticConvention]) -> bool: + return obj.stability == StabilityLevel.STABLE + + +def is_deprecated(obj: typing.Union[SemanticAttribute, BaseSemanticConvention]) -> bool: + return obj.stability == StabilityLevel.DEPRECATED + + +def is_experimental( + obj: typing.Union[SemanticAttribute, BaseSemanticConvention] +) -> bool: + return obj.stability is None or obj.stability == StabilityLevel.EXPERIMENTAL + + +def is_definition(attribute: SemanticAttribute) -> bool: + return attribute.is_local and attribute.ref is None + + +def is_template(attribute: SemanticAttribute) -> bool: + return AttributeType.is_template_type(str(attribute.attr_type)) + + +def is_metric(semconv: BaseSemanticConvention) -> bool: + return isinstance(semconv, MetricSemanticConvention) + + class CodeRenderer: pattern = f"{{{ID_RE.pattern}}}" @@ -188,6 +225,7 @@ def get_data_single_file( "semconvs": semconvset.models, "attributes": semconvset.attributes(), "attribute_templates": semconvset.attribute_templates(), + "attributes_and_templates": self._grouped_attribute_definitions(semconvset), } data.update(self.parameters) return data @@ -206,18 +244,30 @@ def setup_environment(env: Environment, trim_whitespace: bool): env.filters["to_const_name"] = to_const_name env.filters["merge"] = merge env.filters["to_camelcase"] = to_camelcase + env.filters["first_up"] = first_up env.filters["to_html_links"] = to_html_links env.filters["regex_replace"] = regex_replace env.filters["render_markdown"] = render_markdown + env.filters["is_deprecated"] = is_deprecated + env.filters["is_definition"] = is_definition + env.filters["is_stable"] = is_stable + env.filters["is_experimental"] = is_experimental + env.filters["is_template"] = is_template + env.filters["is_metric"] = is_metric + env.tests["is_stable"] = is_stable + env.tests["is_experimental"] = is_experimental + env.tests["is_deprecated"] = is_deprecated + env.tests["is_definition"] = is_definition + env.tests["is_template"] = is_template + env.tests["is_metric"] = is_metric env.trim_blocks = trim_whitespace env.lstrip_blocks = trim_whitespace @staticmethod - def prefix_output_file(file_name, pattern, semconv): + def prefix_output_file(file_name, prefix): basename = os.path.basename(file_name) dirname = os.path.dirname(file_name) - value = getattr(semconv, pattern) - return os.path.join(dirname, to_camelcase(value, True), basename) + return os.path.join(dirname, to_camelcase(prefix, True) + basename) def render( self, @@ -233,19 +283,97 @@ def render( autoescape=select_autoescape([""]), ) self.setup_environment(env, self.trim_whitespace) - if pattern: - for semconv in semconvset.models.values(): - output_name = self.prefix_output_file(output_file, pattern, semconv) - data = self.get_data_multiple_files(semconv, template_path) - template = env.get_template(file_name, globals=data) - template.globals["now"] = datetime.datetime.utcnow() - template.globals["version"] = os.environ.get("ARTIFACT_VERSION", "dev") - template.globals["RequirementLevel"] = RequirementLevel - template.stream(data).dump(output_name) + if pattern == "root_namespace": + self._render_group_by_root_namespace( + semconvset, template_path, file_name, output_file, env + ) + elif pattern is not None: + self._render_by_pattern( + semconvset, template_path, file_name, output_file, pattern, env + ) else: data = self.get_data_single_file(semconvset, template_path) template = env.get_template(file_name, globals=data) - template.globals["now"] = datetime.datetime.utcnow() - template.globals["version"] = os.environ.get("ARTIFACT_VERSION", "dev") - template.globals["RequirementLevel"] = RequirementLevel - template.stream(data).dump(output_file) + self._write_template_to_file(template, data, output_file) + + def _render_by_pattern( + self, + semconvset: SemanticConventionSet, + template_path: str, + file_name: str, + output_file: str, + pattern: str, + env: Environment, + ): + for semconv in semconvset.models.values(): + prefix = getattr(semconv, pattern) + output_name = self.prefix_output_file(output_file, prefix) + data = self.get_data_multiple_files(semconv, template_path) + template = env.get_template(file_name, globals=data) + self._write_template_to_file(template, data, output_name) + + def _render_group_by_root_namespace( + self, + semconvset: SemanticConventionSet, + template_path: str, + file_name: str, + output_file: str, + env: Environment, + ): + attribute_and_templates = self._grouped_attribute_definitions(semconvset) + metrics = self._grouped_metric_definitions(semconvset) + for ns, attribute_and_templates in attribute_and_templates.items(): + sanitized_ns = ns if ns != "" else "other" + output_name = self.prefix_output_file(output_file, sanitized_ns) + + data = { + "template": template_path, + "attributes_and_templates": attribute_and_templates, + "metrics": metrics.get(ns) or [], + "root_namespace": sanitized_ns, + } + data.update(self.parameters) + + template = env.get_template(file_name, globals=data) + self._write_template_to_file(template, data, output_name) + + def _grouped_attribute_definitions(self, semconvset): + grouped_attributes = {} + for semconv in semconvset.models.values(): + for attr in semconv.attributes_and_templates: + if not is_definition(attr): # skip references + continue + if attr.root_namespace not in grouped_attributes: + grouped_attributes[attr.root_namespace] = [] + grouped_attributes[attr.root_namespace].append(attr) + + for ns in grouped_attributes: + grouped_attributes[ns] = sorted(grouped_attributes[ns], key=lambda a: a.fqn) + return grouped_attributes + + def _grouped_metric_definitions(self, semconvset): + grouped_metrics = {} + for semconv in semconvset.models.values(): + if not is_metric(semconv): + continue + + if semconv.root_namespace not in grouped_metrics: + grouped_metrics[semconv.root_namespace] = [] + + grouped_metrics[semconv.root_namespace].append(semconv) + + for ns in grouped_metrics: + grouped_metrics[ns] = sorted( + grouped_metrics[ns], key=lambda a: a.metric_name + ) + return grouped_metrics + + def _write_template_to_file(self, template, data, output_name): + template.globals["now"] = datetime.datetime.utcnow() + template.globals["version"] = os.environ.get("ARTIFACT_VERSION", "dev") + template.globals["RequirementLevel"] = RequirementLevel + + content = template.render(data) + if content != "": + with open(output_name, "w", encoding="utf-8") as f: + f.write(content) diff --git a/semantic-conventions/src/tests/data/jinja/attribute_templates/template b/semantic-conventions/src/tests/data/jinja/attribute_templates/template index 486ddce2..d320e990 100644 --- a/semantic-conventions/src/tests/data/jinja/attribute_templates/template +++ b/semantic-conventions/src/tests/data/jinja/attribute_templates/template @@ -25,22 +25,13 @@ {%- elif type == "double" -%} doubleKey {%- else -%} - {{lowerFirst(type)}}Key + {{ type | to_camelcase(False)}}Key {%- endif -%} {%- endmacro %} -{%- macro print_value(type, value) -%} - {{ "\"" if type == "String"}}{{value}}{{ "\"" if type == "String"}} -{%- endmacro %} -{%- macro upFirst(text) -%} - {{ text[0]|upper}}{{text[1:] }} -{%- endmacro %} -{%- macro lowerFirst(text) -%} - {{ text[0]|lower}}{{text[1:] }} -{%- endmacro %} package io.opentelemetry.instrumentation.api.attributetemplates; class AttributesTemplate { -{%- for attribute_template in attribute_templates if attribute_template.is_local and not attribute_template.ref %} +{%- for attribute_template in attribute_templates | select("is_definition") %} /** * {{attribute_template.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} @@ -49,17 +40,17 @@ class AttributesTemplate { *

Notes:

    {{attribute_template.note | render_markdown(code="{{@code {0}}}", paragraph="
  • {0}
  • ", list="{0}")}}
{%- endif %} - {%- if (attribute_template.stability | string()) == "StabilityLevel.DEPRECATED" %} + {%- if attribute_template | is_deprecated %} * * @deprecated {{attribute_template.brief | to_doc_brief}}. {%- endif %} */ - {%- if (attribute_template.stability | string()) == "StabilityLevel.DEPRECATED" %} + {%- if attribute_template | is_deprecated %} @Deprecated {%- endif %} - public static final AttributeKey<{{upFirst(to_java_return_type(attribute_template.instantiated_type | string))}}> {{attribute_template.fqn | to_const_name}} = {{to_java_key_type(attribute_template.instantiated_type | string)}}("{{attribute_template.fqn}}"); + public static final AttributeKey<{{ to_java_return_type(attribute_template.instantiated_type | string) | to_camelcase(True)}}> {{attribute_template.fqn | to_const_name}} = {{to_java_key_type(attribute_template.instantiated_type | string)}}("{{attribute_template.fqn}}"); {%- endfor %} -{%- for attribute in attributes if attribute.is_local and not attribute.ref %} +{%- for attribute in attributes | select("is_definition") %} /** * {{attribute.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} @@ -68,14 +59,14 @@ class AttributesTemplate { *

Notes:

    {{attribute.note | render_markdown(code="{{@code {0}}}", paragraph="
  • {0}
  • ", list="{0}")}}
{%- endif %} - {%- if (attribute.stability | string()) == "StabilityLevel.DEPRECATED" %} + {%- if attribute | is_deprecated %} * * @deprecated {{attribute.brief | to_doc_brief}}. {%- endif %} */ - {%- if (attribute.stability | string()) == "StabilityLevel.DEPRECATED" %} + {%- if attribute | is_deprecated %} @Deprecated {%- endif %} - public static final AttributeKey<{{upFirst(to_java_return_type(attribute.instantiated_type | string))}}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + public static final AttributeKey<{{ to_java_return_type(attribute.instantiated_type | string) | to_camelcase(True)}}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); {%- endfor %} } diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/FirstAttributes.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/FirstAttributes.java new file mode 100644 index 00000000..9281a5f2 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/FirstAttributes.java @@ -0,0 +1,18 @@ +package io.opentelemetry.instrumentation.api.semconv; + +class FirstAttributes { + /** + * short description of attr_one + */ + public static final AttributeKey FIRST_ATTR_ONE = booleanKey("first.attr_one"); + + /** + * short description of attr_one_a + */ + public static final AttributeKey FIRST_ATTR_ONE_A = longKey("first.attr_one_a"); + + /** + * this is the description of attribute template + */ + public static final AttributeKeyTemplate FIRST_ATTR_TEMPLATE_ONE = stringKey("first.attr_template_one"); +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/SecondAttributes.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/SecondAttributes.java new file mode 100644 index 00000000..abc59440 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/SecondAttributes.java @@ -0,0 +1,8 @@ +package io.opentelemetry.instrumentation.api.semconv; + +class SecondAttributes { + /** + * short description of attr_two + */ + public static final AttributeKey SECOND_ATTR_TWO = stringKey("second.attr_two"); +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/ThirdAttributes.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/ThirdAttributes.java new file mode 100644 index 00000000..e4a9bca1 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/all/ThirdAttributes.java @@ -0,0 +1,13 @@ +package io.opentelemetry.instrumentation.api.semconv; + +class ThirdAttributes { + /** + * this is the description of attribute template + */ + public static final AttributeKeyTemplate THIRD_ATTR_TEMPLATE_THREE = stringKey("third.attr_template_three"); + + /** + * short description of attr_three + */ + public static final AttributeKey THIRD_ATTR_THREE = stringKey("third.attr_three"); +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes.yml b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes.yml new file mode 100644 index 00000000..f4a1a6cc --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes.yml @@ -0,0 +1,56 @@ +groups: + - id: first_group_id + type: attribute_group + brief: first description + prefix: first + attributes: + - id: attr_one + type: boolean + brief: short description of attr_one + - id: attr_template_one + type: template[string] + brief: > + this is the description of attribute template + examples: 'example' + + - id: second_group_id + brief: second description + prefix: second + span_kind: client + extends: first_group_id + attributes: + - id: attr_two + type: string + brief: short description of attr_two + examples: ['example_one', 'example_two'] + - id: first_group_part_two + type: resource + brief: first_a description + prefix: first + attributes: + - id: attr_one_a + type: int + brief: short description of attr_one_a + - ref: second.attr_two + - ref: third.attr_template_three + - id: third_group_id + brief: third description + prefix: third + attributes: + - id: attr_three + type: string + brief: short description of attr_three + examples: "3" + stability: stable + - id: attr_template_three + type: template[string] + brief: > + this is the description of attribute template + examples: 'example' + - id: forth_group_id + brief: forth description + attributes: + - id: attr_four + type: string + brief: short description of attr_four + examples: "4" diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/First.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/First.java new file mode 100644 index 00000000..7ec15b57 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/First.java @@ -0,0 +1,19 @@ +package io.opentelemetry.instrumentation.api.semconv; + +import io.opentelemetry.api.metrics.Meter; + +class First { + /** + * short description of attr_one + */ + public static final AttributeKey FIRST_ATTR_ONE = booleanKey("first.attr_one"); + /** + * first metric description + * Experimental: False + */ + public static final LongCounterBuilder createFirstMetric(Meter meter) { + return meter.counterBuilder("first.metric") + .setDescription("first metric description") + .setUnit("{one}"); + } +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/SecondGroup.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/SecondGroup.java new file mode 100644 index 00000000..20fbc0ed --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/SecondGroup.java @@ -0,0 +1,19 @@ +package io.opentelemetry.instrumentation.api.semconv; + +import io.opentelemetry.api.metrics.Meter; + +class SecondGroup { + /** + * short description of attr_two + */ + public static final AttributeKey SECOND_GROUP_ATTR_TWO = longKey("second_group.attr_two"); + /** + * second metric description + * Experimental: True + */ + public static final DoubleHistogramBuilder createSecondGroupMetric(Meter meter) { + return meter.histogramBuilder("second_group.metric") + .setDescription("second metric description") + .setUnit("s"); + } +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/semconv.yml b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/semconv.yml new file mode 100644 index 00000000..3d2c670b --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/semconv.yml @@ -0,0 +1,30 @@ +groups: + - id: first_group_id + type: attribute_group + brief: first description + prefix: first + attributes: + - id: attr_one + type: boolean + brief: short description of attr_one + + - id: first_metric_id + brief: first metric description + metric_name: first.metric + instrument: counter + type: metric + unit: "{one}" + stability: stable + extends: first_group_id + + - id: second_group_id + brief: second metric description + metric_name: second_group.metric + type: metric + instrument: histogram + unit: "s" + prefix: second_group + attributes: + - id: attr_two + type: int + brief: short description of attr_two diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/template b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/template new file mode 100644 index 00000000..f1cacd96 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/attributes_and_metrics/template @@ -0,0 +1,76 @@ +{%- macro to_java_return_type(type) -%} + {%- if type == "string" -%} + String + {%- elif type == "string[]" -%} + List + {%- elif type == "boolean" -%} + boolean + {%- elif type == "int" -%} + long + {%- elif type == "double" -%} + double + {%- else -%} + {{type}} + {%- endif -%} +{%- endmacro %} +{%- macro to_java_key_type(type) -%} + {%- if type == "string" -%} + stringKey + {%- elif type == "string[]" -%} + stringArrayKey + {%- elif type == "boolean" -%} + booleanKey + {%- elif type == "int" -%} + longKey + {%- elif type == "double" -%} + doubleKey + {%- else -%} + {{ type | to_camelcase(False)}}Key + {%- endif -%} +{%- endmacro %} +{%- macro to_java_instrument_builder_factory(instrument) -%} + {%- if instrument == "counter" -%} + counterBuilder + {%- elif instrument == "histogram" -%} + histogramBuilder + {%- elif instrument == "updowncounter" -%} + upDownCounterBuilder + {%- elif instrument == "gauge" -%} + gaugeBuilder + {%- endif -%} +{%- endmacro %} +{%- macro to_java_instrument_builder_type(instrument) -%} + {%- if instrument == "counter" -%} + LongCounterBuilder + {%- elif instrument == "histogram" -%} + DoubleHistogramBuilder + {%- elif instrument == "updowncounter" -%} + LongUpDownCounterBuilder + {%- elif instrument == "gauge" -%} + DoubleGaugeBuilder + {%- endif -%} +{%- endmacro %} +package io.opentelemetry.instrumentation.api.semconv; + +import io.opentelemetry.api.metrics.Meter; + +class {{ root_namespace | to_camelcase(True) }} { +{%- for attribute in attributes_and_templates %} + + /** + * {{attribute.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} + */ + public static final AttributeKey<{{ to_java_return_type(attribute.instantiated_type | string) | first_up }}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); +{% endfor %} +{%- for metric in metrics %} + /** + * {{metric.brief | to_doc_brief}} + * Experimental: {{ metric | is_experimental }} + */ + public static final {{ to_java_instrument_builder_type(metric.instrument) }} create{{metric.metric_name | to_camelcase(True)}}(Meter meter) { + return meter.{{to_java_instrument_builder_factory(metric.instrument)}}("{{ metric.metric_name }}") + .setDescription("{{ metric.brief }}") + .setUnit("{{ metric.unit }}"); + } +{% endfor %} +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/FooAttributes.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/FooAttributes.java new file mode 100644 index 00000000..b09c7dc5 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/FooAttributes.java @@ -0,0 +1,13 @@ +package io.opentelemetry.instrumentation.api.semconv; + +class FooAttributes { + /** + * short description of attr_one + */ + public static final AttributeKey FOO_ATTR_ONE = booleanKey("foo.attr_one"); + + /** + * short description of foo.attr_two + */ + public static final AttributeKey FOO_ATTR_TWO = stringKey("foo.attr_two"); +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/OtherAttributes.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/OtherAttributes.java new file mode 100644 index 00000000..9a7b38b2 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/OtherAttributes.java @@ -0,0 +1,8 @@ +package io.opentelemetry.instrumentation.api.semconv; + +class OtherAttributes { + /** + * short description of bar_attr + */ + public static final AttributeKey BAR_ATTR = stringKey("bar_attr"); +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/attributes_no_group_prefix.yml b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/attributes_no_group_prefix.yml new file mode 100644 index 00000000..d2dcb72e --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/no_group_prefix/attributes_no_group_prefix.yml @@ -0,0 +1,20 @@ +groups: + - id: group_with_prefix + type: attribute_group + brief: description + prefix: foo + attributes: + - id: attr_one + type: boolean + brief: short description of attr_one + - id: group_with_no_prefix + brief: description + attributes: + - id: foo.attr_two + type: string + brief: short description of foo.attr_two + examples: "foo" + - id: bar_attr + type: string + brief: short description of bar_attr + examples: "bar" \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/All.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/All.java new file mode 100644 index 00000000..cd1cabeb --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/All.java @@ -0,0 +1,34 @@ +package io.opentelemetry.instrumentation.api.semconv; + +class AllAttributes { + class FirstAttributes { + /** + * short description of attr_one + */ + public static final AttributeKey FIRST_ATTR_ONE = booleanKey("first.attr_one"); + + /** + * short description of attr_one_a + */ + public static final AttributeKey FIRST_ATTR_ONE_A = longKey("first.attr_one_a"); + + /** + * this is the description of attribute template + */ + public static final AttributeKeyTemplate FIRST_ATTR_TEMPLATE_ONE = stringKey("first.attr_template_one"); + } + class SecondAttributes { + /** + * short description of attr_two + */ + public static final AttributeKey SECOND_ATTR_TWO = stringKey("second.attr_two"); + } + /** + * short description of attr_four + */ + public static final AttributeKey ATTR_FOUR = stringKey("attr_four"); + /** + * first metric description + */ + public static final String FIRST_METRIC_NAME = "first.metric.name"; +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/semconv.yml b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/semconv.yml new file mode 100644 index 00000000..37d33fc2 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/semconv.yml @@ -0,0 +1,51 @@ +groups: + - id: first_group_id + type: attribute_group + brief: first description + prefix: first + attributes: + - id: attr_one + type: boolean + brief: short description of attr_one + - id: attr_template_one + type: template[string] + brief: > + this is the description of attribute template + examples: 'example' + + - id: second_group_id + brief: second description + prefix: second + span_kind: client + extends: first_group_id + attributes: + - id: attr_two + type: string + brief: short description of attr_two + examples: ['example_one', 'example_two'] + + - id: first_group_part_two + type: resource + brief: first_a description + prefix: first + attributes: + - id: attr_one_a + type: int + brief: short description of attr_one_a + - ref: second.attr_two + + - id: forth_group_id + brief: forth description + attributes: + - id: attr_four + type: string + brief: short description of attr_four + examples: "4" + + - id: first_metric_id + brief: first metric description + metric_name: first.metric.name + instrument: counter + type: metric + unit: "{one}" + extends: first_group_id \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/template_single_file b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/template_single_file new file mode 100644 index 00000000..986ec3d2 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/single_file/template_single_file @@ -0,0 +1,74 @@ +{%- macro to_java_return_type(type) -%} + {%- if type == "string" -%} + String + {%- elif type == "string[]" -%} + List + {%- elif type == "boolean" -%} + boolean + {%- elif type == "int" -%} + long + {%- elif type == "double" -%} + double + {%- else -%} + {{type}} + {%- endif -%} +{%- endmacro %} +{%- macro to_java_key_type(type) -%} + {%- if type == "string" -%} + stringKey + {%- elif type == "string[]" -%} + stringArrayKey + {%- elif type == "boolean" -%} + booleanKey + {%- elif type == "int" -%} + longKey + {%- elif type == "double" -%} + doubleKey + {%- else -%} + {{ type | to_camelcase(False)}}Key + {%- endif -%} +{%- endmacro %} +package io.opentelemetry.instrumentation.api.semconv; + +class AllAttributes { +{%- for root_ns in attributes_and_templates %} + + {% if root_ns != "" %} + class {{root_ns | first_up}}Attributes { + {%- for attribute in attributes_and_templates[root_ns] %} + + /** + * {{attribute.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} + */ + {% if attribute | is_template %} + public static final AttributeKeyTemplate<{{ to_java_return_type(attribute.instantiated_type | string) | first_up}}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% else %} + public static final AttributeKey<{{ to_java_return_type(attribute.instantiated_type | string) | first_up }}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% endif %} + + {%- endfor %} + } + {%- endif %} +{%- endfor %} +{# non-namespaced attributes #} +{%- for attribute in attributes_and_templates[""] %} + /** + * {{attribute.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} + */ + {% if attribute | is_template %} + public static final AttributeKeyTemplate<{{ to_java_return_type(attribute.instantiated_type | string) | first_up}}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% else %} + public static final AttributeKey<{{ to_java_return_type(attribute.instantiated_type | string) | first_up }}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% endif %} + +{%- endfor %} +{%- for id in semconvs %} +{%- if semconvs[id] | is_metric %} +{% set metric = semconvs[id] %} + /** + * {{metric.brief | to_doc_brief}} + */ + public static final String {{metric.metric_name | to_const_name}} = "{{metric.metric_name}}"; +{% endif %} +{% endfor %} +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/stable/ThirdAttributesStable.java b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/stable/ThirdAttributesStable.java new file mode 100644 index 00000000..71263bf3 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/stable/ThirdAttributesStable.java @@ -0,0 +1,8 @@ +package io.opentelemetry.instrumentation.api.semconv; + +class ThirdAttributes { + /** + * short description of attr_three + */ + public static final AttributeKey THIRD_ATTR_THREE = stringKey("third.attr_three"); +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/stable/template_only_stable b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/stable/template_only_stable new file mode 100644 index 00000000..deb83073 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/stable/template_only_stable @@ -0,0 +1,49 @@ +{%- macro to_java_return_type(type) -%} + {%- if type == "string" -%} + String + {%- elif type == "string[]" -%} + List + {%- elif type == "boolean" -%} + boolean + {%- elif type == "int" -%} + long + {%- elif type == "double" -%} + double + {%- else -%} + {{type}} + {%- endif -%} +{%- endmacro %} +{%- macro to_java_key_type(type) -%} + {%- if type == "string" -%} + stringKey + {%- elif type == "string[]" -%} + stringArrayKey + {%- elif type == "boolean" -%} + booleanKey + {%- elif type == "int" -%} + longKey + {%- elif type == "double" -%} + doubleKey + {%- else -%} + {{ type | to_camelcase(False)}}Key + {%- endif -%} +{%- endmacro %} +{%- set stable_attributes_and_templates = attributes_and_templates | select("is_stable") | list %} + +{%- if stable_attributes_and_templates | count > 0 %} +package io.opentelemetry.instrumentation.api.semconv; + +class {{ root_namespace | to_camelcase(True) }}Attributes { +{%- for attribute in stable_attributes_and_templates %} + + /** + * {{attribute.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} + */ + {% if attribute | is_template %} + public static final AttributeKeyTemplate<{{ to_java_return_type(attribute.instantiated_type | string) | first_up}}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% else %} + public static final AttributeKey<{{ to_java_return_type(attribute.instantiated_type | string) | first_up }}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% endif %} +{% endfor %} +} +{%- endif %} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/template_all b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/template_all new file mode 100644 index 00000000..2b26c2a7 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/group_by_root_namespace/template_all @@ -0,0 +1,45 @@ +{%- macro to_java_return_type(type) -%} + {%- if type == "string" -%} + String + {%- elif type == "string[]" -%} + List + {%- elif type == "boolean" -%} + boolean + {%- elif type == "int" -%} + long + {%- elif type == "double" -%} + double + {%- else -%} + {{type}} + {%- endif -%} +{%- endmacro %} +{%- macro to_java_key_type(type) -%} + {%- if type == "string" -%} + stringKey + {%- elif type == "string[]" -%} + stringArrayKey + {%- elif type == "boolean" -%} + booleanKey + {%- elif type == "int" -%} + longKey + {%- elif type == "double" -%} + doubleKey + {%- else -%} + {{ type | to_camelcase(False)}}Key + {%- endif -%} +{%- endmacro %} +package io.opentelemetry.instrumentation.api.semconv; + +class {{ root_namespace | to_camelcase(True) }}Attributes { +{%- for attribute in attributes_and_templates %} + + /** + * {{attribute.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} + */ + {% if attribute | is_template %} + public static final AttributeKeyTemplate<{{ to_java_return_type(attribute.instantiated_type | string) | first_up}}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% else %} + public static final AttributeKey<{{ to_java_return_type(attribute.instantiated_type | string) | first_up }}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.instantiated_type | string)}}("{{attribute.fqn}}"); + {% endif %} +{% endfor %} +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/semconv/templating/test_code.py b/semantic-conventions/src/tests/semconv/templating/test_code.py index 643505c9..e60eb927 100644 --- a/semantic-conventions/src/tests/semconv/templating/test_code.py +++ b/semantic-conventions/src/tests/semconv/templating/test_code.py @@ -1,4 +1,5 @@ -import io +import os +import tempfile from opentelemetry.semconv.model.semantic_convention import SemanticConventionSet from opentelemetry.semconv.templating.code import CodeRenderer @@ -12,9 +13,10 @@ def test_codegen_units(test_file_path, read_test_file): template_path = test_file_path("jinja", "metrics", "units_template") renderer = CodeRenderer({}, trim_whitespace=False) - output = io.StringIO() - renderer.render(semconv, template_path, output, None) - result = output.getvalue() + filename = os.path.join(tempfile.mkdtemp(), "Attributes.java") + renderer.render(semconv, template_path, filename, None) + with open(filename, "r", encoding="utf-8") as f: + result = f.read() expected = read_test_file("jinja", "metrics", "expected.java") @@ -32,9 +34,10 @@ def test_strip_blocks_enabled(test_file_path, read_test_file): ) renderer = CodeRenderer({}, trim_whitespace=True) - output = io.StringIO() - renderer.render(semconv, template_path, output, None) - result = output.getvalue() + filename = os.path.join(tempfile.mkdtemp(), "Attributes.java") + renderer.render(semconv, template_path, filename, None) + with open(filename, "r", encoding="utf-8") as f: + result = f.read() expected = read_test_file( "jinja", "metrics", "expected_trim_whitespace_enabled.java" @@ -53,10 +56,131 @@ def test_codegen_attribute_templates(test_file_path, read_test_file): template_path = test_file_path("jinja", "attribute_templates", "template") renderer = CodeRenderer({}, trim_whitespace=False) - output = io.StringIO() - renderer.render(semconv, template_path, output, None) - result = output.getvalue() - + filename = os.path.join(tempfile.mkdtemp(), "Attributes.java") + renderer.render(semconv, template_path, filename, None) + with open(filename, "r", encoding="utf-8") as f: + result = f.read() expected = read_test_file("jinja", "attribute_templates", "expected.java") assert result == expected + + +def test_codegen_attribute_root_ns(test_file_path, read_test_file): + semconv = SemanticConventionSet(debug=False) + + semconv.parse(test_file_path("jinja", "group_by_root_namespace", "attributes.yml")) + semconv.finish() + + template_path = test_file_path("jinja", "group_by_root_namespace", "template_all") + renderer = CodeRenderer({}, trim_whitespace=True) + + test_path = os.path.join("group_by_root_namespace", "all") + tmppath = tempfile.mkdtemp() + renderer.render( + semconv, + template_path, + os.path.join(tmppath, "Attributes.java"), + "root_namespace", + ) + + first = read_test_file("jinja", test_path, "FirstAttributes.java") + check_file(tmppath, "FirstAttributes.java", first) + + second = read_test_file("jinja", test_path, "SecondAttributes.java") + check_file(tmppath, "SecondAttributes.java", second) + + third = read_test_file("jinja", test_path, "ThirdAttributes.java") + check_file(tmppath, "ThirdAttributes.java", third) + + +def test_codegen_attribute_root_ns_stable(test_file_path, read_test_file): + semconv = SemanticConventionSet(debug=False) + semconv.parse(test_file_path("jinja", "group_by_root_namespace", "attributes.yml")) + semconv.finish() + + test_path = os.path.join("group_by_root_namespace", "stable") + template_path = test_file_path("jinja", test_path, "template_only_stable") + renderer = CodeRenderer({}, trim_whitespace=True) + + tmppath = tempfile.mkdtemp() + renderer.render( + semconv, + template_path, + os.path.join(tmppath, "Attributes.java"), + "root_namespace", + ) + + thirdStable = read_test_file("jinja", test_path, "ThirdAttributesStable.java") + check_file(tmppath, "ThirdAttributes.java", thirdStable) + assert not os.path.isfile(os.path.join(tmppath, "FirstAttributes.java")) + assert not os.path.isfile(os.path.join(tmppath, "SecondAttributes.java")) + + +def test_codegen_attribute_root_ns_no_group_prefix(test_file_path, read_test_file): + semconv = SemanticConventionSet(debug=False) + + test_path = os.path.join("group_by_root_namespace", "no_group_prefix") + semconv.parse(test_file_path("jinja", test_path, "attributes_no_group_prefix.yml")) + semconv.finish() + + template_path = test_file_path("jinja", "group_by_root_namespace", "template_all") + renderer = CodeRenderer({}, trim_whitespace=True) + + tmppath = tempfile.mkdtemp() + renderer.render( + semconv, + template_path, + os.path.join(tmppath, "Attributes.java"), + "root_namespace", + ) + + res = read_test_file("jinja", test_path, "FooAttributes.java") + check_file(tmppath, "FooAttributes.java", res) + + other = read_test_file("jinja", test_path, "OtherAttributes.java") + check_file(tmppath, "OtherAttributes.java", other) + + +def test_codegen_attribute_root_ns_single_file(test_file_path, read_test_file): + semconv = SemanticConventionSet(debug=False) + + test_path = os.path.join("group_by_root_namespace", "single_file") + semconv.parse(test_file_path("jinja", test_path, "semconv.yml")) + semconv.finish() + + template_path = test_file_path("jinja", test_path, "template_single_file") + renderer = CodeRenderer({}, trim_whitespace=True) + + tmppath = tempfile.mkdtemp() + renderer.render(semconv, template_path, os.path.join(tmppath, "All.java"), None) + + result = read_test_file("jinja", test_path, "All.java") + check_file(tmppath, "All.java", result) + + +def test_codegen_attribute_root_ns_metrics(test_file_path, read_test_file): + semconv = SemanticConventionSet(debug=False) + + test_path = os.path.join("group_by_root_namespace", "attributes_and_metrics") + semconv.parse(test_file_path("jinja", test_path, "semconv.yml")) + semconv.finish() + + template_path = test_file_path("jinja", test_path, "template") + renderer = CodeRenderer({}, trim_whitespace=True) + + tmppath = tempfile.mkdtemp() + renderer.render( + semconv, template_path, os.path.join(tmppath, ".java"), "root_namespace" + ) + + first = read_test_file("jinja", test_path, "First.java") + check_file(tmppath, "First.java", first) + + second = read_test_file("jinja", test_path, "SecondGroup.java") + check_file(tmppath, "SecondGroup.java", second) + + +def check_file(tmppath, actual_filename, expected_content): + with open(os.path.join(tmppath, actual_filename), "r", encoding="utf-8") as f: + actual = f.read() + assert actual == expected_content