From 785d84b3a6ad6c1cfbd52aafc589be41936f3334 Mon Sep 17 00:00:00 2001 From: Nev Wylie <54870357+MSNev@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:27:07 -0700 Subject: [PATCH] Refactor Body to AnyValue --- Cargo.lock | 1 + crates/weaver_forge/data/mobile-events.yaml | 88 +- .../semconv_jq_fn/semconv_events.json | 2 +- crates/weaver_forge/src/registry.rs | 4 +- .../weaver_resolved_schema/src/any_value.rs | 71 ++ crates/weaver_resolved_schema/src/body.rs | 99 --- crates/weaver_resolved_schema/src/error.rs | 11 +- crates/weaver_resolved_schema/src/lib.rs | 2 +- crates/weaver_resolved_schema/src/registry.rs | 4 +- crates/weaver_resolved_schema/src/signal.rs | 4 +- .../expected-registry.json | 196 +++-- .../registry/browser-event.yaml | 8 +- .../registry/client-exception-event.yaml | 12 +- .../registry/mobile-events.yaml | 88 +- .../registry/stringbody-event.yaml | 4 + crates/weaver_resolver/src/any_value.rs | 55 ++ crates/weaver_resolver/src/body.rs | 67 -- crates/weaver_resolver/src/lib.rs | 2 +- crates/weaver_resolver/src/registry.rs | 35 +- crates/weaver_semconv/Cargo.toml | 3 + crates/weaver_semconv/data/event.yaml | 92 +- .../data/expected/any_value.json | 1 + .../data/expected/any_value.yaml | 102 +++ crates/weaver_semconv/src/any_value.rs | 816 ++++++++++++++++++ crates/weaver_semconv/src/body.rs | 361 -------- crates/weaver_semconv/src/group.rs | 4 +- crates/weaver_semconv/src/lib.rs | 2 +- 27 files changed, 1333 insertions(+), 801 deletions(-) create mode 100644 crates/weaver_resolved_schema/src/any_value.rs delete mode 100644 crates/weaver_resolved_schema/src/body.rs create mode 100644 crates/weaver_resolver/src/any_value.rs delete mode 100644 crates/weaver_resolver/src/body.rs create mode 100644 crates/weaver_semconv/data/expected/any_value.json create mode 100644 crates/weaver_semconv/data/expected/any_value.yaml create mode 100644 crates/weaver_semconv/src/any_value.rs delete mode 100644 crates/weaver_semconv/src/body.rs diff --git a/Cargo.lock b/Cargo.lock index 8938834a..2655ae78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4313,6 +4313,7 @@ dependencies = [ "ordered-float", "schemars", "serde", + "serde_json", "serde_yaml", "thiserror", "ureq", diff --git a/crates/weaver_forge/data/mobile-events.yaml b/crates/weaver_forge/data/mobile-events.yaml index 568048cc..a8008d98 100644 --- a/crates/weaver_forge/data/mobile-events.yaml +++ b/crates/weaver_forge/data/mobile-events.yaml @@ -11,6 +11,8 @@ groups: mutually exclusive. body: type: map + id: device.app.lifecycle.fields + requirement_level: required fields: - id: ios.state stability: experimental @@ -21,31 +23,31 @@ groups: and from which the `OS terminology` column values are derived. brief: > This attribute represents the state the application has transitioned into at the occurrence of the event. - type: - allow_custom_values: false - members: - - id: active - value: 'active' - brief: > - The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. - - id: inactive - value: 'inactive' - brief: > - The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. - - id: background - value: 'background' - brief: > - The app is now in the background. - This value is associated with UIKit notification `applicationDidEnterBackground`. - - id: foreground - value: 'foreground' - brief: > - The app is now in the foreground. - This value is associated with UIKit notification `applicationWillEnterForeground`. - - id: terminate - value: 'terminate' - brief: > - The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. + type: enum + allow_custom_values: false + members: + - id: active + value: 'active' + brief: > + The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. + - id: inactive + value: 'inactive' + brief: > + The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. + - id: background + value: 'background' + brief: > + The app is now in the background. + This value is associated with UIKit notification `applicationDidEnterBackground`. + - id: foreground + value: 'foreground' + brief: > + The app is now in the foreground. + This value is associated with UIKit notification `applicationWillEnterForeground`. + - id: terminate + value: 'terminate' + brief: > + The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. - id: android.state stability: experimental requirement_level: @@ -55,21 +57,21 @@ groups: 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. - type: - allow_custom_values: false - members: - - id: created - value: 'created' - 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. - - id: background - value: 'background' - 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. - - id: foreground - value: 'foreground' - 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. \ No newline at end of file + type: enum + allow_custom_values: false + members: + - id: created + value: 'created' + 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. + - id: background + value: 'background' + 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. + - id: foreground + value: 'foreground' + 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. \ No newline at end of file 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 94dd5e18..2cb781cd 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"}]}}],"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 +[{"attributes":[],"body":{"fields":[{"allow_custom_values":false,"brief":"This attribute represents the state the application has transitioned into at the occurrence of the event.\n","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"}],"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":"enum","type_display":"enum\u003cios.state\u003e {active, inactive, background, foreground, terminate}"},{"allow_custom_values":false,"brief":"This attribute represents the state the application has transitioned into at the occurrence of the event.\n","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"}],"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":"enum","type_display":"enum\u003candroid.state\u003e {created, background, foreground}"}],"name":"device.app.lifecycle.fields","requirement_level":"required","type":"map","type_display":"map\u003cdevice.app.lifecycle.fields\u003e{ enum\u003cios.state\u003e {active, inactive, background, foreground, terminate}, enum\u003candroid.state\u003e {created, background, 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 diff --git a/crates/weaver_forge/src/registry.rs b/crates/weaver_forge/src/registry.rs index 56e73a6c..9ca549a1 100644 --- a/crates/weaver_forge/src/registry.rs +++ b/crates/weaver_forge/src/registry.rs @@ -7,8 +7,8 @@ use crate::error::Error; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use weaver_resolved_schema::any_value::AnyValue; use weaver_resolved_schema::attribute::Attribute; -use weaver_resolved_schema::body::Body; use weaver_resolved_schema::catalog::Catalog; use weaver_resolved_schema::lineage::GroupLineage; use weaver_resolved_schema::registry::{Constraint, Group, Registry}; @@ -107,7 +107,7 @@ pub struct ResolvedGroup { pub display_name: Option, /// The body specification used for event semantic conventions. #[serde(skip_serializing_if = "Option::is_none")] - pub body: Option, + pub body: Option, } impl ResolvedGroup { diff --git a/crates/weaver_resolved_schema/src/any_value.rs b/crates/weaver_resolved_schema/src/any_value.rs new file mode 100644 index 00000000..e3add63c --- /dev/null +++ b/crates/weaver_resolved_schema/src/any_value.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(rustdoc::invalid_html_tags)] + +//! Specification of a resolved `AnyValue`. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::attribute::{EnumEntriesSpec, Examples, RequirementLevel}; +use weaver_semconv::stability::Stability; + +/// An `AnyValue` definition. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct AnyValue { + /// AnyValue name. + pub name: String, + /// Either a string literal denoting the type as a primitive or an + /// array type, a template type or an enum definition. + pub r#type: String, + /// A description of the type of the AnyValue + /// e.g. "string", "string[]", "int", "enum", "map{ int, string }" + #[serde(skip_serializing_if = "Option::is_none")] + pub type_display: Option, + /// A brief description of the AnyValue. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + pub brief: String, + /// Sequence of example values for the AnyValue or single example + /// value. They are required only for primitive and primitive array + /// values. Example values must be of the same type of the type. + /// 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, + /// Specifies if the value is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the value is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the value is required. + pub requirement_level: RequirementLevel, + /// A more elaborate description of the any value. + /// It defaults to an empty string. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + pub note: String, + /// Specifies the stability of the any value. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + pub stability: Option, + /// Specifies if the value is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, + /// Identifies the definition of the "fields" of the value when the type is "map". + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, + /// Used when the type is "enum". + /// Set to false to not accept values other than the specified members. + /// It defaults to true. + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_custom_values: Option, + /// Used when the type is "enum". + /// List of enum entries. + #[serde(skip_serializing_if = "Option::is_none")] + pub members: Option>, +} diff --git a/crates/weaver_resolved_schema/src/body.rs b/crates/weaver_resolved_schema/src/body.rs deleted file mode 100644 index 3dc0ec19..00000000 --- a/crates/weaver_resolved_schema/src/body.rs +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#![allow(rustdoc::invalid_html_tags)] - -//! Specification of a resolved `BodyField`. - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use weaver_semconv::attribute::{AttributeType, Examples, RequirementLevel}; -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 { - /// 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>, -} - -/// A `BodyField` definition. -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct BodyField { - /// Field name. - pub name: String, - /// Either a string literal denoting the type as a primitive or an - /// array type, a template type or an enum definition. - pub r#type: AttributeType, - /// A brief description of the field. - #[serde(skip_serializing_if = "String::is_empty")] - pub brief: String, - /// Sequence of example values for the 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 - /// field. 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, - /// Specifies if the field is mandatory. Can be "required", - /// "conditionally_required", "recommended" or "opt_in". When omitted, - /// the field is "recommended". When set to - /// "conditionally_required", the string provided as MUST - /// specify the conditions under which the field is required. - pub requirement_level: RequirementLevel, - /// A more elaborate description of the field. - /// It defaults to an empty string. - #[serde(skip_serializing_if = "String::is_empty")] - #[serde(default)] - pub note: String, - /// Specifies the stability of the field. - /// Note that, if stability is missing but deprecated is present, it will - /// automatically set the stability to deprecated. If deprecated is - /// present and stability differs from deprecated, this will result in an - /// error. - #[serde(skip_serializing_if = "Option::is_none")] - pub stability: Option, - /// Specifies if the field is deprecated. The string - /// provided as MUST specify why it's deprecated and/or what - /// to use instead. See also stability. - #[serde(skip_serializing_if = "Option::is_none")] - pub deprecated: Option, -} - -/// An unresolved body definition. -#[derive(Debug, Deserialize, Clone)] -pub struct UnresolvedBody { - /// The body specification. - pub spec: BodySpec, -} - -/// An unresolved body field definition. -#[derive(Debug, Deserialize, Clone)] -pub struct UnresolvedBodyField { - /// The body field specification. - pub spec: BodyFieldSpec, -} diff --git a/crates/weaver_resolved_schema/src/error.rs b/crates/weaver_resolved_schema/src/error.rs index b716910b..58e8d23c 100644 --- a/crates/weaver_resolved_schema/src/error.rs +++ b/crates/weaver_resolved_schema/src/error.rs @@ -3,10 +3,9 @@ //! Error types and utilities. use serde::{Deserialize, Serialize}; -use weaver_semconv::body::BodySpec; use crate::attribute::AttributeRef; -use crate::error::Error::{AttributeNotFound, CompoundError, InvalidBody}; +use crate::error::Error::{AttributeNotFound, CompoundError}; /// Errors emitted by this crate. #[derive(thiserror::Error, Debug, Clone, Deserialize, Serialize)] @@ -23,13 +22,6 @@ pub enum Error { /// A generic container for multiple errors. #[error("Errors:\n{0:#?}")] CompoundError(Vec), - - /// A generic error identifying a feature that has not yet been implemented. - #[error("Unsupported BodySpec")] - InvalidBody { - /// The body specification that is not supported. - body: BodySpec, - }, } /// Handles a list of errors and returns a compound error if the list is not @@ -53,7 +45,6 @@ impl Error { .flat_map(|e| match e { CompoundError(errors) => errors, e @ AttributeNotFound { .. } => vec![e], - e @ InvalidBody { .. } => vec![e], }) .collect(), ) diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index d88d872e..1a0dd865 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -13,8 +13,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use weaver_version::Versions; +pub mod any_value; pub mod attribute; -pub mod body; pub mod catalog; pub mod error; pub mod instrumentation_library; diff --git a/crates/weaver_resolved_schema/src/registry.rs b/crates/weaver_resolved_schema/src/registry.rs index 604c1c90..aa3b7da0 100644 --- a/crates/weaver_resolved_schema/src/registry.rs +++ b/crates/weaver_resolved_schema/src/registry.rs @@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize}; use weaver_semconv::group::{GroupType, InstrumentSpec, SpanKindSpec}; use weaver_semconv::stability::Stability; +use crate::any_value::AnyValue; use crate::attribute::{Attribute, AttributeRef}; -use crate::body::Body; use crate::catalog::Catalog; use crate::error::{handle_errors, Error}; use crate::lineage::GroupLineage; @@ -130,7 +130,7 @@ pub struct Group { /// The body of the event. /// This fields is only used for event groups. #[serde(skip_serializing_if = "Option::is_none")] - pub body: Option, + pub body: Option, } /// Common statistics for a group. diff --git a/crates/weaver_resolved_schema/src/signal.rs b/crates/weaver_resolved_schema/src/signal.rs index 095f7291..0f5d3f26 100644 --- a/crates/weaver_resolved_schema/src/signal.rs +++ b/crates/weaver_resolved_schema/src/signal.rs @@ -5,8 +5,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::any_value::AnyValue; use crate::attribute::AttributeRef; -use crate::body::Body; use crate::metric::MetricRef; use crate::tags::Tags; @@ -64,7 +64,7 @@ pub struct Event { tags: Option, /// The body of the event, not used for Span events. #[serde(skip_serializing_if = "Option::is_none")] - body: Option, + body: Option, } /// A span signal. 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 06b0b3fe..c3d8b4fe 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 @@ -17,17 +17,21 @@ "source_file": "data/registry-test-4-events/registry/browser-event.yaml" }, "body": { + "name": "browser.test.event_with_body.fields", "type": "map", + "type_display": "map{ string }", + "requirement_level": "required", "fields": [ { "name": "some.field", "type": "string", + "type_display": "string", "brief": "A field that is not referenced in the attributes", "examples": [ "some value", "another value" ], - "requirement_level": "recommended", + "requirement_level": "required", "note": "This field is not referenced in the attributes" } ] @@ -43,23 +47,27 @@ "source_file": "data/registry-test-4-events/registry/browser-event.yaml" }, "body": { + "name": "browser.test.event_with_body_details.fields", "type": "map", + "type_display": "map{ string }", "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\" }" ], + "requirement_level": "required", "fields": [ { "name": "some.field", "type": "string", + "type_display": "string", "brief": "A field that is not referenced in the attributes", "examples": [ "some value", "another value" ], - "requirement_level": "recommended", + "requirement_level": "optional", "note": "This field is not referenced in the attributes" } ] @@ -77,43 +85,50 @@ "source_file": "data/registry-test-4-events/registry/client-exception-event.yaml" }, "body": { + "name": "client.exception.event.fields", + "type": "map", + "type_display": "map{ string, string, string, boolean }", + "requirement_level": "optional", "fields": [ { "name": "type", "type": "string", + "type_display": "string", "brief": "The type of the exception.\n", "examples": [ "java.net.ConnectException", "OSError" ], - "requirement_level": "recommended" + "requirement_level": "optional" }, { "name": "message", "type": "string", + "type_display": "string", "brief": "The exception message.", "examples": [ "Division by zero", "Can't convert 'int' object to str implicitly" ], - "requirement_level": "recommended" + "requirement_level": "optional" }, { "name": "stacktrace", "type": "string", + "type_display": "string", "brief": "A stacktrace.\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)", - "requirement_level": "recommended" + "requirement_level": "optional" }, { "name": "escaped", "type": "boolean", + "type_display": "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.\n", - "requirement_level": "recommended", + "requirement_level": "optional", "note": "An exception is considered to have escaped." } - ], - "type": "map" + ] } }, { @@ -195,54 +210,58 @@ "source_file": "data/registry-test-4-events/registry/mobile-events.yaml" }, "body": { + "name": "device.app.lifecycle.fields", + "type": "map", + "type_display": "map{ enum {active, inactive, background, foreground, terminate}, enum {created, background, foreground} }", + "requirement_level": "required", "fields": [ { "name": "ios.state", - "type": { - "allow_custom_values": false, - "members": [ - { - "id": "active", - "value": "active", - "brief": "The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`.\n", - "note": null, - "stability": null, - "deprecated": null - }, - { - "id": "inactive", - "value": "inactive", - "brief": "The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`.\n", - "note": null, - "stability": null, - "deprecated": null - }, - { - "id": "background", - "value": "background", - "brief": "The app is now in the background. This value is associated with UIKit notification `applicationDidEnterBackground`.\n", - "note": null, - "stability": null, - "deprecated": null - }, - { - "id": "foreground", - "value": "foreground", - "brief": "The app is now in the foreground. This value is associated with UIKit notification `applicationWillEnterForeground`.\n", - "note": null, - "stability": null, - "deprecated": null - }, - { - "id": "terminate", - "value": "terminate", - "brief": "The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`.\n", - "note": null, - "stability": null, - "deprecated": null - } - ] - }, + "type": "enum", + "type_display": "enum {active, inactive, background, foreground, terminate}", + "allow_custom_values": false, + "members": [ + { + "id": "active", + "value": "active", + "brief": "The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`.\n", + "note": null, + "stability": null, + "deprecated": null + }, + { + "id": "inactive", + "value": "inactive", + "brief": "The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`.\n", + "note": null, + "stability": null, + "deprecated": null + }, + { + "id": "background", + "value": "background", + "brief": "The app is now in the background. This value is associated with UIKit notification `applicationDidEnterBackground`.\n", + "note": null, + "stability": null, + "deprecated": null + }, + { + "id": "foreground", + "value": "foreground", + "brief": "The app is now in the foreground. This value is associated with UIKit notification `applicationWillEnterForeground`.\n", + "note": null, + "stability": null, + "deprecated": null + }, + { + "id": "terminate", + "value": "terminate", + "brief": "The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`.\n", + "note": null, + "stability": null, + "deprecated": null + } + ], "brief": "This attribute represents the state the application has transitioned into at the occurrence of the event.\n", "requirement_level": { "conditionally_required": "if and only if `os.name` is `ios`" @@ -252,35 +271,35 @@ }, { "name": "android.state", - "type": { - "allow_custom_values": false, - "members": [ - { - "id": "created", - "value": "created", - "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", - "note": null, - "stability": null, - "deprecated": null - }, - { - "id": "background", - "value": "background", - "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", - "note": null, - "stability": null, - "deprecated": null - }, - { - "id": "foreground", - "value": "foreground", - "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.", - "note": null, - "stability": null, - "deprecated": null - } - ] - }, + "type": "enum", + "type_display": "enum {created, background, foreground}", + "allow_custom_values": false, + "members": [ + { + "id": "created", + "value": "created", + "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", + "note": null, + "stability": null, + "deprecated": null + }, + { + "id": "background", + "value": "background", + "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", + "note": null, + "stability": null, + "deprecated": null + }, + { + "id": "foreground", + "value": "foreground", + "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.", + "note": null, + "stability": null, + "deprecated": null + } + ], "brief": "This attribute represents the state the application has transitioned into at the occurrence of the event.\n", "requirement_level": { "conditionally_required": "if and only if `os.name` is `android`" @@ -288,8 +307,7 @@ "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" + ] } }, { @@ -452,11 +470,14 @@ "source_file": "data/registry-test-4-events/registry/stringbody-event.yaml" }, "body": { + "name": "some.string.body.event.fields", "type": "string", + "type_display": "string", "brief": "This is the body of the event which is a JSON encoded string.\n", "examples": [ "{\"key1\":\"value1\",\"key2\":\"value2\"}" - ] + ], + "requirement_level": "required" } }, { @@ -471,13 +492,16 @@ "source_file": "data/registry-test-4-events/registry/stringbody-event.yaml" }, "body": { + "name": "some.string.body.event.fields", "type": "string", + "type_display": "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\"}" - ] + ], + "requirement_level": "required" } }, { 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 00560e0c..2575c9c3 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 @@ -38,13 +38,16 @@ groups: `navigator.language`. examples: [ "en", "en-US", "en-AU", "fr", "fr-FR" ] body: + id: browser.test.event_with_body.fields type: map + requirement_level: required 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" ] + requirement_level: required - id: browser.test.event_with_body_details name: browser.test.event_with_body_details @@ -52,14 +55,17 @@ groups: brief: > An event that adds global attributes for reuse. body: + id: browser.test.event_with_body_details.fields 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" }' ] + requirement_level: required 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" ] \ No newline at end of file + examples: [ "some value", "another value" ] + requirement_level: optional \ No newline at end of file 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 84a0df0d..1c686939 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,25 +6,30 @@ groups: This document defines the log event used to report a client exception. body: + id: client.exception.event.fields type: map + requirement_level: optional fields: - id: type type: string brief: > The type of the exception. examples: ["java.net.ConnectException","OSError"] + requirement_level: optional - id: message type: string brief: The exception message. examples: ["Division by zero","Can't convert 'int' object to str implicitly"] + requirement_level: optional - 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)' + 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)' + requirement_level: optional - id: escaped type: boolean brief: > @@ -32,6 +37,7 @@ groups: it is known that the exception is escaping the scope of the span. note: |- An exception is considered to have escaped. + requirement_level: optional attributes: - id: client.name 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 568048cc..f0e0b79e 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,7 +10,9 @@ groups: the `android.state` and `ios.state` fields. The `android.state` and `ios.state` attributes are mutually exclusive. body: + id: device.app.lifecycle.fields type: map + requirement_level: required fields: - id: ios.state stability: experimental @@ -21,31 +23,31 @@ groups: and from which the `OS terminology` column values are derived. brief: > This attribute represents the state the application has transitioned into at the occurrence of the event. - type: - allow_custom_values: false - members: - - id: active - value: 'active' - brief: > - The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. - - id: inactive - value: 'inactive' - brief: > - The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. - - id: background - value: 'background' - brief: > - The app is now in the background. - This value is associated with UIKit notification `applicationDidEnterBackground`. - - id: foreground - value: 'foreground' - brief: > - The app is now in the foreground. - This value is associated with UIKit notification `applicationWillEnterForeground`. - - id: terminate - value: 'terminate' - brief: > - The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. + type: enum + allow_custom_values: false + members: + - id: active + value: 'active' + brief: > + The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. + - id: inactive + value: 'inactive' + brief: > + The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. + - id: background + value: 'background' + brief: > + The app is now in the background. + This value is associated with UIKit notification `applicationDidEnterBackground`. + - id: foreground + value: 'foreground' + brief: > + The app is now in the foreground. + This value is associated with UIKit notification `applicationWillEnterForeground`. + - id: terminate + value: 'terminate' + brief: > + The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. - id: android.state stability: experimental requirement_level: @@ -55,21 +57,21 @@ groups: 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. - type: - allow_custom_values: false - members: - - id: created - value: 'created' - 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. - - id: background - value: 'background' - 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. - - id: foreground - value: 'foreground' - 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. \ No newline at end of file + type: enum + allow_custom_values: false + members: + - id: created + value: 'created' + 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. + - id: background + value: 'background' + 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. + - id: foreground + value: 'foreground' + 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. \ No newline at end of file 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 index 17b37d70..4cfdf06a 100644 --- 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 @@ -8,7 +8,9 @@ groups: note: > This event transmits the body as a JSON encoded string. body: + id: some.string.body.event.fields type: string + requirement_level: required brief: > This is the body of the event which is a JSON encoded string. examples: ['{"key1":"value1","key2":"value2"}'] @@ -22,7 +24,9 @@ groups: note: > This event transmits the body as a JSON encoded string. body: + id: some.string.body.event.fields type: string + requirement_level: required brief: > This is the body of the event which is a JSON encoded string. note: > diff --git a/crates/weaver_resolver/src/any_value.rs b/crates/weaver_resolver/src/any_value.rs new file mode 100644 index 00000000..a8c97e53 --- /dev/null +++ b/crates/weaver_resolver/src/any_value.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Functions to resolve a semantic convention body. + +use weaver_resolved_schema::any_value::AnyValue; +use weaver_semconv::{any_value::AnyValueSpec, attribute::EnumEntriesSpec}; + +/// Resolve a `Body` specification into a resolved `Body`. +#[must_use] +pub fn resolve_any_value_spec(value: &AnyValueSpec) -> AnyValue { + match value { + AnyValueSpec::Map { fields, .. } => { + let resolved_fields: Vec = + fields.iter().map(resolve_any_value_spec).collect(); + + construct_any_value_common(value, Some(resolved_fields), None, None) + } + AnyValueSpec::Enum { + allow_custom_values, + members, + .. + } => construct_any_value_common( + value, + None, + Some(*allow_custom_values), + Some(members.to_vec()), + ), + _ => construct_any_value_common(value, None, None, None), + } +} + +/// Construct an AnyValue with common fields. +fn construct_any_value_common( + value: &AnyValueSpec, + resolved_fields: Option>, + allow_custom_values: Option, + members: Option>, +) -> AnyValue { + let common = value.common(); + + AnyValue { + name: value.id(), + r#type: value.type_name(), + type_display: Some(value.to_string()), + brief: value.brief(), + note: value.note(), + stability: common.stability.clone(), + examples: common.examples.clone(), + fields: resolved_fields, + requirement_level: common.requirement_level.clone(), + deprecated: common.deprecated.clone(), + allow_custom_values, + members, + } +} diff --git a/crates/weaver_resolver/src/body.rs b/crates/weaver_resolver/src/body.rs deleted file mode 100644 index ae28eac5..00000000 --- a/crates/weaver_resolver/src/body.rs +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Functions to resolve a semantic convention body. - -use weaver_resolved_schema::{ - body::{Body, BodyField}, - error::Error, -}; -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 { - 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(), - examples: field.examples.clone(), - requirement_level: field.requirement_level.clone(), - note: field.note.clone(), - stability: field.stability.clone(), - deprecated: field.deprecated.clone(), - }); - } - Ok(Some(Body { - r#type: BodyType::Map, - brief: brief.clone(), - note: note.clone(), - stability: stability.clone(), - examples: examples.clone(), - fields: Some(body_fields), - })) - } - 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_resolver/src/lib.rs b/crates/weaver_resolver/src/lib.rs index 7fec3d3a..40b1a545 100644 --- a/crates/weaver_resolver/src/lib.rs +++ b/crates/weaver_resolver/src/lib.rs @@ -25,8 +25,8 @@ use weaver_semconv::semconv::SemConvSpec; use crate::attribute::AttributeCatalog; use crate::registry::resolve_semconv_registry; +pub mod any_value; pub mod attribute; -pub mod body; mod constraint; pub mod registry; diff --git a/crates/weaver_resolver/src/registry.rs b/crates/weaver_resolver/src/registry.rs index f1aa82ce..53d95e1a 100644 --- a/crates/weaver_resolver/src/registry.rs +++ b/crates/weaver_resolver/src/registry.rs @@ -8,15 +8,14 @@ use serde::Deserialize; use weaver_common::error::handle_errors; use weaver_resolved_schema::attribute::UnresolvedAttribute; -use weaver_resolved_schema::body::UnresolvedBody; use weaver_resolved_schema::lineage::{AttributeLineage, GroupLineage}; use weaver_resolved_schema::registry::{Constraint, Group, Registry}; use weaver_semconv::attribute::AttributeSpec; use weaver_semconv::group::GroupSpecWithProvenance; use weaver_semconv::registry::SemConvRegistry; +use crate::any_value::resolve_any_value_spec; use crate::attribute::AttributeCatalog; -use crate::body::resolve_body_spec; use crate::constraint::resolve_constraints; use crate::{Error, UnsatisfiedAnyOfConstraint}; @@ -44,9 +43,6 @@ pub struct UnresolvedGroup { /// and other signals, into the group field once they are resolved. pub attributes: Vec, - /// The unresolved body that belongs to this group - pub body: Option, - /// The provenance of the group (URL or path). pub provenance: String, } @@ -87,8 +83,6 @@ pub fn resolve_semconv_registry( resolve_include_constraints(&mut ureg)?; - resolve_body(&mut ureg)?; - // Sort the attribute internal references in each group. // This is needed to ensure that the resolved registry is easy to compare // in unit tests. @@ -337,10 +331,9 @@ fn group_from_spec(group: GroupSpecWithProvenance) -> UnresolvedGroup { name: group.spec.name, lineage: Some(GroupLineage::new(&group.provenance)), display_name: group.spec.display_name, - body: None, // The body is resolved and populated later. + body: group.spec.body.map(|body| resolve_any_value_spec(&body)), }, attributes: attrs, - body: { group.spec.body.map(|body| UnresolvedBody { spec: body }) }, provenance: group.provenance, } } @@ -766,30 +759,6 @@ fn resolve_inheritance_attr( } } -fn resolve_body(ureg: &mut UnresolvedRegistry) -> Result<(), Error> { - let mut errors = vec![]; - - for unresolved_group in ureg.groups.iter_mut() { - if let Some(body) = &unresolved_group.body { - match resolve_body_spec(&body.spec) { - Ok(resolved_body) => { - unresolved_group.group.body = resolved_body; - } - Err(e) => { - errors.push(Error::UnresolvedBody { - group_id: unresolved_group.group.id.clone(), - provenance: unresolved_group.provenance.clone(), - error: e, - }); - } - } - } - } - - handle_errors(errors)?; - Ok(()) -} - #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/crates/weaver_semconv/Cargo.toml b/crates/weaver_semconv/Cargo.toml index 006d5b14..7a7d94ba 100644 --- a/crates/weaver_semconv/Cargo.toml +++ b/crates/weaver_semconv/Cargo.toml @@ -11,6 +11,9 @@ rust-version.workspace = true [lints] workspace = true +[dev-dependencies] +serde_json.workspace = true + [dependencies] weaver_common = { path = "../weaver_common" } diff --git a/crates/weaver_semconv/data/event.yaml b/crates/weaver_semconv/data/event.yaml index 6ea2084d..d17481a8 100644 --- a/crates/weaver_semconv/data/event.yaml +++ b/crates/weaver_semconv/data/event.yaml @@ -10,7 +10,9 @@ groups: the `android.state` and `ios.state` fields. The `android.state` and `ios.state` attributes are mutually exclusive. body: + id: device_lifecycle type: map + requirement_level: optional fields: - id: ios.state stability: experimental @@ -21,31 +23,31 @@ groups: and from which the `OS terminology` column values are derived. brief: > This attribute represents the state the application has transitioned into at the occurrence of the event. - type: - allow_custom_values: false - members: - - id: active - value: 'active' - brief: > - The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. - - id: inactive - value: 'inactive' - brief: > - The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. - - id: background - value: 'background' - brief: > - The app is now in the background. - This value is associated with UIKit notification `applicationDidEnterBackground`. - - id: foreground - value: 'foreground' - brief: > - The app is now in the foreground. - This value is associated with UIKit notification `applicationWillEnterForeground`. - - id: terminate - value: 'terminate' - brief: > - The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. + type: enum + allow_custom_values: false + members: + - id: active + value: 'active' + brief: > + The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. + - id: inactive + value: 'inactive' + brief: > + The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. + - id: background + value: 'background' + brief: > + The app is now in the background. + This value is associated with UIKit notification `applicationDidEnterBackground`. + - id: foreground + value: 'foreground' + brief: > + The app is now in the foreground. + This value is associated with UIKit notification `applicationWillEnterForeground`. + - id: terminate + value: 'terminate' + brief: > + The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. - id: android.state stability: experimental requirement_level: @@ -55,24 +57,24 @@ groups: 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. - type: - allow_custom_values: false - members: - - id: created - value: 'created' - 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. - - id: background - value: 'background' - 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. - - id: foreground - value: 'foreground' - 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. + type: enum + allow_custom_values: false + members: + - id: created + value: 'created' + 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. + - id: background + value: 'background' + 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. + - id: foreground + value: 'foreground' + 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. - id: span.event.test.no_name stability: experimental @@ -135,7 +137,9 @@ groups: requirement_level: conditionally_required: Required if `attribute1` is not set, recommended otherwise. body: + id: body_name type: map + requirement_level: required fields: - id: field1 type: string @@ -170,6 +174,8 @@ groups: requirement_level: conditionally_required: Required if `attribute1` is not set, recommended otherwise. body: + id: body_name type: string brief: 'The body of the event' + requirement_level: required examples: ['{ name: "thename", content: ... }', '{ name: "thename", content: ... }'] diff --git a/crates/weaver_semconv/data/expected/any_value.json b/crates/weaver_semconv/data/expected/any_value.json new file mode 100644 index 00000000..97479e72 --- /dev/null +++ b/crates/weaver_semconv/data/expected/any_value.json @@ -0,0 +1 @@ +{"body":{"type":"map","id":"id","brief":"brief","note":"note","requirement_level":"optional","fields":[{"type":"enum","id":"id_enum","brief":"brief","note":"note","requirement_level":"optional","allow_custom_values":true,"members":[{"id":"id","value":42,"brief":"brief","note":"note","stability":null,"deprecated":null}]},{"type":"map","id":"id_map","brief":"brief","note":"note","requirement_level":"optional","fields":[{"type":"int","id":"id_int","brief":"brief","note":"note","requirement_level":"required"},{"type":"bytes","id":"id_bytes","brief":"brief","note":"note","requirement_level":"required"},{"type":"string","id":"id_string","brief":"brief","note":"note","requirement_level":"optional"},{"type":"boolean","id":"id_bool","brief":"brief","note":"note","requirement_level":"optional"},{"type":"map","id":"id_nested_map","brief":"brief","note":"note","requirement_level":"optional","fields":[{"type":"int[]","id":"id_nested_int","brief":"brief","note":"note","requirement_level":"optional"},{"type":"double[]","id":"id_nested_bytes","brief":"brief","note":"note","requirement_level":"optional"},{"type":"string[]","id":"id_nested_string","brief":"brief","note":"note","requirement_level":"optional"},{"type":"boolean[]","id":"id_nested_bool","brief":"brief","note":"note","requirement_level":"optional"}]}]},{"type":"int","id":"id_int","brief":"brief","note":"note","requirement_level":"optional"},{"type":"bytes","id":"id_bytes","brief":"brief","note":"note","requirement_level":"optional"},{"type":"string","id":"id_string","brief":"brief","note":"note","requirement_level":"recommended"},{"type":"boolean","id":"id_bool","brief":"brief","note":"note","requirement_level":"optional"},{"type":"double","id":"id_double","brief":"brief","note":"note","requirement_level":"optional"},{"type":"double[]","id":"id_doubles","brief":"brief","note":"note","requirement_level":"optional"}]}} \ No newline at end of file diff --git a/crates/weaver_semconv/data/expected/any_value.yaml b/crates/weaver_semconv/data/expected/any_value.yaml new file mode 100644 index 00000000..b3555d79 --- /dev/null +++ b/crates/weaver_semconv/data/expected/any_value.yaml @@ -0,0 +1,102 @@ +body: + type: map + id: id + brief: brief + note: note + requirement_level: optional + fields: + - type: enum + id: id_enum + brief: brief + note: note + requirement_level: optional + allow_custom_values: true + members: + - id: id + value: 42 + brief: brief + note: note + stability: null + deprecated: null + - type: map + id: id_map + brief: brief + note: note + requirement_level: optional + fields: + - type: int + id: id_int + brief: brief + note: note + requirement_level: required + - type: bytes + id: id_bytes + brief: brief + note: note + requirement_level: required + - type: string + id: id_string + brief: brief + note: note + requirement_level: optional + - type: boolean + id: id_bool + brief: brief + note: note + requirement_level: optional + - type: map + id: id_nested_map + brief: brief + note: note + requirement_level: optional + fields: + - type: int[] + id: id_nested_int + brief: brief + note: note + requirement_level: optional + - type: double[] + id: id_nested_bytes + brief: brief + note: note + requirement_level: optional + - type: string[] + id: id_nested_string + brief: brief + note: note + requirement_level: optional + - type: boolean[] + id: id_nested_bool + brief: brief + note: note + requirement_level: optional + - type: int + id: id_int + brief: brief + note: note + requirement_level: optional + - type: bytes + id: id_bytes + brief: brief + note: note + requirement_level: optional + - type: string + id: id_string + brief: brief + note: note + requirement_level: recommended + - type: boolean + id: id_bool + brief: brief + note: note + requirement_level: optional + - type: double + id: id_double + brief: brief + note: note + requirement_level: optional + - type: double[] + id: id_doubles + brief: brief + note: note + requirement_level: optional diff --git a/crates/weaver_semconv/src/any_value.rs b/crates/weaver_semconv/src/any_value.rs new file mode 100644 index 00000000..90e3d277 --- /dev/null +++ b/crates/weaver_semconv/src/any_value.rs @@ -0,0 +1,816 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(rustdoc::invalid_html_tags)] + +//! AnyValue specification. + +use std::fmt::{Display, Formatter}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::attribute::{BasicRequirementLevelSpec, EnumEntriesSpec, Examples, RequirementLevel}; +use crate::stability::Stability; + +/// The AnyValueTypeSpec is a specification of a value that can be of any type. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum AnyValueSpec { + /// A boolean attribute. + Boolean { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// A integer attribute (signed 64 bit integer). + Int { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// A double attribute (double precision floating point (IEEE 754-1985)). + Double { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// A string attribute. + String { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// An array of strings attribute. + #[serde(rename = "string[]")] + Strings { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// An array of integer attribute. + #[serde(rename = "int[]")] + Ints { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// An array of double attribute. + #[serde(rename = "double[]")] + Doubles { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// An array of boolean attribute. + #[serde(rename = "boolean[]")] + Booleans { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// The value type is a map of key, value pairs + Map { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + /// The collection of key, values where the value is an `AnyValueSpec` + fields: Vec, + }, + + /// The value type will just be a bytes. + Bytes { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// The value type is not specified. + Undefined { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + }, + + /// An enum definition type. + Enum { + /// The common value specification + #[serde(flatten)] + common: AnyValueCommonSpec, + + /// Set to false to not accept values other than the specified members. + /// It defaults to true. + #[serde(default = "default_as_true")] + allow_custom_values: bool, + /// List of enum entries. + members: Vec, + }, +} + +/// The Common Value specification for properties associated with an "AnyValue", this +/// is similar to the current `AttributeSpec` as at the proto level an Attribute +/// is defined as a "KeyValue". +/// While this is (currently) a duplication of the existing AttributeSpec, this is +/// to reduce the size of the change set. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct AnyValueCommonSpec { + /// String that uniquely identifies the enum entry. + pub id: String, + /// A brief description of the value + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + pub brief: String, + /// A more elaborate description of the value. + /// It defaults to an empty string. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + pub note: String, + /// Specifies the stability of the value. + #[serde(skip_serializing_if = "Option::is_none")] + pub stability: Option, + /// Sequence of examples for the value or single example + /// value. 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, + /// Specifies if the field is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the field is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the field is required. + pub requirement_level: RequirementLevel, + /// Specifies if the body field is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, +} + +/// Implements a human readable display for AnyValueType, used to populate the type_display field. +impl Display for AnyValueSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AnyValueSpec::Map { fields, .. } => { + let entries = fields + .iter() + .map(|v| format!("{}", v)) + .collect::>() + .join(", "); + write!(f, "map<{}>{{ {} }}", self.id(), entries) + } + AnyValueSpec::Boolean { .. } => write!(f, "boolean"), + AnyValueSpec::Int { .. } => write!(f, "int"), + AnyValueSpec::Double { .. } => write!(f, "double"), + AnyValueSpec::String { .. } => write!(f, "string"), + AnyValueSpec::Strings { .. } => write!(f, "string[]"), + AnyValueSpec::Ints { .. } => write!(f, "int[]"), + AnyValueSpec::Doubles { .. } => write!(f, "double[]"), + AnyValueSpec::Booleans { .. } => write!(f, "boolean[]"), + AnyValueSpec::Bytes { .. } => write!(f, "byte[]"), + AnyValueSpec::Undefined { .. } => write!(f, "undefined"), + AnyValueSpec::Enum { members, .. } => { + let entries = members + .iter() + .map(|m| m.id.clone()) + .collect::>() + .join(", "); + write!(f, "enum<{}> {{{}}}", self.id(), entries) + } + } + } +} + +impl AnyValueSpec { + /// Returns the common value specification for each type. + #[must_use] + pub fn common(&self) -> &AnyValueCommonSpec { + match self { + AnyValueSpec::Boolean { common, .. } => common, + AnyValueSpec::Int { common, .. } => common, + AnyValueSpec::Double { common, .. } => common, + AnyValueSpec::String { common, .. } => common, + AnyValueSpec::Strings { common, .. } => common, + AnyValueSpec::Ints { common, .. } => common, + AnyValueSpec::Doubles { common, .. } => common, + AnyValueSpec::Booleans { common, .. } => common, + AnyValueSpec::Map { common, .. } => common, + AnyValueSpec::Bytes { common, .. } => common, + AnyValueSpec::Undefined { common, .. } => common, + AnyValueSpec::Enum { common, .. } => common, + } + } + + /// Returns true if the any value is required. + #[must_use] + pub fn is_required(&self) -> bool { + matches!( + self.common(), + AnyValueCommonSpec { + requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required), + .. + } + ) + } + + /// Returns the id of the any value. + #[must_use] + pub fn id(&self) -> String { + let AnyValueCommonSpec { id, .. } = self.common(); + id.clone() + } + + /// Returns the brief of the any value. + #[must_use] + pub fn brief(&self) -> String { + let AnyValueCommonSpec { brief, .. } = self.common(); + brief.clone() + } + + /// Returns the note of the any value. + #[must_use] + pub fn note(&self) -> String { + let AnyValueCommonSpec { note, .. } = self.common(); + note.clone() + } + + /// Provides a string representation of the type of the value, with the id for + /// enum and map types. + #[must_use] + pub fn type_name(&self) -> String { + match self { + AnyValueSpec::Map { .. } => "map".to_owned(), + AnyValueSpec::Boolean { .. } => "boolean".to_owned(), + AnyValueSpec::Int { .. } => "int".to_owned(), + AnyValueSpec::Double { .. } => "double".to_owned(), + AnyValueSpec::String { .. } => "string".to_owned(), + AnyValueSpec::Strings { .. } => "string[]".to_owned(), + AnyValueSpec::Ints { .. } => "int[]".to_owned(), + AnyValueSpec::Doubles { .. } => "double[]".to_owned(), + AnyValueSpec::Booleans { .. } => "boolean[]".to_owned(), + AnyValueSpec::Bytes { .. } => "byte[]".to_owned(), + AnyValueSpec::Undefined { .. } => "undefined".to_owned(), + AnyValueSpec::Enum { .. } => "enum".to_owned(), + } + } +} + +/// Specifies the default value for allow_custom_values. +fn default_as_true() -> bool { + true +} + +#[cfg(test)] +mod tests { + use std::fs; + + use crate::attribute::ValueSpec; + + use super::*; + + #[test] + fn test_anyvalue_field_type_display() { + #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] + pub struct BodySpec { + pub body: AnyValueSpec, + } + + let map = AnyValueSpec::Map { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Optional), + deprecated: None, + }, + fields: vec![ + AnyValueSpec::Enum { + common: AnyValueCommonSpec { + id: "id_enum".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + allow_custom_values: true, + members: vec![EnumEntriesSpec { + id: "id".to_owned(), + value: ValueSpec::Int(42), + brief: Some("brief".to_owned()), + note: Some("note".to_owned()), + stability: None, + deprecated: None, + }], + }, + AnyValueSpec::Map { + common: AnyValueCommonSpec { + id: "id_map".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + fields: vec![ + AnyValueSpec::Int { + common: AnyValueCommonSpec { + id: "id_int".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Required, + ), + deprecated: None, + }, + }, + AnyValueSpec::Bytes { + common: AnyValueCommonSpec { + id: "id_bytes".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Required, + ), + deprecated: None, + }, + }, + AnyValueSpec::String { + common: AnyValueCommonSpec { + id: "id_string".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Boolean { + common: AnyValueCommonSpec { + id: "id_bool".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Map { + common: AnyValueCommonSpec { + id: "id_nested_map".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + fields: vec![ + AnyValueSpec::Ints { + common: AnyValueCommonSpec { + id: "id_nested_int".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Doubles { + common: AnyValueCommonSpec { + id: "id_nested_bytes".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Strings { + common: AnyValueCommonSpec { + id: "id_nested_string".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Booleans { + common: AnyValueCommonSpec { + id: "id_nested_bool".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + ], + }, + ], + }, + AnyValueSpec::Int { + common: AnyValueCommonSpec { + id: "id_int".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Bytes { + common: AnyValueCommonSpec { + id: "id_bytes".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::String { + common: AnyValueCommonSpec { + id: "id_string".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Recommended, + ), + deprecated: None, + }, + }, + AnyValueSpec::Boolean { + common: AnyValueCommonSpec { + id: "id_bool".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Double { + common: AnyValueCommonSpec { + id: "id_double".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + AnyValueSpec::Doubles { + common: AnyValueCommonSpec { + id: "id_doubles".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional, + ), + deprecated: None, + }, + }, + ], + }; + let body = BodySpec { body: map.clone() }; + + let expected_yaml = fs::read_to_string("data/expected/any_value.yaml") + .unwrap() + .replace("\r\n", "\n"); + assert_eq!( + expected_yaml, + format!("{}", serde_yaml::to_string(&body).unwrap()), + "{}", + expected_yaml + ); + + let expected_json = fs::read_to_string("data/expected/any_value.json") + .unwrap() + .replace("\r\n", "\n"); + assert_eq!( + expected_json, + format!("{}", serde_json::to_string(&body).unwrap()), + "{}", + expected_json + ); + + assert_eq!(format!("{}", map.type_name()), "map",); + + assert_eq!( + format!( + "{}", + map + ), + "map{ enum {id}, map{ int, byte[], string, boolean, map{ int[], double[], string[], boolean[] } }, int, byte[], string, boolean, double, double[] }", + ); + + assert_eq!( + format!( + "{}", + AnyValueSpec::Boolean { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "boolean" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Int { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "int" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Double { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "double" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::String { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "string" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Strings { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "string[]" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Ints { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "int[]" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Doubles { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "double[]" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Booleans { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "boolean[]" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Bytes { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "byte[]" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Undefined { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + } + } + ), + "undefined" + ); + assert_eq!( + format!( + "{}", + AnyValueSpec::Enum { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + }, + allow_custom_values: true, + members: vec![EnumEntriesSpec { + id: "id".to_owned(), + value: ValueSpec::Int(42), + brief: Some("brief".to_owned()), + note: Some("note".to_owned()), + stability: None, + deprecated: None, + }] + } + .type_name() + ), + "enum" + ); + + assert_eq!( + format!( + "{}", + AnyValueSpec::Enum { + common: AnyValueCommonSpec { + id: "id".to_owned(), + brief: "brief".to_owned(), + note: "note".to_owned(), + stability: None, + examples: None, + requirement_level: RequirementLevel::Basic( + BasicRequirementLevelSpec::Optional + ), + deprecated: None, + }, + allow_custom_values: true, + members: vec![EnumEntriesSpec { + id: "entry1".to_owned(), + value: ValueSpec::Int(42), + brief: Some("brief".to_owned()), + note: Some("note".to_owned()), + stability: None, + deprecated: None, + }] + } + ), + "enum {entry1}" + ); + } +} diff --git a/crates/weaver_semconv/src/body.rs b/crates/weaver_semconv/src/body.rs deleted file mode 100644 index eb6ba229..00000000 --- a/crates/weaver_semconv/src/body.rs +++ /dev/null @@ -1,361 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#![allow(rustdoc::invalid_html_tags)] - -//! Body Field specification. - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; - -use crate::attribute::{AttributeType, BasicRequirementLevelSpec, Examples, RequirementLevel}; -use crate::stability::Stability; - -/// A body specification -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] -#[serde(untagged)] -#[serde(rename_all = "snake_case")] -pub enum BodySpec { - /// The collection of body fields associated with a body definition - Fields { - /// 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 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, - }, -} - -impl BodySpec { - /// Returns true if the body field is required. - #[must_use] - pub fn has_fields(&self) -> bool { - match self { - 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"), - } - } -} - -/// A `BodyField` specification. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "snake_case")] -pub struct BodyFieldSpec { - /// String that uniquely identifies the body field. - pub id: String, - /// Either a string literal denoting the type as a primitive or an - /// array type, a template type or an enum definition. - pub r#type: AttributeType, - /// A brief description of the body field. - 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 - /// body field. 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, - /// Specifies if the body field is mandatory. Can be "required", - /// "conditionally_required", "recommended" or "opt_in". When omitted, - /// the body field is "recommended". When set to - /// "conditionally_required", the string provided as MUST - /// specify the conditions under which the body field is required. - #[serde(default)] - pub requirement_level: RequirementLevel, - /// A more elaborate description of the body field. - /// It defaults to an empty string. - #[serde(default)] - pub note: String, - /// Specifies the stability of the body field. - /// Note that, if stability is missing but deprecated is present, it will - /// automatically set the stability to deprecated. If deprecated is - /// present and stability differs from deprecated, this will result in an - /// error. - #[serde(skip_serializing_if = "Option::is_none")] - pub stability: Option, - /// Specifies if the body field is deprecated. The string - /// provided as MUST specify why it's deprecated and/or what - /// to use instead. See also stability. - #[serde(skip_serializing_if = "Option::is_none")] - pub deprecated: Option, -} - -impl BodyFieldSpec { - /// Returns true if the body field is required. - #[must_use] - pub fn is_required(&self) -> bool { - matches!( - self, - BodyFieldSpec { - requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required), - .. - } - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::attribute::{ - EnumEntriesSpec, PrimitiveOrArrayTypeSpec, TemplateTypeSpec, ValueSpec, - }; - - #[test] - fn test_body_field_type_display() { - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Boolean) - ), - "boolean" - ); - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Int) - ), - "int" - ); - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Double) - ), - "double" - ); - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String) - ), - "string" - ); - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Strings) - ), - "string[]" - ); - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Ints) - ), - "int[]" - ); - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Doubles) - ), - "double[]" - ); - assert_eq!( - format!( - "{}", - AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Booleans) - ), - "boolean[]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::Boolean)), - "template[boolean]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::Int)), - "template[int]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::Double)), - "template[double]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::String)), - "template[string]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::Strings)), - "template[string[]]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::Ints)), - "template[int[]]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::Doubles)), - "template[double[]]" - ); - assert_eq!( - format!("{}", AttributeType::Template(TemplateTypeSpec::Booleans)), - "template[boolean[]]" - ); - assert_eq!( - format!( - "{}", - AttributeType::Enum { - allow_custom_values: true, - members: vec![EnumEntriesSpec { - id: "id".to_owned(), - value: ValueSpec::Int(42), - brief: Some("brief".to_owned()), - note: Some("note".to_owned()), - stability: None, - deprecated: None, - }] - } - ), - "enum {id}" - ); - } - - #[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: "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_eq!(field.id, "id"); - assert_eq!(field.brief.to_owned(), "brief".to_owned()); - assert_eq!(field.note, "note"); - assert!(field.is_required()); - } -} - -/// A Body Field definition with its provenance (path or URL). -#[derive(Debug, Clone)] -pub struct BodyFieldSpecWithProvenance { - /// The body field definition. - pub body_field: BodyFieldSpec, - /// The provenance of the body field (path or URL). - pub provenance: String, -} diff --git a/crates/weaver_semconv/src/group.rs b/crates/weaver_semconv/src/group.rs index 1724af70..8f870db6 100644 --- a/crates/weaver_semconv/src/group.rs +++ b/crates/weaver_semconv/src/group.rs @@ -9,8 +9,8 @@ use std::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize}; +use crate::any_value::AnyValueSpec; use crate::attribute::{AttributeSpec, AttributeType, PrimitiveOrArrayTypeSpec}; -use crate::body::BodySpec; use crate::group::InstrumentSpec::{Counter, Gauge, Histogram, UpDownCounter}; use crate::stability::Stability; use crate::Error; @@ -90,7 +90,7 @@ pub struct GroupSpec { /// The event body definition /// Note: only valid if type is event #[serde(skip_serializing_if = "Option::is_none")] - pub body: Option, + pub body: Option, } impl GroupSpec { diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs index 6131ceb0..3f7a298e 100644 --- a/crates/weaver_semconv/src/lib.rs +++ b/crates/weaver_semconv/src/lib.rs @@ -8,8 +8,8 @@ use serde::Serialize; use weaver_common::diagnostic::{DiagnosticMessage, DiagnosticMessages}; use weaver_common::error::{format_errors, WeaverError}; +pub mod any_value; pub mod attribute; -pub mod body; pub mod group; pub mod metric; pub mod registry;