diff --git a/semantic-conventions/CHANGELOG.md b/semantic-conventions/CHANGELOG.md index 5b787343..b6b6ef25 100644 --- a/semantic-conventions/CHANGELOG.md +++ b/semantic-conventions/CHANGELOG.md @@ -4,6 +4,9 @@ Please update the changelog as part of any significant pull request. ## Unreleased +- Render template-type attributes from yaml files + ([#186](https://github.com/open-telemetry/build-tools/pull/186)) + ## v0.20.0 - Change default stability level to experimental diff --git a/semantic-conventions/semconv.schema.json b/semantic-conventions/semconv.schema.json index c9204fe6..8e5c993a 100644 --- a/semantic-conventions/semconv.schema.json +++ b/semantic-conventions/semconv.schema.json @@ -287,7 +287,15 @@ "string[]", "int[]", "double[]", - "boolean[]" + "boolean[]", + "template[string]", + "template[int]", + "template[double]", + "template[boolean]", + "template[string[]]", + "template[int[]]", + "template[double[]]", + "template[boolean[]]" ], "description": "literal denoting the type" }, diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py index e6f0b35f..08a5f865 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_attribute.py @@ -27,6 +27,9 @@ validate_values, ) +TEMPLATE_PREFIX = "template[" +TEMPLATE_SUFFIX = "]" + class RequirementLevel(Enum): REQUIRED = 1 @@ -74,6 +77,10 @@ def import_attribute(self): def inherit_attribute(self): return replace(self, inherited=True) + @property + def instantiated_type(self): + return AttributeType.get_instantiated_type(self.attr_type) + @property def is_local(self): return not self.imported and not self.inherited @@ -117,7 +124,9 @@ def parse( raise ValidationError.from_yaml_pos(position, msg) if attr_id is not None: validate_id(attr_id, position_data["id"]) - attr_type, brief, examples = SemanticAttribute.parse_id(attribute) + attr_type, brief, examples = SemanticAttribute.parse_attribute( + attribute + ) if prefix: fqn = f"{prefix}.{attr_id}" else: @@ -231,7 +240,7 @@ def parse( return attributes @staticmethod - def parse_id(attribute): + def parse_attribute(attribute): check_no_missing_keys(attribute, ["type", "brief"]) attr_val = attribute["type"] try: @@ -242,14 +251,9 @@ def parse_id(attribute): position = attribute.lc.data["type"] raise ValidationError.from_yaml_pos(position, e.message) from e brief = attribute["brief"] - zlass = ( - AttributeType.type_mapper(attr_type) - if isinstance(attr_type, str) - else "enum" - ) - examples = attribute.get("examples") is_simple_type = AttributeType.is_simple_type(attr_type) + is_template_type = AttributeType.is_template_type(attr_type) # if we are an array, examples must already be an array if ( is_simple_type @@ -263,19 +267,32 @@ def parse_id(attribute): # TODO: If validation fails later, this will crash when trying to access position data # since a list, contrary to a CommentedSeq, does not have position data examples = [examples] - if is_simple_type and attr_type not in ( - "boolean", - "boolean[]", - "int", - "int[]", - "double", - "double[]", + if is_template_type or ( + is_simple_type + and attr_type + not in ( + "boolean", + "boolean[]", + "int", + "int[]", + "double", + "double[]", + ) ): if not examples: position = attribute.lc.data[list(attribute)[0]] msg = f"Empty examples for {attr_type} are not allowed" raise ValidationError.from_yaml_pos(position, msg) + if is_template_type: + return attr_type, str(brief), examples + + zlass = ( + AttributeType.type_mapper(attr_type) + if isinstance(attr_type, str) + else "enum" + ) + # TODO: Implement type check for enum examples or forbid them if examples is not None and is_simple_type: AttributeType.check_examples_type(attr_type, examples, zlass) @@ -358,6 +375,29 @@ def is_simple_type(attr_type: str): "boolean[]", ) + @staticmethod + def is_template_type(attr_type: str): + if not isinstance(attr_type, str): + return False + + return ( + attr_type.startswith(TEMPLATE_PREFIX) + and attr_type.endswith(TEMPLATE_SUFFIX) + and AttributeType.is_simple_type( + attr_type[len(TEMPLATE_PREFIX) : len(attr_type) - len(TEMPLATE_SUFFIX)] + ) + ) + + @staticmethod + def get_instantiated_type(attr_type: str): + if AttributeType.is_template_type(attr_type): + return attr_type[ + len(TEMPLATE_PREFIX) : len(attr_type) - len(TEMPLATE_SUFFIX) + ] + if AttributeType.is_simple_type(attr_type): + return attr_type + return "enum" + @staticmethod def type_mapper(attr_type: str): type_mapper = { @@ -432,7 +472,9 @@ def parse(attribute_type): otherwise it returns the basic type as string. """ if isinstance(attribute_type, str): - if AttributeType.is_simple_type(attribute_type): + if AttributeType.is_simple_type( + attribute_type + ) or AttributeType.is_template_type(attribute_type): return attribute_type # Wrong type used - raise the exception and fill the missing data in the parent raise ValidationError( diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py index b62468b9..4e533831 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py @@ -16,13 +16,14 @@ import typing from dataclasses import dataclass, field from enum import Enum -from typing import Dict, Tuple, Union +from typing import Dict, Optional, Tuple, Union from ruamel.yaml import YAML from opentelemetry.semconv.model.constraints import AnyOf, Include, parse_constraints from opentelemetry.semconv.model.exceptions import ValidationError from opentelemetry.semconv.model.semantic_attribute import ( + AttributeType, RequirementLevel, SemanticAttribute, unique_attributes, @@ -111,10 +112,26 @@ class BaseSemanticConvention(ValidatableYamlNode): @property def attributes(self): + return self._get_attributes(False) + + @property + def attribute_templates(self): + return self._get_attributes(True) + + @property + def attributes_and_templates(self): + return self._get_attributes(None) + + def _get_attributes(self, templates: Optional[bool]): if not hasattr(self, "attrs_by_name"): return [] - return list(self.attrs_by_name.values()) + return [ + attr + for attr in self.attrs_by_name.values() + if templates is None + or templates == AttributeType.is_template_type(attr.attr_type) + ] def __init__(self, group): super().__init__(group) @@ -565,6 +582,12 @@ def attributes(self): output.extend(semconv.attributes) return output + def attribute_templates(self): + output = [] + for semconv in self.models.values(): + output.extend(semconv.attribute_templates) + return output + CONVENTION_CLS_BY_GROUP_TYPE = { cls.GROUP_TYPE_NAME: cls diff --git a/semantic-conventions/src/opentelemetry/semconv/templating/code.py b/semantic-conventions/src/opentelemetry/semconv/templating/code.py index 1efd0c56..ddb96838 100644 --- a/semantic-conventions/src/opentelemetry/semconv/templating/code.py +++ b/semantic-conventions/src/opentelemetry/semconv/templating/code.py @@ -188,6 +188,7 @@ def get_data_single_file( "template": template_path, "semconvs": semconvset.models, "attributes": semconvset.attributes(), + "attribute_templates": semconvset.attribute_templates(), } data.update(self.parameters) return data diff --git a/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py index cd154ef7..9aec9aaa 100644 --- a/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py +++ b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py @@ -22,6 +22,7 @@ from opentelemetry.semconv.model.constraints import AnyOf, Include from opentelemetry.semconv.model.semantic_attribute import ( + AttributeType, EnumAttributeType, EnumMember, RequirementLevel, @@ -97,11 +98,11 @@ def to_markdown_attr( """ This method renders attributes as markdown table entry """ - name = self.render_attribute_id(attribute.fqn) + name = self.render_fqn_for_attribute(attribute) attr_type = ( "enum" if isinstance(attribute.attr_type, EnumAttributeType) - else attribute.attr_type + else AttributeType.get_instantiated_type(attribute.attr_type) ) description = "" if attribute.deprecated and self.options.enable_deprecated: @@ -184,7 +185,8 @@ def to_markdown_attribute_table( ): attr_to_print = [] for attr in sorted( - semconv.attributes, key=lambda a: "" if a.ref is None else a.ref + semconv.attributes_and_templates, + key=lambda a: "" if a.ref is None else a.ref, ): if self.render_ctx.group_key is not None: if attr.tag == self.render_ctx.group_key: @@ -192,6 +194,7 @@ def to_markdown_attribute_table( continue if self.render_ctx.is_full or attr.is_local: attr_to_print.append(attr) + if self.render_ctx.group_key is not None and not attr_to_print: raise ValueError( f"No attributes retained for '{semconv.semconv_id}' filtering by '{self.render_ctx.group_key}'" @@ -275,7 +278,7 @@ def to_creation_time_attributes( ) for attr in sampling_relevant_attrs: - output.write("* " + self.render_attribute_id(attr.fqn) + "\n") + output.write("* " + self.render_fqn_for_attribute(attr) + "\n") @staticmethod def to_markdown_unit_table(members, output: io.StringIO): @@ -325,19 +328,37 @@ def to_markdown_enum(self, output: io.StringIO): if notes: output.write("\n") + def render_fqn_for_attribute(self, attribute): + rel_path = self.get_attr_reference_relative_path(attribute.fqn) + name = attribute.fqn + if AttributeType.is_template_type(attribute.attr_type): + name = f"{attribute.fqn}." + + if rel_path is not None: + return f"[`{name}`]({rel_path})" + return f"`{name}`" + def render_attribute_id(self, attribute_id): """ Method to render in markdown an attribute id. If the id points to an attribute in another rendered table, a markdown link is introduced. """ + rel_path = self.get_attr_reference_relative_path(attribute_id) + if rel_path is not None: + return f"[`{attribute_id}`]({rel_path})" + return f"`{attribute_id}`" + + def get_attr_reference_relative_path(self, attribute_id): md_file = self.filename_for_attr_fqn.get(attribute_id) if md_file: path = PurePath(self.render_ctx.current_md) if path.as_posix() != PurePath(md_file).as_posix(): - diff = PurePath(os.path.relpath(md_file, start=path.parent)).as_posix() - if diff != ".": - return f"[`{attribute_id}`]({diff})" - return f"`{attribute_id}`" + rel_path = PurePath( + os.path.relpath(md_file, start=path.parent) + ).as_posix() + if rel_path != ".": + return rel_path + return None def to_markdown_constraint( self, @@ -390,7 +411,9 @@ def _create_attribute_location_dict(self): ) a: SemanticAttribute valid_attr = ( - a for a in semconv.attributes if a.is_local and not a.ref + a + for a in semconv.attributes_and_templates + if a.is_local and not a.ref ) for attr in valid_attr: m[attr.fqn] = md diff --git a/semantic-conventions/src/tests/data/jinja/attribute_templates/expected.java b/semantic-conventions/src/tests/data/jinja/attribute_templates/expected.java new file mode 100644 index 00000000..c91b2c46 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/attribute_templates/expected.java @@ -0,0 +1,20 @@ + +package io.opentelemetry.instrumentation.api.attributetemplates; + +class AttributesTemplate { + + /** + * this is the description of the first attribute template + */ + public static final AttributeKey ATTRIBUTE_TEMPLATE_ONE = stringKey("attribute_template_one"); + + /** + * this is the description of the second attribute template. It's a number. + */ + public static final AttributeKey ATTRIBUTE_TEMPLATE_TWO = longKey("attribute_template_two"); + + /** + * this is the description of the third attribute template. It's a boolean. + */ + public static final AttributeKey ATTRIBUTE_THREE = booleanKey("attribute_three"); +} \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/jinja/attribute_templates/template b/semantic-conventions/src/tests/data/jinja/attribute_templates/template new file mode 100644 index 00000000..486ddce2 --- /dev/null +++ b/semantic-conventions/src/tests/data/jinja/attribute_templates/template @@ -0,0 +1,81 @@ +{%- 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 -%} + {{lowerFirst(type)}}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 %} + + /** + * {{attribute_template.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} + {%- if attribute_template.note %} + * + *

Notes: +

    {{attribute_template.note | render_markdown(code="{{@code {0}}}", paragraph="
  • {0}
  • ", list="{0}")}}
+ {%- endif %} + {%- if (attribute_template.stability | string()) == "StabilityLevel.DEPRECATED" %} + * + * @deprecated {{attribute_template.brief | to_doc_brief}}. + {%- endif %} + */ + {%- if (attribute_template.stability | string()) == "StabilityLevel.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}}"); +{%- endfor %} +{%- for attribute in attributes if attribute.is_local and not attribute.ref %} + + /** + * {{attribute.brief | render_markdown(code="{{@code {0}}}", paragraph="{0}")}} + {%- if attribute.note %} + * + *

Notes: +

    {{attribute.note | render_markdown(code="{{@code {0}}}", paragraph="
  • {0}
  • ", list="{0}")}}
+ {%- endif %} + {%- if (attribute.stability | string()) == "StabilityLevel.DEPRECATED" %} + * + * @deprecated {{attribute.brief | to_doc_brief}}. + {%- endif %} + */ + {%- if (attribute.stability | string()) == "StabilityLevel.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}}"); +{%- endfor %} +} diff --git a/semantic-conventions/src/tests/data/markdown/attribute_templates/attribute_templates.yaml b/semantic-conventions/src/tests/data/markdown/attribute_templates/attribute_templates.yaml new file mode 100644 index 00000000..aad943a6 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/attribute_templates/attribute_templates.yaml @@ -0,0 +1,21 @@ +groups: + - id: custom_http + type: span + prefix: custom_http + brief: 'This document defines semantic conventions for HTTP client and server Spans.' + note: > + These conventions can be used for http and https schemes + and various HTTP versions like 1.1, 2 and SPDY. + attributes: + - id: request.header + type: template[string[]] + brief: > + HTTP request headers, `` being the normalized HTTP Header name + (lowercase, with - characters replaced by _), the value being the header values. + examples: '`http.request.header.content_type=["application/json"]`' + - id: request.method + type: string + requirement_level: required + sampling_relevant: false + brief: 'HTTP request method.' + examples: ["GET", "POST", "HEAD"] diff --git a/semantic-conventions/src/tests/data/markdown/attribute_templates/expected.md b/semantic-conventions/src/tests/data/markdown/attribute_templates/expected.md new file mode 100644 index 00000000..24499613 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/attribute_templates/expected.md @@ -0,0 +1,8 @@ +# Custom HTTP Semantic Conventions + + +| Attribute | Type | Description | Examples | Requirement Level | +|---|---|---|---|---| +| `custom_http.request.header.` | string[] | HTTP request headers, `` being the normalized HTTP Header name (lowercase, with - characters replaced by _), the value being the header values. | ``http.request.header.content_type=["application/json"]`` | Recommended | +| `custom_http.request.method` | string | HTTP request method. | `GET`; `POST`; `HEAD` | Required | + diff --git a/semantic-conventions/src/tests/data/markdown/attribute_templates/input.md b/semantic-conventions/src/tests/data/markdown/attribute_templates/input.md new file mode 100644 index 00000000..9d5e48a8 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/attribute_templates/input.md @@ -0,0 +1,4 @@ +# Custom HTTP Semantic Conventions + + + diff --git a/semantic-conventions/src/tests/data/yaml/attr_templates_code/attribute_templates.yml b/semantic-conventions/src/tests/data/yaml/attr_templates_code/attribute_templates.yml new file mode 100644 index 00000000..d56a0bba --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/attr_templates_code/attribute_templates.yml @@ -0,0 +1,25 @@ +groups: + - id: test + type: attribute_group + brief: 'brief' + attributes: + - id: attribute_template_one + tag: tag-one + type: template[string] + brief: > + this is the description of + the first attribute template + examples: 'This is a good example of the first attribute template' + - id: attribute_template_two + tag: tag-two + type: template[int] + brief: > + this is the description of + the second attribute template. It's a number. + examples: [1000, 10, 1] + - id: attribute_three + tag: tag-three + type: boolean + brief: > + this is the description of + the third attribute template. It's a boolean. \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/yaml/attribute_templates.yml b/semantic-conventions/src/tests/data/yaml/attribute_templates.yml new file mode 100644 index 00000000..d9ec04c8 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/attribute_templates.yml @@ -0,0 +1,21 @@ +attributes: + - id: attribute_template_one + tag: tag-one + type: template[string] + brief: > + this is the description of + the first attribute template + examples: 'This is a good example of the first attribute template' + - id: attribute_template_two + tag: tag-two + type: template[int] + brief: > + this is the description of + the second attribute template. It's a number. + examples: [1000, 10, 1] + - id: attribute_three + tag: tag-three + type: boolean + brief: > + this is the description of + the second attribute template. It's a boolean. \ No newline at end of file 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 db614cfb..63466ca4 100644 --- a/semantic-conventions/src/tests/semconv/model/test_semantic_attribute.py +++ b/semantic-conventions/src/tests/semconv/model/test_semantic_attribute.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + from opentelemetry.semconv.model.semantic_attribute import SemanticAttribute @@ -49,3 +51,44 @@ def test_parse_deprecated(load_yaml): assert list(attributes.keys()) == ["deprecated_attribute"] assert attributes["deprecated_attribute"].deprecated == "don't use this one anymore" + + +def test_parse_regex(): + TEMPLATE_TYPE_RE = re.compile("template\\[([a-z\\[\\]]+)\\]") + matchObj = TEMPLATE_TYPE_RE.fullmatch("template[string[]]") + assert matchObj is not None + assert matchObj.group(1) == "string[]" + + +def test_parse_attribute_templates(load_yaml): + yaml = load_yaml("attribute_templates.yml") + attribute_templates = SemanticAttribute.parse("prefix", "", yaml.get("attributes")) + + assert len(attribute_templates) == 3 + + expected_keys = sorted( + [ + "prefix.attribute_template_one", + "prefix.attribute_template_two", + "prefix.attribute_three", + ] + ) + actual_keys = sorted(list(attribute_templates.keys())) + + assert actual_keys == expected_keys + + first_attribute = attribute_templates["prefix.attribute_template_one"] + assert first_attribute.fqn == "prefix.attribute_template_one" + assert first_attribute.tag == "tag-one" + assert first_attribute.ref is None + assert first_attribute.attr_type == "template[string]" + assert ( + first_attribute.brief + == "this is the description of the first attribute template" + ) + assert first_attribute.examples == [ + "This is a good example of the first attribute template" + ] + + second_attribute = attribute_templates["prefix.attribute_template_two"] + assert second_attribute.fqn == "prefix.attribute_template_two" diff --git a/semantic-conventions/src/tests/semconv/templating/test_code.py b/semantic-conventions/src/tests/semconv/templating/test_code.py index 7090addb..643505c9 100644 --- a/semantic-conventions/src/tests/semconv/templating/test_code.py +++ b/semantic-conventions/src/tests/semconv/templating/test_code.py @@ -41,3 +41,22 @@ def test_strip_blocks_enabled(test_file_path, read_test_file): ) assert result == expected + + +def test_codegen_attribute_templates(test_file_path, read_test_file): + semconv = SemanticConventionSet(debug=False) + semconv.parse( + test_file_path("yaml", "attr_templates_code", "attribute_templates.yml") + ) + semconv.finish() + + 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() + + expected = read_test_file("jinja", "attribute_templates", "expected.java") + + assert result == expected diff --git a/semantic-conventions/src/tests/semconv/templating/test_markdown.py b/semantic-conventions/src/tests/semconv/templating/test_markdown.py index 474105a2..c35d81bb 100644 --- a/semantic-conventions/src/tests/semconv/templating/test_markdown.py +++ b/semantic-conventions/src/tests/semconv/templating/test_markdown.py @@ -142,6 +142,9 @@ def test_scope(self): def test_attribute_group(self): self.check("markdown/attribute_group/") + def test_attribute_templates(self): + self.check("markdown/attribute_templates/") + def check( self, input_dir: str, diff --git a/semantic-conventions/syntax.md b/semantic-conventions/syntax.md index 9633ba05..adcfdc73 100644 --- a/semantic-conventions/syntax.md +++ b/semantic-conventions/syntax.md @@ -73,7 +73,11 @@ attributes ::= (id type brief examples | ref [brief] [examples]) [tag] [stabilit # ref MUST point to an existing attribute id ref ::= id -type ::= "string" +type ::= simple_type + | template_type + | enum + +simple_type ::= "string" | "int" | "double" | "boolean" @@ -81,7 +85,8 @@ type ::= "string" | "int[]" | "double[]" | "boolean[]" - | enum + +template_type ::= "template[" simple_type "]" # As a single string enum ::= [allow_custom_values] members @@ -213,17 +218,18 @@ Attribute groups don't have any specific fields and follow the general `semconv` An attribute is defined by: - `id`, string that uniquely identifies the attribute. -- `type`, either a string literal denoting the type or an enum definition (See later). +- `type`, either a string literal denoting the type as a primitive or an array type, a template type or an enum definition (See later). The accepted string literals are: - - * `"string"`: String attributes. - * `"int"`: Integer attributes. - * `"double"`: Double attributes. - * `"boolean"`: Boolean attributes. - * `"string[]"`: Array of strings attributes. - * `"int[]"`: Array of integer attributes. - * `"double[]"`: Array of double attributes. - * `"boolean[]"`: Array of booleans attributes. + * _primitive and array types as string literals:_ + * `"string"`: String attributes. + * `"int"`: Integer attributes. + * `"double"`: Double attributes. + * `"boolean"`: Boolean attributes. + * `"string[]"`: Array of strings attributes. + * `"int[]"`: Array of integer attributes. + * `"double[]"`: Array of double attributes. + * `"boolean[]"`: Array of booleans attributes. + * _template type as string literal:_ `"template[]"` (See [below](#template-type)) See the [specification of Attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute) for the definition of the value types. - `ref`, optional string, reference an existing attribute, see [below](#ref). @@ -333,7 +339,38 @@ fields are present in the current attribute definition, they override the inheri #### Type An attribute type can either be a string, int, double, boolean, array of strings, array of int, array of double, -array of booleans, or an enumeration. If it is an enumeration, additional fields are required: +array of booleans, a template type or an enumeration. + +##### Template type + +A template type attribute represents a _dictionary_ of attributes with a common attribute name prefix. The syntax for defining template type attributes is the following: + +`type: template[]` + +The `` is one of the above-mentioned primitive or array types (_not_ an enum) and specifies the type of the `value` in the dictionary. + +The following is an example for defining a template type attribute and it's resolution: + +```yaml +groups: + - id: trace.http.common + type: attribute_group + brief: "..." + attributes: + - id: http.request.header + type: template[string[]] + brief: > + HTTP request headers, the key being the normalized HTTP header name (lowercase, with `-` characters replaced by `_`), the value being the header values. + examples: ['http.request.header.content_type=["application/json"]', 'http.request.header.x_forwarded_for=["1.2.3.4", "1.2.3.5"]'] + note: | + ... +``` + +In this example the definition will be resolved into a dictionary of attributes `http.request.header.` where `` will be replaced by the actual HTTP header name, and the value of the attributes is of type `string[]` that carries the HTTP header value. + +##### Enumeration + +If the type is an enumeration, additional fields are required: - `allow_custom_values`, optional boolean, set to false to not accept values other than the specified members. It defaults to `true`.