diff --git a/semantic-conventions/CHANGELOG.md b/semantic-conventions/CHANGELOG.md index a8b5810a..55d43a5e 100644 --- a/semantic-conventions/CHANGELOG.md +++ b/semantic-conventions/CHANGELOG.md @@ -2,6 +2,11 @@ Please update the changelog as part of any significant pull request. +## v0.5.0 + +- Add event semantic convention type & events span field + ([#57](https://github.com/open-telemetry/build-tools/pull/57)). + ## v0.4.0 - Add stability fields. (#35) diff --git a/semantic-conventions/semconv.schema.json b/semantic-conventions/semconv.schema.json index a458c69b..86ba3c28 100644 --- a/semantic-conventions/semconv.schema.json +++ b/semantic-conventions/semconv.schema.json @@ -7,122 +7,154 @@ "groups": { "type": "array", "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "id", - "brief" - ], "anyOf": [ { - "required": [ - "attributes" - ] + "allOf": [{"$ref": "#/definitions/SemanticConvention"}] }, { - "required": [ - "extends" - ] + "allOf": [{"$ref": "#/definitions/SpanSemanticConvention"}] } - ], - "properties": { - "id": { - "type": "string", - "description": "unique string" - }, - "type": { - "type": "string", - "enum": [ - "span", - "resource", - "metric" - ] - }, - "brief": { - "type": "string", - "description": "a brief description of the semantic convention" - }, - "note": { - "type": "string", - "description": "a more elaborate description of the semantic convention. It defaults to an empty string" - }, - "prefix": { - "type": "string", - "description": "prefix of the attribute for this semconv. It defaults to an empty string." - }, - "extends": { - "type": "string", - "description": "reference another semantic convention ID. It inherits all attributes from the specified semconv." - }, - "span_kind": { - "type": "string", - "enum": [ - "client", - "server", - "producer", - "consumer", - "internal" - ], - "description": "specifies the kind of the span. Leaf semconv nodes (in the hierarchy tree) that do not have this field set will generate a warning." - }, - "attributes": { - "type": "array", - "items": { - "$ref": "#/definitions/Attribute" - }, - "description": "list of attributes that belong to the semconv" + ] + } + } + }, + "definitions": { + "SemanticConventionBase": { + "type": "object", + "required": [ + "id", + "brief" + ], + "anyOf": [ + { + "required": [ + "attributes" + ] + }, + { + "required": [ + "extends" + ] + } + ], + "properties": { + "id": { + "type": "string", + "description": "unique string" + }, + "type": { + "type": "string", + "enum": [ + "span", + "resource", + "metric", + "event" + ], + "description": "The (signal) type of the semantic convention" + }, + "brief": { + "type": "string", + "description": "a brief description of the semantic convention" + }, + "note": { + "type": "string", + "description": "a more elaborate description of the semantic convention. It defaults to an empty string" + }, + "prefix": { + "type": "string", + "description": "prefix of the attribute for this semconv. It defaults to an empty string." + }, + "extends": { + "type": "string", + "description": "reference another semantic convention ID. It inherits all attributes from the specified semconv." + }, + "attributes": { + "type": "array", + "items": { + "$ref": "#/definitions/Attribute" }, - "constraints": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "any_of" - ], - "properties": { - "any_of": { - "type": "array", - "description": " accepts a list of sequences. Each sequence contains a list of attribute ids that are required. any_of enforces that all attributes of at least one of the sequences are set.", - "items": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { + "description": "list of attributes that belong to the semconv" + }, + "constraints": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "any_of" + ], + "properties": { + "any_of": { + "type": "array", + "description": " accepts a list of sequences. Each sequence contains a list of attribute ids that are required. any_of enforces that all attributes of at least one of the sequences are set.", + "items": { + "anyOf": [ + { + "type": "array", + "items": { "type": "string" } - ] - } + }, + { + "type": "string" + } + ] } } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "include" - ], - "properties": { - "include": { - "type": "string", - "description": "accepts a semantic conventions id. It includes as part of this semantic convention all constraints and required attributes that are not already defined in the current semantic convention." - } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "include" + ], + "properties": { + "include": { + "type": "string", + "description": "accepts a semantic conventions id. It includes as part of this semantic convention all constraints and required attributes that are not already defined in the current semantic convention." } } - ] - } + } + ] } } } - } - }, - "definitions": { + }, + "SpanSemanticConvention": { + "allOf": [{ "$ref": "#/definitions/SemanticConventionBase" }], + "properties": { + "type": { + "type": "string", + "const": "span" + }, + "span_kind": { + "type": "string", + "enum": [ + "client", + "server", + "producer", + "consumer", + "internal" + ], + "description": "specifies the kind of the span. Leaf semconv nodes (in the hierarchy tree) that do not have this field set will generate a warning." + } + } + }, + "SemanticConvention": { + "allOf": [{ "$ref": "#/definitions/SemanticConventionBase" }], + "required": ["type"], + "properties": { + "type": { + "type": "string", + "not": { + "const": "span" + } + } + } + }, "AttributeEnumType": { "type": "object", "additionalProperties": false, diff --git a/semantic-conventions/src/opentelemetry/semconv/main.py b/semantic-conventions/src/opentelemetry/semconv/main.py index e40fd13d..10a0e978 100644 --- a/semantic-conventions/src/opentelemetry/semconv/main.py +++ b/semantic-conventions/src/opentelemetry/semconv/main.py @@ -19,7 +19,14 @@ import sys from typing import List -from opentelemetry.semconv.model.semantic_convention import SemanticConventionSet +from opentelemetry.semconv.model.semantic_convention import ( + SemanticConventionSet, + SpanSemanticConvention, + ResourceSemanticConvention, + EventSemanticConvention, + MetricSemanticConvention, + UnitSemanticConvention, +) from opentelemetry.semconv.templating.code import CodeRenderer from opentelemetry.semconv.templating.markdown import MarkdownRenderer @@ -199,7 +206,13 @@ def setup_parser(): ) parser.add_argument( "--only", - choices=["span", "resource", "metric", "units"], + choices=[ + SpanSemanticConvention.GROUP_TYPE_NAME, + ResourceSemanticConvention.GROUP_TYPE_NAME, + EventSemanticConvention.GROUP_TYPE_NAME, + MetricSemanticConvention.GROUP_TYPE_NAME, + UnitSemanticConvention.GROUP_TYPE_NAME, + ], help="Process only semantic conventions of the specified type.", ) parser.add_argument( diff --git a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py index 1051165e..cfedc352 100644 --- a/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py +++ b/semantic-conventions/src/opentelemetry/semconv/model/semantic_convention.py @@ -67,6 +67,7 @@ def parse_semantic_convention_type(type_value): for cls in [ SpanSemanticConvention, ResourceSemanticConvention, + EventSemanticConvention, MetricSemanticConvention, UnitSemanticConvention, ] @@ -123,6 +124,7 @@ def __init__(self, group): stability, deprecated, position_data ) self.extends = group.get("extends", "").strip() + self.events = group.get("events", ()) self.constraints = parse_constraints(group.get("constraints", ())) @property @@ -215,6 +217,7 @@ class SpanSemanticConvention(HasAttributes, BaseSemanticConvention): "prefix", "stability", "extends", + "events", "span_kind", "attributes", "constraints", @@ -230,6 +233,26 @@ def __init__(self, group): raise ValidationError.from_yaml_pos(position, msg) +class EventSemanticConvention(HasAttributes, BaseSemanticConvention): + GROUP_TYPE_NAME = "event" + + allowed_keys = ( + "id", + "type", + "brief", + "note", + "prefix", + "stability", + "extends", + "attributes", + "constraints", + ) + + def __init__(self, group): + super().__init__(group) + self._set_attributes(self.prefix, self.stability, group) + + class UnitSemanticConvention(BaseSemanticConvention): GROUP_TYPE_NAME = "units" @@ -324,6 +347,8 @@ def finish(self): self._populate_extends() # From string containing attribute ids to SemanticAttribute objects self._populate_anyof_attributes() + # From strings containing Semantic Conventions for Events ids to SemanticConvention objects + self._populate_events() def _populate_extends(self): """ @@ -423,6 +448,28 @@ def _populate_anyof_attributes(self): if constraint_attrs: any_of.add_attributes(constraint_attrs) + def _populate_events(self): + for semconv in self.models.values(): + events: typing.List[EventSemanticConvention] = [] + for event_id in semconv.events: + event = self.models.get(event_id) + if event is None: + raise ValidationError.from_yaml_pos( + semconv._position, + "Semantic Convention {} has {} as event but the latter cannot be found!".format( + semconv.semconv_id, event_id + ), + ) + if not isinstance(event, EventSemanticConvention): + raise ValidationError.from_yaml_pos( + semconv._position, + "Semantic Convention {} has {} as event but the latter is not a semantic convention for events!".format( + semconv.semconv_id, event_id + ), + ) + events.append(event) + semconv.events = events + def resolve_ref(self, semconv): fixpoint_ref = True attr: SemanticAttribute diff --git a/semantic-conventions/src/opentelemetry/semconv/version.py b/semantic-conventions/src/opentelemetry/semconv/version.py index f71970e3..8081ee68 100644 --- a/semantic-conventions/src/opentelemetry/semconv/version.py +++ b/semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.2" +__version__ = "0.5.0" diff --git a/semantic-conventions/src/tests/data/markdown/event/event.yaml b/semantic-conventions/src/tests/data/markdown/event/event.yaml new file mode 100644 index 00000000..e90e4a21 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/event/event.yaml @@ -0,0 +1,36 @@ +groups: + - id: event + type: event + prefix: exception + brief: > + This document defines the attributes used to + report a single exception associated with a span. + attributes: + - id: type + type: string + brief: > + The type of the exception. + examples: ["java.net.ConnectException","OSError"] + - id: message + type: string + brief: The exception message. + examples: ["Division by zero","Can't convert 'int' object to str implicitly"] + - id: stacktrace + type: string + brief: > + A stacktrace. + examples: 'Exception in thread "main" java.lang.RuntimeException: Test exception\n + at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\n + at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\n + at com.example.GenerateTrace.main(GenerateTrace.java:5)' + - id: escaped + type: boolean + brief: > + SHOULD be set to true if the exception event is recorded at a point where + it is known that the exception is escaping the scope of the span. + note: |- + An exception is considered to have escaped. + constraints: + - any_of: + - "exception.type" + - "exception.message" \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/markdown/event/expected.md b/semantic-conventions/src/tests/data/markdown/event/expected.md new file mode 100644 index 00000000..7d4e5055 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/event/expected.md @@ -0,0 +1,17 @@ +# Test + + +| Attribute | Type | Description | Examples | Required | +|---|---|---|---|---| +| `exception.type` | string | The type of the exception. | `java.net.ConnectException`; `OSError` | See below | +| `exception.message` | string | The exception message. | `Division by zero`; `Can't convert 'int' object to str implicitly` | See below | +| `exception.stacktrace` | string | A stacktrace. | `Exception in thread "main" java.lang.RuntimeException: Test exception\n at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\n at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\n at com.example.GenerateTrace.main(GenerateTrace.java:5)` | No | +| `exception.escaped` | boolean | SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. [1] | | No | + +**[1]:** An exception is considered to have escaped. + +**Additional attribute requirements:** At least one of the following sets of attributes is required: + +* `exception.type` +* `exception.message` + \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/markdown/event/input.md b/semantic-conventions/src/tests/data/markdown/event/input.md new file mode 100644 index 00000000..59862542 --- /dev/null +++ b/semantic-conventions/src/tests/data/markdown/event/input.md @@ -0,0 +1,5 @@ +# Test + + + + \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/yaml/errors/events/missing_event.yaml b/semantic-conventions/src/tests/data/yaml/errors/events/missing_event.yaml new file mode 100644 index 00000000..3a1f1940 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/errors/events/missing_event.yaml @@ -0,0 +1,21 @@ +groups: + - id: has.events + type: span + prefix: has.events + events: + - exception + - random.event + brief: example + attributes: + - id: flag + type: boolean + brief: An attribute. + + - id: random.event + type: event + prefix: random + brief: example + attributes: + - id: flag + type: boolean + brief: An attribute. diff --git a/semantic-conventions/src/tests/data/yaml/errors/events/no_event_type.yaml b/semantic-conventions/src/tests/data/yaml/errors/events/no_event_type.yaml new file mode 100644 index 00000000..4b3b6fd9 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/errors/events/no_event_type.yaml @@ -0,0 +1,20 @@ +groups: + - id: has.events + type: span + prefix: has.events + events: + - random.event + brief: example + attributes: + - id: flag + type: boolean + brief: An attribute. + + - id: random.event + type: span + prefix: random + brief: example + attributes: + - id: flag + type: boolean + brief: An attribute. diff --git a/semantic-conventions/src/tests/data/yaml/event.yaml b/semantic-conventions/src/tests/data/yaml/event.yaml new file mode 100644 index 00000000..d0c69d06 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/event.yaml @@ -0,0 +1,36 @@ +groups: + - id: exception + type: event + prefix: exception + brief: > + This document defines the attributes used to + report a single exception associated with a span. + attributes: + - id: type + type: string + brief: > + The type of the exception. + examples: ["java.net.ConnectException","OSError"] + - id: message + type: string + brief: The exception message. + examples: ["Division by zero","Can't convert 'int' object to str implicitly"] + - id: stacktrace + type: string + brief: > + A stacktrace. + examples: 'Exception in thread "main" java.lang.RuntimeException: Test exception\n + at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\n + at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\n + at com.example.GenerateTrace.main(GenerateTrace.java:5)' + - id: escaped + type: boolean + brief: > + SHOULD be set to true if the exception event is recorded at a point where + it is known that the exception is escaping the scope of the span. + note: |- + An exception is considered to have escaped. + constraints: + - any_of: + - "exception.type" + - "exception.message" \ No newline at end of file diff --git a/semantic-conventions/src/tests/data/yaml/span_event.yaml b/semantic-conventions/src/tests/data/yaml/span_event.yaml new file mode 100644 index 00000000..3a1f1940 --- /dev/null +++ b/semantic-conventions/src/tests/data/yaml/span_event.yaml @@ -0,0 +1,21 @@ +groups: + - id: has.events + type: span + prefix: has.events + events: + - exception + - random.event + brief: example + attributes: + - id: flag + type: boolean + brief: An attribute. + + - id: random.event + type: event + prefix: random + brief: example + attributes: + - id: flag + type: boolean + brief: An attribute. 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 b906f1c4..b48f9b88 100644 --- a/semantic-conventions/src/tests/semconv/model/test_correct_parse.py +++ b/semantic-conventions/src/tests/semconv/model/test_correct_parse.py @@ -15,7 +15,7 @@ import os import unittest -from opentelemetry.semconv.model.constraints import Include +from opentelemetry.semconv.model.constraints import Include, AnyOf from opentelemetry.semconv.model.semantic_attribute import ( SemanticAttribute, StabilityLevel, @@ -23,6 +23,8 @@ from opentelemetry.semconv.model.semantic_convention import ( parse_semantic_convention_groups, SemanticConventionSet, + SpanSemanticConvention, + EventSemanticConvention, ) @@ -214,6 +216,54 @@ def test_resource(self): } self.semantic_convention_check(cloud, expected) + def test_event(self): + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("yaml/event.yaml")) + semconv.finish() + self.assertEqual(len(semconv.models), 1) + event = list(semconv.models.values())[0] + expected = { + "id": "exception", + "prefix": "exception", + "extends": "", + "n_constraints": 1, + "attributes": [ + "exception.type", + "exception.message", + "exception.stacktrace", + "exception.escaped", + ], + } + self.semantic_convention_check(event, expected) + constraint = event.constraints[0] + self.assertTrue(isinstance(constraint, AnyOf)) + constraint: AnyOf + for choice_index in range(len(constraint.choice_list_ids)): + attr_list = constraint.choice_list_ids[choice_index] + for attr_index in range(len(attr_list)): + attr = attr_list[attr_index] + self.assertEqual( + event.attrs_by_name.get(attr), + constraint.choice_list_attributes[choice_index][attr_index], + ) + + def test_span_with_event(self): + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("yaml/event.yaml")) + semconv.parse(self.load_file("yaml/span_event.yaml")) + semconv.finish() + self.assertEqual(len(semconv.models), 3) + semconvs = list(semconv.models.values()) + self.assertTrue(isinstance(semconvs[0], EventSemanticConvention)) + self.assertTrue(isinstance(semconvs[1], SpanSemanticConvention)) + self.assertTrue(isinstance(semconvs[2], EventSemanticConvention)) + event_semconv = semconvs[1] + self.assertEqual(2, len(event_semconv.events)) + self.assertTrue(isinstance(event_semconv.events[0], EventSemanticConvention)) + self.assertTrue(isinstance(event_semconv.events[1], EventSemanticConvention)) + self.assertEqual("exception", event_semconv.events[0].semconv_id) + self.assertEqual("random.event", event_semconv.events[1].semconv_id) + def test_rpc(self): semconv = SemanticConventionSet(debug=False) semconv.parse(self.load_file("yaml/rpc.yaml")) diff --git a/semantic-conventions/src/tests/semconv/model/test_error_detection.py b/semantic-conventions/src/tests/semconv/model/test_error_detection.py index 3a217889..3617d347 100644 --- a/semantic-conventions/src/tests/semconv/model/test_error_detection.py +++ b/semantic-conventions/src/tests/semconv/model/test_error_detection.py @@ -421,6 +421,28 @@ def test_validate_anyof_attributes(self): self.assertIn("does not exists", msg) self.assertEqual(e.line, 15) + def test_missing_event(self): + with self.assertRaises(ValidationError) as ex: + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("yaml/errors/events/missing_event.yaml")) + semconv.finish() + e = ex.exception + msg = e.message.lower() + self.assertIn("as event but the latter cannot be found!", msg) + self.assertEqual(e.line, 2) + + def test_wrong_event_type(self): + with self.assertRaises(ValidationError) as ex: + semconv = SemanticConventionSet(debug=False) + semconv.parse(self.load_file("yaml/errors/events/no_event_type.yaml")) + semconv.finish() + e = ex.exception + msg = e.message.lower() + self.assertIn( + "as event but the latter is not a semantic convention for events", msg + ) + self.assertEqual(e.line, 2) + def open_yaml(self, path): with open(self.load_file(path), encoding="utf-8") as file: return parse_semantic_convention_groups(file) diff --git a/semantic-conventions/src/tests/semconv/templating/test_markdown.py b/semantic-conventions/src/tests/semconv/templating/test_markdown.py index 374ccd04..1dfa963e 100644 --- a/semantic-conventions/src/tests/semconv/templating/test_markdown.py +++ b/semantic-conventions/src/tests/semconv/templating/test_markdown.py @@ -114,6 +114,9 @@ def test_wrong_duplicate(self): def test_units(self): self.check("markdown/metrics_unit/", extra_yaml_dirs=["yaml/metrics/"]) + def test_event(self): + self.check("markdown/event/") + def check( self, input_dir: str, diff --git a/semantic-conventions/syntax.md b/semantic-conventions/syntax.md index 471e75f2..aa2a7699 100644 --- a/semantic-conventions/syntax.md +++ b/semantic-conventions/syntax.md @@ -36,12 +36,13 @@ All attributes are lower case. groups ::= semconv | semconv groups -semconv ::= id [type] brief [note] [prefix] [extends] [stability] [deprecated] [span_kind] attributes [constraints] +semconv ::= id [type] brief [note] [prefix] [events] [extends] [stability] [deprecated] [span_kind] attributes [constraints] id ::= string type ::= "span" # Default if not specified | "resource" + | "event" | "metric" brief ::= string @@ -49,7 +50,6 @@ note ::= string prefix ::= string -# extends MUST point to an existing semconv id extends ::= string stability ::= "deprecated" @@ -142,6 +142,8 @@ The field `semconv` represents a semantic convention and it is made by: The following is only valid if `type` is `span` (the default): - `span_kind`, optional enum, specifies the kind of the span. +- `events`, optional list of strings that specify the ids of + event semantic conventions associated with this span semantic convention. ### Attributes