diff --git a/semantic-conventions/CHANGELOG.md b/semantic-conventions/CHANGELOG.md index 5efc4bae..d8a15f05 100644 --- a/semantic-conventions/CHANGELOG.md +++ b/semantic-conventions/CHANGELOG.md @@ -4,6 +4,8 @@ Please update the changelog as part of any significant pull request. ## Unreleased +- Add a semantic convention type for Metrics ("metric" and "metric_group") + ([#79](https://github.com/open-telemetry/build-tools/pull/79)) - Add a semantic convention type for generic attribute group ("attribute_group") ([#124](https://github.com/open-telemetry/build-tools/pull/124)). diff --git a/semantic-conventions/README.md b/semantic-conventions/README.md index 937cdcf5..9d1bc5a7 100644 --- a/semantic-conventions/README.md +++ b/semantic-conventions/README.md @@ -85,6 +85,9 @@ 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`. + ## Code Generator The image supports [Jinja](https://jinja.palletsprojects.com/en/2.11.x/) templates to generate code from the models. diff --git a/semantic-conventions/semconv.schema.json b/semantic-conventions/semconv.schema.json index 862c4b73..620d7e0a 100644 --- a/semantic-conventions/semconv.schema.json +++ b/semantic-conventions/semconv.schema.json @@ -16,6 +16,9 @@ }, { "allOf": [{"$ref": "#/definitions/EventSemanticConvention"}] + }, + { + "allOf": [{"$ref": "#/definitions/MetricSemanticConvention"}] } ] } @@ -51,6 +54,7 @@ "span", "resource", "metric", + "metric_group", "event", "scope", "attribute_group" @@ -165,6 +169,49 @@ {"required": ["name"]} ] }, + "MetricGroupSemanticConvention": { + "allOf": [{ "$ref": "#/definitions/SemanticConventionBase" }], + "required": ["type"], + "properties": { + "type": { + "type": "string", + "const": "metric_group" + } + } + }, + "MetricSemanticConvention": { + "allOf": [{ "$ref": "#/definitions/SemanticConventionBase" }], + "required": [ + "type", + "metric_name", + "instrument", + "unit" + ], + "properties": { + "instrument": { + "type": "string", + "description": "The instrument used to record the metric.", + "enum": [ + "counter", + "gauge", + "histogram", + "updowncounter" + ] + }, + "metric_name": { + "type": "string", + "description": "The name of the metric." + }, + "type": { + "type": "string", + "const": "metric" + }, + "unit": { + "type": "string", + "description": "The unit in which the metric is measured in." + } + } + }, "SemanticConvention": { "allOf": [{ "$ref": "#/definitions/SemanticConventionBase" }], "required": ["type"], diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py index 1deda606..5127cd71 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py @@ -16,7 +16,7 @@ import typing from dataclasses import dataclass, field from enum import Enum -from typing import Tuple, Union +from typing import Dict, Tuple, Union from ruamel.yaml import YAML @@ -246,10 +246,50 @@ def __init__(self, group): self.members = UnitMember.parse(group.get("members")) -class MetricSemanticConvention(BaseSemanticConvention): +class MetricGroupSemanticConvention(BaseSemanticConvention): + GROUP_TYPE_NAME = "metric_group" + + +class MetricSemanticConvention(MetricGroupSemanticConvention): GROUP_TYPE_NAME = "metric" - allowed_keys = () + allowed_keys: Tuple[str, ...] = BaseSemanticConvention.allowed_keys + ( + "metric_name", + "unit", + "instrument", + ) + + canonical_instrument_name_by_yaml_name: Dict[str, str] = { + "counter": "Counter", + "updowncounter": "UpDownCounter", + "histogram": "Histogram", + "gauge": "Gauge", + } + + allowed_instruments: Tuple[str, ...] = tuple( + canonical_instrument_name_by_yaml_name.keys() + ) + + def __init__(self, group): + super().__init__(group) + self.metric_name = group.get("metric_name") + self.unit = group.get("unit") + self.instrument = group.get("instrument") + self.validate() + + def validate(self): + val_tuple = (self.metric_name, self.unit, self.instrument) + if not all(val_tuple): + raise ValidationError.from_yaml_pos( + self._position, + "All of metric_name, units, and instrument must be defined", + ) + + if self.instrument not in self.allowed_instruments: + raise ValidationError.from_yaml_pos( + self._position, + f"Instrument '{self.instrument}' is not a valid instrument name", + ) @dataclass @@ -532,6 +572,7 @@ def attributes(self): SpanSemanticConvention, ResourceSemanticConvention, EventSemanticConvention, + MetricGroupSemanticConvention, MetricSemanticConvention, UnitSemanticConvention, ScopeSemanticConvention, diff --git a/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py index 7bf5e12d..fc57d9ac 100644 --- a/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py +++ b/semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py @@ -29,7 +29,9 @@ StabilityLevel, ) from opentelemetry.semconv.model.semantic_convention import ( + BaseSemanticConvention, EventSemanticConvention, + MetricSemanticConvention, SemanticConventionSet, UnitSemanticConvention, ) @@ -41,6 +43,7 @@ class RenderContext: def __init__(self): self.is_full = False self.is_remove_constraint = False + self.is_metric_table = False self.group_key = "" self.enums = [] self.notes = [] @@ -66,7 +69,7 @@ class MarkdownRenderer: ) p_end = re.compile("") default_break_conditional_labels = 50 - valid_parameters = ["tag", "full", "remove_constraints"] + valid_parameters = ["tag", "full", "remove_constraints", "metric_table"] prelude = "\n" table_headers = "| Attribute | Type | Description | Examples | Requirement Level |\n|---|---|---|---|---|\n" @@ -176,6 +179,55 @@ def to_markdown_attr( f"| {name} | {attr_type} | {description} | {examples} | {required} |\n" ) + def to_markdown_attribute_table( + self, semconv: BaseSemanticConvention, output: io.StringIO + ): + attr_to_print = [] + for attr in sorted( + semconv.attributes, key=lambda a: "" if a.ref is None else a.ref + ): + if self.render_ctx.group_key is not None: + if attr.tag == self.render_ctx.group_key: + attr_to_print.append(attr) + continue + if self.render_ctx.is_full or attr.is_local: + attr_to_print.append(attr) + if self.render_ctx.group_key is not None and not attr_to_print: + raise ValueError( + f"No attributes retained for '{semconv.semconv_id}' filtering by '{self.render_ctx.group_key}'" + ) + if attr_to_print: + output.write(MarkdownRenderer.table_headers) + for attr in attr_to_print: + self.to_markdown_attr(attr, output) + attr_sampling_relevant = [ + attr for attr in attr_to_print if attr.sampling_relevant + ] + self.to_creation_time_attributes(attr_sampling_relevant, output) + + @staticmethod + def to_markdown_metric_table( + semconv: MetricSemanticConvention, output: io.StringIO + ): + """ + This method renders metrics as markdown table entry + """ + if not isinstance(semconv, MetricSemanticConvention): + raise ValueError( + f"semconv `{semconv.semconv_id}` was specified with `metric_table`, but it is not a metric convention" + ) + + instrument = MetricSemanticConvention.canonical_instrument_name_by_yaml_name[ + semconv.instrument + ] + output.write( + "| Name | Instrument Type | Unit (UCUM) | Description |\n" + "| -------- | --------------- | ----------- | -------------- |\n" + ) + output.write( + f"| `{semconv.metric_name}` | {instrument} | `{semconv.unit}` | {semconv.brief} |\n" + ) + def to_markdown_anyof(self, anyof: AnyOf, output: io.StringIO): """ This method renders anyof constraints into markdown lists @@ -412,29 +464,15 @@ def _render_group(self, semconv, parameters, output): self.render_ctx.is_remove_constraint = "remove_constraints" in parameters self.render_ctx.group_key = parameters.get("tag") self.render_ctx.is_full = "full" in parameters + self.render_ctx.is_metric_table = "metric_table" in parameters - if isinstance(semconv, EventSemanticConvention): - output.write(f"The event name MUST be `{semconv.name}`.\n\n") + if self.render_ctx.is_metric_table: + self.to_markdown_metric_table(semconv, output) + else: + if isinstance(semconv, EventSemanticConvention): + output.write(f"The event name MUST be `{semconv.name}`.\n\n") + self.to_markdown_attribute_table(semconv, output) - attr_to_print = [] - attr: SemanticAttribute - for attr in sorted( - semconv.attributes, key=lambda a: "" if a.ref is None else a.ref - ): - if self.render_ctx.group_key is not None: - if attr.tag == self.render_ctx.group_key: - attr_to_print.append(attr) - continue - if self.render_ctx.is_full or attr.is_local: - attr_to_print.append(attr) - if self.render_ctx.group_key is not None and not attr_to_print: - raise ValueError( - f"No attributes retained for '{semconv.semconv_id}' filtering by '{self.render_ctx.group_key}'" - ) - if attr_to_print: - output.write(MarkdownRenderer.table_headers) - for attr in attr_to_print: - self.to_markdown_attr(attr, output) self.to_markdown_notes(output) if not self.render_ctx.is_remove_constraint: for cnst in semconv.constraints: @@ -444,9 +482,4 @@ def _render_group(self, semconv, parameters, output): if isinstance(semconv, UnitSemanticConvention): self.to_markdown_unit_table(semconv.members, output) - attr_sampling_relevant = [ - attr for attr in attr_to_print if attr.sampling_relevant - ] - self.to_creation_time_attributes(attr_sampling_relevant, output) - output.write("") diff --git a/semantic-conventions/src/tests/data/markdown/metrics_tables/expected.md b/semantic-conventions/src/tests/data/markdown/metrics_tables/expected.md new file mode 100644 index 00000000..3b2b3228 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/metrics_tables/expected.md @@ -0,0 +1,31 @@ +# Test Markdown + +**`foo.size`** + +| Name | Instrument Type | Unit (UCUM) | Description | +| -------- | --------------- | ----------- | -------------- | +| `foo.size` | Histogram | `{bars}` | Measures the size of foo. | + + +**Attributes for `foo.size`** + +| Attribute | Type | Description | Examples | Requirement Level | +|---|---|---|---|---| +| `http.method` | string | HTTP request method. | `GET`; `POST`; `HEAD` | Required | +| `http.status_code` | int | [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). | `200` | Conditionally Required: if and only if one was received/sent. | + + +**`foo.active_eggs`** + +| Name | Instrument Type | Unit (UCUM) | Description | +| -------- | --------------- | ----------- | -------------- | +| `foo.active_eggs` | UpDownCounter | `{cartons}` | Measures how many eggs are currently active. | + + +**Attributes for `foo.active_eggs`** + +| Attribute | Type | Description | Examples | Requirement Level | +|---|---|---|---|---| +| `bar.egg.type` | string | Type of egg. | `chicken`; `emu`; `dragon` | Conditionally Required: if available to instrumentation. | +| `http.method` | string | HTTP request method. | `GET`; `POST`; `HEAD` | Optional | + diff --git a/semantic-conventions/src/tests/data/markdown/metrics_tables/input.md b/semantic-conventions/src/tests/data/markdown/metrics_tables/input.md new file mode 100644 index 00000000..55e98373 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/metrics_tables/input.md @@ -0,0 +1,17 @@ +# Test Markdown + +**`foo.size`** + + + +**Attributes for `foo.size`** + + + +**`foo.active_eggs`** + + + +**Attributes for `foo.active_eggs`** + + diff --git a/semantic-conventions/src/tests/data/yaml/metrics.yaml b/semantic-conventions/src/tests/data/yaml/metrics.yaml new file mode 100644 index 00000000..b6ef1627 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/metrics.yaml @@ -0,0 +1,40 @@ +groups: + - id: metric.foo + prefix: bar + type: metric_group + brief: "This document defines foo." + note: > + Details about foo. + attributes: + - id: egg.type + type: string + brief: 'Type of egg.' + examples: ["chicken", "emu", "dragon"] + + - id: metric.foo.size + prefix: foo + type: metric + metric_name: foo.size + brief: "Measures the size of foo." + instrument: histogram + unit: "{bars}" + attributes: + - ref: http.method + requirement_level: required + - ref: http.status_code + requirement_level: + conditionally_required: "if and only if one was received/sent." + + - id: metric.foo.active_eggs + prefix: foo + type: metric + metric_name: foo.active_eggs + brief: "Measures how many eggs are currently active." + instrument: updowncounter + unit: "{cartons}" + attributes: + - ref: http.method + requirement_level: optional + - ref: bar.egg.type + requirement_level: + conditionally_required: "if available to instrumentation." 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 316f19b3..cabb4e55 100644 --- a/semantic-conventions/src/tests/semconv/model/test_correct_parse.py +++ b/semantic-conventions/src/tests/semconv/model/test_correct_parse.py @@ -14,11 +14,13 @@ import os import unittest +from typing import List, cast from opentelemetry.semconv.model.constraints import AnyOf, Include from opentelemetry.semconv.model.semantic_attribute import StabilityLevel from opentelemetry.semconv.model.semantic_convention import ( EventSemanticConvention, + MetricSemanticConvention, SemanticConventionSet, SpanSemanticConvention, ) @@ -193,6 +195,44 @@ def test_http(self): } self.semantic_convention_check(list(semconv.models.values())[2], expected) + def test_metrics(self): + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("yaml/metrics.yaml")) + self.assertEqual(len(semconv.models), 3) + semconv.parse(self.load_file("yaml/general.yaml")) + semconv.parse(self.load_file("yaml/http.yaml")) + + metric_semconvs = cast( + List[MetricSemanticConvention], list(semconv.models.values())[:2] + ) + + expected = { + "id": "metric.foo", + "prefix": "bar", + "extends": "", + "n_constraints": 0, + "attributes": ["bar.egg.type"], + } + self.semantic_convention_check(metric_semconvs[0], expected) + + expected = { + "id": "metric.foo.size", + "prefix": "foo", + "extends": "", + "n_constraints": 0, + "metric_name": "foo.size", + "unit": "{bars}", + "instrument": "histogram", + "attributes": [ + "http.method", + "http.status_code", + ], + } + self.semantic_convention_check(metric_semconvs[1], expected) + self.assertEqual(metric_semconvs[1].unit, expected["unit"]) + self.assertEqual(metric_semconvs[1].instrument, expected["instrument"]) + self.assertEqual(metric_semconvs[1].metric_name, expected["metric_name"]) + def test_resource(self): semconv = SemanticConventionSet(debug=False) semconv.parse(self.load_file("yaml/cloud.yaml")) diff --git a/semantic-conventions/src/tests/semconv/templating/test_markdown.py b/semantic-conventions/src/tests/semconv/templating/test_markdown.py index aeb6a174..474105a2 100644 --- a/semantic-conventions/src/tests/semconv/templating/test_markdown.py +++ b/semantic-conventions/src/tests/semconv/templating/test_markdown.py @@ -123,6 +123,16 @@ def test_event_noprefix(self): def test_event_renamed(self): self.check("markdown/event_renamed/") + def test_metric_tables(self): + self.check( + "markdown/metrics_tables", + extra_yaml_files=[ + "yaml/general.yaml", + "yaml/http.yaml", + "yaml/metrics.yaml", + ], + ) + def testSamplingRelevant(self): self.check("markdown/sampling_relevant/") @@ -139,6 +149,7 @@ def check( *, expected_name="expected.md", extra_yaml_dirs: Sequence[str] = (), + extra_yaml_files: Sequence[str] = (), assert_raises=None ) -> Optional[BaseException]: dirpath = Path(self.get_file_path(input_dir)) @@ -154,6 +165,9 @@ def check( for fname in Path(self.get_file_path(extra_dir)).glob("*.yaml"): print("Parsing", fname) semconv.parse(fname) + for fname in map(self.get_file_path, extra_yaml_files): + print("Parsing ", fname) + semconv.parse(fname) semconv.finish() diff --git a/semantic-conventions/syntax.md b/semantic-conventions/syntax.md index 5448b7b3..277794ad 100644 --- a/semantic-conventions/syntax.md +++ b/semantic-conventions/syntax.md @@ -15,6 +15,9 @@ Then, the semantic of each field is described. - [Semantic Convention](#semantic-convention) - [Span semantic convention](#span-semantic-convention) - [Event semantic convention](#event-semantic-convention) + - [Metric Group semantic convention](#metric-group-semantic-convention) + - [Metric semantic convention](#metric-semantic-convention) + - [Attribute group semantic convention](#attribute-group-semantic-convention) - [Attributes](#attributes) - [Examples (for examples)](#examples-for-examples) - [Ref](#ref) @@ -45,10 +48,11 @@ semconv ::= id [convtype] brief [note] [prefix] [extends] [stability] [deprecate id ::= string convtype ::= "span" # Default if not specified - | "resource" # see spanfields - | "event" # see eventfields - | "metric" # (currently non-functional) - | "scope" # no specific fields defined + | "resource" # see spanspecificfields + | "event" # see eventspecificfields + | "metric" # see metricfields + | "metric_group" + | "scope" | "attribute_group" # no specific fields defined brief ::= string @@ -108,6 +112,7 @@ include ::= id specificfields ::= spanfields | eventfields + | metricfields spanfields ::= [events] [span_kind] eventfields ::= [name] @@ -122,6 +127,14 @@ events ::= id {id} # MUST point to an existing event group name ::= string +metricfields ::= metric_name instrument unit + +metric_name ::= string +instrument ::= "counter" + | "histogram" + | "gauge" + | "updowncounter" +unit ::= string ``` ## Semantics @@ -169,6 +182,26 @@ The following is only valid if `type` is `event`: If not specified, the `prefix` is used. If `prefix` is empty (or unspecified), `name` is required. +#### Metric Group semantic convention + +Metric group inherits all from the base semantic convention, and does not +add any additional fields. + +The metric group semconv is a group where related metric attributes +can be defined and then referenced from other `metric` groups using `ref`. + +#### Metric semantic convention + +The following is only valid if `type` is `metric`: + + - `metric_name`, required, the metric name as described by the [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#timeseries-model). + - `instrument`, required, the [instrument type]( https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument) + that should be used to record the metric. Note that the semantic conventions must be written + using the names of the synchronous instrument types (`counter`, `gauge`, `updowncounter` and `histogram`). + For more details: [Metrics semantic conventions - Instrument types](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-types). + - `unit`, required, the unit in which the metric is measured, which should adhere to + [the guidelines](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-units). + #### Attribute group semantic convention Attribute group (`attribute_group` type) defines a set of attributes that can be