diff --git a/crates/weaver_forge/data/mobile-events.yaml b/crates/weaver_forge/data/mobile-events.yaml index b1b219d5..568048cc 100644 --- a/crates/weaver_forge/data/mobile-events.yaml +++ b/crates/weaver_forge/data/mobile-events.yaml @@ -10,6 +10,7 @@ groups: the `android.state` and `ios.state` fields. The `android.state` and `ios.state` attributes are mutually exclusive. body: + type: map fields: - id: ios.state stability: experimental diff --git a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_events.json b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_events.json index 16f63a79..94dd5e18 100644 --- a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_events.json +++ b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_events.json @@ -1 +1 @@ -[{"attributes":[],"body":{"fields":[{"brief":"This attribute represents the state the application has transitioned into at the occurrence of the event.\n","name":"ios.state","note":"The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate#1656902), and from which the `OS terminology` column values are derived.\n","requirement_level":{"conditionally_required":"if and only if `os.name` is `ios`"},"stability":"experimental","type":{"allow_custom_values":false,"members":[{"brief":"The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`.\n","deprecated":null,"id":"active","note":null,"stability":null,"value":"active"},{"brief":"The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`.\n","deprecated":null,"id":"inactive","note":null,"stability":null,"value":"inactive"},{"brief":"The app is now in the background. This value is associated with UIKit notification `applicationDidEnterBackground`.\n","deprecated":null,"id":"background","note":null,"stability":null,"value":"background"},{"brief":"The app is now in the foreground. This value is associated with UIKit notification `applicationWillEnterForeground`.\n","deprecated":null,"id":"foreground","note":null,"stability":null,"value":"foreground"},{"brief":"The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`.\n","deprecated":null,"id":"terminate","note":null,"stability":null,"value":"terminate"}]}},{"brief":"This attribute represents the state the application has transitioned into at the occurrence of the event.\n","name":"android.state","note":"The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lc), and from which the `OS identifiers` are derived.\n","requirement_level":{"conditionally_required":"if and only if `os.name` is `android`"},"stability":"experimental","type":{"allow_custom_values":false,"members":[{"brief":"Any time before Activity.onResume() or, if the app has no Activity, Context.startService() has been called in the app for the first time.\n","deprecated":null,"id":"created","note":null,"stability":null,"value":"created"},{"brief":"Any time after Activity.onPause() or, if the app has no Activity, Context.stopService() has been called when the app was in the foreground state.\n","deprecated":null,"id":"background","note":null,"stability":null,"value":"background"},{"brief":"Any time after Activity.onResume() or, if the app has no Activity, Context.startService() has been called when the app was in either the created or background states.","deprecated":null,"id":"foreground","note":null,"stability":null,"value":"foreground"}]}}]},"brief":"This event represents an occurrence of a lifecycle transition on Android or iOS platform.\n","event_namespace":"device.app","events":[],"id":"device.app.lifecycle","instrument":null,"lineage":{"source_file":"data/mobile-events.yaml"},"metric_name":null,"name":"device.app.lifecycle","note":"This event identifies the fields that are common to all lifecycle events for android and iOS using the `android.state` and `ios.state` fields. The `android.state` and `ios.state` attributes are mutually exclusive.\n","span_kind":null,"stability":"experimental","type":"event","unit":null},{"attributes":[{"brief":"A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG.\n","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)","name":"exception.stacktrace","requirement_level":"recommended","stability":"stable","type":"string"},{"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.\n","name":"exception.escaped","note":"An exception is considered to have escaped (or left) the scope of a span,\nif that span is ended while the exception is still logically \"in flight\".\nThis may be actually \"in flight\" in some languages (e.g. if the exception\nis passed to a Context manager\u0027s `__exit__` method in Python) but will\nusually be caught at the point of recording the exception in most languages.\n\nIt is usually not possible to determine at the point where an exception is thrown\nwhether it will escape the scope of a span.\nHowever, it is trivial to know that an exception\nwill escape, if one checks for an active exception just before ending the span,\nas done in the [example for recording span exceptions](https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/#recording-an-exception).\n\nIt follows that an exception may still escape the scope of the span\neven if the `exception.escaped` attribute was not set or set to false,\nsince the event might have been recorded at a time where it was not\nclear whether the exception will escape.","requirement_level":"recommended","stability":"stable","type":"boolean"},{"brief":"The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it.\n","examples":["java.net.ConnectException","OSError"],"name":"exception.type","requirement_level":{"conditionally_required":"Required if `exception.message` is not set, recommended otherwise."},"stability":"stable","type":"string"},{"brief":"The exception message.","examples":["Division by zero","Can\u0027t convert \u0027int\u0027 object to str implicitly"],"name":"exception.message","requirement_level":{"conditionally_required":"Required if `exception.type` is not set, recommended otherwise."},"stability":"stable","type":"string"}],"brief":"This document defines the attributes used to report a single exception associated with a span.\n","event_namespace":"other","events":[],"id":"trace-exception","instrument":null,"lineage":{"attributes":{"exception.escaped":{"inherited_fields":["brief","note","requirement_level","stability"],"source_group":"registry.exception"},"exception.message":{"inherited_fields":["brief","examples","note","stability"],"locally_overridden_fields":["requirement_level"],"source_group":"registry.exception"},"exception.stacktrace":{"inherited_fields":["brief","examples","note","requirement_level","stability"],"source_group":"registry.exception"},"exception.type":{"inherited_fields":["brief","examples","note","stability"],"locally_overridden_fields":["requirement_level"],"source_group":"registry.exception"}},"source_file":"data/trace-exception.yaml"},"metric_name":null,"name":null,"prefix":"exception","span_kind":null,"type":"event","unit":null}] \ No newline at end of file +[{"attributes":[],"body":{"fields":[{"brief":"This attribute represents the state the application has transitioned into at the occurrence of the event.\n","name":"ios.state","note":"The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate#1656902), and from which the `OS terminology` column values are derived.\n","requirement_level":{"conditionally_required":"if and only if `os.name` is `ios`"},"stability":"experimental","type":{"allow_custom_values":false,"members":[{"brief":"The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`.\n","deprecated":null,"id":"active","note":null,"stability":null,"value":"active"},{"brief":"The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`.\n","deprecated":null,"id":"inactive","note":null,"stability":null,"value":"inactive"},{"brief":"The app is now in the background. This value is associated with UIKit notification `applicationDidEnterBackground`.\n","deprecated":null,"id":"background","note":null,"stability":null,"value":"background"},{"brief":"The app is now in the foreground. This value is associated with UIKit notification `applicationWillEnterForeground`.\n","deprecated":null,"id":"foreground","note":null,"stability":null,"value":"foreground"},{"brief":"The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`.\n","deprecated":null,"id":"terminate","note":null,"stability":null,"value":"terminate"}]}},{"brief":"This attribute represents the state the application has transitioned into at the occurrence of the event.\n","name":"android.state","note":"The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lc), and from which the `OS identifiers` are derived.\n","requirement_level":{"conditionally_required":"if and only if `os.name` is `android`"},"stability":"experimental","type":{"allow_custom_values":false,"members":[{"brief":"Any time before Activity.onResume() or, if the app has no Activity, Context.startService() has been called in the app for the first time.\n","deprecated":null,"id":"created","note":null,"stability":null,"value":"created"},{"brief":"Any time after Activity.onPause() or, if the app has no Activity, Context.stopService() has been called when the app was in the foreground state.\n","deprecated":null,"id":"background","note":null,"stability":null,"value":"background"},{"brief":"Any time after Activity.onResume() or, if the app has no Activity, Context.startService() has been called when the app was in either the created or background states.","deprecated":null,"id":"foreground","note":null,"stability":null,"value":"foreground"}]}}],"type":"map"},"brief":"This event represents an occurrence of a lifecycle transition on Android or iOS platform.\n","event_namespace":"device.app","events":[],"id":"device.app.lifecycle","instrument":null,"lineage":{"source_file":"data/mobile-events.yaml"},"metric_name":null,"name":"device.app.lifecycle","note":"This event identifies the fields that are common to all lifecycle events for android and iOS using the `android.state` and `ios.state` fields. The `android.state` and `ios.state` attributes are mutually exclusive.\n","span_kind":null,"stability":"experimental","type":"event","unit":null},{"attributes":[{"brief":"A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG.\n","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)","name":"exception.stacktrace","requirement_level":"recommended","stability":"stable","type":"string"},{"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.\n","name":"exception.escaped","note":"An exception is considered to have escaped (or left) the scope of a span,\nif that span is ended while the exception is still logically \"in flight\".\nThis may be actually \"in flight\" in some languages (e.g. if the exception\nis passed to a Context manager\u0027s `__exit__` method in Python) but will\nusually be caught at the point of recording the exception in most languages.\n\nIt is usually not possible to determine at the point where an exception is thrown\nwhether it will escape the scope of a span.\nHowever, it is trivial to know that an exception\nwill escape, if one checks for an active exception just before ending the span,\nas done in the [example for recording span exceptions](https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/#recording-an-exception).\n\nIt follows that an exception may still escape the scope of the span\neven if the `exception.escaped` attribute was not set or set to false,\nsince the event might have been recorded at a time where it was not\nclear whether the exception will escape.","requirement_level":"recommended","stability":"stable","type":"boolean"},{"brief":"The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it.\n","examples":["java.net.ConnectException","OSError"],"name":"exception.type","requirement_level":{"conditionally_required":"Required if `exception.message` is not set, recommended otherwise."},"stability":"stable","type":"string"},{"brief":"The exception message.","examples":["Division by zero","Can\u0027t convert \u0027int\u0027 object to str implicitly"],"name":"exception.message","requirement_level":{"conditionally_required":"Required if `exception.type` is not set, recommended otherwise."},"stability":"stable","type":"string"}],"brief":"This document defines the attributes used to report a single exception associated with a span.\n","event_namespace":"other","events":[],"id":"trace-exception","instrument":null,"lineage":{"attributes":{"exception.escaped":{"inherited_fields":["brief","note","requirement_level","stability"],"source_group":"registry.exception"},"exception.message":{"inherited_fields":["brief","examples","note","stability"],"locally_overridden_fields":["requirement_level"],"source_group":"registry.exception"},"exception.stacktrace":{"inherited_fields":["brief","examples","note","requirement_level","stability"],"source_group":"registry.exception"},"exception.type":{"inherited_fields":["brief","examples","note","stability"],"locally_overridden_fields":["requirement_level"],"source_group":"registry.exception"}},"source_file":"data/trace-exception.yaml"},"metric_name":null,"name":null,"prefix":"exception","span_kind":null,"type":"event","unit":null}] \ No newline at end of file diff --git a/crates/weaver_resolved_schema/src/body.rs b/crates/weaver_resolved_schema/src/body.rs index 3595b8d7..3dc0ec19 100644 --- a/crates/weaver_resolved_schema/src/body.rs +++ b/crates/weaver_resolved_schema/src/body.rs @@ -7,20 +7,37 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use weaver_semconv::attribute::{AttributeType, Examples, RequirementLevel}; -use weaver_semconv::body::{BodyFieldSpec, BodySpec}; +use weaver_semconv::body::{BodyFieldSpec, BodySpec, BodyType}; use weaver_semconv::stability::Stability; /// A `Body` definition. #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Body { - /// The body specification. + /// Identifies the type of the body. It can be "map", "string". + pub r#type: BodyType, + /// A brief description of the body. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + pub brief: String, + /// A more elaborate description of the body. + /// It defaults to an empty string. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + pub note: String, + /// Specifies the stability of the body. + #[serde(skip_serializing_if = "Option::is_none")] + pub stability: Option, + /// Sequence of example values for the body or single example + /// value. They are required only for string types. Example values + /// must be of the same type of the body. If only a single example is + /// provided, it can directly be reported without encapsulating it + /// into a sequence/dictionary. + #[serde(skip_serializing_if = "Option::is_none")] + pub examples: Option, + /// Identifies the definition of the "fields" of the body when the body type is "map". #[serde(skip_serializing_if = "Option::is_none")] pub fields: Option>, - // Not yet defined in the spec or implemented in the resolver - // The body value when there are no fields - // #[serde(skip_serializing_if = "Option::is_none")] - // pub value: Option } /// A `BodyField` definition. diff --git a/crates/weaver_resolved_schema/src/error.rs b/crates/weaver_resolved_schema/src/error.rs index 39cdb2cb..b716910b 100644 --- a/crates/weaver_resolved_schema/src/error.rs +++ b/crates/weaver_resolved_schema/src/error.rs @@ -3,9 +3,10 @@ //! Error types and utilities. use serde::{Deserialize, Serialize}; +use weaver_semconv::body::BodySpec; use crate::attribute::AttributeRef; -use crate::error::Error::{AttributeNotFound, CompoundError, NotImplemented}; +use crate::error::Error::{AttributeNotFound, CompoundError, InvalidBody}; /// Errors emitted by this crate. #[derive(thiserror::Error, Debug, Clone, Deserialize, Serialize)] @@ -24,10 +25,10 @@ pub enum Error { CompoundError(Vec), /// A generic error identifying a feature that has not yet been implemented. - #[error("Not Implemented: {message}")] - NotImplemented { - /// A message describing the feature that has not been implemented. - message: String, + #[error("Unsupported BodySpec")] + InvalidBody { + /// The body specification that is not supported. + body: BodySpec, }, } @@ -52,7 +53,7 @@ impl Error { .flat_map(|e| match e { CompoundError(errors) => errors, e @ AttributeNotFound { .. } => vec![e], - e @ NotImplemented { .. } => vec![e], + e @ InvalidBody { .. } => vec![e], }) .collect(), ) diff --git a/crates/weaver_resolved_schema/src/signal.rs b/crates/weaver_resolved_schema/src/signal.rs index 0c609abb..095f7291 100644 --- a/crates/weaver_resolved_schema/src/signal.rs +++ b/crates/weaver_resolved_schema/src/signal.rs @@ -49,7 +49,7 @@ pub struct MultivariateMetric { #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Event { - /// The name of the event. + /// The name of the event name: String, /// References to attributes defined in the catalog. #[serde(skip_serializing_if = "Vec::is_empty")] diff --git a/crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json index 210bbf71..1050f62e 100644 --- a/crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json +++ b/crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json @@ -25,7 +25,7 @@ "Linux" ], "requirement_level": "recommended", - "note": "This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.platform`). If unavailable, the legacy `navigator.platform` API SHOULD NOT be used instead and this attribute SHOULD be left unset in order for the values to be consistent. The list of possible values is defined in the [W3C User-Agent Client Hints specification](https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform). Note that some (but not all) of these values can overlap with values in the [`os.type` and `os.name` attributes](./os.md). However, for consistency, the values in the `browser.platform` attribute should capture the exact value that the user agent provides.\n" + "note": "This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.platform`). \n" }, { "name": "browser.mobile", diff --git a/crates/weaver_resolver/data/registry-test-4-events/expected-events.json b/crates/weaver_resolver/data/registry-test-4-events/expected-events.json index b0986c21..2ccc397e 100644 --- a/crates/weaver_resolver/data/registry-test-4-events/expected-events.json +++ b/crates/weaver_resolver/data/registry-test-4-events/expected-events.json @@ -95,7 +95,8 @@ ] } } - ] + ], + "type": "map" }, "brief": "This event represents an occurrence of a lifecycle transition on Android or iOS platform.\n", "event_namespace": "device.app", diff --git a/crates/weaver_resolver/data/registry-test-4-events/expected-registry.json b/crates/weaver_resolver/data/registry-test-4-events/expected-registry.json index 34e82263..06b0b3fe 100644 --- a/crates/weaver_resolver/data/registry-test-4-events/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-4-events/expected-registry.json @@ -2,7 +2,7 @@ "registry_url": "https://127.0.0.1", "groups": [ { - "id": "registry.browser", + "id": "browser.test.event_with_fields", "type": "event", "brief": "An event that adds global attributes for reuse.\n", "prefix": "browser", @@ -12,11 +12,44 @@ 2, 3 ], - "name": "browser.test.event", + "name": "browser.test.event_with_fields", "lineage": { "source_file": "data/registry-test-4-events/registry/browser-event.yaml" }, "body": { + "type": "map", + "fields": [ + { + "name": "some.field", + "type": "string", + "brief": "A field that is not referenced in the attributes", + "examples": [ + "some value", + "another value" + ], + "requirement_level": "recommended", + "note": "This field is not referenced in the attributes" + } + ] + } + }, + { + "id": "browser.test.event_with_body_details", + "type": "event", + "brief": "An event that adds global attributes for reuse.\n", + "attributes": [], + "name": "browser.test.event_with_body_details", + "lineage": { + "source_file": "data/registry-test-4-events/registry/browser-event.yaml" + }, + "body": { + "type": "map", + "brief": "A map of fields that are not referenced in the attributes", + "note": "This map is not referenced in the attributes", + "stability": "experimental", + "examples": [ + "{ \"some.field\": \"some value\" }" + ], "fields": [ { "name": "some.field", @@ -79,7 +112,8 @@ "requirement_level": "recommended", "note": "An exception is considered to have escaped." } - ] + ], + "type": "map" } }, { @@ -254,7 +288,8 @@ "note": "The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lc), and from which the `OS identifiers` are derived.\n", "stability": "experimental" } - ] + ], + "type": "map" } }, { @@ -405,6 +440,46 @@ "source_file": "data/registry-test-4-events/registry/referenced-server.yaml" } }, + { + "id": "some.string.body.event", + "type": "event", + "brief": "This event represents an occurrence of a something.\n", + "note": "This event transmits the body as a JSON encoded string.\n", + "stability": "experimental", + "attributes": [], + "name": "string.body.event", + "lineage": { + "source_file": "data/registry-test-4-events/registry/stringbody-event.yaml" + }, + "body": { + "type": "string", + "brief": "This is the body of the event which is a JSON encoded string.\n", + "examples": [ + "{\"key1\":\"value1\",\"key2\":\"value2\"}" + ] + } + }, + { + "id": "some.string.body.detailed.event", + "type": "event", + "brief": "This event represents an occurrence of a something.\n", + "note": "This event transmits the body as a JSON encoded string.\n", + "stability": "experimental", + "attributes": [], + "name": "string.body.event", + "lineage": { + "source_file": "data/registry-test-4-events/registry/stringbody-event.yaml" + }, + "body": { + "type": "string", + "brief": "This is the body of the event which is a JSON encoded string.\n", + "note": "This is a detailed note about the body.\n", + "stability": "experimental", + "examples": [ + "{\"key1\":\"value1\",\"key2\":\"value2\"}" + ] + } + }, { "id": "trace-exception", "type": "event", diff --git a/crates/weaver_resolver/data/registry-test-4-events/registry/browser-event.yaml b/crates/weaver_resolver/data/registry-test-4-events/registry/browser-event.yaml index 126a24a6..00560e0c 100644 --- a/crates/weaver_resolver/data/registry-test-4-events/registry/browser-event.yaml +++ b/crates/weaver_resolver/data/registry-test-4-events/registry/browser-event.yaml @@ -1,6 +1,6 @@ groups: - - id: registry.browser - name: browser.test.event + - id: browser.test.event_with_fields + name: browser.test.event_with_fields prefix: browser type: event brief: > @@ -20,15 +20,7 @@ groups: note: > This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) - (`navigator.userAgentData.platform`). If unavailable, the legacy - `navigator.platform` API SHOULD NOT be used instead and this attribute - SHOULD be left unset in order for the values to be consistent. - The list of possible values is defined in the - [W3C User-Agent Client Hints specification](https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform). - Note that some (but not all) of these values can overlap with values - in the [`os.type` and `os.name` attributes](./os.md). - However, for consistency, the values in the `browser.platform` attribute - should capture the exact value that the user agent provides. + (`navigator.userAgentData.platform`). examples: [ "Windows", "macOS", "Android", "iOS", "Linux" ] - id: mobile type: boolean @@ -46,6 +38,25 @@ groups: `navigator.language`. examples: [ "en", "en-US", "en-AU", "fr", "fr-FR" ] body: + type: map + fields: + - id: some.field + type: string + brief: 'A field that is not referenced in the attributes' + note: 'This field is not referenced in the attributes' + examples: [ "some value", "another value" ] + + - id: browser.test.event_with_body_details + name: browser.test.event_with_body_details + type: event + brief: > + An event that adds global attributes for reuse. + body: + type: map + brief: A map of fields that are not referenced in the attributes + note: This map is not referenced in the attributes + stability: experimental + examples: [ '{ "some.field": "some value" }' ] fields: - id: some.field type: string diff --git a/crates/weaver_resolver/data/registry-test-4-events/registry/client-exception-event.yaml b/crates/weaver_resolver/data/registry-test-4-events/registry/client-exception-event.yaml index 5e6819fa..84a0df0d 100644 --- a/crates/weaver_resolver/data/registry-test-4-events/registry/client-exception-event.yaml +++ b/crates/weaver_resolver/data/registry-test-4-events/registry/client-exception-event.yaml @@ -6,6 +6,7 @@ groups: This document defines the log event used to report a client exception. body: + type: map fields: - id: type type: string diff --git a/crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml b/crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml index b1b219d5..568048cc 100644 --- a/crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml +++ b/crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml @@ -10,6 +10,7 @@ groups: the `android.state` and `ios.state` fields. The `android.state` and `ios.state` attributes are mutually exclusive. body: + type: map fields: - id: ios.state stability: experimental diff --git a/crates/weaver_resolver/data/registry-test-4-events/registry/stringbody-event.yaml b/crates/weaver_resolver/data/registry-test-4-events/registry/stringbody-event.yaml new file mode 100644 index 00000000..17b37d70 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-4-events/registry/stringbody-event.yaml @@ -0,0 +1,31 @@ +groups: + - id: some.string.body.event + stability: experimental + type: event + name: string.body.event + brief: > + This event represents an occurrence of a something. + note: > + This event transmits the body as a JSON encoded string. + body: + type: string + brief: > + This is the body of the event which is a JSON encoded string. + examples: ['{"key1":"value1","key2":"value2"}'] + + - id: some.string.body.detailed.event + stability: experimental + type: event + name: string.body.event + brief: > + This event represents an occurrence of a something. + note: > + This event transmits the body as a JSON encoded string. + body: + type: string + brief: > + This is the body of the event which is a JSON encoded string. + note: > + This is a detailed note about the body. + stability: experimental + examples: ['{"key1":"value1","key2":"value2"}'] \ No newline at end of file diff --git a/crates/weaver_resolver/src/body.rs b/crates/weaver_resolver/src/body.rs index ee70af9f..ae28eac5 100644 --- a/crates/weaver_resolver/src/body.rs +++ b/crates/weaver_resolver/src/body.rs @@ -6,18 +6,26 @@ use weaver_resolved_schema::{ body::{Body, BodyField}, error::Error, }; -use weaver_semconv::body::BodySpec; +use weaver_semconv::body::{BodySpec, BodyType}; /// Resolve a `Body` specification into a resolved `Body`. pub fn resolve_body_spec(body: &BodySpec) -> Result, Error> { match body { - BodySpec::Fields { fields } => { + BodySpec::Fields { + r#type: BodyType::Map, + brief, + note, + stability, + examples, + fields, + .. + } => { let mut body_fields = Vec::new(); for field in fields.iter() { body_fields.push(BodyField { name: field.id.clone(), r#type: field.r#type.clone(), - brief: field.brief.clone().unwrap_or_else(|| "".to_owned()), + brief: field.brief.clone(), examples: field.examples.clone(), requirement_level: field.requirement_level.clone(), note: field.note.clone(), @@ -26,15 +34,34 @@ pub fn resolve_body_spec(body: &BodySpec) -> Result, Error> { }); } Ok(Some(Body { + r#type: BodyType::Map, + brief: brief.clone(), + note: note.clone(), + stability: stability.clone(), + examples: examples.clone(), fields: Some(body_fields), - // value: None, // Not yet implemented })) } - BodySpec::Value { value: _ } => { - // Add as a placeholder for now of where to resolve the value. - Err(Error::NotImplemented { - message: "Value type for body is not currently implemented.".to_owned(), - }) + BodySpec::String { + r#type: BodyType::String, + brief, + note, + stability, + examples, + } => { + // string types must have a brief and examples + if brief.is_empty() || examples.is_none() { + return Err(Error::InvalidBody { body: body.clone() }); + } + Ok(Some(Body { + r#type: BodyType::String, + brief: brief.clone(), + note: note.clone(), + stability: stability.clone(), + examples: examples.clone(), + fields: None, + })) } + _ => Err(Error::InvalidBody { body: body.clone() }), } } diff --git a/crates/weaver_semconv/data/event.yaml b/crates/weaver_semconv/data/event.yaml index 241f88b7..6ea2084d 100644 --- a/crates/weaver_semconv/data/event.yaml +++ b/crates/weaver_semconv/data/event.yaml @@ -10,6 +10,7 @@ groups: the `android.state` and `ios.state` fields. The `android.state` and `ios.state` attributes are mutually exclusive. body: + type: map fields: - id: ios.state stability: experimental @@ -134,6 +135,7 @@ groups: requirement_level: conditionally_required: Required if `attribute1` is not set, recommended otherwise. body: + type: map fields: - id: field1 type: string @@ -146,4 +148,28 @@ groups: brief: 'The field2' examples: ['value1', 'value2'] requirement_level: - conditionally_required: Required if `field1` is not set, recommended otherwise. \ No newline at end of file + conditionally_required: Required if `field1` is not set, recommended otherwise. + + - id: test.event.with_string_body + name: test.event.with_string_body + stability: experimental + type: event + brief: > + This event represents an occurrence of a test event. + attributes: + - id: attribute1 + type: string + brief: 'The attribute1' + examples: ['value1', 'value2'] + requirement_level: + conditionally_required: Required if `attribute2` is not set, recommended otherwise. + - id: attribute2 + type: string + brief: 'The attribute2' + examples: ['value1', 'value2'] + requirement_level: + conditionally_required: Required if `attribute1` is not set, recommended otherwise. + body: + type: string + brief: 'The body of the event' + examples: ['{ name: "thename", content: ... }', '{ name: "thename", content: ... }'] diff --git a/crates/weaver_semconv/src/body.rs b/crates/weaver_semconv/src/body.rs index 19a70660..eb6ba229 100644 --- a/crates/weaver_semconv/src/body.rs +++ b/crates/weaver_semconv/src/body.rs @@ -4,11 +4,11 @@ //! Body Field specification. +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; -use crate::attribute::{ - AttributeType, BasicRequirementLevelSpec, Examples, RequirementLevel, ValueSpec, -}; +use crate::attribute::{AttributeType, BasicRequirementLevelSpec, Examples, RequirementLevel}; use crate::stability::Stability; /// A body specification @@ -19,14 +19,54 @@ use crate::stability::Stability; pub enum BodySpec { /// The collection of body fields associated with a body definition Fields { - /// The collection of body fields associated with a body definition + /// Identifies that the type of the body is a map of fields or a string. + r#type: BodyType, + /// A brief description of the body. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + brief: String, + /// A more elaborate description of the body. + /// It defaults to an empty string. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + note: String, + /// Specifies the stability of the body. + #[serde(skip_serializing_if = "Option::is_none")] + stability: Option, + /// Sequence of example values for the body or single example + /// value. They are required only for string types. Example values + /// must be of the same type of the body. If only a single example is + /// provided, it can directly be reported without encapsulating it + /// into a sequence/dictionary. + #[serde(skip_serializing_if = "Option::is_none")] + examples: Option, + /// Identifies the definition of the "fields" of the body when the body type is "map". #[serde(skip_serializing_if = "Vec::is_empty")] fields: Vec, }, - /// The body field value. - Value { - /// The body field value. - value: ValueSpec, + /// The body will just be a string. + String { + /// Identifies that the type of the body is a string. + r#type: BodyType, + /// A brief description of the body. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + brief: String, + /// A more elaborate description of the body. + /// It defaults to an empty string. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + note: String, + /// Specifies the stability of the body. + #[serde(skip_serializing_if = "Option::is_none")] + stability: Option, + /// Sequence of example values for the body or single example + /// value. They are required only for string types. Example values + /// must be of the same type of the body. If only a single example is + /// provided, it can directly be reported without encapsulating it + /// into a sequence/dictionary. + #[serde(skip_serializing_if = "Option::is_none")] + examples: Option, }, } @@ -35,8 +75,28 @@ impl BodySpec { #[must_use] pub fn has_fields(&self) -> bool { match self { - BodySpec::Fields { fields } => !fields.is_empty(), - BodySpec::Value { value: _ } => false, + BodySpec::Fields { fields, .. } => !fields.is_empty(), + BodySpec::String { .. } => false, + } + } +} + +/// Identifies the different types of body (specification). +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum BodyType { + /// A map body type. + Map, + /// A string body type. + String, +} + +/// Implements a human readable display for PrimitiveOrArrayType. +impl Display for BodyType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BodyType::String => write!(f, "string"), + BodyType::Map => write!(f, "map"), } } } @@ -52,7 +112,7 @@ pub struct BodyFieldSpec { /// array type, a template type or an enum definition. pub r#type: AttributeType, /// A brief description of the body field. - pub brief: Option, + pub brief: String, /// Sequence of example values for the body field or single example /// value. They are required only for string and string array /// fields. Example values must be of the same type of the @@ -215,12 +275,69 @@ mod tests { ); } + #[test] + fn test_field_body() { + let body = BodySpec::Fields { + r#type: BodyType::Map, + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: Some(Stability::Stable), + examples: Some(Examples::Int(42)), + fields: vec![BodyFieldSpec { + id: "id".to_owned(), + r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Int), + brief: "brief".to_owned(), + examples: Some(Examples::Int(42)), + requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required), + note: "note".to_owned(), + stability: Some(Stability::Stable), + deprecated: Some("deprecated".to_owned()), + }], + }; + + assert!(matches!(body, BodySpec::Fields { .. })); + assert!(!matches!(body, BodySpec::String { .. })); + assert!(body.has_fields()); + + if let BodySpec::Fields { + brief, + note, + fields, + .. + } = body + { + assert_eq!(brief, "brief"); + assert_eq!(note, "note"); + assert!(fields.len() == 1); + } + } + + #[test] + fn test_string_body() { + let body = BodySpec::String { + r#type: BodyType::String, + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: Some(Stability::Stable), + examples: Some(Examples::String("{key: value}".to_owned())), + }; + + assert!(matches!(body, BodySpec::String { .. })); + assert!(!matches!(body, BodySpec::Fields { .. })); + assert!(!body.has_fields()); + + if let BodySpec::String { brief, note, .. } = body { + assert_eq!(brief, "brief"); + assert_eq!(note, "note"); + } + } + #[test] fn test_body_field() { let field = BodyFieldSpec { id: "id".to_owned(), r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Int), - brief: Some("brief".to_owned()), + brief: "brief".to_owned(), examples: Some(Examples::Int(42)), requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required), note: "note".to_owned(), @@ -228,7 +345,7 @@ mod tests { deprecated: Some("deprecated".to_owned()), }; assert_eq!(field.id, "id"); - assert_eq!(field.brief.to_owned(), Some("brief".to_owned())); + assert_eq!(field.brief.to_owned(), "brief".to_owned()); assert_eq!(field.note, "note"); assert!(field.is_required()); } diff --git a/crates/weaver_semconv/src/group.rs b/crates/weaver_semconv/src/group.rs index d580081d..1724af70 100644 --- a/crates/weaver_semconv/src/group.rs +++ b/crates/weaver_semconv/src/group.rs @@ -89,6 +89,7 @@ pub struct GroupSpec { pub display_name: Option, /// The event body definition /// Note: only valid if type is event + #[serde(skip_serializing_if = "Option::is_none")] pub body: Option, } diff --git a/schemas/semconv-syntax.md b/schemas/semconv-syntax.md index ade9af87..221f35f2 100644 --- a/schemas/semconv-syntax.md +++ b/schemas/semconv-syntax.md @@ -106,9 +106,12 @@ specificfields ::= spanfields spanfields ::= [events] [span_kind] -eventfields ::= [name] [body] +eventfields ::= name [body] -body ::= body_fields +body ::= body_type [brief] [examples] [stability] [note] [body_fields] + +body_type ::= "map" + | "string" body_fields ::= id type brief [examples] stability [deprecated] [requirement_level] [note] @@ -214,13 +217,10 @@ The following is only valid if `type` is `event` and `body` is present and `fiel brief: "Describes the event." stability: experimental attributes: # Optional - - id: registry.attribute.id - type: string - requirement_level: required - brief: Adds a standard attribute to the global registry. - examples: ["some_value"] - - ref: registry.some.attribute.id # Reference to an existing global attribute + - ref: registry.attribute.id + - ref: registry.some_other.attribute.id # Reference to an existing global attribute body: # Optional + type: map fields: - id: method type: string diff --git a/schemas/semconv.schema.json b/schemas/semconv.schema.json index 5f15af06..819ce020 100644 --- a/schemas/semconv.schema.json +++ b/schemas/semconv.schema.json @@ -166,9 +166,41 @@ "BodySemanticConvention": { "type": "object", "required": [ - "fields" + "type" ], "properties": { + "type": { + "$ref": "#/$defs/BodyType" + }, + "brief": { + "type": "string", + "description": "a brief description of the field." + }, + "note": { + "type": "string", + "description": "a more elaborate description of the field. It defaults to an empty string." + }, + "stability": { + "allOf": [ + { + "$ref": "#/$defs/StabilityLevel" + } + ] + }, + "examples": { + "anyOf": [ + { + "$ref": "#/$defs/ValueType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ValueType" + } + } + ], + "description": "sequence/dictionary of example values for the field. They are optional for boolean, int, double, and enum attributes. Example values must be of the same type of the field. If only a single example is provided, it can directly be reported without encapsulating it into a sequence/dictionary." + }, "fields": { "type": "array", "items": { @@ -556,6 +588,19 @@ "$ref": "#/$defs/AttributeEnumType" } ] + }, + "BodyType": { + "description": "specifies the supported body types.", + "oneOf": [ + { + "type": "string", + "enum": [ + "string", + "map" + ], + "description": "literal denoting the type" + } + ] } } }