From 59aefac4ae13c16a039a1432bff158ad71e74f21 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 19 Sep 2025 15:33:15 -0400 Subject: [PATCH 01/46] First cut at converting from v1 resolved schema to v2. Only handles spans, no real tests. --- crates/weaver_resolved_schema/src/catalog.rs | 2 + crates/weaver_resolved_schema/src/lib.rs | 3 +- .../src/v2/attribute.rs | 45 +++++ .../weaver_resolved_schema/src/v2/catalog.rs | 64 +++++++ crates/weaver_resolved_schema/src/v2/mod.rs | 181 ++++++++++++++++++ .../weaver_resolved_schema/src/v2/registry.rs | 18 ++ crates/weaver_resolved_schema/src/v2/span.rs | 59 ++++++ crates/weaver_semconv/src/v2/mod.rs | 2 +- crates/weaver_semconv/src/v2/signal_id.rs | 8 +- crates/weaver_semconv/src/v2/span.rs | 8 +- 10 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 crates/weaver_resolved_schema/src/v2/attribute.rs create mode 100644 crates/weaver_resolved_schema/src/v2/catalog.rs create mode 100644 crates/weaver_resolved_schema/src/v2/mod.rs create mode 100644 crates/weaver_resolved_schema/src/v2/registry.rs create mode 100644 crates/weaver_resolved_schema/src/v2/span.rs diff --git a/crates/weaver_resolved_schema/src/catalog.rs b/crates/weaver_resolved_schema/src/catalog.rs index f74a72173..8177c8fc8 100644 --- a/crates/weaver_resolved_schema/src/catalog.rs +++ b/crates/weaver_resolved_schema/src/catalog.rs @@ -21,6 +21,8 @@ use weaver_semconv::stability::Stability; pub struct Catalog { /// Catalog of attributes used in the schema. #[serde(skip_serializing_if = "Vec::is_empty")] + pub(crate) + attributes: Vec, } diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index a02c291a1..55ee318e8 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -29,6 +29,7 @@ pub mod registry; pub mod resource; pub mod signal; pub mod tags; +pub mod v2; pub mod value; /// The registry ID for the OpenTelemetry semantic conventions. @@ -156,7 +157,7 @@ impl ResolvedTelemetrySchema { let al = AttributeLineage::new(group_id); lineage.add_attribute_lineage(attr.name.clone(), al); } - let attr_refs = self.catalog.add_attributes(attrs); + let attr_refs: Vec = self.catalog.add_attributes(attrs); self.registry.groups.push(Group { id: group_id.to_owned(), r#type: GroupType::AttributeGroup, diff --git a/crates/weaver_resolved_schema/src/v2/attribute.rs b/crates/weaver_resolved_schema/src/v2/attribute.rs new file mode 100644 index 000000000..2436aada8 --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/attribute.rs @@ -0,0 +1,45 @@ +//! Attribute definitions for resolved schema. + +use std::fmt::Display; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::{AttributeType, Examples}, + v2::CommonFields, +}; + +/// The definition of an Attribute. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq, Hash, Eq)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct Attribute { + /// String that uniquely identifies the attribute. + pub key: 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, + /// Sequence of example values for the attribute or single example + /// value. They are required only for string and string array + /// attributes. Example values must be of the same type of the + /// attribute. If only a single example is provided, it can directly + /// be reported without encapsulating it into a sequence/dictionary. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub examples: Option, + /// Common fields (like brief, note, attributes). + #[serde(flatten)] + pub common: CommonFields, +} + +/// Reference to an attribute in the catalog. +#[derive( + Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, JsonSchema, Hash, +)] +pub struct AttributeRef(pub u32); + +impl Display for AttributeRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AttributeRef({})", self.0) + } +} diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs new file mode 100644 index 000000000..f46d65b8f --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -0,0 +1,64 @@ +//! Catalog of attributes and other. + +use crate::v2::attribute::{Attribute, AttributeRef}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// A catalog of indexed attributes shared across semconv groups, or signals. +/// Attribute references are used to refer to attributes in the catalog. +/// +/// Note : In the future, this catalog could be extended with other entities. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] +#[serde(deny_unknown_fields)] +#[must_use] +pub struct Catalog { + /// Catalog of attributes used in the schema. + #[serde(skip_serializing_if = "Vec::is_empty")] + attributes: Vec, +} + +// TODO - statistics. + +impl Catalog { + /// Creates a catalog from a list of attributes. + pub fn from_attributes(attributes: Vec) -> Self { + Self { attributes } + } + + /// Returns the attribute name from an attribute ref if it exists + /// in the catalog or None if it does not exist. + #[must_use] + pub fn attribute_key(&self, attribute_ref: &AttributeRef) -> Option<&str> { + self.attributes + .get(attribute_ref.0 as usize) + .map(|attr| attr.key.as_ref()) + } + + /// Returns the attribute from an attribute ref if it exists. + #[must_use] + pub fn attribute(&self, attribute_ref: &AttributeRef) -> Option<&Attribute> { + self.attributes.get(attribute_ref.0 as usize) + } + + #[must_use] + pub(crate) fn convert_ref( + &self, + attribute: &crate::attribute::Attribute, + ) -> Option { + self.attributes + .iter() + .position( + |a| { + a.key == attribute.name + // TODO check everything + && a.r#type == attribute.r#type + && a.examples == attribute.examples + && a.common.brief == attribute.brief + && a.common.note == attribute.note + && a.common.deprecated == attribute.deprecated + }, // && a.common.stability == attribute.stability + // && a.common.annotations == attribute.annotations + ) + .map(|idx| AttributeRef(idx as u32)) + } +} diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs new file mode 100644 index 000000000..1ab134733 --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -0,0 +1,181 @@ +//! Version 2 of semantic convention schema. + +use std::collections::HashSet; + +use weaver_semconv::{ + group::GroupType, + v2::{span::SpanName, CommonFields}, +}; + +use crate::v2::span::Span; + +pub mod attribute; +pub mod catalog; +pub mod registry; +pub mod span; + +/// Converts a V1 registry + catalog to V2. +pub fn convert_v1_to_v2( + c: crate::catalog::Catalog, + r: crate::registry::Registry, +) -> Result<(catalog::Catalog, registry::Registry), crate::error::Error> { + // When pulling attributes, as we collapse things, we need to filter + // to just unique. + let attributes: HashSet = c + .attributes + .iter() + .cloned() + .map(|a| { + attribute::Attribute { + key: a.name, + r#type: a.r#type, + examples: a.examples, + common: CommonFields { + brief: a.brief, + note: a.note, + // TODO - Check this assumption. + stability: a + .stability + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: a.deprecated, + annotations: a.annotations.unwrap_or_default(), + }, + } + }) + .collect(); + + let v2_catalog = catalog::Catalog::from_attributes(attributes.into_iter().collect()); + + // TODO - pull spans. + let mut spans = Vec::new(); + for g in r.groups.iter() { + if g.r#type == GroupType::Span { + let mut span_attributes = Vec::new(); + for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { + if let Some(a) = v2_catalog.convert_ref(attr) { + span_attributes.push(span::SpanAttributeRef { + base: a, + requirement_level: attr.requirement_level.clone(), + sampling_relevant: attr.sampling_relevant.clone(), + }); + } else { + // TODO logic error! + } + } + spans.push(Span { + // TODO - Strip the `span.` from the name if it exists. + r#type: g.id.clone().into(), + kind: g + .span_kind + .clone() + .unwrap_or(weaver_semconv::group::SpanKindSpec::Internal), + // TODO - Pass advanced name controls through V1 groups. + name: SpanName { + note: g.name.clone().unwrap_or_default(), + }, + entity_associations: g.entity_associations.clone(), + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), + }, + attributes: span_attributes, + }); + } + } + + let v2_registry = registry::Registry { + registry_url: r.registry_url, + spans, + }; + Ok((v2_catalog, v2_registry)) +} + +#[cfg(test)] +mod tests { + + use weaver_semconv::{stability::Stability, v2}; + + use crate::{attribute::Attribute, registry::Group}; + + use super::*; + + #[test] + fn test_convert_v1_to_v2() { + let mut v1_catalog = crate::catalog::Catalog::from_attributes(vec![]); + let test_refs = v1_catalog.add_attributes([ + Attribute { + name: "test.key".to_owned(), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray(weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String), + brief: "".to_owned(), + examples: None, + tag: None, + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic(weaver_semconv::attribute::BasicRequirementLevelSpec::Required), + sampling_relevant: None, + note: "".to_string(), + stability: Some(Stability::Stable), + deprecated: None, + prefix: false, + tags: None, + annotations: None, + value: None, + role: None, + }, + Attribute { + name: "test.key".to_owned(), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray(weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String), + brief: "".to_owned(), + examples: None, + tag: None, + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic(weaver_semconv::attribute::BasicRequirementLevelSpec::Recommended), + sampling_relevant: Some(true), + note: "".to_string(), + stability: Some(Stability::Stable), + deprecated: None, + prefix: false, + tags: None, + annotations: None, + value: None, + role: None, + }, + ]); + let v1_registry = crate::registry::Registry { + registry_url: "my.schema.url".to_owned(), + groups: vec![ + Group { + id: "span.my-span".to_owned(), + r#type: GroupType::Span, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[1].clone()], + span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), + events: vec![], + metric_name: None, + instrument: None, + unit: None, + name: Some("my span name".to_owned()), + lineage: None, + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + }, + ], + }; + + let (v2_catalog, v2_registry) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); + println!("Catalog: {v2_catalog:?}"); + // TODO - assert only ONE attribute due to sharing. + println!("Registry: {v2_registry:?}"); + // TODO - assert attribute fields not shared show up on ref in span. + } +} diff --git a/crates/weaver_resolved_schema/src/v2/registry.rs b/crates/weaver_resolved_schema/src/v2/registry.rs new file mode 100644 index 000000000..268e55795 --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/registry.rs @@ -0,0 +1,18 @@ +//! A semantic convention registry. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v2::span::Span; + +/// A semantic convention registry. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Registry { + /// The semantic convention registry url. + pub registry_url: String, + + /// A list of span definitions. + pub spans: Vec, + // TODO - Signal types. +} diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs new file mode 100644 index 000000000..bb75342c0 --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -0,0 +1,59 @@ +//! Span related definitions structs. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + group::SpanKindSpec, + v2::{signal_id::SignalId, span::SpanName, CommonFields}, +}; + +use crate::v2::attribute::AttributeRef; + +/// The definition of a Span signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Span { + /// The type of the Span. This denotes the identity + /// of the "shape" of this span, and must be unique. + pub r#type: SignalId, + /// Specifies the kind of the span. + /// Note: only valid if type is span + pub kind: SpanKindSpec, + /// The name pattern for the span. + pub name: SpanName, + // TODO - Should we split attributes into "sampling_relevant" and "other" groups here? + /// List of attributes that belong to this span. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + // TODO - Should Entity Associations be "strong" links? + /// Which resources this span should be associated with. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub entity_associations: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of attribute reference that remembers if something +/// is sampling relevant. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SpanAttributeRef { + /// Underlying attribute. + pub base: AttributeRef, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + pub requirement_level: RequirementLevel, + /// Specifies if the attribute is (especially) relevant for sampling + /// and thus should be set at span start. It defaults to false. + /// Note: this field is experimental. + #[serde(skip_serializing_if = "Option::is_none")] + pub sampling_relevant: Option, +} diff --git a/crates/weaver_semconv/src/v2/mod.rs b/crates/weaver_semconv/src/v2/mod.rs index e27c16107..ff038ccb5 100644 --- a/crates/weaver_semconv/src/v2/mod.rs +++ b/crates/weaver_semconv/src/v2/mod.rs @@ -28,7 +28,7 @@ pub mod signal_id; pub mod span; /// Common fields we want on all major components of semantic conventions. -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq, Hash, Eq)] #[serde(deny_unknown_fields)] pub struct CommonFields { /// A brief description of the attribute or signal. diff --git a/crates/weaver_semconv/src/v2/signal_id.rs b/crates/weaver_semconv/src/v2/signal_id.rs index c7c15ee9f..c5502e5bc 100644 --- a/crates/weaver_semconv/src/v2/signal_id.rs +++ b/crates/weaver_semconv/src/v2/signal_id.rs @@ -7,7 +7,7 @@ use std::{fmt, ops::Deref}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; -#[derive(Serialize, JsonSchema, Clone, Debug)] +#[derive(Serialize, JsonSchema, Clone, Debug, PartialEq)] /// An identifier for a signal. Should be `.` separated namespaces and names. pub struct SignalId(String); @@ -19,6 +19,12 @@ impl SignalId { } } +impl From for SignalId { + fn from(value: String) -> Self { + SignalId(value) + } +} + // Allow `&SignalId` to be used for getting `&str`. impl Deref for SignalId { type Target = str; diff --git a/crates/weaver_semconv/src/v2/span.rs b/crates/weaver_semconv/src/v2/span.rs index 86ec5c9e6..1e3d93b7a 100644 --- a/crates/weaver_semconv/src/v2/span.rs +++ b/crates/weaver_semconv/src/v2/span.rs @@ -52,11 +52,7 @@ pub fn split_span_attributes_and_groups( (attribute_refs, groups) } -/// A group defines an attribute group, an entity, or a signal. -/// Supported group types are: `attribute_group`, `span`, `event`, `metric`, `entity`, `scope`. -/// Mandatory fields are: `id` and `brief`. -/// -/// Note: The `resource` type is no longer used and is an alias for `entity`. +/// Defines a new Span signal. #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Span { @@ -117,7 +113,7 @@ impl Span { } /// Specification of the span name. -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] #[serde(rename_all = "snake_case")] pub struct SpanName { From 54fc192ad6cb11f6125b825c515743e3bd2dd300 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 8 Oct 2025 14:23:38 -0400 Subject: [PATCH 02/46] Add group lineage tracking so we can determine group refinement when creating V2 schema. --- .../semconv_jq_fn/semconv_metrics.json | 4 + .../semconv_metrics_not_deprecated.json | 4 + crates/weaver_forge/src/lib.rs | 13 ++ crates/weaver_resolved_schema/src/catalog.rs | 4 +- crates/weaver_resolved_schema/src/lib.rs | 10 ++ crates/weaver_resolved_schema/src/lineage.rs | 11 ++ crates/weaver_resolved_schema/src/v2/mod.rs | 136 +++++++++++------- crates/weaver_resolver/src/registry.rs | 3 + 8 files changed, 129 insertions(+), 56 deletions(-) diff --git a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json index 28c72d06f..d0c5981b4 100644 --- a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json +++ b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json @@ -438,6 +438,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" @@ -557,6 +558,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" @@ -680,6 +682,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" @@ -775,6 +778,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" diff --git a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json index 8776bd7a9..358f740ac 100644 --- a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json +++ b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json @@ -428,6 +428,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" @@ -513,6 +514,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" @@ -615,6 +617,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" @@ -700,6 +703,7 @@ "source_group": "attributes.jvm.memory" } }, + "extends_group": "attributes.jvm.memory", "provenance": { "path": "data/jvm-metrics.yaml", "registry_id": "default" diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index c293bfef2..d03b71c62 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -966,6 +966,19 @@ mod tests { .expect("Failed to generate registry assets"); assert!(diff_dir("expected_output/test", "observed_output/test").unwrap()); + + // TODO - Remove this. + let (rv2, cv2) = schema.create_v2_registry().unwrap(); + fs::write( + "observed_output/test/v2_registry.yaml", + serde_yaml::to_string(&rv2).unwrap(), + ) + .unwrap(); + fs::write( + "observed_output/test/v2_catalog.yaml", + serde_yaml::to_string(&cv2).unwrap(), + ) + .unwrap(); } #[test] diff --git a/crates/weaver_resolved_schema/src/catalog.rs b/crates/weaver_resolved_schema/src/catalog.rs index 8177c8fc8..c84bbb887 100644 --- a/crates/weaver_resolved_schema/src/catalog.rs +++ b/crates/weaver_resolved_schema/src/catalog.rs @@ -21,9 +21,7 @@ use weaver_semconv::stability::Stability; pub struct Catalog { /// Catalog of attributes used in the schema. #[serde(skip_serializing_if = "Vec::is_empty")] - pub(crate) - - attributes: Vec, + pub(crate) attributes: Vec, } /// Statistics on a catalog. diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index 55ee318e8..804642e24 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -10,6 +10,7 @@ use crate::catalog::Catalog; use crate::instrumentation_library::InstrumentationLibrary; use crate::registry::{Group, Registry}; use crate::resource::Resource; +use crate::v2::convert_v1_to_v2; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -107,6 +108,15 @@ impl ResolvedTelemetrySchema { } } + // For testing for now. + /// Convert this schema into V2 + #[must_use] + pub fn create_v2_registry( + self, + ) -> Result<(v2::catalog::Catalog, v2::registry::Registry), error::Error> { + convert_v1_to_v2(self.catalog, self.registry) + } + #[cfg(test)] pub(crate) fn add_metric_group( &mut self, diff --git a/crates/weaver_resolved_schema/src/lineage.rs b/crates/weaver_resolved_schema/src/lineage.rs index ac9437b11..d1c175581 100644 --- a/crates/weaver_resolved_schema/src/lineage.rs +++ b/crates/weaver_resolved_schema/src/lineage.rs @@ -38,6 +38,11 @@ pub struct GroupLineage { /// The provenance of the source file where the group is defined. provenance: Provenance, + /// The group that this group extended, if available. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub extends_group: Option, + /// The lineage per attribute. /// /// Note: Use a BTreeMap to ensure a deterministic order of attributes. @@ -470,10 +475,16 @@ impl GroupLineage { pub fn new(provenance: Provenance) -> Self { Self { provenance, + extends_group: None, attributes: Default::default(), } } + /// Declares this group extended another group. + pub fn extends(&mut self, extends_group: &str) { + self.extends_group = Some(extends_group.to_owned()); + } + /// Adds an attribute lineage. pub fn add_attribute_lineage(&mut self, attr_id: String, attribute_lineage: AttributeLineage) { _ = self.attributes.insert(attr_id, attribute_lineage); diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 1ab134733..b85548e1b 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -1,6 +1,6 @@ //! Version 2 of semantic convention schema. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use weaver_semconv::{ group::GroupType, @@ -46,10 +46,31 @@ pub fn convert_v1_to_v2( let v2_catalog = catalog::Catalog::from_attributes(attributes.into_iter().collect()); - // TODO - pull spans. + // Create a lookup so we can check inheritance. + let mut group_type_lookup = HashMap::new(); + for g in r.groups.iter() { + println!("Group {} is type: {:?}", &g.id, g.r#type); + let _ = group_type_lookup.insert(g.id.clone(), g.r#type.clone()); + } + // Pull spans from the registry and create a new span-focused registry. let mut spans = Vec::new(); for g in r.groups.iter() { if g.r#type == GroupType::Span { + // TODO - Check if we extend another span. + let extend_type = g.extends.as_ref().and_then(|id| group_type_lookup.get(id)); + + let is_refinement = g + .lineage + .as_ref() + .and_then(|l| l.extends_group.as_ref()) + .and_then(|parent| group_type_lookup.get(parent)) + .map(|t| *t == GroupType::Span) + .unwrap_or(false); + println!( + "Group {} is_refinement: {}, extends: {:?}", + &g.id, is_refinement, extend_type + ); + // If so, we need to be a refinement, not a new span. let mut span_attributes = Vec::new(); for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { if let Some(a) = v2_catalog.convert_ref(attr) { @@ -62,30 +83,33 @@ pub fn convert_v1_to_v2( // TODO logic error! } } - spans.push(Span { - // TODO - Strip the `span.` from the name if it exists. - r#type: g.id.clone().into(), - kind: g - .span_kind - .clone() - .unwrap_or(weaver_semconv::group::SpanKindSpec::Internal), - // TODO - Pass advanced name controls through V1 groups. - name: SpanName { - note: g.name.clone().unwrap_or_default(), - }, - entity_associations: g.entity_associations.clone(), - common: CommonFields { - brief: g.brief.clone(), - note: g.note.clone(), - stability: g - .stability + if !is_refinement { + spans.push(Span { + // TODO - Strip the `span.` from the name if it exists. + // TODO - Understand 'extends' and 'refinement' here. + r#type: g.id.clone().into(), + kind: g + .span_kind .clone() - .unwrap_or(weaver_semconv::stability::Stability::Alpha), - deprecated: g.deprecated.clone(), - annotations: g.annotations.clone().unwrap_or_default(), - }, - attributes: span_attributes, - }); + .unwrap_or(weaver_semconv::group::SpanKindSpec::Internal), + // TODO - Pass advanced name controls through V1 groups. + name: SpanName { + note: g.name.clone().unwrap_or_default(), + }, + entity_associations: g.entity_associations.clone(), + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), + }, + attributes: span_attributes, + }); + } } } @@ -111,11 +135,15 @@ mod tests { let test_refs = v1_catalog.add_attributes([ Attribute { name: "test.key".to_owned(), - r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray(weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), brief: "".to_owned(), examples: None, tag: None, - requirement_level: weaver_semconv::attribute::RequirementLevel::Basic(weaver_semconv::attribute::BasicRequirementLevelSpec::Required), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), sampling_relevant: None, note: "".to_string(), stability: Some(Stability::Stable), @@ -128,11 +156,15 @@ mod tests { }, Attribute { name: "test.key".to_owned(), - r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray(weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), brief: "".to_owned(), examples: None, tag: None, - requirement_level: weaver_semconv::attribute::RequirementLevel::Basic(weaver_semconv::attribute::BasicRequirementLevelSpec::Recommended), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Recommended, + ), sampling_relevant: Some(true), note: "".to_string(), stability: Some(Stability::Stable), @@ -146,30 +178,28 @@ mod tests { ]); let v1_registry = crate::registry::Registry { registry_url: "my.schema.url".to_owned(), - groups: vec![ - Group { - id: "span.my-span".to_owned(), - r#type: GroupType::Span, - brief: "".to_owned(), - note: "".to_owned(), - prefix: "".to_owned(), - extends: None, - stability: Some(Stability::Stable), - deprecated: None, - attributes: vec![test_refs[1].clone()], - span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), - events: vec![], - metric_name: None, - instrument: None, - unit: None, - name: Some("my span name".to_owned()), - lineage: None, - display_name: None, - body: None, - annotations: None, - entity_associations: vec![], - }, - ], + groups: vec![Group { + id: "span.my-span".to_owned(), + r#type: GroupType::Span, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[1].clone()], + span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), + events: vec![], + metric_name: None, + instrument: None, + unit: None, + name: Some("my span name".to_owned()), + lineage: None, + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + }], }; let (v2_catalog, v2_registry) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); diff --git a/crates/weaver_resolver/src/registry.rs b/crates/weaver_resolver/src/registry.rs index 9ea5aa02b..fa60e20e7 100644 --- a/crates/weaver_resolver/src/registry.rs +++ b/crates/weaver_resolver/src/registry.rs @@ -590,6 +590,9 @@ fn resolve_extends_references(ureg: &mut UnresolvedRegistry) -> Result<(), Error vec![(extends, attrs)], unresolved_group.group.lineage.as_mut(), ); + if let Some(lineage) = unresolved_group.group.lineage.as_mut() { + lineage.extends(extends); + } add_resolved_group_to_index( &mut group_index, unresolved_group, From f770723abeba1059e30c99f768bb33333d9b09c9 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 8 Oct 2025 20:18:23 -0400 Subject: [PATCH 03/46] Fix up tests for V2 span/span_refinement conversion. --- crates/weaver_forge/src/lib.rs | 2 +- .../weaver_resolved_schema/src/v2/catalog.rs | 5 + crates/weaver_resolved_schema/src/v2/mod.rs | 163 ++++++++++++++---- .../weaver_resolved_schema/src/v2/registry.rs | 5 +- crates/weaver_resolved_schema/src/v2/span.rs | 15 ++ 5 files changed, 152 insertions(+), 38 deletions(-) diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index d03b71c62..78740020c 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -968,7 +968,7 @@ mod tests { assert!(diff_dir("expected_output/test", "observed_output/test").unwrap()); // TODO - Remove this. - let (rv2, cv2) = schema.create_v2_registry().unwrap(); + let (cv2, rv2) = schema.create_v2_registry().unwrap(); fs::write( "observed_output/test/v2_registry.yaml", serde_yaml::to_string(&rv2).unwrap(), diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index f46d65b8f..aaae40718 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -25,6 +25,11 @@ impl Catalog { Self { attributes } } + #[cfg(test)] + pub(crate) fn attributes(&self) -> &Vec { + &self.attributes + } + /// Returns the attribute name from an attribute ref if it exists /// in the catalog or None if it does not exist. #[must_use] diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index b85548e1b..36d697cda 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -4,16 +4,28 @@ use std::collections::{HashMap, HashSet}; use weaver_semconv::{ group::GroupType, - v2::{span::SpanName, CommonFields}, + v2::{signal_id::SignalId, span::SpanName, CommonFields}, }; -use crate::v2::span::Span; +use crate::v2::span::{Span, SpanRefinement}; pub mod attribute; pub mod catalog; pub mod registry; pub mod span; +fn fix_group_id(prefix: &'static str, group_id: &str) -> SignalId { + if group_id.starts_with(prefix) { + group_id.trim_start_matches(prefix).to_owned().into() + } else { + group_id.to_owned().into() + } +} + +fn fix_span_group_id(group_id: &str) -> SignalId { + fix_group_id("span.", group_id) +} + /// Converts a V1 registry + catalog to V2. pub fn convert_v1_to_v2( c: crate::catalog::Catalog, @@ -54,11 +66,11 @@ pub fn convert_v1_to_v2( } // Pull spans from the registry and create a new span-focused registry. let mut spans = Vec::new(); + let mut span_refinements = Vec::new(); for g in r.groups.iter() { if g.r#type == GroupType::Span { // TODO - Check if we extend another span. let extend_type = g.extends.as_ref().and_then(|id| group_type_lookup.get(id)); - let is_refinement = g .lineage .as_ref() @@ -84,10 +96,8 @@ pub fn convert_v1_to_v2( } } if !is_refinement { - spans.push(Span { - // TODO - Strip the `span.` from the name if it exists. - // TODO - Understand 'extends' and 'refinement' here. - r#type: g.id.clone().into(), + let span = Span { + r#type: fix_span_group_id(&g.id), kind: g .span_kind .clone() @@ -108,7 +118,46 @@ pub fn convert_v1_to_v2( annotations: g.annotations.clone().unwrap_or_default(), }, attributes: span_attributes, + }; + spans.push(span.clone()); + span_refinements.push(SpanRefinement { + id: span.r#type.clone(), + span, }); + } else { + // unwrap should be safe becasue we verified this is a refinement earlier. + let span_type = g + .lineage + .as_ref() + .and_then(|l| l.extends_group.as_ref()) + .map(|id| fix_span_group_id(id)) + .unwrap(); + span_refinements.push(SpanRefinement { + id: fix_span_group_id(&g.id), + span: Span { + r#type: span_type, + kind: g + .span_kind + .clone() + .unwrap_or(weaver_semconv::group::SpanKindSpec::Internal), + // TODO - Pass advanced name controls through V1 groups. + name: SpanName { + note: g.name.clone().unwrap_or_default(), + }, + entity_associations: g.entity_associations.clone(), + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), + }, + attributes: span_attributes, + }, + }) } } } @@ -116,6 +165,7 @@ pub fn convert_v1_to_v2( let v2_registry = registry::Registry { registry_url: r.registry_url, spans, + span_refinements, }; Ok((v2_catalog, v2_registry)) } @@ -123,14 +173,14 @@ pub fn convert_v1_to_v2( #[cfg(test)] mod tests { - use weaver_semconv::{stability::Stability, v2}; + use weaver_semconv::{provenance::Provenance, stability::Stability, v2}; - use crate::{attribute::Attribute, registry::Group}; + use crate::{attribute::Attribute, lineage::GroupLineage, registry::Group}; use super::*; #[test] - fn test_convert_v1_to_v2() { + fn test_convert_span_v1_to_v2() { let mut v1_catalog = crate::catalog::Catalog::from_attributes(vec![]); let test_refs = v1_catalog.add_attributes([ Attribute { @@ -176,36 +226,77 @@ mod tests { role: None, }, ]); + let mut refinement_span_lineage = GroupLineage::new(Provenance::new("tmp", "tmp")); + refinement_span_lineage.extends("span.my-span"); let v1_registry = crate::registry::Registry { registry_url: "my.schema.url".to_owned(), - groups: vec![Group { - id: "span.my-span".to_owned(), - r#type: GroupType::Span, - brief: "".to_owned(), - note: "".to_owned(), - prefix: "".to_owned(), - extends: None, - stability: Some(Stability::Stable), - deprecated: None, - attributes: vec![test_refs[1].clone()], - span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), - events: vec![], - metric_name: None, - instrument: None, - unit: None, - name: Some("my span name".to_owned()), - lineage: None, - display_name: None, - body: None, - annotations: None, - entity_associations: vec![], - }], + groups: vec![ + Group { + id: "span.my-span".to_owned(), + r#type: GroupType::Span, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[1].clone()], + span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), + events: vec![], + metric_name: None, + instrument: None, + unit: None, + name: Some("my span name".to_owned()), + lineage: None, + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + }, + Group { + id: "span.custom".to_owned(), + r#type: GroupType::Span, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[1].clone()], + span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), + events: vec![], + metric_name: None, + instrument: None, + unit: None, + name: Some("my span name".to_owned()), + lineage: Some(refinement_span_lineage), + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + }, + ], }; let (v2_catalog, v2_registry) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); - println!("Catalog: {v2_catalog:?}"); - // TODO - assert only ONE attribute due to sharing. - println!("Registry: {v2_registry:?}"); - // TODO - assert attribute fields not shared show up on ref in span. + // assert only ONE attribute due to sharing. + assert_eq!(v2_catalog.attributes().len(), 1); + // assert attribute fields not shared show up on ref in span. + assert_eq!(v2_registry.spans.len(), 1); + if let Some(span) = v2_registry.spans.iter().next() { + assert_eq!(span.r#type, "my-span".to_owned().into()); + // Make sure attribute ref carries sampling relevant. + } + // Assert we have two refinements (e.g. one real span, one refinement). + assert_eq!(v2_registry.span_refinements.len(), 2); + let span_ref_ids: Vec = v2_registry + .span_refinements + .iter() + .map(|s| s.id.to_string()) + .collect(); + assert_eq!( + span_ref_ids, + vec!["my-span".to_owned(), "custom".to_owned()] + ); } } diff --git a/crates/weaver_resolved_schema/src/v2/registry.rs b/crates/weaver_resolved_schema/src/v2/registry.rs index 268e55795..0d45a3b45 100644 --- a/crates/weaver_resolved_schema/src/v2/registry.rs +++ b/crates/weaver_resolved_schema/src/v2/registry.rs @@ -3,7 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::v2::span::Span; +use crate::v2::span::{Span, SpanRefinement}; /// A semantic convention registry. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] @@ -14,5 +14,8 @@ pub struct Registry { /// A list of span definitions. pub spans: Vec, + + /// A list of span refinements. + pub span_refinements: Vec, // TODO - Signal types. } diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs index bb75342c0..f29a84cf4 100644 --- a/crates/weaver_resolved_schema/src/v2/span.rs +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -57,3 +57,18 @@ pub struct SpanAttributeRef { #[serde(skip_serializing_if = "Option::is_none")] pub sampling_relevant: Option, } + +/// A refinement of a span, for use in code-gen or specific library application. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct SpanRefinement { + /// The identity of the refinement + pub id: SignalId, + + // TODO - This is a lazy way of doing this. We use `type` to refer + // to the underlying span defintiion, but override all fields here. + // We probably should copy-paste all the "span" attributes here + // including the `ty` + /// The definition of the span refinement. + #[serde(flatten)] + pub span: Span, +} From b14dcd39a487dc601bc6b36e0f3a2a5eb80b4beb Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 9 Oct 2025 09:20:43 -0400 Subject: [PATCH 04/46] Start fixing up documentation and add metric v2 extraction. --- .../weaver_resolved_schema/src/v2/metric.rs | 70 ++++++ crates/weaver_resolved_schema/src/v2/mod.rs | 210 ++++++++++++------ .../weaver_resolved_schema/src/v2/registry.rs | 30 ++- 3 files changed, 240 insertions(+), 70 deletions(-) create mode 100644 crates/weaver_resolved_schema/src/v2/metric.rs diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs new file mode 100644 index 000000000..70116880e --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -0,0 +1,70 @@ +//! Metric related definitions structs. + +use crate::v2::attribute::AttributeRef; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + group::InstrumentSpec, + v2::{signal_id::SignalId, CommonFields}, +}; + +/// The definition of a metric signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Metric { + /// The name of the metric. + pub name: SignalId, + /// The instrument type that should be used to record the metric. Note that + /// the semantic conventions must be written using the names of the + /// synchronous instrument types (counter, gauge, updowncounter and + /// histogram). + /// For more details: [Metrics semantic conventions - Instrument types](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-types). + /// Note: This field is required if type is metric. + pub instrument: InstrumentSpec, + /// The unit in which the metric is measured, which should adhere to the + /// [guidelines](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-units). + pub unit: String, + /// List of attributes that belong to the semantic convention. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + // TODO - Should Entity Associations be "strong" links? + /// Which resources this span should be associated with. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub entity_associations: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of reference to attributes that remembers metric-specicific information. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct MetricAttributeRef { + /// Underlying attribute. + pub base: AttributeRef, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + pub requirement_level: RequirementLevel, +} + +/// A refinement of a metric signal, for use in code-gen or specific library application. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct MetricRefinement { + /// The identity of the refinement + pub id: SignalId, + + // TODO - This is a lazy way of doing this. We use `type` to refer + // to the underlying span defintiion, but override all fields here. + // We probably should copy-paste all the "span" attributes here + // including the `ty` + /// The definition of the metric refinement. + #[serde(flatten)] + pub metric: Metric, +} diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 36d697cda..2c8b1c310 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -7,10 +7,14 @@ use weaver_semconv::{ v2::{signal_id::SignalId, span::SpanName, CommonFields}, }; -use crate::v2::span::{Span, SpanRefinement}; +use crate::v2::{ + metric::Metric, + span::{Span, SpanRefinement}, +}; pub mod attribute; pub mod catalog; +pub mod metric; pub mod registry; pub mod span; @@ -64,78 +68,40 @@ pub fn convert_v1_to_v2( println!("Group {} is type: {:?}", &g.id, g.r#type); let _ = group_type_lookup.insert(g.id.clone(), g.r#type.clone()); } - // Pull spans from the registry and create a new span-focused registry. + // Pull signals from the registry and create a new span-focused registry. let mut spans = Vec::new(); let mut span_refinements = Vec::new(); + let mut metrics = Vec::new(); + let mut metric_refinements = Vec::new(); + for g in r.groups.iter() { - if g.r#type == GroupType::Span { - // TODO - Check if we extend another span. - let extend_type = g.extends.as_ref().and_then(|id| group_type_lookup.get(id)); - let is_refinement = g - .lineage - .as_ref() - .and_then(|l| l.extends_group.as_ref()) - .and_then(|parent| group_type_lookup.get(parent)) - .map(|t| *t == GroupType::Span) - .unwrap_or(false); - println!( - "Group {} is_refinement: {}, extends: {:?}", - &g.id, is_refinement, extend_type - ); - // If so, we need to be a refinement, not a new span. - let mut span_attributes = Vec::new(); - for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { - if let Some(a) = v2_catalog.convert_ref(attr) { - span_attributes.push(span::SpanAttributeRef { - base: a, - requirement_level: attr.requirement_level.clone(), - sampling_relevant: attr.sampling_relevant.clone(), - }); - } else { - // TODO logic error! - } - } - if !is_refinement { - let span = Span { - r#type: fix_span_group_id(&g.id), - kind: g - .span_kind - .clone() - .unwrap_or(weaver_semconv::group::SpanKindSpec::Internal), - // TODO - Pass advanced name controls through V1 groups. - name: SpanName { - note: g.name.clone().unwrap_or_default(), - }, - entity_associations: g.entity_associations.clone(), - common: CommonFields { - brief: g.brief.clone(), - note: g.note.clone(), - stability: g - .stability - .clone() - .unwrap_or(weaver_semconv::stability::Stability::Alpha), - deprecated: g.deprecated.clone(), - annotations: g.annotations.clone().unwrap_or_default(), - }, - attributes: span_attributes, - }; - spans.push(span.clone()); - span_refinements.push(SpanRefinement { - id: span.r#type.clone(), - span, - }); - } else { - // unwrap should be safe becasue we verified this is a refinement earlier. - let span_type = g + let extend_type = g.extends.as_ref().and_then(|id| group_type_lookup.get(id)); + match g.r#type { + GroupType::Span => { + // Check if we extend another span. + let is_refinement = g .lineage .as_ref() .and_then(|l| l.extends_group.as_ref()) - .map(|id| fix_span_group_id(id)) - .unwrap(); - span_refinements.push(SpanRefinement { - id: fix_span_group_id(&g.id), - span: Span { - r#type: span_type, + .and_then(|parent| group_type_lookup.get(parent)) + .map(|t| *t == GroupType::Span) + .unwrap_or(false); + // Pull all the attribute references. + let mut span_attributes = Vec::new(); + for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { + if let Some(a) = v2_catalog.convert_ref(attr) { + span_attributes.push(span::SpanAttributeRef { + base: a, + requirement_level: attr.requirement_level.clone(), + sampling_relevant: attr.sampling_relevant.clone(), + }); + } else { + // TODO logic error! + } + } + if !is_refinement { + let span = Span { + r#type: fix_span_group_id(&g.id), kind: g .span_kind .clone() @@ -156,8 +122,114 @@ pub fn convert_v1_to_v2( annotations: g.annotations.clone().unwrap_or_default(), }, attributes: span_attributes, + }; + spans.push(span.clone()); + span_refinements.push(SpanRefinement { + id: span.r#type.clone(), + span, + }); + } else { + // unwrap should be safe becasue we verified this is a refinement earlier. + let span_type = g + .lineage + .as_ref() + .and_then(|l| l.extends_group.as_ref()) + .map(|id| fix_span_group_id(id)) + .unwrap(); + span_refinements.push(SpanRefinement { + id: fix_span_group_id(&g.id), + span: Span { + r#type: span_type, + kind: g + .span_kind + .clone() + .unwrap_or(weaver_semconv::group::SpanKindSpec::Internal), + // TODO - Pass advanced name controls through V1 groups. + name: SpanName { + note: g.name.clone().unwrap_or_default(), + }, + entity_associations: g.entity_associations.clone(), + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), + }, + attributes: span_attributes, + }, + }) + } + } + GroupType::Event => { + // todo!() + } + GroupType::Metric => { + // Check if we extend another metric. + let is_refinement = g + .lineage + .as_ref() + .and_then(|l| l.extends_group.as_ref()) + .and_then(|parent| group_type_lookup.get(parent)) + .map(|t| *t == GroupType::Metric) + .unwrap_or(false); + println!( + "Group {} is_refinement: {}, extends: {:?}", + &g.id, is_refinement, extend_type + ); + let mut metric_attributes = Vec::new(); + for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { + if let Some(a) = v2_catalog.convert_ref(attr) { + metric_attributes.push(metric::MetricAttributeRef { + base: a, + requirement_level: attr.requirement_level.clone(), + }); + } else { + // TODO logic error! + } + } + // TODO - deal with unwrap errors. + let metric = Metric { + name: g.metric_name.clone().unwrap().into(), + instrument: g.instrument.clone().unwrap(), + unit: g.unit.clone().unwrap(), + attributes: metric_attributes, + entity_associations: g.entity_associations.clone(), + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), }, - }) + }; + if is_refinement { + metric_refinements.push(metric::MetricRefinement { + id: fix_group_id("metric.", &g.id), + metric, + }); + } else { + metrics.push(metric.clone()); + metric_refinements.push(metric::MetricRefinement { + id: metric.name.clone(), + metric, + }); + } + } + GroupType::Entity => { + // todo!() + } + GroupType::AttributeGroup + | GroupType::MetricGroup + | GroupType::Scope + | GroupType::Undefined => { + // Ignored for now, we should probably issue warnings. } } } @@ -166,6 +238,8 @@ pub fn convert_v1_to_v2( registry_url: r.registry_url, spans, span_refinements, + metrics, + metric_refinements, }; Ok((v2_catalog, v2_registry)) } diff --git a/crates/weaver_resolved_schema/src/v2/registry.rs b/crates/weaver_resolved_schema/src/v2/registry.rs index 0d45a3b45..8b4668f7e 100644 --- a/crates/weaver_resolved_schema/src/v2/registry.rs +++ b/crates/weaver_resolved_schema/src/v2/registry.rs @@ -3,19 +3,45 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::v2::span::{Span, SpanRefinement}; +use crate::v2::{ + metric::{Metric, MetricRefinement}, + span::{Span, SpanRefinement}, +}; /// A semantic convention registry. +/// +/// The semantic convention is composed of two major components: +/// +/// - Signals: Definitions of metrics, logs, etc. that will be sent over the wire (e.g. OTLP). +/// - Refinements: Specialization of a signal that can be used to optimise documentation, +/// or code generation. A refinement will *always* match the conventions defined by the +/// signal it refines. Refinements cannot be inferred from signals over the wire (e.g. OTLP). +/// This is because any identifying feature of a refinement is used purely for codgen but has +/// no storage location in OTLP. +/// +/// Note: Refinements will always include a "base" refinement for every signal definition. +/// For example, if a Metric signal named `my_metric` is defined, there will be +/// a metric refinement named `my_metric` as well. +/// This allows codegen to *only* interact with refinements, if desired, to +/// provide optimised methods for generating telemetry signals. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Registry { /// The semantic convention registry url. + /// + /// This is the base URL, under which this registry can be found. pub registry_url: String, - /// A list of span definitions. + /// A list of span signal definitions. pub spans: Vec, + /// A list of metric signal definitions. + pub metrics: Vec, + /// A list of span refinements. pub span_refinements: Vec, + + /// A list of metric refinements. + pub metric_refinements: Vec, // TODO - Signal types. } From c917f02db84a5d24a2acc8ea0c1069a0be86ee3c Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 9 Oct 2025 12:59:16 -0400 Subject: [PATCH 05/46] More documentation and test cleanup for spans/metrics. --- .../weaver_resolved_schema/src/v2/metric.rs | 18 ++- crates/weaver_resolved_schema/src/v2/mod.rs | 121 ++++++++++++++++++ crates/weaver_resolved_schema/src/v2/span.rs | 14 +- 3 files changed, 144 insertions(+), 9 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs index 70116880e..cf74dbd33 100644 --- a/crates/weaver_resolved_schema/src/v2/metric.rs +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -20,17 +20,19 @@ pub struct Metric { /// synchronous instrument types (counter, gauge, updowncounter and /// histogram). /// For more details: [Metrics semantic conventions - Instrument types](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-types). - /// Note: This field is required if type is metric. pub instrument: InstrumentSpec, /// The unit in which the metric is measured, which should adhere to the /// [guidelines](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-units). pub unit: String, - /// List of attributes that belong to the semantic convention. + /// List of attributes that should be included on this metric. #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. + /// + /// This list is an "any of" list, where a metric may be associated with one or more entities, but should + /// be associated with at least one in this list. #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub entity_associations: Vec, @@ -44,20 +46,28 @@ pub struct Metric { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct MetricAttributeRef { - /// Underlying attribute. + /// Reference, by index, to the attribute catalog. pub base: AttributeRef, /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to /// "conditionally_required", the string provided as MUST /// specify the conditions under which the attribute is required. + /// + /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will + /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present + /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. pub requirement_level: RequirementLevel, } /// A refinement of a metric signal, for use in code-gen or specific library application. +/// +/// A refinement represents a "view" of a Metric that is highly optimised for a particular implementation. +/// e.g. for HTTP metrics, there may be a refinement that provides only the necessary information for dealing with Java's HTTP +/// client library, and drops optional or extraneous information from the underlying http metric. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] pub struct MetricRefinement { - /// The identity of the refinement + /// The identity of the refinement. pub id: SignalId, // TODO - This is a lazy way of doing this. We use `type` to refer diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 2c8b1c310..50bcca02e 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -373,4 +373,125 @@ mod tests { vec!["my-span".to_owned(), "custom".to_owned()] ); } + + #[test] + fn test_convert_metric_v1_to_v2() { + let mut v1_catalog = crate::catalog::Catalog::from_attributes(vec![]); + let test_refs = v1_catalog.add_attributes([ + Attribute { + name: "test.key".to_owned(), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), + brief: "".to_owned(), + examples: None, + tag: None, + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + sampling_relevant: None, + note: "".to_string(), + stability: Some(Stability::Stable), + deprecated: None, + prefix: false, + tags: None, + annotations: None, + value: None, + role: None, + }, + Attribute { + name: "test.key".to_owned(), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), + brief: "".to_owned(), + examples: None, + tag: None, + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Recommended, + ), + sampling_relevant: Some(true), + note: "".to_string(), + stability: Some(Stability::Stable), + deprecated: None, + prefix: false, + tags: None, + annotations: None, + value: None, + role: None, + }, + ]); + let mut refinement_metric_lineage = GroupLineage::new(Provenance::new("tmp", "tmp")); + refinement_metric_lineage.extends("metric.http"); + let v1_registry = crate::registry::Registry { + registry_url: "my.schema.url".to_owned(), + groups: vec![ + Group { + id: "metric.http".to_owned(), + r#type: GroupType::Metric, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[0].clone()], + span_kind: None, + events: vec![], + metric_name: Some("http".to_owned()), + instrument: Some(weaver_semconv::group::InstrumentSpec::UpDownCounter), + unit: Some("s".to_owned()), + name: None, + lineage: None, + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + }, + Group { + id: "metric.http.custom".to_owned(), + r#type: GroupType::Metric, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[1].clone()], + span_kind: None, + events: vec![], + metric_name: Some("http".to_owned()), + instrument: Some(weaver_semconv::group::InstrumentSpec::UpDownCounter), + unit: Some("s".to_owned()), + name: None, + lineage: Some(refinement_metric_lineage), + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + }, + ], + }; + + let (v2_catalog, v2_registry) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); + // assert only ONE attribute due to sharing. + assert_eq!(v2_catalog.attributes().len(), 1); + // assert attribute fields not shared show up on ref in span. + assert_eq!(v2_registry.metrics.len(), 1); + if let Some(metric) = v2_registry.metrics.iter().next() { + assert_eq!(metric.name, "http".to_owned().into()); + // Make sure attribute ref carries sampling relevant. + } + // Assert we have two refinements (e.g. one real span, one refinement). + assert_eq!(v2_registry.metric_refinements.len(), 2); + let metric_ref_ids: Vec = v2_registry + .metric_refinements + .iter() + .map(|s| s.id.to_string()) + .collect(); + assert_eq!( + metric_ref_ids, + vec!["http".to_owned(), "http.custom".to_owned()] + ); + } } diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs index f29a84cf4..9bd4b04d4 100644 --- a/crates/weaver_resolved_schema/src/v2/span.rs +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -18,7 +18,6 @@ pub struct Span { /// of the "shape" of this span, and must be unique. pub r#type: SignalId, /// Specifies the kind of the span. - /// Note: only valid if type is span pub kind: SpanKindSpec, /// The name pattern for the span. pub name: SpanName, @@ -29,6 +28,9 @@ pub struct Span { pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. + /// + /// This list is an "any of" list, where a span may be associated with one or more entities, but should + /// be associated with at least one in this list. #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub entity_associations: Vec, @@ -38,12 +40,11 @@ pub struct Span { pub common: CommonFields, } -/// A special type of attribute reference that remembers if something -/// is sampling relevant. +/// A special type of reference to attributes that remembers span-specicific information. #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SpanAttributeRef { - /// Underlying attribute. + /// Reference, by index, to the attribute catalog. pub base: AttributeRef, /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, @@ -53,12 +54,15 @@ pub struct SpanAttributeRef { pub requirement_level: RequirementLevel, /// Specifies if the attribute is (especially) relevant for sampling /// and thus should be set at span start. It defaults to false. - /// Note: this field is experimental. #[serde(skip_serializing_if = "Option::is_none")] pub sampling_relevant: Option, } /// A refinement of a span, for use in code-gen or specific library application. +/// +/// A refinement represents a "view" of a Span that is highly optimised for a particular implementation. +/// e.g. for HTTP spans, there may be a refinement that provides only the necessary information for dealing with Java's HTTP +/// client library, and drops optional or extraneous information from the underlying http span. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] pub struct SpanRefinement { /// The identity of the refinement From 1baa3cdf1e54140a00b4b2adddaf036b77ba5eb1 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 9 Oct 2025 19:00:07 -0400 Subject: [PATCH 06/46] Finish implementation (not full testing) of v2 conversion for basics. --- .../weaver_resolved_schema/src/v2/entity.rs | 43 +++++++++ crates/weaver_resolved_schema/src/v2/event.rs | 69 ++++++++++++++ crates/weaver_resolved_schema/src/v2/mod.rs | 94 ++++++++++++++++++- .../weaver_resolved_schema/src/v2/registry.rs | 12 ++- 4 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 crates/weaver_resolved_schema/src/v2/entity.rs create mode 100644 crates/weaver_resolved_schema/src/v2/event.rs diff --git a/crates/weaver_resolved_schema/src/v2/entity.rs b/crates/weaver_resolved_schema/src/v2/entity.rs new file mode 100644 index 000000000..d7a7284d2 --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/entity.rs @@ -0,0 +1,43 @@ +//! Event related definition structs. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + v2::{signal_id::SignalId, CommonFields}, +}; + +use crate::v2::attribute::AttributeRef; + +/// The definition of an Entity signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Entity { + /// The type of the Entity. + pub r#type: SignalId, + + /// The attributes that make the identity of the Entity. + pub identity: Vec, + /// The attributes that make the description of the Entity. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub description: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of reference to attributes that remembers entity-specicific information. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct EntityAttributeRef { + /// Reference, by index, to the attribute catalog. + pub base: AttributeRef, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + pub requirement_level: RequirementLevel, +} diff --git a/crates/weaver_resolved_schema/src/v2/event.rs b/crates/weaver_resolved_schema/src/v2/event.rs new file mode 100644 index 000000000..be4cd250d --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/event.rs @@ -0,0 +1,69 @@ +//! Event related definition structs. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + v2::{signal_id::SignalId, CommonFields}, +}; + +use crate::v2::attribute::AttributeRef; + +/// The definition of an Event signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Event { + /// The name of the event. + pub name: SignalId, + + /// List of attributes that belong to this event. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + + // TODO - Should Entity Associations be "strong" links? + /// Which resources this span should be associated with. + /// + /// This list is an "any of" list, where a metric may be associated with one or more entities, but should + /// be associated with at least one in this list. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub entity_associations: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of reference to attributes that remembers event-specicific information. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct EventAttributeRef { + /// Reference, by index, to the attribute catalog. + pub base: AttributeRef, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + pub requirement_level: RequirementLevel, +} + +/// A refinement of an event, for use in code-gen or specific library application. +/// +/// A refinement represents a "view" of a Event that is highly optimised for a particular implementation. +/// e.g. for HTTP events, there may be a refinement that provides only the necessary information for dealing with Java's HTTP +/// client library, and drops optional or extraneous information from the underlying http event. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct EventRefinement { + /// The identity of the refinement + pub id: SignalId, + + // TODO - This is a lazy way of doing this. We use `name` to refer + // to the underlying span defintiion, but override all fields here. + // We probably should copy-paste all the "span" attributes here + // including the `ty` + /// The definition of the event refinement. + #[serde(flatten)] + pub event: Event, +} diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 50bcca02e..47de19e9a 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -8,12 +8,15 @@ use weaver_semconv::{ }; use crate::v2::{ + entity::Entity, metric::Metric, span::{Span, SpanRefinement}, }; pub mod attribute; pub mod catalog; +pub mod entity; +pub mod event; pub mod metric; pub mod registry; pub mod span; @@ -73,7 +76,9 @@ pub fn convert_v1_to_v2( let mut span_refinements = Vec::new(); let mut metrics = Vec::new(); let mut metric_refinements = Vec::new(); - + let mut events = Vec::new(); + let mut event_refinements = Vec::new(); + let mut entities = Vec::new(); for g in r.groups.iter() { let extend_type = g.extends.as_ref().and_then(|id| group_type_lookup.get(id)); match g.r#type { @@ -165,7 +170,51 @@ pub fn convert_v1_to_v2( } } GroupType::Event => { - // todo!() + let is_refinement = g + .lineage + .as_ref() + .and_then(|l| l.extends_group.as_ref()) + .and_then(|parent| group_type_lookup.get(parent)) + .map(|t| *t == GroupType::Event) + .unwrap_or(false); + let mut event_attributes = Vec::new(); + for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { + if let Some(a) = v2_catalog.convert_ref(attr) { + event_attributes.push(event::EventAttributeRef { + base: a, + requirement_level: attr.requirement_level.clone(), + }); + } else { + // TODO logic error! + } + } + let event = event::Event { + name: g.name.clone().unwrap().into(), + attributes: event_attributes, + entity_associations: g.entity_associations.clone(), + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), + }, + }; + if !is_refinement { + events.push(event.clone()); + event_refinements.push(event::EventRefinement { + id: event.name.clone(), + event, + }); + } else { + event_refinements.push(event::EventRefinement { + id: fix_group_id("event.", &g.id), + event, + }); + } } GroupType::Metric => { // Check if we extend another metric. @@ -223,7 +272,43 @@ pub fn convert_v1_to_v2( } } GroupType::Entity => { - // todo!() + let mut id_attrs = Vec::new(); + let mut desc_attrs = Vec::new(); + for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { + if let Some(a) = v2_catalog.convert_ref(attr) { + match attr.role { + Some(weaver_semconv::attribute::AttributeRole::Identifying) => { + id_attrs.push(entity::EntityAttributeRef { + base: a, + requirement_level: attr.requirement_level.clone(), + }); + } + _ => { + desc_attrs.push(entity::EntityAttributeRef { + base: a, + requirement_level: attr.requirement_level.clone(), + }); + } + } + } else { + // TODO logic error! + } + } + entities.push(Entity { + r#type: fix_group_id("entity.", &g.id), + identity: id_attrs, + description: desc_attrs, + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), + }, + }); } GroupType::AttributeGroup | GroupType::MetricGroup @@ -240,6 +325,9 @@ pub fn convert_v1_to_v2( span_refinements, metrics, metric_refinements, + events, + event_refinements, + entities, }; Ok((v2_catalog, v2_registry)) } diff --git a/crates/weaver_resolved_schema/src/v2/registry.rs b/crates/weaver_resolved_schema/src/v2/registry.rs index 8b4668f7e..65a94c1d6 100644 --- a/crates/weaver_resolved_schema/src/v2/registry.rs +++ b/crates/weaver_resolved_schema/src/v2/registry.rs @@ -4,6 +4,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::v2::{ + entity::Entity, + event::{Event, EventRefinement}, metric::{Metric, MetricRefinement}, span::{Span, SpanRefinement}, }; @@ -38,10 +40,18 @@ pub struct Registry { /// A list of metric signal definitions. pub metrics: Vec, + /// A list of event signal definitions. + pub events: Vec, + + /// A list of entity signal definitions. + pub entities: Vec, + /// A list of span refinements. pub span_refinements: Vec, /// A list of metric refinements. pub metric_refinements: Vec, - // TODO - Signal types. + + /// A list of event refinements. + pub event_refinements: Vec, } From a3bb434992542eb81a2bee54447f67dc59701b1d Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 10 Oct 2025 14:11:06 -0400 Subject: [PATCH 07/46] Hackily hook up the v2 resolution and template for output in weaver. --- crates/weaver_forge/src/lib.rs | 7 +- crates/weaver_forge/src/v2/attribute.rs | 31 +++++ crates/weaver_forge/src/v2/metric.rs | 81 +++++++++++ crates/weaver_forge/src/v2/mod.rs | 5 + crates/weaver_forge/src/v2/registry.rs | 142 ++++++++++++++++++++ crates/weaver_resolved_schema/src/lib.rs | 15 ++- crates/weaver_resolved_schema/src/v2/mod.rs | 35 +++-- src/registry/resolve.rs | 71 +++++++--- src/util.rs | 124 +++++++++++++++++ 9 files changed, 476 insertions(+), 35 deletions(-) create mode 100644 crates/weaver_forge/src/v2/attribute.rs create mode 100644 crates/weaver_forge/src/v2/metric.rs create mode 100644 crates/weaver_forge/src/v2/mod.rs create mode 100644 crates/weaver_forge/src/v2/registry.rs diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 78740020c..529a3fb95 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -42,6 +42,7 @@ mod filter; mod formats; pub mod jq; pub mod registry; +pub mod v2; /// Name of the Weaver configuration file. pub const WEAVER_YAML: &str = "weaver.yaml"; @@ -968,15 +969,15 @@ mod tests { assert!(diff_dir("expected_output/test", "observed_output/test").unwrap()); // TODO - Remove this. - let (cv2, rv2) = schema.create_v2_registry().unwrap(); + let schema = schema.create_v2_schema().unwrap(); fs::write( "observed_output/test/v2_registry.yaml", - serde_yaml::to_string(&rv2).unwrap(), + serde_yaml::to_string(&schema.registry).unwrap(), ) .unwrap(); fs::write( "observed_output/test/v2_catalog.yaml", - serde_yaml::to_string(&cv2).unwrap(), + serde_yaml::to_string(&schema.catalog).unwrap(), ) .unwrap(); } diff --git a/crates/weaver_forge/src/v2/attribute.rs b/crates/weaver_forge/src/v2/attribute.rs new file mode 100644 index 000000000..28d21d291 --- /dev/null +++ b/crates/weaver_forge/src/v2/attribute.rs @@ -0,0 +1,31 @@ +//! Attribute definitions for template schema. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::{AttributeType, Examples}, + v2::CommonFields, +}; + +/// The definition of an Attribute. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq, Hash, Eq)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct Attribute { + /// String that uniquely identifies the attribute. + pub key: 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, + /// Sequence of example values for the attribute or single example + /// value. They are required only for string and string array + /// attributes. Example values must be of the same type of the + /// attribute. If only a single example is provided, it can directly + /// be reported without encapsulating it into a sequence/dictionary. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub examples: Option, + /// Common fields (like brief, note, attributes). + #[serde(flatten)] + pub common: CommonFields, +} diff --git a/crates/weaver_forge/src/v2/metric.rs b/crates/weaver_forge/src/v2/metric.rs new file mode 100644 index 000000000..fab44572d --- /dev/null +++ b/crates/weaver_forge/src/v2/metric.rs @@ -0,0 +1,81 @@ +//! Metric related definitions structs. + +use crate::v2::attribute::Attribute; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + group::InstrumentSpec, + v2::{signal_id::SignalId, CommonFields}, +}; + +/// The definition of a metric signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Metric { + /// The name of the metric. + pub name: SignalId, + /// The instrument type that should be used to record the metric. Note that + /// the semantic conventions must be written using the names of the + /// synchronous instrument types (counter, gauge, updowncounter and + /// histogram). + /// For more details: [Metrics semantic conventions - Instrument types](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-types). + pub instrument: InstrumentSpec, + /// The unit in which the metric is measured, which should adhere to the + /// [guidelines](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-units). + pub unit: String, + /// List of attributes that should be included on this metric. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + // TODO - Should Entity Associations be "strong" links? + /// Which resources this span should be associated with. + /// + /// This list is an "any of" list, where a metric may be associated with one or more entities, but should + /// be associated with at least one in this list. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub entity_associations: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of reference to attributes that remembers metric-specicific information. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct MetricAttribute { + /// Base metric definitions. + #[serde(flatten)] + pub base: Attribute, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + /// + /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will + /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present + /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. + pub requirement_level: RequirementLevel, +} + +/// A refinement of a metric signal, for use in code-gen or specific library application. +/// +/// A refinement represents a "view" of a Metric that is highly optimised for a particular implementation. +/// e.g. for HTTP metrics, there may be a refinement that provides only the necessary information for dealing with Java's HTTP +/// client library, and drops optional or extraneous information from the underlying http metric. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct MetricRefinement { + /// The identity of the refinement. + pub id: SignalId, + + // TODO - This is a lazy way of doing this. We use `type` to refer + // to the underlying span defintiion, but override all fields here. + // We probably should copy-paste all the "span" attributes here + // including the `ty` + /// The definition of the metric refinement. + #[serde(flatten)] + pub metric: Metric, +} diff --git a/crates/weaver_forge/src/v2/mod.rs b/crates/weaver_forge/src/v2/mod.rs new file mode 100644 index 000000000..431c0482a --- /dev/null +++ b/crates/weaver_forge/src/v2/mod.rs @@ -0,0 +1,5 @@ +//! Version two of weaver model. + +pub mod attribute; +pub mod metric; +pub mod registry; diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs new file mode 100644 index 000000000..6aaaee2f2 --- /dev/null +++ b/crates/weaver_forge/src/v2/registry.rs @@ -0,0 +1,142 @@ +//! Version two of registry specification. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_resolved_schema::attribute::AttributeRef; + +use crate::{ + error::Error, + v2::{ + attribute::Attribute, + metric::{Metric, MetricAttribute, MetricRefinement}, + }, +}; + +/// A resolved semantic convention registry used in the context of the template and policy +/// engines. +/// +/// This includes all registrys fully fleshed out and ready for codegen. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ResolvedRegistry { + /// The semantic convention registry url. + #[serde(skip_serializing_if = "String::is_empty")] + pub registry_url: String, + // TODO - Attribute registry? + /// The signals defined in this registry. + pub signals: Signals, + /// The set of refinments defined in this registry. + pub refinements: Refinements, +} + +/// The set of all defined signals for a given semantic convention registry. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Signals { + /// The metric signals defined. + pub metrics: Vec, +} + +/// The set of all refinements for a semantic convention registry. +/// +/// A refinement is a specialization of a signal for a particular purpose, +/// e.g. creating a MySQL specific instance of a database span for the purpose +/// of codegeneration for MySQL. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Refinements { + /// The metric refinements defined. + pub metrics: Vec, +} + +impl ResolvedRegistry { + /// Create a new template registry from a resolved schema registry. + pub fn try_from_resolved_schema( + schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema, + ) -> Result { + let mut errors = Vec::new(); + let mut metrics = Vec::new(); + for metric in schema.registry.metrics { + let attributes = metric + .attributes + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| MetricAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("metric.{}", &metric.name), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + metrics.push(Metric { + name: metric.name, + instrument: metric.instrument, + unit: metric.unit, + attributes, + entity_associations: metric.entity_associations, + common: metric.common, + }); + } + + let mut metric_refinements: Vec = Vec::new(); + for metric in schema.registry.metric_refinements { + let attributes = metric + .metric + .attributes + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| MetricAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("metric.{}", &metric.metric.name), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + metric_refinements.push(MetricRefinement { + id: metric.id.clone(), + metric: Metric { + name: metric.metric.name, + instrument: metric.metric.instrument, + unit: metric.metric.unit, + attributes, + entity_associations: metric.metric.entity_associations, + common: metric.metric.common, + }, + }); + } + + if !errors.is_empty() { + return Err(Error::CompoundError(errors)); + } + + Ok(Self { + registry_url: schema.schema_url.clone(), + signals: Signals { metrics }, + refinements: Refinements { + metrics: metric_refinements, + }, + }) + } +} diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index 804642e24..b55390d28 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -111,10 +111,17 @@ impl ResolvedTelemetrySchema { // For testing for now. /// Convert this schema into V2 #[must_use] - pub fn create_v2_registry( - self, - ) -> Result<(v2::catalog::Catalog, v2::registry::Registry), error::Error> { - convert_v1_to_v2(self.catalog, self.registry) + pub fn create_v2_schema(self) -> Result { + convert_v1_to_v2(self.catalog, self.registry).map(|(c, r)| { + v2::ResolvedTelemetrySchema { + // TODO - bump file format version. + file_format: self.file_format, + schema_url: self.schema_url, + registry_id: self.registry_id, + catalog: c, + registry: r, + } + }) } #[cfg(test)] diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 47de19e9a..dbcfcbcb9 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -2,14 +2,18 @@ use std::collections::{HashMap, HashSet}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use weaver_semconv::{ group::GroupType, v2::{signal_id::SignalId, span::SpanName, CommonFields}, }; use crate::v2::{ + catalog::Catalog, entity::Entity, metric::Metric, + registry::Registry, span::{Span, SpanRefinement}, }; @@ -21,6 +25,26 @@ pub mod metric; pub mod registry; pub mod span; +/// A Resolved Telemetry Schema. +/// A Resolved Telemetry Schema is self-contained and doesn't contain any +/// external references to other schemas or semantic conventions. +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ResolvedTelemetrySchema { + /// Version of the file structure. + pub file_format: String, + /// Schema URL that this file is published at. + pub schema_url: String, + /// The ID of the registry that this schema belongs to. + pub registry_id: String, + /// The registry that this schema belongs to. + pub registry: Registry, + /// Catalog of unique items that are shared across multiple registries + /// and signals. + pub catalog: Catalog, + // TODO - versions, dependencies and other options. +} + fn fix_group_id(prefix: &'static str, group_id: &str) -> SignalId { if group_id.starts_with(prefix) { group_id.trim_start_matches(prefix).to_owned().into() @@ -37,7 +61,7 @@ fn fix_span_group_id(group_id: &str) -> SignalId { pub fn convert_v1_to_v2( c: crate::catalog::Catalog, r: crate::registry::Registry, -) -> Result<(catalog::Catalog, registry::Registry), crate::error::Error> { +) -> Result<(Catalog, Registry), crate::error::Error> { // When pulling attributes, as we collapse things, we need to filter // to just unique. let attributes: HashSet = c @@ -63,12 +87,11 @@ pub fn convert_v1_to_v2( }) .collect(); - let v2_catalog = catalog::Catalog::from_attributes(attributes.into_iter().collect()); + let v2_catalog = Catalog::from_attributes(attributes.into_iter().collect()); // Create a lookup so we can check inheritance. let mut group_type_lookup = HashMap::new(); for g in r.groups.iter() { - println!("Group {} is type: {:?}", &g.id, g.r#type); let _ = group_type_lookup.insert(g.id.clone(), g.r#type.clone()); } // Pull signals from the registry and create a new span-focused registry. @@ -225,10 +248,6 @@ pub fn convert_v1_to_v2( .and_then(|parent| group_type_lookup.get(parent)) .map(|t| *t == GroupType::Metric) .unwrap_or(false); - println!( - "Group {} is_refinement: {}, extends: {:?}", - &g.id, is_refinement, extend_type - ); let mut metric_attributes = Vec::new(); for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { if let Some(a) = v2_catalog.convert_ref(attr) { @@ -319,7 +338,7 @@ pub fn convert_v1_to_v2( } } - let v2_registry = registry::Registry { + let v2_registry = Registry { registry_url: r.registry_url, spans, span_refinements, diff --git a/src/registry/resolve.rs b/src/registry/resolve.rs index 45f90516c..d7d9e9173 100644 --- a/src/registry/resolve.rs +++ b/src/registry/resolve.rs @@ -11,7 +11,7 @@ use weaver_common::diagnostic::DiagnosticMessages; use crate::format::{apply_format, Format}; use crate::registry::{PolicyArgs, RegistryArgs}; -use crate::util::prepare_main_registry; +use crate::util::{prepare_main_registry, prepare_main_registry_v2}; use crate::{DiagnosticArgs, ExitDirectives}; /// Parameters for the `registry resolve` sub-command @@ -39,6 +39,11 @@ pub struct RegistryResolveArgs { #[arg(short, long, default_value = "yaml")] format: Format, + // TODO - Figure out long term plan for verisons here. + /// Whether or not to output version 2 of the schema. + #[arg(long, default_value = "false")] + v2: bool, + /// Policy parameters #[command(flatten)] policy: PolicyArgs, @@ -54,25 +59,49 @@ pub(crate) fn command(args: &RegistryResolveArgs) -> Result Result<(weaver_forge::v2::registry::ResolvedRegistry, Option), DiagnosticMessages> { + let registry_path = ®istry_args.registry; + + let main_registry_repo = RegistryRepo::try_new("main", registry_path)?; + + // Load the semantic convention specs + let main_semconv_specs = load_semconv_specs(&main_registry_repo, registry_args.follow_symlinks) + .capture_non_fatal_errors(diag_msgs)?; + + // Optionally init policy engine + let mut policy_engine = if !policy_args.skip_policies { + // Create and hold all VirtualDirectory instances to keep them from being dropped + let policy_vdirs: Vec = policy_args + .policies + .iter() + .map(|path| { + VirtualDirectory::try_new(path).map_err(|e| { + DiagnosticMessages::from_error(weaver_common::Error::InvalidVirtualDirectory { + path: path.to_string(), + error: e.to_string(), + }) + }) + }) + .collect::>()?; + + // Extract paths from VirtualDirectory instances + let policy_paths: Vec = policy_vdirs + .iter() + .map(|vdir| vdir.path().to_owned()) + .collect(); + + Some(init_policy_engine( + &main_registry_repo, + &policy_paths, + policy_args.display_policy_coverage, + )?) + } else { + None + }; + + // Check pre-resolution policies + if let Some(engine) = policy_engine.as_ref() { + check_policy(engine, &main_semconv_specs) + .inspect(|_, violations| { + if let Some(violations) = violations { + log_success(format!( + "All `before_resolution` policies checked ({} violations found)", + violations.len() + )); + } else { + log_success("No `before_resolution` policy violation"); + } + }) + .capture_non_fatal_errors(diag_msgs)?; + } + + // Resolve the main registry + let mut main_registry = + SemConvRegistry::from_semconv_specs(&main_registry_repo, main_semconv_specs)?; + // Resolve the semantic convention specifications. + // If there are any resolution errors, they should be captured into the ongoing list of + // diagnostic messages and returned immediately because there is no point in continuing + // as the resolution is a prerequisite for the next stages. + let main_resolved_schema = + resolve_semconv_specs(&mut main_registry, registry_args.include_unreferenced) + .capture_non_fatal_errors(diag_msgs)?; + + // This creates the template/json friendly registry. + let main_resolved_registry = ResolvedRegistry::try_from_resolved_registry( + &main_resolved_schema.registry, + main_resolved_schema.catalog(), + ) + .combine_diag_msgs_with(diag_msgs)?; + + // Check post-resolution policies + if let Some(engine) = policy_engine.as_mut() { + check_policy_stage::( + engine, + PolicyStage::AfterResolution, + main_registry_repo.registry_path_repr(), + &main_resolved_registry, + &[], + ) + .inspect(|_, violations| { + if let Some(violations) = violations { + log_success(format!( + "All `after_resolution` policies checked ({} violations found)", + violations.len() + )); + } else { + log_success("No `after_resolution` policy violation"); + } + }) + .capture_non_fatal_errors(diag_msgs)?; + } + + // TODO - fix error passing here. + let v2_schema = main_resolved_schema.create_v2_schema().unwrap(); + let v2_resolved_registry = + weaver_forge::v2::registry::ResolvedRegistry::try_from_resolved_schema(v2_schema)?; + Ok((v2_resolved_registry, policy_engine)) +} \ No newline at end of file From 4a9f6bfaf7dcbb86189bf2ed986e8d8543f8e321 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 10 Oct 2025 17:09:37 -0400 Subject: [PATCH 08/46] Add spans to resolved registry. --- crates/weaver_forge/src/v2/mod.rs | 1 + crates/weaver_forge/src/v2/registry.rs | 82 +++++++++++++++++++++++++- crates/weaver_forge/src/v2/span.rs | 81 +++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 crates/weaver_forge/src/v2/span.rs diff --git a/crates/weaver_forge/src/v2/mod.rs b/crates/weaver_forge/src/v2/mod.rs index 431c0482a..ccee71970 100644 --- a/crates/weaver_forge/src/v2/mod.rs +++ b/crates/weaver_forge/src/v2/mod.rs @@ -3,3 +3,4 @@ pub mod attribute; pub mod metric; pub mod registry; +pub mod span; diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 6aaaee2f2..e1f6bea7f 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -8,7 +8,7 @@ use crate::{ error::Error, v2::{ attribute::Attribute, - metric::{Metric, MetricAttribute, MetricRefinement}, + metric::{Metric, MetricAttribute, MetricRefinement}, span::{Span, SpanAttribute, SpanRefinement}, }, }; @@ -35,6 +35,8 @@ pub struct ResolvedRegistry { pub struct Signals { /// The metric signals defined. pub metrics: Vec, + /// The span signals defined. + pub spans: Vec, } /// The set of all refinements for a semantic convention registry. @@ -47,6 +49,8 @@ pub struct Signals { pub struct Refinements { /// The metric refinements defined. pub metrics: Vec, + /// The span refinements defined. + pub spans: Vec, } impl ResolvedRegistry { @@ -127,15 +131,89 @@ impl ResolvedRegistry { }); } + let mut spans = Vec::new(); + for span in schema.registry.spans { + let attributes = span + .attributes + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| SpanAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + sampling_relevant: ar.sampling_relevant.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("span.{}", &span.r#type), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + spans.push(Span { + r#type: span.r#type, + kind: span.kind, + name: span.name, + attributes, + entity_associations: span.entity_associations, + common: span.common, + }); + } + let mut span_refinements = Vec::new(); + for span in schema.registry.span_refinements { + let attributes = span + .span + .attributes + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| SpanAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + sampling_relevant: ar.sampling_relevant.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("span.{}", &span.id), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + span_refinements.push( + SpanRefinement { + id: span.id, + span: Span { + r#type: span.span.r#type, + kind: span.span.kind, + name: span.span.name, + attributes, + entity_associations: span.span.entity_associations, + common: span.span.common, + } + }); + } if !errors.is_empty() { return Err(Error::CompoundError(errors)); } Ok(Self { registry_url: schema.schema_url.clone(), - signals: Signals { metrics }, + signals: Signals { metrics, spans }, refinements: Refinements { metrics: metric_refinements, + spans: span_refinements, }, }) } diff --git a/crates/weaver_forge/src/v2/span.rs b/crates/weaver_forge/src/v2/span.rs new file mode 100644 index 000000000..3517fc795 --- /dev/null +++ b/crates/weaver_forge/src/v2/span.rs @@ -0,0 +1,81 @@ +//! Span related definitions structs. + +use crate::v2::attribute::Attribute; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + group::SpanKindSpec, + v2::{signal_id::SignalId, span::SpanName, CommonFields}, +}; + +/// The definition of a span signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Span { + /// The type of the Span. This denotes the identity + /// of the "shape" of this span, and must be unique. + pub r#type: SignalId, + /// Specifies the kind of the span. + pub kind: SpanKindSpec, + /// The name pattern for the span. + pub name: SpanName, + /// List of attributes that should be included on this span. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Which resources this span should be associated with. + /// + /// This list is an "any of" list, where a span may be associated with one or more entities, but should + /// be associated with at least one in this list. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub entity_associations: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of reference to attributes that remembers span-specicific information. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SpanAttribute { + /// Base attribute definitions. + #[serde(flatten)] + pub base: Attribute, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + /// + /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will + /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present + /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. + pub requirement_level: RequirementLevel, + + /// Specifies if the attribute is (especially) relevant for sampling + /// and thus should be set at span start. It defaults to false. + #[serde(skip_serializing_if = "Option::is_none")] + pub sampling_relevant: Option, +} + +/// A refinement of a span signal, for use in code-gen or specific library application. +/// +/// A refinement represents a "view" of a Span that is highly optimised for a particular implementation. +/// e.g. for HTTP spans, there may be a refinement that provides only the necessary information for dealing with Java's HTTP +/// client library, and drops optional or extraneous information from the underlying http span. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct SpanRefinement { + /// The identity of the refinement. + pub id: SignalId, + + // TODO - This is a lazy way of doing this. We use `type` to refer + // to the underlying span defintiion, but override all fields here. + // We probably should copy-paste all the "span" attributes here + // including the `ty` + /// The definition of the metric refinement. + #[serde(flatten)] + pub span: Span, +} \ No newline at end of file From edbe850cfd98b41a8a8c07fb640a2b84654f6af3 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 10 Oct 2025 17:54:58 -0400 Subject: [PATCH 09/46] Add the remaining missing pieces to V2 resolved schema. --- crates/weaver_forge/src/v2/entity.rs | 51 +++++ crates/weaver_forge/src/v2/event.rs | 73 +++++++ crates/weaver_forge/src/v2/mod.rs | 2 + crates/weaver_forge/src/v2/registry.rs | 190 ++++++++++++++++-- crates/weaver_forge/src/v2/span.rs | 2 +- .../weaver_resolved_schema/src/v2/catalog.rs | 4 +- src/util.rs | 8 +- 7 files changed, 307 insertions(+), 23 deletions(-) create mode 100644 crates/weaver_forge/src/v2/entity.rs create mode 100644 crates/weaver_forge/src/v2/event.rs diff --git a/crates/weaver_forge/src/v2/entity.rs b/crates/weaver_forge/src/v2/entity.rs new file mode 100644 index 000000000..8902badab --- /dev/null +++ b/crates/weaver_forge/src/v2/entity.rs @@ -0,0 +1,51 @@ +//! Event related definitions structs. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + v2::{signal_id::SignalId, CommonFields}, +}; + +use crate::v2::attribute::Attribute; + +/// The definition of an event signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Entity { + /// The type of the entity. + pub r#type: SignalId, + + /// List of attributes that belong to this event. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub identity: Vec, + + /// List of attributes that belong to this event. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub description: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of reference to attributes that remembers event-specicific information. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct EntityAttribute { + /// Base attribute definitions. + #[serde(flatten)] + pub base: Attribute, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + /// + /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will + /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present + /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. + pub requirement_level: RequirementLevel, +} diff --git a/crates/weaver_forge/src/v2/event.rs b/crates/weaver_forge/src/v2/event.rs new file mode 100644 index 000000000..7de2a1033 --- /dev/null +++ b/crates/weaver_forge/src/v2/event.rs @@ -0,0 +1,73 @@ +//! Event related definitions structs. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::{ + attribute::RequirementLevel, + v2::{signal_id::SignalId, CommonFields}, +}; + +use crate::v2::attribute::Attribute; + +/// The definition of an event signal. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Event { + /// The name of the event. + pub name: SignalId, + + /// List of attributes that belong to this event. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + + /// Which resources this span should be associated with. + /// + /// This list is an "any of" list, where a metric may be associated with one or more entities, but should + /// be associated with at least one in this list. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub entity_associations: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A special type of reference to attributes that remembers event-specicific information. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct EventAttribute { + /// Base attribute definitions. + #[serde(flatten)] + pub base: Attribute, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + /// + /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will + /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present + /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. + pub requirement_level: RequirementLevel, +} + +/// A refinement of an event signal, for use in code-gen or specific library application. +/// +/// A refinement represents a "view" of an Event that is highly optimised for a particular implementation. +/// e.g. for HTTP events, there may be a refinement that provides only the necessary information for dealing with Java's HTTP +/// client library, and drops optional or extraneous information from the underlying http event. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct EventRefinement { + /// The identity of the refinement. + pub id: SignalId, + + // TODO - This is a lazy way of doing this. We use `type` to refer + // to the underlying span defintiion, but override all fields here. + // We probably should copy-paste all the "span" attributes here + // including the `ty` + /// The definition of the metric refinement. + #[serde(flatten)] + pub event: Event, +} diff --git a/crates/weaver_forge/src/v2/mod.rs b/crates/weaver_forge/src/v2/mod.rs index ccee71970..0a1ae2f5c 100644 --- a/crates/weaver_forge/src/v2/mod.rs +++ b/crates/weaver_forge/src/v2/mod.rs @@ -1,6 +1,8 @@ //! Version two of weaver model. pub mod attribute; +pub mod entity; +pub mod event; pub mod metric; pub mod registry; pub mod span; diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index e1f6bea7f..02affc1ca 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -8,7 +8,10 @@ use crate::{ error::Error, v2::{ attribute::Attribute, - metric::{Metric, MetricAttribute, MetricRefinement}, span::{Span, SpanAttribute, SpanRefinement}, + entity::{Entity, EntityAttribute}, + event::{Event, EventAttribute, EventRefinement}, + metric::{Metric, MetricAttribute, MetricRefinement}, + span::{Span, SpanAttribute, SpanRefinement}, }, }; @@ -22,7 +25,8 @@ pub struct ResolvedRegistry { /// The semantic convention registry url. #[serde(skip_serializing_if = "String::is_empty")] pub registry_url: String, - // TODO - Attribute registry? + /// The raw attributes in this registry. + pub attributes: Vec, /// The signals defined in this registry. pub signals: Signals, /// The set of refinments defined in this registry. @@ -37,6 +41,10 @@ pub struct Signals { pub metrics: Vec, /// The span signals defined. pub spans: Vec, + /// The event signals defined. + pub events: Vec, + /// The entity signals defined. + pub entities: Vec, } /// The set of all refinements for a semantic convention registry. @@ -51,6 +59,8 @@ pub struct Refinements { pub metrics: Vec, /// The span refinements defined. pub spans: Vec, + /// The event refinements defined. + pub events: Vec, } impl ResolvedRegistry { @@ -59,6 +69,18 @@ impl ResolvedRegistry { schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema, ) -> Result { let mut errors = Vec::new(); + let mut attributes: Vec = + schema.catalog.attributes() + .iter() + .map(|a| Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }) + .collect(); + attributes.sort_by(|l,r| l.key.cmp(&r.key)); + let mut metrics = Vec::new(); for metric in schema.registry.metrics { let attributes = metric @@ -92,6 +114,7 @@ impl ResolvedRegistry { common: metric.common, }); } + metrics.sort_by(|l,r| l.name.cmp(&r.name)); let mut metric_refinements: Vec = Vec::new(); for metric in schema.registry.metric_refinements { @@ -130,6 +153,7 @@ impl ResolvedRegistry { }, }); } + metric_refinements.sort_by(|l,r| l.id.cmp(&r.id)); let mut spans = Vec::new(); for span in schema.registry.spans { @@ -165,6 +189,7 @@ impl ResolvedRegistry { common: span.common, }); } + spans.sort_by(|l,r| l.r#type.cmp(&r.r#type)); let mut span_refinements = Vec::new(); for span in schema.registry.span_refinements { let attributes = span @@ -191,29 +216,164 @@ impl ResolvedRegistry { attr }) .collect(); - span_refinements.push( - SpanRefinement { - id: span.id, - span: Span { - r#type: span.span.r#type, - kind: span.span.kind, - name: span.span.name, - attributes, - entity_associations: span.span.entity_associations, - common: span.span.common, - } - }); + span_refinements.push(SpanRefinement { + id: span.id, + span: Span { + r#type: span.span.r#type, + kind: span.span.kind, + name: span.span.name, + attributes, + entity_associations: span.span.entity_associations, + common: span.span.common, + }, + }); + } + span_refinements.sort_by(|l,r| l.id.cmp(&r.id)); + + let mut events = Vec::new(); + for event in schema.registry.events { + let attributes = event + .attributes + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| EventAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("event.{}", &event.name), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + events.push(Event { + name: event.name, + attributes: attributes, + entity_associations: event.entity_associations, + common: event.common, + }); + } + events.sort_by(|l,r| l.name.cmp(&r.name)); + + // convert event refinements. + let mut event_refinements = Vec::new(); + for event in schema.registry.event_refinements { + let attributes = event + .event + .attributes + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| EventAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("event.{}", &event.id), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + event_refinements.push(EventRefinement { + id: event.id, + event: Event { + name: event.event.name, + attributes: attributes, + entity_associations: event.event.entity_associations, + common: event.event.common, + }, + }); + } + event_refinements.sort_by(|l,r| l.id.cmp(&r.id)); + + let mut entities = Vec::new(); + for e in schema.registry.entities { + let identity = e + .identity + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| EntityAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("entity.{}", &e.r#type), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + + let description = e + .description + .iter() + .filter_map(|ar| { + let attr = schema.catalog.attribute(&ar.base).map(|a| EntityAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("entity.{}", &e.r#type), + attr_ref: AttributeRef(ar.base.0), + }); + } + attr + }) + .collect(); + entities.push(Entity { + r#type: e.r#type, + identity, + description, + common: e.common, + }); } + entities.sort_by(|l,r| l.r#type.cmp(&r.r#type)); + if !errors.is_empty() { return Err(Error::CompoundError(errors)); } Ok(Self { registry_url: schema.schema_url.clone(), - signals: Signals { metrics, spans }, + attributes, + signals: Signals { + metrics, + spans, + events, + entities, + }, refinements: Refinements { metrics: metric_refinements, spans: span_refinements, + events: event_refinements, }, }) } diff --git a/crates/weaver_forge/src/v2/span.rs b/crates/weaver_forge/src/v2/span.rs index 3517fc795..090139e3f 100644 --- a/crates/weaver_forge/src/v2/span.rs +++ b/crates/weaver_forge/src/v2/span.rs @@ -78,4 +78,4 @@ pub struct SpanRefinement { /// The definition of the metric refinement. #[serde(flatten)] pub span: Span, -} \ No newline at end of file +} diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index aaae40718..d1bd56124 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -25,8 +25,8 @@ impl Catalog { Self { attributes } } - #[cfg(test)] - pub(crate) fn attributes(&self) -> &Vec { + /// Lists all the attributes in the registry. + pub fn attributes(&self) -> &Vec { &self.attributes } diff --git a/src/util.rs b/src/util.rs index 88d11d592..428c790ca 100644 --- a/src/util.rs +++ b/src/util.rs @@ -346,8 +346,6 @@ pub(crate) fn prepare_main_registry( Ok((main_resolved_registry, policy_engine)) } - - /// Resolves the main registry and optionally checks policies. /// This is a common starting point for some `registry` commands. /// e.g., `check`, `generate`, `resolve` @@ -464,7 +462,7 @@ pub(crate) fn prepare_main_registry_v2( // TODO - fix error passing here. let v2_schema = main_resolved_schema.create_v2_schema().unwrap(); - let v2_resolved_registry = - weaver_forge::v2::registry::ResolvedRegistry::try_from_resolved_schema(v2_schema)?; + let v2_resolved_registry = + weaver_forge::v2::registry::ResolvedRegistry::try_from_resolved_schema(v2_schema)?; Ok((v2_resolved_registry, policy_engine)) -} \ No newline at end of file +} From 209883f760734ca26a7f0632f176c2bb0d9a82dc Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 20 Oct 2025 15:25:17 -0400 Subject: [PATCH 10/46] Fix all tests. --- .../expected-registry.json | 1 + .../data/registry-test-3-extends/expected-registry.json | 9 +++++++++ .../data/registry-test-7-spans/expected-registry.json | 9 +++++++++ .../data/registry-test-8-http/expected-registry.json | 3 +++ .../expected-registry.json | 1 + .../data/registry-test-lineage-1/expected-registry.json | 2 ++ .../data/registry-test-lineage-2/expected-registry.json | 2 ++ 7 files changed, 27 insertions(+) diff --git a/crates/weaver_resolver/data/registry-test-11-prefix-refs-extends/expected-registry.json b/crates/weaver_resolver/data/registry-test-11-prefix-refs-extends/expected-registry.json index f97800f7a..b1b613ff0 100644 --- a/crates/weaver_resolver/data/registry-test-11-prefix-refs-extends/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-11-prefix-refs-extends/expected-registry.json @@ -129,6 +129,7 @@ "registry_id": "default", "path": "data/registry-test-11-prefix-refs-extends/registry/usage2.yaml" }, + "extends_group": "usage", "attributes": { "client.geo.lat": { "source_group": "registry.client", diff --git a/crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json b/crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json index 4e2ad6bb2..6644307bb 100644 --- a/crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json @@ -100,6 +100,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/http-common.yaml" }, + "extends_group": "attributes.http.common", "attributes": { "error.type": { "source_group": "registry.error", @@ -220,6 +221,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/http-common.yaml" }, + "extends_group": "attributes.http.common", "attributes": { "error.type": { "source_group": "registry.error", @@ -443,6 +445,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/metrics-messaging.yaml" }, + "extends_group": "messaging.attributes.common", "attributes": { "error.type": { "source_group": "registry.error", @@ -565,6 +568,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/metrics-messaging.yaml" }, + "extends_group": "metric.messaging.attributes", "attributes": { "error.type": { "source_group": "registry.error", @@ -687,6 +691,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/metrics-messaging.yaml" }, + "extends_group": "metric.messaging.attributes", "attributes": { "error.type": { "source_group": "registry.error", @@ -809,6 +814,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/metrics-messaging.yaml" }, + "extends_group": "metric.messaging.attributes", "attributes": { "error.type": { "source_group": "registry.error", @@ -931,6 +937,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/metrics-messaging.yaml" }, + "extends_group": "metric.messaging.attributes", "attributes": { "error.type": { "source_group": "registry.error", @@ -1053,6 +1060,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/metrics-messaging.yaml" }, + "extends_group": "metric.messaging.attributes", "attributes": { "error.type": { "source_group": "registry.error", @@ -1175,6 +1183,7 @@ "registry_id": "default", "path": "data/registry-test-3-extends/registry/metrics-messaging.yaml" }, + "extends_group": "metric.messaging.attributes", "attributes": { "error.type": { "source_group": "registry.error", diff --git a/crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json b/crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json index 2c89fc942..a1be14868 100644 --- a/crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json @@ -381,6 +381,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", @@ -613,6 +614,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.cassandra.consistency_level": { "source_group": "registry.db", @@ -914,6 +916,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", @@ -1126,6 +1129,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", @@ -1339,6 +1343,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", @@ -1565,6 +1570,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", @@ -1795,6 +1801,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", @@ -2073,6 +2080,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", @@ -2307,6 +2315,7 @@ "registry_id": "default", "path": "data/registry-test-7-spans/registry/trace-database.yaml" }, + "extends_group": "db", "attributes": { "db.connection_string": { "source_group": "registry.db", diff --git a/crates/weaver_resolver/data/registry-test-8-http/expected-registry.json b/crates/weaver_resolver/data/registry-test-8-http/expected-registry.json index 3ed49a878..a9cd3fac4 100644 --- a/crates/weaver_resolver/data/registry-test-8-http/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-8-http/expected-registry.json @@ -42,6 +42,7 @@ "registry_id": "default", "path": "data/registry-test-8-http/registry/http-common.yaml" }, + "extends_group": "attributes.http.common", "attributes": { "network.protocol.name": { "source_group": "registry.network", @@ -83,6 +84,7 @@ "registry_id": "default", "path": "data/registry-test-8-http/registry/http.yaml" }, + "extends_group": "attributes.http.server", "attributes": { "network.protocol.name": { "source_group": "registry.network", @@ -128,6 +130,7 @@ "registry_id": "default", "path": "data/registry-test-8-http/registry/http.yaml" }, + "extends_group": "metric_attributes.http.server", "attributes": { "network.protocol.name": { "source_group": "registry.network", diff --git a/crates/weaver_resolver/data/registry-test-9-metric-extends/expected-registry.json b/crates/weaver_resolver/data/registry-test-9-metric-extends/expected-registry.json index 00b25cca7..ab84d8316 100644 --- a/crates/weaver_resolver/data/registry-test-9-metric-extends/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-9-metric-extends/expected-registry.json @@ -33,6 +33,7 @@ "registry_id": "default", "path": "data/registry-test-9-metric-extends/registry/jvm-metrics.yaml" }, + "extends_group": "attributes.jvm.memory", "attributes": { "jvm.memory.pool.name": { "source_group": "attributes.jvm.memory", diff --git a/crates/weaver_resolver/data/registry-test-lineage-1/expected-registry.json b/crates/weaver_resolver/data/registry-test-lineage-1/expected-registry.json index 690eda330..f2f7e01ea 100644 --- a/crates/weaver_resolver/data/registry-test-lineage-1/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-lineage-1/expected-registry.json @@ -13,6 +13,7 @@ "registry_id": "default", "path": "data/registry-test-lineage-1/registry/groups.yaml" }, + "extends_group": "intermediate.level", "attributes": { "server.port": { "source_group": "intermediate.level", @@ -39,6 +40,7 @@ "registry_id": "default", "path": "data/registry-test-lineage-1/registry/groups.yaml" }, + "extends_group": "base.level", "attributes": { "server.port": { "source_group": "base.level", diff --git a/crates/weaver_resolver/data/registry-test-lineage-2/expected-registry.json b/crates/weaver_resolver/data/registry-test-lineage-2/expected-registry.json index d117280f2..70d6dd38a 100644 --- a/crates/weaver_resolver/data/registry-test-lineage-2/expected-registry.json +++ b/crates/weaver_resolver/data/registry-test-lineage-2/expected-registry.json @@ -44,6 +44,7 @@ "registry_id": "default", "path": "data/registry-test-lineage-2/registry/groups.yaml" }, + "extends_group": "base.level", "attributes": { "server.port": { "source_group": "base.level", @@ -76,6 +77,7 @@ "registry_id": "default", "path": "data/registry-test-lineage-2/registry/groups.yaml" }, + "extends_group": "intermediate.level", "attributes": { "network.protocol.name": { "source_group": "registry.xyz", From e02235ce53dd79a73f69147874d306c98250c42d Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 09:14:36 -0400 Subject: [PATCH 11/46] Start of refactoring of 'regisry' vs. 'catalog' vs. 'refinements' --- crates/weaver_forge/src/lib.rs | 7 +- crates/weaver_forge/src/v2/registry.rs | 119 ++++++++++-------- crates/weaver_resolved_schema/src/lib.rs | 16 --- .../src/v2/attribute.rs | 4 +- crates/weaver_resolved_schema/src/v2/mod.rs | 57 ++++++--- .../src/v2/refinements.rs | 32 +++++ .../weaver_resolved_schema/src/v2/registry.rs | 49 ++++---- src/util.rs | 3 +- 8 files changed, 169 insertions(+), 118 deletions(-) create mode 100644 crates/weaver_resolved_schema/src/v2/refinements.rs diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 529a3fb95..119745f71 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -969,15 +969,16 @@ mod tests { assert!(diff_dir("expected_output/test", "observed_output/test").unwrap()); // TODO - Remove this. - let schema = schema.create_v2_schema().unwrap(); + let schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema = + schema.try_into().unwrap(); fs::write( "observed_output/test/v2_registry.yaml", serde_yaml::to_string(&schema.registry).unwrap(), ) .unwrap(); fs::write( - "observed_output/test/v2_catalog.yaml", - serde_yaml::to_string(&schema.catalog).unwrap(), + "observed_output/test/v2_refinements.yaml", + serde_yaml::to_string(&schema.refinements).unwrap(), ) .unwrap(); } diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 02affc1ca..7d1210b1b 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -69,8 +69,9 @@ impl ResolvedRegistry { schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema, ) -> Result { let mut errors = Vec::new(); - let mut attributes: Vec = - schema.catalog.attributes() + let mut attributes: Vec = schema + .registry + .attributes .iter() .map(|a| Attribute { key: a.key.clone(), @@ -79,7 +80,7 @@ impl ResolvedRegistry { common: a.common.clone(), }) .collect(); - attributes.sort_by(|l,r| l.key.cmp(&r.key)); + attributes.sort_by(|l, r| l.key.cmp(&r.key)); let mut metrics = Vec::new(); for metric in schema.registry.metrics { @@ -87,15 +88,18 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| MetricAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = schema + .registry + .attribute(&ar.base) + .map(|a| MetricAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("metric.{}", &metric.name), @@ -114,24 +118,27 @@ impl ResolvedRegistry { common: metric.common, }); } - metrics.sort_by(|l,r| l.name.cmp(&r.name)); + metrics.sort_by(|l, r| l.name.cmp(&r.name)); let mut metric_refinements: Vec = Vec::new(); - for metric in schema.registry.metric_refinements { + for metric in schema.refinements.metrics { let attributes = metric .metric .attributes .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| MetricAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = schema + .registry + .attribute(&ar.base) + .map(|a| MetricAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("metric.{}", &metric.metric.name), @@ -153,7 +160,7 @@ impl ResolvedRegistry { }, }); } - metric_refinements.sort_by(|l,r| l.id.cmp(&r.id)); + metric_refinements.sort_by(|l, r| l.id.cmp(&r.id)); let mut spans = Vec::new(); for span in schema.registry.spans { @@ -161,7 +168,7 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| SpanAttribute { + let attr = schema.registry.attribute(&ar.base).map(|a| SpanAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -189,15 +196,15 @@ impl ResolvedRegistry { common: span.common, }); } - spans.sort_by(|l,r| l.r#type.cmp(&r.r#type)); + spans.sort_by(|l, r| l.r#type.cmp(&r.r#type)); let mut span_refinements = Vec::new(); - for span in schema.registry.span_refinements { + for span in schema.refinements.spans { let attributes = span .span .attributes .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| SpanAttribute { + let attr = schema.registry.attribute(&ar.base).map(|a| SpanAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -228,7 +235,7 @@ impl ResolvedRegistry { }, }); } - span_refinements.sort_by(|l,r| l.id.cmp(&r.id)); + span_refinements.sort_by(|l, r| l.id.cmp(&r.id)); let mut events = Vec::new(); for event in schema.registry.events { @@ -236,7 +243,7 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| EventAttribute { + let attr = schema.registry.attribute(&ar.base).map(|a| EventAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -261,17 +268,17 @@ impl ResolvedRegistry { common: event.common, }); } - events.sort_by(|l,r| l.name.cmp(&r.name)); + events.sort_by(|l, r| l.name.cmp(&r.name)); // convert event refinements. let mut event_refinements = Vec::new(); - for event in schema.registry.event_refinements { + for event in schema.refinements.events { let attributes = event .event .attributes .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| EventAttribute { + let attr = schema.registry.attribute(&ar.base).map(|a| EventAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -299,7 +306,7 @@ impl ResolvedRegistry { }, }); } - event_refinements.sort_by(|l,r| l.id.cmp(&r.id)); + event_refinements.sort_by(|l, r| l.id.cmp(&r.id)); let mut entities = Vec::new(); for e in schema.registry.entities { @@ -307,15 +314,18 @@ impl ResolvedRegistry { .identity .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| EntityAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = schema + .registry + .attribute(&ar.base) + .map(|a| EntityAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("entity.{}", &e.r#type), @@ -330,15 +340,18 @@ impl ResolvedRegistry { .description .iter() .filter_map(|ar| { - let attr = schema.catalog.attribute(&ar.base).map(|a| EntityAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = schema + .registry + .attribute(&ar.base) + .map(|a| EntityAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("entity.{}", &e.r#type), @@ -355,7 +368,7 @@ impl ResolvedRegistry { common: e.common, }); } - entities.sort_by(|l,r| l.r#type.cmp(&r.r#type)); + entities.sort_by(|l, r| l.r#type.cmp(&r.r#type)); if !errors.is_empty() { return Err(Error::CompoundError(errors)); diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index b55390d28..3960b664f 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -108,22 +108,6 @@ impl ResolvedTelemetrySchema { } } - // For testing for now. - /// Convert this schema into V2 - #[must_use] - pub fn create_v2_schema(self) -> Result { - convert_v1_to_v2(self.catalog, self.registry).map(|(c, r)| { - v2::ResolvedTelemetrySchema { - // TODO - bump file format version. - file_format: self.file_format, - schema_url: self.schema_url, - registry_id: self.registry_id, - catalog: c, - registry: r, - } - }) - } - #[cfg(test)] pub(crate) fn add_metric_group( &mut self, diff --git a/crates/weaver_resolved_schema/src/v2/attribute.rs b/crates/weaver_resolved_schema/src/v2/attribute.rs index 2436aada8..28373d6e0 100644 --- a/crates/weaver_resolved_schema/src/v2/attribute.rs +++ b/crates/weaver_resolved_schema/src/v2/attribute.rs @@ -1,11 +1,11 @@ //! Attribute definitions for resolved schema. -use std::fmt::Display; +use std::{collections::HashMap, fmt::Display}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use weaver_semconv::{ - attribute::{AttributeType, Examples}, + attribute::{self, AttributeType, Examples}, v2::CommonFields, }; diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index dbcfcbcb9..4d17ba758 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -13,6 +13,7 @@ use crate::v2::{ catalog::Catalog, entity::Entity, metric::Metric, + refinements::Refinements, registry::Registry, span::{Span, SpanRefinement}, }; @@ -22,6 +23,7 @@ pub mod catalog; pub mod entity; pub mod event; pub mod metric; +pub mod refinements; pub mod registry; pub mod span; @@ -39,12 +41,27 @@ pub struct ResolvedTelemetrySchema { pub registry_id: String, /// The registry that this schema belongs to. pub registry: Registry, - /// Catalog of unique items that are shared across multiple registries - /// and signals. - pub catalog: Catalog, + /// Refinements for the registry + pub refinements: Refinements, // TODO - versions, dependencies and other options. } +/// Easy conversion from v1 to v2. +impl TryFrom for ResolvedTelemetrySchema { + type Error = crate::error::Error; + fn try_from(value: crate::ResolvedTelemetrySchema) -> Result { + let (registry, refinements) = convert_v1_to_v2(value.catalog, value.registry)?; + Ok(ResolvedTelemetrySchema { + // TODO - bump file format? + file_format: value.file_format, + schema_url: value.schema_url, + registry_id: value.registry_id, + registry, + refinements, + }) + } +} + fn fix_group_id(prefix: &'static str, group_id: &str) -> SignalId { if group_id.starts_with(prefix) { group_id.trim_start_matches(prefix).to_owned().into() @@ -61,7 +78,7 @@ fn fix_span_group_id(group_id: &str) -> SignalId { pub fn convert_v1_to_v2( c: crate::catalog::Catalog, r: crate::registry::Registry, -) -> Result<(Catalog, Registry), crate::error::Error> { +) -> Result<(Registry, Refinements), crate::error::Error> { // When pulling attributes, as we collapse things, we need to filter // to just unique. let attributes: HashSet = c @@ -103,7 +120,6 @@ pub fn convert_v1_to_v2( let mut event_refinements = Vec::new(); let mut entities = Vec::new(); for g in r.groups.iter() { - let extend_type = g.extends.as_ref().and_then(|id| group_type_lookup.get(id)); match g.r#type { GroupType::Span => { // Check if we extend another span. @@ -340,15 +356,18 @@ pub fn convert_v1_to_v2( let v2_registry = Registry { registry_url: r.registry_url, + attributes: v2_catalog.attributes().clone(), spans, - span_refinements, metrics, - metric_refinements, events, - event_refinements, entities, }; - Ok((v2_catalog, v2_registry)) + let v2_refinements = Refinements { + spans: span_refinements, + metrics: metric_refinements, + events: event_refinements, + }; + Ok((v2_registry, v2_refinements)) } #[cfg(test)] @@ -459,9 +478,9 @@ mod tests { ], }; - let (v2_catalog, v2_registry) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); + let (v2_registry, v2_refinements) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); // assert only ONE attribute due to sharing. - assert_eq!(v2_catalog.attributes().len(), 1); + assert_eq!(v2_registry.attributes.len(), 1); // assert attribute fields not shared show up on ref in span. assert_eq!(v2_registry.spans.len(), 1); if let Some(span) = v2_registry.spans.iter().next() { @@ -469,9 +488,9 @@ mod tests { // Make sure attribute ref carries sampling relevant. } // Assert we have two refinements (e.g. one real span, one refinement). - assert_eq!(v2_registry.span_refinements.len(), 2); - let span_ref_ids: Vec = v2_registry - .span_refinements + assert_eq!(v2_refinements.spans.len(), 2); + let span_ref_ids: Vec = v2_refinements + .spans .iter() .map(|s| s.id.to_string()) .collect(); @@ -580,9 +599,9 @@ mod tests { ], }; - let (v2_catalog, v2_registry) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); + let (v2_registry, v2_refinements) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); // assert only ONE attribute due to sharing. - assert_eq!(v2_catalog.attributes().len(), 1); + assert_eq!(v2_registry.attributes.len(), 1); // assert attribute fields not shared show up on ref in span. assert_eq!(v2_registry.metrics.len(), 1); if let Some(metric) = v2_registry.metrics.iter().next() { @@ -590,9 +609,9 @@ mod tests { // Make sure attribute ref carries sampling relevant. } // Assert we have two refinements (e.g. one real span, one refinement). - assert_eq!(v2_registry.metric_refinements.len(), 2); - let metric_ref_ids: Vec = v2_registry - .metric_refinements + assert_eq!(v2_refinements.metrics.len(), 2); + let metric_ref_ids: Vec = v2_refinements + .metrics .iter() .map(|s| s.id.to_string()) .collect(); diff --git a/crates/weaver_resolved_schema/src/v2/refinements.rs b/crates/weaver_resolved_schema/src/v2/refinements.rs new file mode 100644 index 000000000..2bd006891 --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/refinements.rs @@ -0,0 +1,32 @@ +//! A semantic convention refinements. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v2::{event::EventRefinement, metric::MetricRefinement, span::SpanRefinement}; + +/// Semantic convention refinements. +/// +/// Refinements are a specialization of a signal that can be used to optimise documentation, +/// or code generation. A refinement will *always* match the conventions defined by the +/// signal it refines. Refinements cannot be inferred from signals over the wire (e.g. OTLP). +/// This is because any identifying feature of a refinement is used purely for codgen but has +/// no storage location in OTLP. +/// +/// Note: Refinements will always include a "base" refinement for every signal definition. +/// For example, if a Metric signal named `my_metric` is defined, there will be +/// a metric refinement named `my_metric` as well. +/// This allows codegen to *only* interact with refinements, if desired, to +/// provide optimised methods for generating telemetry signals. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Refinements { + /// A list of span refinements. + pub spans: Vec, + + /// A list of metric refinements. + pub metrics: Vec, + + /// A list of event refinements. + pub events: Vec, +} diff --git a/crates/weaver_resolved_schema/src/v2/registry.rs b/crates/weaver_resolved_schema/src/v2/registry.rs index 65a94c1d6..e902ea0d4 100644 --- a/crates/weaver_resolved_schema/src/v2/registry.rs +++ b/crates/weaver_resolved_schema/src/v2/registry.rs @@ -4,31 +4,25 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::v2::{ + attribute::{Attribute, AttributeRef}, entity::Entity, - event::{Event, EventRefinement}, - metric::{Metric, MetricRefinement}, - span::{Span, SpanRefinement}, + event::Event, + metric::Metric, + span::Span, }; /// A semantic convention registry. /// -/// The semantic convention is composed of two major components: +/// The semantic convention is composed of definitions of +/// attributes, metrics, logs, etc. that will be sent over the wire (e.g. OTLP). /// -/// - Signals: Definitions of metrics, logs, etc. that will be sent over the wire (e.g. OTLP). -/// - Refinements: Specialization of a signal that can be used to optimise documentation, -/// or code generation. A refinement will *always* match the conventions defined by the -/// signal it refines. Refinements cannot be inferred from signals over the wire (e.g. OTLP). -/// This is because any identifying feature of a refinement is used purely for codgen but has -/// no storage location in OTLP. -/// -/// Note: Refinements will always include a "base" refinement for every signal definition. -/// For example, if a Metric signal named `my_metric` is defined, there will be -/// a metric refinement named `my_metric` as well. -/// This allows codegen to *only* interact with refinements, if desired, to -/// provide optimised methods for generating telemetry signals. +/// Note: The registry does not include signal refinements. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Registry { + /// Catalog of attributes used in the schema. + pub attributes: Vec, + /// The semantic convention registry url. /// /// This is the base URL, under which this registry can be found. @@ -45,13 +39,20 @@ pub struct Registry { /// A list of entity signal definitions. pub entities: Vec, +} - /// A list of span refinements. - pub span_refinements: Vec, - - /// A list of metric refinements. - pub metric_refinements: Vec, - - /// A list of event refinements. - pub event_refinements: Vec, +impl Registry { + /// Returns the attribute from an attribute ref if it exists. + #[must_use] + pub fn attribute(&self, attribute_ref: &AttributeRef) -> Option<&Attribute> { + self.attributes.get(attribute_ref.0 as usize) + } + /// Returns the attribute name from an attribute ref if it exists + /// in the catalog or None if it does not exist. + #[must_use] + pub fn attribute_key(&self, attribute_ref: &AttributeRef) -> Option<&str> { + self.attributes + .get(attribute_ref.0 as usize) + .map(|attr| attr.key.as_ref()) + } } diff --git a/src/util.rs b/src/util.rs index 428c790ca..6dcdffd34 100644 --- a/src/util.rs +++ b/src/util.rs @@ -461,7 +461,8 @@ pub(crate) fn prepare_main_registry_v2( } // TODO - fix error passing here. - let v2_schema = main_resolved_schema.create_v2_schema().unwrap(); + let v2_schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema = + main_resolved_schema.try_into().unwrap(); let v2_resolved_registry = weaver_forge::v2::registry::ResolvedRegistry::try_from_resolved_schema(v2_schema)?; Ok((v2_resolved_registry, policy_engine)) From a99d72eef0e3613b85555735af14bd458738d9cb Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 09:26:50 -0400 Subject: [PATCH 12/46] Fix forge unwinding of registry. --- crates/weaver_forge/src/v2/registry.rs | 101 ++++++++++++------------- 1 file changed, 47 insertions(+), 54 deletions(-) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 7d1210b1b..0a5717899 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -69,6 +69,8 @@ impl ResolvedRegistry { schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema, ) -> Result { let mut errors = Vec::new(); + + // We create an attribute lookup map. let mut attributes: Vec = schema .registry .attributes @@ -80,26 +82,23 @@ impl ResolvedRegistry { common: a.common.clone(), }) .collect(); - attributes.sort_by(|l, r| l.key.cmp(&r.key)); - + let attribute_lookup = + |r: &weaver_resolved_schema::v2::attribute::AttributeRef| attributes.get(r.0 as usize); let mut metrics = Vec::new(); for metric in schema.registry.metrics { let attributes = metric .attributes .iter() .filter_map(|ar| { - let attr = schema - .registry - .attribute(&ar.base) - .map(|a| MetricAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = attribute_lookup(&ar.base).map(|a| MetricAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("metric.{}", &metric.name), @@ -127,18 +126,15 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema - .registry - .attribute(&ar.base) - .map(|a| MetricAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = attribute_lookup(&ar.base).map(|a| MetricAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("metric.{}", &metric.metric.name), @@ -168,7 +164,7 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema.registry.attribute(&ar.base).map(|a| SpanAttribute { + let attr = attribute_lookup(&ar.base).map(|a| SpanAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -204,7 +200,7 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema.registry.attribute(&ar.base).map(|a| SpanAttribute { + let attr = attribute_lookup(&ar.base).map(|a| SpanAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -243,7 +239,7 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema.registry.attribute(&ar.base).map(|a| EventAttribute { + let attr = attribute_lookup(&ar.base).map(|a| EventAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -278,7 +274,7 @@ impl ResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = schema.registry.attribute(&ar.base).map(|a| EventAttribute { + let attr = attribute_lookup(&ar.base).map(|a| EventAttribute { base: Attribute { key: a.key.clone(), r#type: a.r#type.clone(), @@ -314,18 +310,15 @@ impl ResolvedRegistry { .identity .iter() .filter_map(|ar| { - let attr = schema - .registry - .attribute(&ar.base) - .map(|a| EntityAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = attribute_lookup(&ar.base).map(|a| EntityAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("entity.{}", &e.r#type), @@ -340,18 +333,15 @@ impl ResolvedRegistry { .description .iter() .filter_map(|ar| { - let attr = schema - .registry - .attribute(&ar.base) - .map(|a| EntityAttribute { - base: Attribute { - key: a.key.clone(), - r#type: a.r#type.clone(), - examples: a.examples.clone(), - common: a.common.clone(), - }, - requirement_level: ar.requirement_level.clone(), - }); + let attr = attribute_lookup(&ar.base).map(|a| EntityAttribute { + base: Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }, + requirement_level: ar.requirement_level.clone(), + }); if attr.is_none() { errors.push(Error::AttributeNotFound { group_id: format!("entity.{}", &e.r#type), @@ -370,6 +360,9 @@ impl ResolvedRegistry { } entities.sort_by(|l, r| l.r#type.cmp(&r.r#type)); + // Now we sort the attributes, since we aren't looking them up anymore. + attributes.sort_by(|l, r| l.key.cmp(&r.key)); + if !errors.is_empty() { return Err(Error::CompoundError(errors)); } From 5a01aaec562a5bb6ba98171f9a3565f3eb75f0b8 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 13:38:40 -0400 Subject: [PATCH 13/46] Optimise lookup up attributes in catalog when building V2. --- .../weaver_resolved_schema/src/v2/catalog.rs | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index d1bd56124..401968d7d 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -1,5 +1,7 @@ //! Catalog of attributes and other. +use std::collections::BTreeMap; + use crate::v2::attribute::{Attribute, AttributeRef}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,18 +13,22 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] #[serde(deny_unknown_fields)] #[must_use] -pub struct Catalog { +pub(crate) struct Catalog { /// Catalog of attributes used in the schema. #[serde(skip_serializing_if = "Vec::is_empty")] attributes: Vec, + /// Lookup map to more efficiently find attributes. + lookup: BTreeMap>, } -// TODO - statistics. - impl Catalog { /// Creates a catalog from a list of attributes. pub fn from_attributes(attributes: Vec) -> Self { - Self { attributes } + let mut lookup: BTreeMap> = BTreeMap::new(); + for (idx, attr) in attributes.iter().enumerate() { + lookup.entry(attr.key.clone()).or_default().push(idx); + } + Self { attributes, lookup } } /// Lists all the attributes in the registry. @@ -30,40 +36,31 @@ impl Catalog { &self.attributes } - /// Returns the attribute name from an attribute ref if it exists - /// in the catalog or None if it does not exist. - #[must_use] - pub fn attribute_key(&self, attribute_ref: &AttributeRef) -> Option<&str> { - self.attributes - .get(attribute_ref.0 as usize) - .map(|attr| attr.key.as_ref()) - } - - /// Returns the attribute from an attribute ref if it exists. - #[must_use] - pub fn attribute(&self, attribute_ref: &AttributeRef) -> Option<&Attribute> { - self.attributes.get(attribute_ref.0 as usize) - } - #[must_use] pub(crate) fn convert_ref( &self, attribute: &crate::attribute::Attribute, ) -> Option { - self.attributes + return self + .lookup + .get(&attribute.name)? .iter() - .position( - |a| { - a.key == attribute.name - // TODO check everything - && a.r#type == attribute.r#type - && a.examples == attribute.examples - && a.common.brief == attribute.brief - && a.common.note == attribute.note - && a.common.deprecated == attribute.deprecated - }, // && a.common.stability == attribute.stability - // && a.common.annotations == attribute.annotations - ) - .map(|idx| AttributeRef(idx as u32)) + .filter_map(|idx| { + self.attributes + .get(*idx) + .filter(|a| { + a.key == attribute.name + // TODO check everything + && a.r#type == attribute.r#type + && a.examples == attribute.examples + && a.common.brief == attribute.brief + && a.common.note == attribute.note + && a.common.deprecated == attribute.deprecated + // && a.common.stability == attribute.stability + // && a.common.annotations == attribute.annotations + }) + .map(|_| AttributeRef(*idx as u32)) + }) + .next(); } } From 42d49764f58436301eff88011482841d6c6ae0d1 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 14:00:23 -0400 Subject: [PATCH 14/46] Finish attribute mapping equality for v1->v2 --- .../weaver_resolved_schema/src/v2/catalog.rs | 39 ++++++++++++------- crates/weaver_resolved_schema/src/v2/mod.rs | 2 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index 401968d7d..5312716e1 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -21,9 +21,16 @@ pub(crate) struct Catalog { lookup: BTreeMap>, } +/// Collapses this catalog into the attribute list, preserving order. +impl Into> for Catalog { + fn into(self) -> Vec { + self.attributes + } +} + impl Catalog { /// Creates a catalog from a list of attributes. - pub fn from_attributes(attributes: Vec) -> Self { + pub(crate) fn from_attributes(attributes: Vec) -> Self { let mut lookup: BTreeMap> = BTreeMap::new(); for (idx, attr) in attributes.iter().enumerate() { lookup.entry(attr.key.clone()).or_default().push(idx); @@ -31,11 +38,8 @@ impl Catalog { Self { attributes, lookup } } - /// Lists all the attributes in the registry. - pub fn attributes(&self) -> &Vec { - &self.attributes - } - + /// Converts an attribute from V1 into an AttributeRef + /// on the current list of attributes in the order of this catalog. #[must_use] pub(crate) fn convert_ref( &self, @@ -50,14 +54,21 @@ impl Catalog { .get(*idx) .filter(|a| { a.key == attribute.name - // TODO check everything - && a.r#type == attribute.r#type - && a.examples == attribute.examples - && a.common.brief == attribute.brief - && a.common.note == attribute.note - && a.common.deprecated == attribute.deprecated - // && a.common.stability == attribute.stability - // && a.common.annotations == attribute.annotations + && a.r#type == attribute.r#type + && a.examples == attribute.examples + && a.common.brief == attribute.brief + && a.common.note == attribute.note + && a.common.deprecated == attribute.deprecated + && attribute + .stability + .as_ref() + .map(|s| a.common.stability == *s) + .unwrap_or(false) + && attribute + .annotations + .as_ref() + .map(|ans| a.common.annotations == *ans) + .unwrap_or(false) }) .map(|_| AttributeRef(*idx as u32)) }) diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 4d17ba758..c3653dc96 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -356,7 +356,7 @@ pub fn convert_v1_to_v2( let v2_registry = Registry { registry_url: r.registry_url, - attributes: v2_catalog.attributes().clone(), + attributes: v2_catalog.into(), spans, metrics, events, From 1a36d26dae0951310b10b112b903236757e4d119 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 14:13:27 -0400 Subject: [PATCH 15/46] Fix error reporting on v2 schema creation. --- crates/weaver_resolved_schema/src/v2/catalog.rs | 3 +++ src/util.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index 5312716e1..233f45188 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -45,6 +45,9 @@ impl Catalog { &self, attribute: &crate::attribute::Attribute, ) -> Option { + // Note - we do a fast lookup to contentious attributes, + // then linear scan of attributes with same key but different + // other aspects. return self .lookup .get(&attribute.name)? diff --git a/src/util.rs b/src/util.rs index 6dcdffd34..17972f25b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -460,9 +460,14 @@ pub(crate) fn prepare_main_registry_v2( .capture_non_fatal_errors(diag_msgs)?; } - // TODO - fix error passing here. + // TODO - fix error passing here so original error is diagnostic. let v2_schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema = - main_resolved_schema.try_into().unwrap(); + main_resolved_schema.try_into() + .map_err(|e: weaver_resolved_schema::error::Error| { + weaver_forge::error::Error::TemplateEngineError { + error: e.to_string(), + } + })?; let v2_resolved_registry = weaver_forge::v2::registry::ResolvedRegistry::try_from_resolved_schema(v2_schema)?; Ok((v2_resolved_registry, policy_engine)) From 8889f7137eb7e6983e402730cb5efc753236152f Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 14:18:35 -0400 Subject: [PATCH 16/46] Clippy fixes. --- crates/weaver_forge/src/config.rs | 2 +- crates/weaver_forge/src/v2/registry.rs | 8 +++--- crates/weaver_live_check/src/advice.rs | 3 +-- crates/weaver_resolved_schema/src/lib.rs | 1 - .../src/v2/attribute.rs | 4 +-- .../weaver_resolved_schema/src/v2/catalog.rs | 10 +++---- crates/weaver_resolved_schema/src/v2/mod.rs | 26 +++++++++---------- crates/weaver_resolver/src/registry.rs | 4 +-- crates/weaver_semconv/src/lib.rs | 3 +-- crates/weaver_semconv/src/v2/mod.rs | 2 +- crates/xtask/src/validate.rs | 10 +++---- 11 files changed, 32 insertions(+), 41 deletions(-) diff --git a/crates/weaver_forge/src/config.rs b/crates/weaver_forge/src/config.rs index 8b372840f..6df2948f2 100644 --- a/crates/weaver_forge/src/config.rs +++ b/crates/weaver_forge/src/config.rs @@ -522,7 +522,7 @@ impl WeaverConfig { config.override_with(weaver_config); } - log::trace!("Using the following Weaver configuration: {:#?}", config); + log::trace!("Using the following Weaver configuration: {config:#?}"); Ok(config) } diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 0a5717899..2d9ff0a08 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -172,7 +172,7 @@ impl ResolvedRegistry { common: a.common.clone(), }, requirement_level: ar.requirement_level.clone(), - sampling_relevant: ar.sampling_relevant.clone(), + sampling_relevant: ar.sampling_relevant, }); if attr.is_none() { errors.push(Error::AttributeNotFound { @@ -208,7 +208,7 @@ impl ResolvedRegistry { common: a.common.clone(), }, requirement_level: ar.requirement_level.clone(), - sampling_relevant: ar.sampling_relevant.clone(), + sampling_relevant: ar.sampling_relevant, }); if attr.is_none() { errors.push(Error::AttributeNotFound { @@ -259,7 +259,7 @@ impl ResolvedRegistry { .collect(); events.push(Event { name: event.name, - attributes: attributes, + attributes, entity_associations: event.entity_associations, common: event.common, }); @@ -296,7 +296,7 @@ impl ResolvedRegistry { id: event.id, event: Event { name: event.event.name, - attributes: attributes, + attributes, entity_associations: event.event.entity_associations, common: event.event.common, }, diff --git a/crates/weaver_live_check/src/advice.rs b/crates/weaver_live_check/src/advice.rs index dd3374bf1..240a74ef0 100644 --- a/crates/weaver_live_check/src/advice.rs +++ b/crates/weaver_live_check/src/advice.rs @@ -183,8 +183,7 @@ impl Advisor for StabilityAdvisor { STABILITY_ADVICE_CONTEXT_KEY: stability, }), message: format!( - "Metric is not stable; stability = {}.", - stability + "Metric is not stable; stability = {stability}." ), advice_level: AdviceLevel::Improvement, signal_type: parent_signal.signal_type(), diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index 3960b664f..55ee318e8 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -10,7 +10,6 @@ use crate::catalog::Catalog; use crate::instrumentation_library::InstrumentationLibrary; use crate::registry::{Group, Registry}; use crate::resource::Resource; -use crate::v2::convert_v1_to_v2; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/crates/weaver_resolved_schema/src/v2/attribute.rs b/crates/weaver_resolved_schema/src/v2/attribute.rs index 28373d6e0..2436aada8 100644 --- a/crates/weaver_resolved_schema/src/v2/attribute.rs +++ b/crates/weaver_resolved_schema/src/v2/attribute.rs @@ -1,11 +1,11 @@ //! Attribute definitions for resolved schema. -use std::{collections::HashMap, fmt::Display}; +use std::fmt::Display; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use weaver_semconv::{ - attribute::{self, AttributeType, Examples}, + attribute::{AttributeType, Examples}, v2::CommonFields, }; diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index 233f45188..dd218ff3c 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -22,9 +22,9 @@ pub(crate) struct Catalog { } /// Collapses this catalog into the attribute list, preserving order. -impl Into> for Catalog { - fn into(self) -> Vec { - self.attributes +impl From for Vec { + fn from(val: Catalog) -> Self { + val.attributes } } @@ -48,7 +48,7 @@ impl Catalog { // Note - we do a fast lookup to contentious attributes, // then linear scan of attributes with same key but different // other aspects. - return self + self .lookup .get(&attribute.name)? .iter() @@ -75,6 +75,6 @@ impl Catalog { }) .map(|_| AttributeRef(*idx as u32)) }) - .next(); + .next() } } diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index c3653dc96..c9e624835 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -137,7 +137,7 @@ pub fn convert_v1_to_v2( span_attributes.push(span::SpanAttributeRef { base: a, requirement_level: attr.requirement_level.clone(), - sampling_relevant: attr.sampling_relevant.clone(), + sampling_relevant: attr.sampling_relevant, }); } else { // TODO logic error! @@ -205,7 +205,7 @@ pub fn convert_v1_to_v2( }, attributes: span_attributes, }, - }) + }); } } GroupType::Event => { @@ -373,7 +373,7 @@ pub fn convert_v1_to_v2( #[cfg(test)] mod tests { - use weaver_semconv::{provenance::Provenance, stability::Stability, v2}; + use weaver_semconv::{provenance::Provenance, stability::Stability}; use crate::{attribute::Attribute, lineage::GroupLineage, registry::Group}; @@ -395,7 +395,7 @@ mod tests { weaver_semconv::attribute::BasicRequirementLevelSpec::Required, ), sampling_relevant: None, - note: "".to_string(), + note: "".to_owned(), stability: Some(Stability::Stable), deprecated: None, prefix: false, @@ -416,7 +416,7 @@ mod tests { weaver_semconv::attribute::BasicRequirementLevelSpec::Recommended, ), sampling_relevant: Some(true), - note: "".to_string(), + note: "".to_owned(), stability: Some(Stability::Stable), deprecated: None, prefix: false, @@ -440,7 +440,7 @@ mod tests { extends: None, stability: Some(Stability::Stable), deprecated: None, - attributes: vec![test_refs[1].clone()], + attributes: vec![test_refs[1]], span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), events: vec![], metric_name: None, @@ -462,7 +462,7 @@ mod tests { extends: None, stability: Some(Stability::Stable), deprecated: None, - attributes: vec![test_refs[1].clone()], + attributes: vec![test_refs[1]], span_kind: Some(weaver_semconv::group::SpanKindSpec::Client), events: vec![], metric_name: None, @@ -483,7 +483,7 @@ mod tests { assert_eq!(v2_registry.attributes.len(), 1); // assert attribute fields not shared show up on ref in span. assert_eq!(v2_registry.spans.len(), 1); - if let Some(span) = v2_registry.spans.iter().next() { + if let Some(span) = v2_registry.spans.first() { assert_eq!(span.r#type, "my-span".to_owned().into()); // Make sure attribute ref carries sampling relevant. } @@ -516,7 +516,7 @@ mod tests { weaver_semconv::attribute::BasicRequirementLevelSpec::Required, ), sampling_relevant: None, - note: "".to_string(), + note: "".to_owned(), stability: Some(Stability::Stable), deprecated: None, prefix: false, @@ -537,7 +537,7 @@ mod tests { weaver_semconv::attribute::BasicRequirementLevelSpec::Recommended, ), sampling_relevant: Some(true), - note: "".to_string(), + note: "".to_owned(), stability: Some(Stability::Stable), deprecated: None, prefix: false, @@ -561,7 +561,7 @@ mod tests { extends: None, stability: Some(Stability::Stable), deprecated: None, - attributes: vec![test_refs[0].clone()], + attributes: vec![test_refs[0]], span_kind: None, events: vec![], metric_name: Some("http".to_owned()), @@ -583,7 +583,7 @@ mod tests { extends: None, stability: Some(Stability::Stable), deprecated: None, - attributes: vec![test_refs[1].clone()], + attributes: vec![test_refs[1]], span_kind: None, events: vec![], metric_name: Some("http".to_owned()), @@ -604,7 +604,7 @@ mod tests { assert_eq!(v2_registry.attributes.len(), 1); // assert attribute fields not shared show up on ref in span. assert_eq!(v2_registry.metrics.len(), 1); - if let Some(metric) = v2_registry.metrics.iter().next() { + if let Some(metric) = v2_registry.metrics.first() { assert_eq!(metric.name, "http".to_owned().into()); // Make sure attribute ref carries sampling relevant. } diff --git a/crates/weaver_resolver/src/registry.rs b/crates/weaver_resolver/src/registry.rs index fa60e20e7..ceb0ef0f0 100644 --- a/crates/weaver_resolver/src/registry.rs +++ b/crates/weaver_resolver/src/registry.rs @@ -662,9 +662,7 @@ fn resolve_extends_references(ureg: &mut UnresolvedRegistry) -> Result<(), Error } log::info!( - "Resolved {} extends in this iteration, found errors {:#?}", - resolved_group_count, - errors + "Resolved {resolved_group_count} extends in this iteration, found errors {errors:#?}" ); // If we still have unresolved `extends` but we did not resolve any // `extends` in the last iteration, we are stuck in an infinite loop. diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs index 2ad33a8db..13dbea16b 100644 --- a/crates/weaver_semconv/src/lib.rs +++ b/crates/weaver_semconv/src/lib.rs @@ -550,8 +550,7 @@ mod tests { assert_eq!( diag_msgs.len(), 1, - "Unexpected diagnostics: {:#?}", - diag_msgs + "Unexpected diagnostics: {diag_msgs:#?}" ); assert!(!output.is_empty()); } diff --git a/crates/weaver_semconv/src/v2/mod.rs b/crates/weaver_semconv/src/v2/mod.rs index ff038ccb5..4e3362f91 100644 --- a/crates/weaver_semconv/src/v2/mod.rs +++ b/crates/weaver_semconv/src/v2/mod.rs @@ -80,7 +80,7 @@ pub struct SemConvSpecV2 { impl SemConvSpecV2 { /// Converts the version 2 schema into the version 1 group spec. pub(crate) fn into_v1_specification(self, file_name: &str) -> SemConvSpecV1 { - log::debug!("Translating v2 spec into v1 spec for {}", file_name); + log::debug!("Translating v2 spec into v1 spec for {file_name}"); let mut groups = Vec::new(); diff --git a/crates/xtask/src/validate.rs b/crates/xtask/src/validate.rs index 32308a517..247bc11e5 100644 --- a/crates/xtask/src/validate.rs +++ b/crates/xtask/src/validate.rs @@ -36,8 +36,7 @@ pub fn run() -> anyhow::Result<()> { // Check that the crate name starts with `weaver_` if !crate_name.starts_with("weaver_") { errors.push(anyhow::anyhow!( - "Crate `{}` does not start with `weaver_`", - crate_name + "Crate `{crate_name}` does not start with `weaver_`" )); } @@ -53,8 +52,7 @@ pub fn run() -> anyhow::Result<()> { let cargo_toml_path = path.join("Cargo.toml"); if !cargo_toml_path.exists() { errors.push(anyhow::anyhow!( - "Missing Cargo.toml in the `{}` crate", - crate_name + "Missing Cargo.toml in the `{crate_name}` crate" )); } let maybe_toml: Result = @@ -98,9 +96,7 @@ fn check_presence_of(path: &Path, file_name: &str, crate_name: &str, errors: &mu let readme_path = path.join(file_name); if !readme_path.exists() { errors.push(anyhow::anyhow!( - "Missing {} in the `{}` crate", - file_name, - crate_name + "Missing {file_name} in the `{crate_name}` crate" )); } } From 27b7a0f0b0ce3762a452a8d6ea02bda383d556b2 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 16:28:17 -0400 Subject: [PATCH 17/46] Start supporting attribute groups. Add lineage tracking so we can convert resolved schema to v2. --- .../semconv_jq_fn/semconv_metrics.json | 3 ++ .../semconv_metrics_not_deprecated.json | 3 ++ crates/weaver_resolved_schema/src/lineage.rs | 13 ++++- .../src/v2/attribute_group.rs | 47 +++++++++++++++++++ .../weaver_resolved_schema/src/v2/catalog.rs | 5 +- crates/weaver_resolved_schema/src/v2/mod.rs | 2 + crates/weaver_resolver/src/registry.rs | 7 +++ crates/weaver_semconv/src/v2/attribute.rs | 10 ++-- 8 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 crates/weaver_resolved_schema/src/v2/attribute_group.rs diff --git a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json index d0c5981b4..c1a7d6abe 100644 --- a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json +++ b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics.json @@ -193,6 +193,9 @@ "source_group": "registry.url" } }, + "includes_group": [ + "http.client.server_and_port" + ], "provenance": { "path": "data/http.yaml", "registry_id": "default" diff --git a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json index 358f740ac..e0ec69e7b 100644 --- a/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json +++ b/crates/weaver_forge/expected_output/semconv_jq_fn/semconv_metrics_not_deprecated.json @@ -193,6 +193,9 @@ "source_group": "registry.url" } }, + "includes_group": [ + "http.client.server_and_port" + ], "provenance": { "path": "data/http.yaml", "registry_id": "default" diff --git a/crates/weaver_resolved_schema/src/lineage.rs b/crates/weaver_resolved_schema/src/lineage.rs index d1c175581..203b281be 100644 --- a/crates/weaver_resolved_schema/src/lineage.rs +++ b/crates/weaver_resolved_schema/src/lineage.rs @@ -11,7 +11,7 @@ use weaver_semconv::attribute::{AttributeRole, AttributeSpec, Examples, Requirem use weaver_semconv::deprecated::Deprecated; use weaver_semconv::provenance::Provenance; use weaver_semconv::stability::Stability; -use weaver_semconv::YamlValue; +use weaver_semconv::{YamlValue, group}; /// Attribute lineage (at the field level). #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] @@ -50,6 +50,11 @@ pub struct GroupLineage { #[serde(skip_serializing_if = "BTreeMap::is_empty")] #[serde(default)] attributes: BTreeMap, + + /// (V2 Only) Attribute groups included in this group. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub includes_group: Vec, } impl AttributeLineage { @@ -477,6 +482,7 @@ impl GroupLineage { provenance, extends_group: None, attributes: Default::default(), + includes_group: Default::default(), } } @@ -485,6 +491,11 @@ impl GroupLineage { self.extends_group = Some(extends_group.to_owned()); } + /// Records what attribute groups were included (v2 only). + pub fn includes_group(&mut self, group_id: &str) { + self.includes_group.push(group_id.to_owned()); + } + /// Adds an attribute lineage. pub fn add_attribute_lineage(&mut self, attr_id: String, attribute_lineage: AttributeLineage) { _ = self.attributes.insert(attr_id, attribute_lineage); diff --git a/crates/weaver_resolved_schema/src/v2/attribute_group.rs b/crates/weaver_resolved_schema/src/v2/attribute_group.rs new file mode 100644 index 000000000..87306c915 --- /dev/null +++ b/crates/weaver_resolved_schema/src/v2/attribute_group.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The new way we want to define attribute groups going forward. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::v2::{CommonFields, signal_id::SignalId}; + +use crate::v2::attribute::AttributeRef; + +/// Public attribute group. +/// +/// An attribute group is a grouping of attributes that can be leveraged +/// in codegen. For example, rather than passing attributes on at a time, +/// a temporary structure could be made to contain all of them and report +/// the bundle as a group to different signals. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct AttributeGroup { + /// The name of the attribute group, must be unique. + pub id: SignalId, + + /// List of attributes and group references that belong to this group + pub attributes: Vec, + + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} + +/// A reference to either an attribute or an attribute group. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)] +#[serde(untagged)] +pub enum AttributeOrGroupRef { + /// Reference to an attribute. + Attribute(AttributeRef), + /// Reference to an attribute group. + Group(GroupRef), +} + +/// A reference to an attribute group. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)] +pub struct GroupRef { + /// Reference an existing attribute group by id. + pub ref_group: SignalId, +} \ No newline at end of file diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index dd218ff3c..18097a035 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -8,8 +8,9 @@ use serde::{Deserialize, Serialize}; /// A catalog of indexed attributes shared across semconv groups, or signals. /// Attribute references are used to refer to attributes in the catalog. -/// -/// Note : In the future, this catalog could be extended with other entities. +/// +/// Note: This is meant to be a temporary datastructure used for creating +/// the registry. #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] #[serde(deny_unknown_fields)] #[must_use] diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index c9e624835..d55dd2b1b 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -19,6 +19,7 @@ use crate::v2::{ }; pub mod attribute; +pub mod attribute_group; pub mod catalog; pub mod entity; pub mod event; @@ -27,6 +28,7 @@ pub mod refinements; pub mod registry; pub mod span; + /// A Resolved Telemetry Schema. /// A Resolved Telemetry Schema is self-contained and doesn't contain any /// external references to other schemas or semantic conventions. diff --git a/crates/weaver_resolver/src/registry.rs b/crates/weaver_resolver/src/registry.rs index ceb0ef0f0..dde5d908c 100644 --- a/crates/weaver_resolver/src/registry.rs +++ b/crates/weaver_resolver/src/registry.rs @@ -628,6 +628,13 @@ fn resolve_extends_references(ureg: &mut UnresolvedRegistry) -> Result<(), Error } } _ = attrs_by_group.insert(include_group.clone(), attrs); + + // We'll need to reverse engineer if it was a private group later in V2 mapping. + if let Some(lineage) = unresolved_group.group.lineage.as_mut() { + // update lineage so we know a group was included. + lineage.includes_group(include_group); + } + } else { errors.push(Error::UnresolvedExtendsRef { group_id: unresolved_group.group.id.clone(), diff --git a/crates/weaver_semconv/src/v2/attribute.rs b/crates/weaver_semconv/src/v2/attribute.rs index ccde647e2..47a6f5d12 100644 --- a/crates/weaver_semconv/src/v2/attribute.rs +++ b/crates/weaver_semconv/src/v2/attribute.rs @@ -8,11 +8,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ - attribute::{AttributeRole, AttributeSpec, AttributeType, Examples, RequirementLevel}, - deprecated::Deprecated, - stability::Stability, - v2::CommonFields, - YamlValue, + YamlValue, attribute::{AttributeRole, AttributeSpec, AttributeType, Examples, RequirementLevel}, deprecated::Deprecated, stability::Stability, v2::{CommonFields, signal_id::SignalId} }; /// A refinement of an Attribute for a signal. @@ -158,7 +154,7 @@ impl AttributeDef { #[serde(deny_unknown_fields)] pub struct GroupRef { /// Reference an existing attribute group by id. - pub ref_group: String, + pub ref_group: SignalId, } /// A reference to either an attribute or an attribute group. @@ -185,7 +181,7 @@ pub fn split_attributes_and_groups( AttributeOrGroupRef::Attribute(attr_ref) => { attributes.push(attr_ref.into_v1_attribute()); } - AttributeOrGroupRef::Group(group_ref) => groups.push(group_ref.ref_group), + AttributeOrGroupRef::Group(group_ref) => groups.push(group_ref.ref_group.into_v1()), } } From 75072fe8966502c573d127c57da4bd3068d15ee8 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 16:48:39 -0400 Subject: [PATCH 18/46] Initial attribute group support in resolved registry. --- crates/weaver_live_check/src/advice.rs | 4 +- crates/weaver_resolved_schema/src/lib.rs | 2 + crates/weaver_resolved_schema/src/lineage.rs | 2 +- crates/weaver_resolved_schema/src/registry.rs | 7 +++ .../src/v2/attribute_group.rs | 8 +-- .../weaver_resolved_schema/src/v2/catalog.rs | 5 +- crates/weaver_resolved_schema/src/v2/mod.rs | 49 ++++++++++++++++--- .../weaver_resolved_schema/src/v2/registry.rs | 4 ++ crates/weaver_resolver/src/registry.rs | 4 +- crates/weaver_semconv/src/lib.rs | 6 +-- crates/weaver_semconv/src/v2/attribute.rs | 6 ++- src/util.rs | 6 +-- 12 files changed, 75 insertions(+), 28 deletions(-) diff --git a/crates/weaver_live_check/src/advice.rs b/crates/weaver_live_check/src/advice.rs index 240a74ef0..3341b7238 100644 --- a/crates/weaver_live_check/src/advice.rs +++ b/crates/weaver_live_check/src/advice.rs @@ -182,9 +182,7 @@ impl Advisor for StabilityAdvisor { advice_context: json!({ STABILITY_ADVICE_CONTEXT_KEY: stability, }), - message: format!( - "Metric is not stable; stability = {stability}." - ), + message: format!("Metric is not stable; stability = {stability}."), advice_level: AdviceLevel::Improvement, signal_type: parent_signal.signal_type(), signal_name: parent_signal.signal_name(), diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index 55ee318e8..037dd0138 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -137,6 +137,7 @@ impl ResolvedTelemetrySchema { body: None, annotations: None, entity_associations: vec![], + visibility: None, }); } @@ -179,6 +180,7 @@ impl ResolvedTelemetrySchema { body: None, annotations: None, entity_associations: vec![], + visibility: None, }); } diff --git a/crates/weaver_resolved_schema/src/lineage.rs b/crates/weaver_resolved_schema/src/lineage.rs index 203b281be..450932093 100644 --- a/crates/weaver_resolved_schema/src/lineage.rs +++ b/crates/weaver_resolved_schema/src/lineage.rs @@ -11,7 +11,7 @@ use weaver_semconv::attribute::{AttributeRole, AttributeSpec, Examples, Requirem use weaver_semconv::deprecated::Deprecated; use weaver_semconv::provenance::Provenance; use weaver_semconv::stability::Stability; -use weaver_semconv::{YamlValue, group}; +use weaver_semconv::{group, YamlValue}; /// Attribute lineage (at the field level). #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] diff --git a/crates/weaver_resolved_schema/src/registry.rs b/crates/weaver_resolved_schema/src/registry.rs index 224dd300c..3ce1e8322 100644 --- a/crates/weaver_resolved_schema/src/registry.rs +++ b/crates/weaver_resolved_schema/src/registry.rs @@ -20,6 +20,7 @@ use weaver_semconv::deprecated::Deprecated; use weaver_semconv::group::{GroupType, InstrumentSpec, SpanKindSpec}; use weaver_semconv::provenance::Provenance; use weaver_semconv::stability::Stability; +use weaver_semconv::v2::attribute_group::AttributeGroupVisibilitySpec; use weaver_semconv::YamlValue; /// A semantic convention registry. @@ -134,6 +135,12 @@ pub struct Group { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub entity_associations: Vec, + /// Visibility of the attribute group. + /// This is only used for v2 conversion. + #[serde(default)] + #[serde(skip_serializing)] + #[schemars(skip)] + pub visibility: Option, } impl Group { diff --git a/crates/weaver_resolved_schema/src/v2/attribute_group.rs b/crates/weaver_resolved_schema/src/v2/attribute_group.rs index 87306c915..dd22a71f8 100644 --- a/crates/weaver_resolved_schema/src/v2/attribute_group.rs +++ b/crates/weaver_resolved_schema/src/v2/attribute_group.rs @@ -4,12 +4,12 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use weaver_semconv::v2::{CommonFields, signal_id::SignalId}; +use weaver_semconv::v2::{signal_id::SignalId, CommonFields}; use crate::v2::attribute::AttributeRef; /// Public attribute group. -/// +/// /// An attribute group is a grouping of attributes that can be leveraged /// in codegen. For example, rather than passing attributes on at a time, /// a temporary structure could be made to contain all of them and report @@ -24,7 +24,7 @@ pub struct AttributeGroup { /// List of attributes and group references that belong to this group pub attributes: Vec, - /// Common fields (like brief, note, annotations). + /// Common fields (like brief, note, annotations). #[serde(flatten)] pub common: CommonFields, } @@ -44,4 +44,4 @@ pub enum AttributeOrGroupRef { pub struct GroupRef { /// Reference an existing attribute group by id. pub ref_group: SignalId, -} \ No newline at end of file +} diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index 18097a035..b656051e1 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; /// A catalog of indexed attributes shared across semconv groups, or signals. /// Attribute references are used to refer to attributes in the catalog. -/// +/// /// Note: This is meant to be a temporary datastructure used for creating /// the registry. #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] @@ -49,8 +49,7 @@ impl Catalog { // Note - we do a fast lookup to contentious attributes, // then linear scan of attributes with same key but different // other aspects. - self - .lookup + self.lookup .get(&attribute.name)? .iter() .filter_map(|idx| { diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index d55dd2b1b..bcf7ee815 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -6,10 +6,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use weaver_semconv::{ group::GroupType, - v2::{signal_id::SignalId, span::SpanName, CommonFields}, + v2::{ + attribute_group::AttributeGroupVisibilitySpec, signal_id::SignalId, span::SpanName, + CommonFields, + }, }; use crate::v2::{ + attribute_group::{AttributeGroup, AttributeOrGroupRef}, catalog::Catalog, entity::Entity, metric::Metric, @@ -28,7 +32,6 @@ pub mod refinements; pub mod registry; pub mod span; - /// A Resolved Telemetry Schema. /// A Resolved Telemetry Schema is self-contained and doesn't contain any /// external references to other schemas or semantic conventions. @@ -121,6 +124,7 @@ pub fn convert_v1_to_v2( let mut events = Vec::new(); let mut event_refinements = Vec::new(); let mut entities = Vec::new(); + let mut attribute_groups = Vec::new(); for g in r.groups.iter() { match g.r#type { GroupType::Span => { @@ -347,10 +351,38 @@ pub fn convert_v1_to_v2( }, }); } - GroupType::AttributeGroup - | GroupType::MetricGroup - | GroupType::Scope - | GroupType::Undefined => { + GroupType::AttributeGroup => { + if g.visibility + .as_ref() + .is_some_and(|v| AttributeGroupVisibilitySpec::Public == *v) + { + // Now we need to convert the group. + let mut attributes = Vec::new(); + // TODO - we need to check lineage and remove parent groups. + for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { + if let Some(a) = v2_catalog.convert_ref(attr) { + attributes.push(AttributeOrGroupRef::Attribute(a)); + } else { + // TODO logic error! + } + } + attribute_groups.push(AttributeGroup { + id: fix_group_id("attribute_group.", &g.id), + attributes, + common: CommonFields { + brief: g.brief.clone(), + note: g.note.clone(), + stability: g + .stability + .clone() + .unwrap_or(weaver_semconv::stability::Stability::Alpha), + deprecated: g.deprecated.clone(), + annotations: g.annotations.clone().unwrap_or_default(), + }, + }); + } + } + GroupType::MetricGroup | GroupType::Scope | GroupType::Undefined => { // Ignored for now, we should probably issue warnings. } } @@ -363,6 +395,7 @@ pub fn convert_v1_to_v2( metrics, events, entities, + attribute_groups, }; let v2_refinements = Refinements { spans: span_refinements, @@ -454,6 +487,7 @@ mod tests { body: None, annotations: None, entity_associations: vec![], + visibility: None, }, Group { id: "span.custom".to_owned(), @@ -476,6 +510,7 @@ mod tests { body: None, annotations: None, entity_associations: vec![], + visibility: None, }, ], }; @@ -575,6 +610,7 @@ mod tests { body: None, annotations: None, entity_associations: vec![], + visibility: None, }, Group { id: "metric.http.custom".to_owned(), @@ -597,6 +633,7 @@ mod tests { body: None, annotations: None, entity_associations: vec![], + visibility: None, }, ], }; diff --git a/crates/weaver_resolved_schema/src/v2/registry.rs b/crates/weaver_resolved_schema/src/v2/registry.rs index e902ea0d4..05fc7fa16 100644 --- a/crates/weaver_resolved_schema/src/v2/registry.rs +++ b/crates/weaver_resolved_schema/src/v2/registry.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::v2::{ attribute::{Attribute, AttributeRef}, + attribute_group::AttributeGroup, entity::Entity, event::Event, metric::Metric, @@ -23,6 +24,9 @@ pub struct Registry { /// Catalog of attributes used in the schema. pub attributes: Vec, + /// Catalog of (public) attribute groups. + pub attribute_groups: Vec, + /// The semantic convention registry url. /// /// This is the base URL, under which this registry can be found. diff --git a/crates/weaver_resolver/src/registry.rs b/crates/weaver_resolver/src/registry.rs index dde5d908c..eee09f1ac 100644 --- a/crates/weaver_resolver/src/registry.rs +++ b/crates/weaver_resolver/src/registry.rs @@ -427,6 +427,7 @@ fn group_from_spec(group: GroupSpecWithProvenance) -> UnresolvedGroup { body: group.spec.body, annotations: group.spec.annotations, entity_associations: group.spec.entity_associations, + visibility: group.spec.visibility.clone(), }, attributes: attrs, provenance: group.provenance, @@ -628,13 +629,12 @@ fn resolve_extends_references(ureg: &mut UnresolvedRegistry) -> Result<(), Error } } _ = attrs_by_group.insert(include_group.clone(), attrs); - + // We'll need to reverse engineer if it was a private group later in V2 mapping. if let Some(lineage) = unresolved_group.group.lineage.as_mut() { // update lineage so we know a group was included. lineage.includes_group(include_group); } - } else { errors.push(Error::UnresolvedExtendsRef { group_id: unresolved_group.group.id.clone(), diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs index 13dbea16b..2806fbc7a 100644 --- a/crates/weaver_semconv/src/lib.rs +++ b/crates/weaver_semconv/src/lib.rs @@ -547,11 +547,7 @@ mod tests { if let Err(err) = result { let output = format!("{err}"); let diag_msgs: DiagnosticMessages = err.into(); - assert_eq!( - diag_msgs.len(), - 1, - "Unexpected diagnostics: {diag_msgs:#?}" - ); + assert_eq!(diag_msgs.len(), 1, "Unexpected diagnostics: {diag_msgs:#?}"); assert!(!output.is_empty()); } } diff --git a/crates/weaver_semconv/src/v2/attribute.rs b/crates/weaver_semconv/src/v2/attribute.rs index 47a6f5d12..a3bf5dbf1 100644 --- a/crates/weaver_semconv/src/v2/attribute.rs +++ b/crates/weaver_semconv/src/v2/attribute.rs @@ -8,7 +8,11 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ - YamlValue, attribute::{AttributeRole, AttributeSpec, AttributeType, Examples, RequirementLevel}, deprecated::Deprecated, stability::Stability, v2::{CommonFields, signal_id::SignalId} + attribute::{AttributeRole, AttributeSpec, AttributeType, Examples, RequirementLevel}, + deprecated::Deprecated, + stability::Stability, + v2::{signal_id::SignalId, CommonFields}, + YamlValue, }; /// A refinement of an Attribute for a signal. diff --git a/src/util.rs b/src/util.rs index 17972f25b..966f5d2fe 100644 --- a/src/util.rs +++ b/src/util.rs @@ -461,10 +461,10 @@ pub(crate) fn prepare_main_registry_v2( } // TODO - fix error passing here so original error is diagnostic. - let v2_schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema = - main_resolved_schema.try_into() + let v2_schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema = main_resolved_schema + .try_into() .map_err(|e: weaver_resolved_schema::error::Error| { - weaver_forge::error::Error::TemplateEngineError { + weaver_forge::error::Error::TemplateEngineError { error: e.to_string(), } })?; From 0c32132e54acc732fce4103b3f185046c5f6f0d5 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 16:49:38 -0400 Subject: [PATCH 19/46] Cargo fix. --- crates/weaver_resolved_schema/src/lineage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/weaver_resolved_schema/src/lineage.rs b/crates/weaver_resolved_schema/src/lineage.rs index 450932093..df0126548 100644 --- a/crates/weaver_resolved_schema/src/lineage.rs +++ b/crates/weaver_resolved_schema/src/lineage.rs @@ -11,7 +11,7 @@ use weaver_semconv::attribute::{AttributeRole, AttributeSpec, Examples, Requirem use weaver_semconv::deprecated::Deprecated; use weaver_semconv::provenance::Provenance; use weaver_semconv::stability::Stability; -use weaver_semconv::{group, YamlValue}; +use weaver_semconv::YamlValue; /// Attribute lineage (at the field level). #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] From b7b3a487b6479890aaa631e5b24b547b400a0a7a Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2025 18:48:54 -0400 Subject: [PATCH 20/46] Add include groups to v2 resolved schema. --- crates/weaver_resolved_schema/src/v2/event.rs | 6 ++ .../weaver_resolved_schema/src/v2/metric.rs | 6 ++ crates/weaver_resolved_schema/src/v2/mod.rs | 63 ++++++++++++++++--- crates/weaver_resolved_schema/src/v2/span.rs | 7 +++ 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/event.rs b/crates/weaver_resolved_schema/src/v2/event.rs index be4cd250d..1731a0d36 100644 --- a/crates/weaver_resolved_schema/src/v2/event.rs +++ b/crates/weaver_resolved_schema/src/v2/event.rs @@ -20,6 +20,12 @@ pub struct Event { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, + // TODO - How do we reference attribute groups? + /// List of public attribute groups that this signal uses. These are by-name references, + /// all atributes will already be flattened into the `attributes` field. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub include_groups: Vec, // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs index cf74dbd33..14b27f519 100644 --- a/crates/weaver_resolved_schema/src/v2/metric.rs +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -28,6 +28,12 @@ pub struct Metric { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, + // TODO - How do we reference attribute groups? + /// List of public attribute groups that this signal uses. These are by-name references, + /// all atributes will already be flattened into the `attributes` field. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub include_groups: Vec, // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. /// diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index bcf7ee815..61d67f497 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -12,14 +12,17 @@ use weaver_semconv::{ }, }; -use crate::v2::{ - attribute_group::{AttributeGroup, AttributeOrGroupRef}, - catalog::Catalog, - entity::Entity, - metric::Metric, - refinements::Refinements, - registry::Registry, - span::{Span, SpanRefinement}, +use crate::{ + registry::Group, + v2::{ + attribute_group::{AttributeGroup, AttributeOrGroupRef}, + catalog::Catalog, + entity::Entity, + metric::Metric, + refinements::Refinements, + registry::Registry, + span::{Span, SpanRefinement}, + }, }; pub mod attribute; @@ -79,11 +82,35 @@ fn fix_span_group_id(group_id: &str) -> SignalId { fix_group_id("span.", group_id) } +/// Helper to find all include groups (transitiviely) given a lookup. +fn lookup_group_includes( + lineage: &crate::lineage::GroupLineage, + lookup: &HashMap<&str, &Group>, +) -> Vec { + // We need to sort through ALL the groups in our lineage. + let mut groups = lineage + .extends_group + .as_ref() + .and_then(|id| { + lookup + .get(id.as_str()) + .and_then(|g| g.lineage.as_ref()) + .map(|l| lookup_group_includes(l, lookup)) + }) + .unwrap_or_default(); + for ig in &lineage.includes_group { + groups.push(ig.to_owned().into()); + } + groups +} + /// Converts a V1 registry + catalog to V2. pub fn convert_v1_to_v2( c: crate::catalog::Catalog, r: crate::registry::Registry, ) -> Result<(Registry, Refinements), crate::error::Error> { + let lookup: HashMap<&str, &Group> = r.groups.iter().map(|g| (g.id.as_str(), g)).collect(); + // When pulling attributes, as we collapse things, we need to filter // to just unique. let attributes: HashSet = c @@ -172,6 +199,11 @@ pub fn convert_v1_to_v2( annotations: g.annotations.clone().unwrap_or_default(), }, attributes: span_attributes, + include_groups: g + .lineage + .as_ref() + .map(|l| lookup_group_includes(l, &lookup)) + .unwrap_or_default(), }; spans.push(span.clone()); span_refinements.push(SpanRefinement { @@ -210,6 +242,11 @@ pub fn convert_v1_to_v2( annotations: g.annotations.clone().unwrap_or_default(), }, attributes: span_attributes, + include_groups: g + .lineage + .as_ref() + .map(|l| lookup_group_includes(l, &lookup)) + .unwrap_or_default(), }, }); } @@ -237,6 +274,11 @@ pub fn convert_v1_to_v2( name: g.name.clone().unwrap().into(), attributes: event_attributes, entity_associations: g.entity_associations.clone(), + include_groups: g + .lineage + .as_ref() + .map(|l| lookup_group_includes(l, &lookup)) + .unwrap_or_default(), common: CommonFields { brief: g.brief.clone(), note: g.note.clone(), @@ -288,6 +330,11 @@ pub fn convert_v1_to_v2( unit: g.unit.clone().unwrap(), attributes: metric_attributes, entity_associations: g.entity_associations.clone(), + include_groups: g + .lineage + .as_ref() + .map(|l| lookup_group_includes(l, &lookup)) + .unwrap_or_default(), common: CommonFields { brief: g.brief.clone(), note: g.note.clone(), diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs index 9bd4b04d4..07651bf7c 100644 --- a/crates/weaver_resolved_schema/src/v2/span.rs +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -26,6 +26,13 @@ pub struct Span { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, + // TODO - How do we reference attribute groups? + /// List of public attribute groups that this signal uses. These are by-name references, + /// all atributes will already be flattened into the `attributes` field. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub include_groups: Vec, + // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. /// From 60227acbcdfed684b1faa37517d58385979b80ec Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 29 Oct 2025 11:55:14 -0400 Subject: [PATCH 21/46] Remove attribute group tracking from in-group feedback. --- crates/weaver_forge/src/v2/registry.rs | 13 +++++- crates/weaver_resolved_schema/src/v2/event.rs | 6 --- .../weaver_resolved_schema/src/v2/metric.rs | 6 --- crates/weaver_resolved_schema/src/v2/mod.rs | 43 ------------------- crates/weaver_resolved_schema/src/v2/span.rs | 6 --- src/util.rs | 4 +- 6 files changed, 13 insertions(+), 65 deletions(-) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 2d9ff0a08..86712fd86 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -21,12 +21,13 @@ use crate::{ /// This includes all registrys fully fleshed out and ready for codegen. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct ResolvedRegistry { +pub struct ForgeResolvedRegistry { /// The semantic convention registry url. #[serde(skip_serializing_if = "String::is_empty")] pub registry_url: String, /// The raw attributes in this registry. pub attributes: Vec, + // TODO - Attribute Groups /// The signals defined in this registry. pub signals: Signals, /// The set of refinments defined in this registry. @@ -63,7 +64,15 @@ pub struct Refinements { pub events: Vec, } -impl ResolvedRegistry { +/// Conversion from Resolved schema to the "template schema". +impl TryFrom for ForgeResolvedRegistry { + type Error = Error; + fn try_from(value: weaver_resolved_schema::v2::ResolvedTelemetrySchema) -> Result { + ForgeResolvedRegistry::try_from_resolved_schema(value) + } +} + +impl ForgeResolvedRegistry { /// Create a new template registry from a resolved schema registry. pub fn try_from_resolved_schema( schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema, diff --git a/crates/weaver_resolved_schema/src/v2/event.rs b/crates/weaver_resolved_schema/src/v2/event.rs index 1731a0d36..be4cd250d 100644 --- a/crates/weaver_resolved_schema/src/v2/event.rs +++ b/crates/weaver_resolved_schema/src/v2/event.rs @@ -20,12 +20,6 @@ pub struct Event { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, - // TODO - How do we reference attribute groups? - /// List of public attribute groups that this signal uses. These are by-name references, - /// all atributes will already be flattened into the `attributes` field. - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub include_groups: Vec, // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs index 14b27f519..cf74dbd33 100644 --- a/crates/weaver_resolved_schema/src/v2/metric.rs +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -28,12 +28,6 @@ pub struct Metric { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, - // TODO - How do we reference attribute groups? - /// List of public attribute groups that this signal uses. These are by-name references, - /// all atributes will already be flattened into the `attributes` field. - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub include_groups: Vec, // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. /// diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 61d67f497..be8d8b8d4 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -82,34 +82,11 @@ fn fix_span_group_id(group_id: &str) -> SignalId { fix_group_id("span.", group_id) } -/// Helper to find all include groups (transitiviely) given a lookup. -fn lookup_group_includes( - lineage: &crate::lineage::GroupLineage, - lookup: &HashMap<&str, &Group>, -) -> Vec { - // We need to sort through ALL the groups in our lineage. - let mut groups = lineage - .extends_group - .as_ref() - .and_then(|id| { - lookup - .get(id.as_str()) - .and_then(|g| g.lineage.as_ref()) - .map(|l| lookup_group_includes(l, lookup)) - }) - .unwrap_or_default(); - for ig in &lineage.includes_group { - groups.push(ig.to_owned().into()); - } - groups -} - /// Converts a V1 registry + catalog to V2. pub fn convert_v1_to_v2( c: crate::catalog::Catalog, r: crate::registry::Registry, ) -> Result<(Registry, Refinements), crate::error::Error> { - let lookup: HashMap<&str, &Group> = r.groups.iter().map(|g| (g.id.as_str(), g)).collect(); // When pulling attributes, as we collapse things, we need to filter // to just unique. @@ -199,11 +176,6 @@ pub fn convert_v1_to_v2( annotations: g.annotations.clone().unwrap_or_default(), }, attributes: span_attributes, - include_groups: g - .lineage - .as_ref() - .map(|l| lookup_group_includes(l, &lookup)) - .unwrap_or_default(), }; spans.push(span.clone()); span_refinements.push(SpanRefinement { @@ -242,11 +214,6 @@ pub fn convert_v1_to_v2( annotations: g.annotations.clone().unwrap_or_default(), }, attributes: span_attributes, - include_groups: g - .lineage - .as_ref() - .map(|l| lookup_group_includes(l, &lookup)) - .unwrap_or_default(), }, }); } @@ -274,11 +241,6 @@ pub fn convert_v1_to_v2( name: g.name.clone().unwrap().into(), attributes: event_attributes, entity_associations: g.entity_associations.clone(), - include_groups: g - .lineage - .as_ref() - .map(|l| lookup_group_includes(l, &lookup)) - .unwrap_or_default(), common: CommonFields { brief: g.brief.clone(), note: g.note.clone(), @@ -330,11 +292,6 @@ pub fn convert_v1_to_v2( unit: g.unit.clone().unwrap(), attributes: metric_attributes, entity_associations: g.entity_associations.clone(), - include_groups: g - .lineage - .as_ref() - .map(|l| lookup_group_includes(l, &lookup)) - .unwrap_or_default(), common: CommonFields { brief: g.brief.clone(), note: g.note.clone(), diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs index 07651bf7c..6999c87f8 100644 --- a/crates/weaver_resolved_schema/src/v2/span.rs +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -26,12 +26,6 @@ pub struct Span { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, - // TODO - How do we reference attribute groups? - /// List of public attribute groups that this signal uses. These are by-name references, - /// all atributes will already be flattened into the `attributes` field. - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub include_groups: Vec, // TODO - Should Entity Associations be "strong" links? /// Which resources this span should be associated with. diff --git a/src/util.rs b/src/util.rs index 966f5d2fe..de9c17877 100644 --- a/src/util.rs +++ b/src/util.rs @@ -364,7 +364,7 @@ pub(crate) fn prepare_main_registry_v2( registry_args: &RegistryArgs, policy_args: &PolicyArgs, diag_msgs: &mut DiagnosticMessages, -) -> Result<(weaver_forge::v2::registry::ResolvedRegistry, Option), DiagnosticMessages> { +) -> Result<(weaver_forge::v2::registry::ForgeResolvedRegistry, Option), DiagnosticMessages> { let registry_path = ®istry_args.registry; let main_registry_repo = RegistryRepo::try_new("main", registry_path)?; @@ -469,6 +469,6 @@ pub(crate) fn prepare_main_registry_v2( } })?; let v2_resolved_registry = - weaver_forge::v2::registry::ResolvedRegistry::try_from_resolved_schema(v2_schema)?; + weaver_forge::v2::registry::ForgeResolvedRegistry::try_from_resolved_schema(v2_schema)?; Ok((v2_resolved_registry, policy_engine)) } From e1a2de8e6a526b8f4e309577adde14b5dc0e2004 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 31 Oct 2025 10:56:54 -0400 Subject: [PATCH 22/46] Add attribute group to forge schema. --- crates/weaver_forge/src/v2/attribute_group.rs | 20 +++++++++++ crates/weaver_forge/src/v2/mod.rs | 1 + crates/weaver_forge/src/v2/registry.rs | 36 ++++++++++++++++++- .../src/v2/attribute_group.rs | 19 +--------- crates/weaver_resolved_schema/src/v2/mod.rs | 5 ++- 5 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 crates/weaver_forge/src/v2/attribute_group.rs diff --git a/crates/weaver_forge/src/v2/attribute_group.rs b/crates/weaver_forge/src/v2/attribute_group.rs new file mode 100644 index 000000000..48b450019 --- /dev/null +++ b/crates/weaver_forge/src/v2/attribute_group.rs @@ -0,0 +1,20 @@ +//! Version two of attribute groups. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use weaver_semconv::v2::{signal_id::SignalId, CommonFields}; + +use crate::v2::attribute::Attribute; + +/// Public attribute group. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct AttributeGroup { + /// The name of the attribute group, must be unique. + pub id: SignalId, + /// List of attributes. + pub attributes: Vec, + /// Common fields (like brief, note, annotations). + #[serde(flatten)] + pub common: CommonFields, +} diff --git a/crates/weaver_forge/src/v2/mod.rs b/crates/weaver_forge/src/v2/mod.rs index 0a1ae2f5c..6743bf6e4 100644 --- a/crates/weaver_forge/src/v2/mod.rs +++ b/crates/weaver_forge/src/v2/mod.rs @@ -1,6 +1,7 @@ //! Version two of weaver model. pub mod attribute; +pub mod attribute_group; pub mod entity; pub mod event; pub mod metric; diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 86712fd86..47857486a 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -8,6 +8,7 @@ use crate::{ error::Error, v2::{ attribute::Attribute, + attribute_group::AttributeGroup, entity::{Entity, EntityAttribute}, event::{Event, EventAttribute, EventRefinement}, metric::{Metric, MetricAttribute, MetricRefinement}, @@ -27,6 +28,8 @@ pub struct ForgeResolvedRegistry { pub registry_url: String, /// The raw attributes in this registry. pub attributes: Vec, + /// The public attribute groups in this registry. + pub attribute_groups: Vec, // TODO - Attribute Groups /// The signals defined in this registry. pub signals: Signals, @@ -67,7 +70,9 @@ pub struct Refinements { /// Conversion from Resolved schema to the "template schema". impl TryFrom for ForgeResolvedRegistry { type Error = Error; - fn try_from(value: weaver_resolved_schema::v2::ResolvedTelemetrySchema) -> Result { + fn try_from( + value: weaver_resolved_schema::v2::ResolvedTelemetrySchema, + ) -> Result { ForgeResolvedRegistry::try_from_resolved_schema(value) } } @@ -369,6 +374,34 @@ impl ForgeResolvedRegistry { } entities.sort_by(|l, r| l.r#type.cmp(&r.r#type)); + let mut attribute_groups = Vec::new(); + for ag in schema.registry.attribute_groups { + let attributes = ag + .attributes + .iter() + .filter_map(|ar| { + let attr = attribute_lookup(&ar).map(|a| Attribute { + key: a.key.clone(), + r#type: a.r#type.clone(), + examples: a.examples.clone(), + common: a.common.clone(), + }); + if attr.is_none() { + errors.push(Error::AttributeNotFound { + group_id: format!("attribute_group.{}", &ag.id), + attr_ref: AttributeRef(ar.0), + }); + } + attr + }) + .collect(); + attribute_groups.push(AttributeGroup { + id: ag.id, + attributes, + common: ag.common.clone(), + }); + } + // Now we sort the attributes, since we aren't looking them up anymore. attributes.sort_by(|l, r| l.key.cmp(&r.key)); @@ -379,6 +412,7 @@ impl ForgeResolvedRegistry { Ok(Self { registry_url: schema.schema_url.clone(), attributes, + attribute_groups, signals: Signals { metrics, spans, diff --git a/crates/weaver_resolved_schema/src/v2/attribute_group.rs b/crates/weaver_resolved_schema/src/v2/attribute_group.rs index dd22a71f8..9a09f77f2 100644 --- a/crates/weaver_resolved_schema/src/v2/attribute_group.rs +++ b/crates/weaver_resolved_schema/src/v2/attribute_group.rs @@ -22,26 +22,9 @@ pub struct AttributeGroup { pub id: SignalId, /// List of attributes and group references that belong to this group - pub attributes: Vec, + pub attributes: Vec, /// Common fields (like brief, note, annotations). #[serde(flatten)] pub common: CommonFields, } - -/// A reference to either an attribute or an attribute group. -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)] -#[serde(untagged)] -pub enum AttributeOrGroupRef { - /// Reference to an attribute. - Attribute(AttributeRef), - /// Reference to an attribute group. - Group(GroupRef), -} - -/// A reference to an attribute group. -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)] -pub struct GroupRef { - /// Reference an existing attribute group by id. - pub ref_group: SignalId, -} diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index be8d8b8d4..66faf5043 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -15,7 +15,7 @@ use weaver_semconv::{ use crate::{ registry::Group, v2::{ - attribute_group::{AttributeGroup, AttributeOrGroupRef}, + attribute_group::AttributeGroup, catalog::Catalog, entity::Entity, metric::Metric, @@ -87,7 +87,6 @@ pub fn convert_v1_to_v2( c: crate::catalog::Catalog, r: crate::registry::Registry, ) -> Result<(Registry, Refinements), crate::error::Error> { - // When pulling attributes, as we collapse things, we need to filter // to just unique. let attributes: HashSet = c @@ -365,7 +364,7 @@ pub fn convert_v1_to_v2( // TODO - we need to check lineage and remove parent groups. for attr in g.attributes.iter().filter_map(|a| c.attribute(a)) { if let Some(a) = v2_catalog.convert_ref(attr) { - attributes.push(AttributeOrGroupRef::Attribute(a)); + attributes.push(a); } else { // TODO logic error! } From e7617ee83bc576dec94777f081a82b245e457693 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 31 Oct 2025 15:13:47 -0400 Subject: [PATCH 23/46] Add v2 to weaver generate and write a test. --- src/registry/generate.rs | 90 +++++++++++++++++-- src/registry/resolve.rs | 3 +- src/util.rs | 11 ++- tests/v2_forge/expected_output/registry.md | 27 ++++++ tests/v2_forge/model/test.yaml | 48 ++++++++++ .../templates/markdown/registry.md.j2 | 26 ++++++ tests/v2_forge/templates/markdown/weaver.yaml | 6 ++ 7 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 tests/v2_forge/expected_output/registry.md create mode 100644 tests/v2_forge/model/test.yaml create mode 100644 tests/v2_forge/templates/markdown/registry.md.j2 create mode 100644 tests/v2_forge/templates/markdown/weaver.yaml diff --git a/src/registry/generate.rs b/src/registry/generate.rs index 4b4a00b74..b4e1f6c3c 100644 --- a/src/registry/generate.rs +++ b/src/registry/generate.rs @@ -15,7 +15,7 @@ use weaver_forge::file_loader::{FileLoader, FileSystemFileLoader}; use weaver_forge::{OutputDirective, TemplateEngine}; use crate::registry::{Error, PolicyArgs, RegistryArgs}; -use crate::util::prepare_main_registry; +use crate::util::{prepare_main_registry, prepare_main_registry_v2}; use crate::{DiagnosticArgs, ExitDirectives}; use weaver_common::vdir::VirtualDirectory; use weaver_common::vdir::VirtualDirectoryPath; @@ -64,6 +64,10 @@ pub struct RegistryGenerateArgs { #[arg(long, default_value = "false")] pub future: bool, + /// Enable the V2 output for generating templates. + #[arg(long, default_value = "false")] + pub v2: bool, + /// Parameters to specify the diagnostic format. #[command(flatten)] pub diagnostic: DiagnosticArgs, @@ -91,8 +95,17 @@ pub(crate) fn command(args: &RegistryGenerateArgs) -> Result Result { + engine.generate(®istry, args.output.as_path(), &OutputDirective::File)? + } + None => engine.generate(&v1, args.output.as_path(), &OutputDirective::File)?, + } if !diag_msgs.is_empty() { return Err(diag_msgs); @@ -176,9 +190,10 @@ pub(crate) fn generate_params_shared( #[cfg(test)] mod tests { - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use tempdir::TempDir; + use weaver_diff::diff_dir; use crate::cli::{Cli, Commands}; use crate::registry::generate::RegistryGenerateArgs; @@ -218,6 +233,7 @@ mod tests { display_policy_coverage: false, }, future: false, + v2: false, diagnostic: Default::default(), }), })), @@ -296,6 +312,7 @@ mod tests { display_policy_coverage: false, }, future: false, + v2: false, diagnostic: Default::default(), }), })), @@ -345,6 +362,7 @@ mod tests { display_policy_coverage: false, }, future: false, + v2: false, diagnostic: Default::default(), }), })), @@ -453,6 +471,7 @@ mod tests { display_policy_coverage: false, }, future: false, + v2: false, diagnostic: Default::default(), }), })), @@ -493,4 +512,57 @@ mod tests { ); } } + + #[test] + fn test_registry_generate_v2() { + let temp_output = Path::new("tests/v2_forge/observed_output"); + + // Delete all the files in the observed_output/target directory + // before generating the new files. + std::fs::remove_dir_all(&temp_output).unwrap_or_default(); + + + + let cli = Cli { + debug: 0, + quiet: false, + future: false, + command: Some(Commands::Registry(RegistryCommand { + command: RegistrySubCommand::Generate(RegistryGenerateArgs { + target: "markdown".to_owned(), + output: temp_output.to_path_buf(), + templates: VirtualDirectoryPath::LocalFolder { + path: "tests/v2_forge/templates/".to_owned(), + }, + config: None, + param: None, + params: None, + registry: RegistryArgs { + registry: VirtualDirectoryPath::LocalFolder { + path: "tests/v2_forge/model/".to_owned(), + }, + follow_symlinks: false, + include_unreferenced: false, + }, + policy: PolicyArgs { + policies: vec![], + skip_policies: true, + display_policy_coverage: false, + }, + future: false, + v2: true, + diagnostic: Default::default(), + }), + })), + }; + + let exit_directive = run_command(&cli); + // The command should succeed. + assert_eq!(exit_directive.exit_code, 0); + + // validate expected = observed. + let expected_output = Path::new("tests/v2_forge/expected_output"); + assert!(diff_dir(expected_output, temp_output).unwrap()); + + } } diff --git a/src/registry/resolve.rs b/src/registry/resolve.rs index d7d9e9173..ba2bdf04d 100644 --- a/src/registry/resolve.rs +++ b/src/registry/resolve.rs @@ -62,7 +62,8 @@ pub(crate) fn command(args: &RegistryResolveArgs) -> Result Result<(weaver_forge::v2::registry::ForgeResolvedRegistry, Option), DiagnosticMessages> { +) -> Result< + ( + ResolvedRegistry, + weaver_forge::v2::registry::ForgeResolvedRegistry, + Option, + ), + DiagnosticMessages, +> { let registry_path = ®istry_args.registry; let main_registry_repo = RegistryRepo::try_new("main", registry_path)?; @@ -470,5 +477,5 @@ pub(crate) fn prepare_main_registry_v2( })?; let v2_resolved_registry = weaver_forge::v2::registry::ForgeResolvedRegistry::try_from_resolved_schema(v2_schema)?; - Ok((v2_resolved_registry, policy_engine)) + Ok((main_resolved_registry, v2_resolved_registry, policy_engine)) } diff --git a/tests/v2_forge/expected_output/registry.md b/tests/v2_forge/expected_output/registry.md new file mode 100644 index 000000000..d8e90270c --- /dev/null +++ b/tests/v2_forge/expected_output/registry.md @@ -0,0 +1,27 @@ +# Registry + +## Attributes + +- `attr2`: Another attribute + +- `my.attr`: A test attribute + +## Attribute Groups + +- `my`: test group + +## Events + +- `my.event`: A test event + +## Entities + +- `my.entity`: A test entity + +## Metrics + +- `my.metric`: A count of something. + +## Spans + +- `my.span`: A test span diff --git a/tests/v2_forge/model/test.yaml b/tests/v2_forge/model/test.yaml new file mode 100644 index 000000000..dddaa9832 --- /dev/null +++ b/tests/v2_forge/model/test.yaml @@ -0,0 +1,48 @@ +version: "2" +attributes: + - key: my.attr + type: string + brief: A test attribute + stability: stable + - key: attr2 + type: int + brief: Another attribute + stability: stable +attribute_groups: + - id: my + brief: test group + stability: stable + attributes: + - ref: my.attr + visibility: public +events: + - name: my.event + brief: A test event + stability: stable + attributes: + - ref: my.attr +entities: + - type: my.entity + identity: + - ref: my.attr + description: + - ref: attr2 + brief: A test entity + stability: stable +metrics: + - name: my.metric + brief: A count of something. + instrument: counter + unit: "{1}" + stability: stable + attributes: + - ref: my.attr +spans: + - type: my.span + name: + note: Some name pattern + kind: client + attributes: + - ref: my.attr + brief: A test span + stability: stable diff --git a/tests/v2_forge/templates/markdown/registry.md.j2 b/tests/v2_forge/templates/markdown/registry.md.j2 new file mode 100644 index 000000000..e62b32022 --- /dev/null +++ b/tests/v2_forge/templates/markdown/registry.md.j2 @@ -0,0 +1,26 @@ +# Registry + +## Attributes +{% for attr in ctx.attributes %} +- `{{attr.key}}`: {{attr.brief}} +{% endfor %} +## Attribute Groups +{% for g in ctx.attribute_groups %} +- `{{g.id}}`: {{g.brief}} +{% endfor %} +## Events +{% for e in ctx.signals.events %} +- `{{e.name}}`: {{e.brief}} +{% endfor %} +## Entities +{% for e in ctx.signals.entities %} +- `{{e.type}}`: {{e.brief}} +{% endfor %} +## Metrics +{% for m in ctx.signals.metrics %} +- `{{m.name}}`: {{m.brief}} +{% endfor %} +## Spans +{% for s in ctx.signals.spans %} +- `{{s.type}}`: {{s.brief}} +{% endfor %} \ No newline at end of file diff --git a/tests/v2_forge/templates/markdown/weaver.yaml b/tests/v2_forge/templates/markdown/weaver.yaml new file mode 100644 index 000000000..c0e7fbe32 --- /dev/null +++ b/tests/v2_forge/templates/markdown/weaver.yaml @@ -0,0 +1,6 @@ +templates: + - template: "registry.md.j2" + filter: "." + application_mode: single + file_name: registry.md + # TODO - application_mode: each will be broken given the split, we may need new application modes. \ No newline at end of file From 8b4a7bc8fc55d94423daaa82e1490acbac60b080 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 31 Oct 2025 15:14:02 -0400 Subject: [PATCH 24/46] cargo fmt --- src/registry/generate.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/registry/generate.rs b/src/registry/generate.rs index b4e1f6c3c..b29531549 100644 --- a/src/registry/generate.rs +++ b/src/registry/generate.rs @@ -514,15 +514,13 @@ mod tests { } #[test] - fn test_registry_generate_v2() { + fn test_registry_generate_v2() { let temp_output = Path::new("tests/v2_forge/observed_output"); - + // Delete all the files in the observed_output/target directory // before generating the new files. std::fs::remove_dir_all(&temp_output).unwrap_or_default(); - - let cli = Cli { debug: 0, quiet: false, @@ -563,6 +561,5 @@ mod tests { // validate expected = observed. let expected_output = Path::new("tests/v2_forge/expected_output"); assert!(diff_dir(expected_output, temp_output).unwrap()); - } } From ad750721c1a038ff117ce14ab59407beee806b67 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 13:37:51 -0500 Subject: [PATCH 25/46] Fix some types and copy-paste errors. --- crates/weaver_forge/src/lib.rs | 14 -------------- crates/weaver_forge/src/v2/entity.rs | 12 ++++-------- crates/weaver_forge/src/v2/event.rs | 16 ++++++---------- crates/weaver_forge/src/v2/metric.rs | 8 ++++---- crates/weaver_forge/src/v2/span.rs | 4 ---- crates/weaver_resolved_schema/src/v2/entity.rs | 2 +- crates/weaver_resolved_schema/src/v2/event.rs | 8 ++++---- crates/weaver_resolved_schema/src/v2/metric.rs | 8 ++++---- 8 files changed, 23 insertions(+), 49 deletions(-) diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 119745f71..e2d8bf1fa 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -967,20 +967,6 @@ mod tests { .expect("Failed to generate registry assets"); assert!(diff_dir("expected_output/test", "observed_output/test").unwrap()); - - // TODO - Remove this. - let schema: weaver_resolved_schema::v2::ResolvedTelemetrySchema = - schema.try_into().unwrap(); - fs::write( - "observed_output/test/v2_registry.yaml", - serde_yaml::to_string(&schema.registry).unwrap(), - ) - .unwrap(); - fs::write( - "observed_output/test/v2_refinements.yaml", - serde_yaml::to_string(&schema.refinements).unwrap(), - ) - .unwrap(); } #[test] diff --git a/crates/weaver_forge/src/v2/entity.rs b/crates/weaver_forge/src/v2/entity.rs index 8902badab..ea26f8ae4 100644 --- a/crates/weaver_forge/src/v2/entity.rs +++ b/crates/weaver_forge/src/v2/entity.rs @@ -9,19 +9,19 @@ use weaver_semconv::{ use crate::v2::attribute::Attribute; -/// The definition of an event signal. +/// The definition of an entity signal. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Entity { /// The type of the entity. pub r#type: SignalId, - /// List of attributes that belong to this event. + /// List of attributes that identify this entity. #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub identity: Vec, - /// List of attributes that belong to this event. + /// List of attributes that describe to this entity. #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub description: Vec, @@ -31,7 +31,7 @@ pub struct Entity { pub common: CommonFields, } -/// A special type of reference to attributes that remembers event-specicific information. +/// A special type of reference to attributes that remembers entity-specicific information. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct EntityAttribute { @@ -43,9 +43,5 @@ pub struct EntityAttribute { /// the attribute is "recommended". When set to /// "conditionally_required", the string provided as MUST /// specify the conditions under which the attribute is required. - /// - /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will - /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present - /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. pub requirement_level: RequirementLevel, } diff --git a/crates/weaver_forge/src/v2/event.rs b/crates/weaver_forge/src/v2/event.rs index 7de2a1033..aa98b0fa4 100644 --- a/crates/weaver_forge/src/v2/event.rs +++ b/crates/weaver_forge/src/v2/event.rs @@ -21,9 +21,9 @@ pub struct Event { #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, - /// Which resources this span should be associated with. + /// Which resources this event should be associated with. /// - /// This list is an "any of" list, where a metric may be associated with one or more entities, but should + /// This list is an "any of" list, where a event may be associated with one or more entities, but should /// be associated with at least one in this list. #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] @@ -46,10 +46,6 @@ pub struct EventAttribute { /// the attribute is "recommended". When set to /// "conditionally_required", the string provided as MUST /// specify the conditions under which the attribute is required. - /// - /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will - /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present - /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. pub requirement_level: RequirementLevel, } @@ -64,10 +60,10 @@ pub struct EventRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `type` to refer - // to the underlying span defintiion, but override all fields here. - // We probably should copy-paste all the "span" attributes here - // including the `ty` - /// The definition of the metric refinement. + // to the underlying event defintiion, but override all fields here. + // We probably should copy-paste all the "event" attributes here + // including the `name`. + /// The definition of the event refinement. #[serde(flatten)] pub event: Event, } diff --git a/crates/weaver_forge/src/v2/metric.rs b/crates/weaver_forge/src/v2/metric.rs index fab44572d..79160ece0 100644 --- a/crates/weaver_forge/src/v2/metric.rs +++ b/crates/weaver_forge/src/v2/metric.rs @@ -29,7 +29,7 @@ pub struct Metric { #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? - /// Which resources this span should be associated with. + /// Which resources this metric should be associated with. /// /// This list is an "any of" list, where a metric may be associated with one or more entities, but should /// be associated with at least one in this list. @@ -42,7 +42,7 @@ pub struct Metric { pub common: CommonFields, } -/// A special type of reference to attributes that remembers metric-specicific information. +/// A special type of reference to attributes that remembers metric-specific information. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct MetricAttribute { @@ -72,8 +72,8 @@ pub struct MetricRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `type` to refer - // to the underlying span defintiion, but override all fields here. - // We probably should copy-paste all the "span" attributes here + // to the underlying metric defintiion, but override all fields here. + // We probably should copy-paste all the "metric" attributes here // including the `ty` /// The definition of the metric refinement. #[serde(flatten)] diff --git a/crates/weaver_forge/src/v2/span.rs b/crates/weaver_forge/src/v2/span.rs index 090139e3f..2827114e8 100644 --- a/crates/weaver_forge/src/v2/span.rs +++ b/crates/weaver_forge/src/v2/span.rs @@ -49,10 +49,6 @@ pub struct SpanAttribute { /// the attribute is "recommended". When set to /// "conditionally_required", the string provided as MUST /// specify the conditions under which the attribute is required. - /// - /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will - /// create timeseries with these attributes, but for any given timeseries instance, the attributes that *were* present - /// should *remain* present. That is - a metric timeseries cannot drop attributes during its lifetime. pub requirement_level: RequirementLevel, /// Specifies if the attribute is (especially) relevant for sampling diff --git a/crates/weaver_resolved_schema/src/v2/entity.rs b/crates/weaver_resolved_schema/src/v2/entity.rs index d7a7284d2..a37ab406f 100644 --- a/crates/weaver_resolved_schema/src/v2/entity.rs +++ b/crates/weaver_resolved_schema/src/v2/entity.rs @@ -1,4 +1,4 @@ -//! Event related definition structs. +//! Entity related definition structs. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/weaver_resolved_schema/src/v2/event.rs b/crates/weaver_resolved_schema/src/v2/event.rs index be4cd250d..b103c4ca8 100644 --- a/crates/weaver_resolved_schema/src/v2/event.rs +++ b/crates/weaver_resolved_schema/src/v2/event.rs @@ -22,9 +22,9 @@ pub struct Event { pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? - /// Which resources this span should be associated with. + /// Which resources this event should be associated with. /// - /// This list is an "any of" list, where a metric may be associated with one or more entities, but should + /// This list is an "any of" list, where a event may be associated with one or more entities, but should /// be associated with at least one in this list. #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] @@ -60,8 +60,8 @@ pub struct EventRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `name` to refer - // to the underlying span defintiion, but override all fields here. - // We probably should copy-paste all the "span" attributes here + // to the underlying event defintiion, but override all fields here. + // We probably should copy-paste all the "event" attributes here // including the `ty` /// The definition of the event refinement. #[serde(flatten)] diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs index cf74dbd33..05f9f1b73 100644 --- a/crates/weaver_resolved_schema/src/v2/metric.rs +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -29,7 +29,7 @@ pub struct Metric { #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? - /// Which resources this span should be associated with. + /// Which resources this metric should be associated with. /// /// This list is an "any of" list, where a metric may be associated with one or more entities, but should /// be associated with at least one in this list. @@ -70,9 +70,9 @@ pub struct MetricRefinement { /// The identity of the refinement. pub id: SignalId, - // TODO - This is a lazy way of doing this. We use `type` to refer - // to the underlying span defintiion, but override all fields here. - // We probably should copy-paste all the "span" attributes here + // TODO - This is a lazy way of doing this. We use `name` to refer + // to the underlying metric defintiion, but override all fields here. + // We probably should copy-paste all the "metric" attributes here // including the `ty` /// The definition of the metric refinement. #[serde(flatten)] From b72b7544d3083978b3380030dcc09e63f1c76dd0 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 13:46:30 -0500 Subject: [PATCH 26/46] Fix typos. --- crates/weaver_resolved_schema/src/v2/event.rs | 2 +- crates/weaver_resolved_schema/src/v2/metric.rs | 2 +- crates/weaver_resolved_schema/src/v2/span.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/event.rs b/crates/weaver_resolved_schema/src/v2/event.rs index b103c4ca8..f64d392c3 100644 --- a/crates/weaver_resolved_schema/src/v2/event.rs +++ b/crates/weaver_resolved_schema/src/v2/event.rs @@ -22,7 +22,7 @@ pub struct Event { pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? - /// Which resources this event should be associated with. + /// Which entities this event should be associated with. /// /// This list is an "any of" list, where a event may be associated with one or more entities, but should /// be associated with at least one in this list. diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs index 05f9f1b73..b13bf4908 100644 --- a/crates/weaver_resolved_schema/src/v2/metric.rs +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -29,7 +29,7 @@ pub struct Metric { #[serde(skip_serializing_if = "Vec::is_empty")] pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? - /// Which resources this metric should be associated with. + /// Which entities this metric should be associated with. /// /// This list is an "any of" list, where a metric may be associated with one or more entities, but should /// be associated with at least one in this list. diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs index 6999c87f8..b61c55f81 100644 --- a/crates/weaver_resolved_schema/src/v2/span.rs +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -28,7 +28,7 @@ pub struct Span { pub attributes: Vec, // TODO - Should Entity Associations be "strong" links? - /// Which resources this span should be associated with. + /// Which entities this span should be associated with. /// /// This list is an "any of" list, where a span may be associated with one or more entities, but should /// be associated with at least one in this list. From fb4a367e358e48deb8629b62ddaeacb36ca789b4 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 13:50:15 -0500 Subject: [PATCH 27/46] More spell fixes. --- crates/weaver_forge/src/v2/metric.rs | 2 +- crates/weaver_forge/src/v2/span.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/weaver_forge/src/v2/metric.rs b/crates/weaver_forge/src/v2/metric.rs index 79160ece0..44fcdccf1 100644 --- a/crates/weaver_forge/src/v2/metric.rs +++ b/crates/weaver_forge/src/v2/metric.rs @@ -46,7 +46,7 @@ pub struct Metric { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct MetricAttribute { - /// Base metric definitions. + /// Base attribute definitions. #[serde(flatten)] pub base: Attribute, /// Specifies if the attribute is mandatory. Can be "required", diff --git a/crates/weaver_forge/src/v2/span.rs b/crates/weaver_forge/src/v2/span.rs index 2827114e8..48d2ba89b 100644 --- a/crates/weaver_forge/src/v2/span.rs +++ b/crates/weaver_forge/src/v2/span.rs @@ -68,7 +68,7 @@ pub struct SpanRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `type` to refer - // to the underlying span defintiion, but override all fields here. + // to the underlying span definition, but override all fields here. // We probably should copy-paste all the "span" attributes here // including the `ty` /// The definition of the metric refinement. From dff77691d3c70ca40a1165af8c01beeb120c23aa Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 13:55:32 -0500 Subject: [PATCH 28/46] Fix typos. --- crates/weaver_forge/src/v2/event.rs | 4 ++-- crates/weaver_forge/src/v2/metric.rs | 2 +- crates/weaver_resolved_schema/src/v2/mod.rs | 3 +-- crates/weaver_resolved_schema/src/v2/refinements.rs | 2 +- src/registry/resolve.rs | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/weaver_forge/src/v2/event.rs b/crates/weaver_forge/src/v2/event.rs index aa98b0fa4..a453e7ac5 100644 --- a/crates/weaver_forge/src/v2/event.rs +++ b/crates/weaver_forge/src/v2/event.rs @@ -49,7 +49,7 @@ pub struct EventAttribute { pub requirement_level: RequirementLevel, } -/// A refinement of an event signal, for use in code-gen or specific library application. +/// A refinement of an event signal, for use in code generation or specific library application. /// /// A refinement represents a "view" of an Event that is highly optimised for a particular implementation. /// e.g. for HTTP events, there may be a refinement that provides only the necessary information for dealing with Java's HTTP @@ -60,7 +60,7 @@ pub struct EventRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `type` to refer - // to the underlying event defintiion, but override all fields here. + // to the underlying event definition, but override all fields here. // We probably should copy-paste all the "event" attributes here // including the `name`. /// The definition of the event refinement. diff --git a/crates/weaver_forge/src/v2/metric.rs b/crates/weaver_forge/src/v2/metric.rs index 44fcdccf1..c173b8d56 100644 --- a/crates/weaver_forge/src/v2/metric.rs +++ b/crates/weaver_forge/src/v2/metric.rs @@ -72,7 +72,7 @@ pub struct MetricRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `type` to refer - // to the underlying metric defintiion, but override all fields here. + // to the underlying metric definition, but override all fields here. // We probably should copy-paste all the "metric" attributes here // including the `ty` /// The definition of the metric refinement. diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 66faf5043..560c92ab9 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -13,7 +13,6 @@ use weaver_semconv::{ }; use crate::{ - registry::Group, v2::{ attribute_group::AttributeGroup, catalog::Catalog, @@ -182,7 +181,7 @@ pub fn convert_v1_to_v2( span, }); } else { - // unwrap should be safe becasue we verified this is a refinement earlier. + // unwrap should be safe because we verified this is a refinement earlier. let span_type = g .lineage .as_ref() diff --git a/crates/weaver_resolved_schema/src/v2/refinements.rs b/crates/weaver_resolved_schema/src/v2/refinements.rs index 2bd006891..29f01f58b 100644 --- a/crates/weaver_resolved_schema/src/v2/refinements.rs +++ b/crates/weaver_resolved_schema/src/v2/refinements.rs @@ -16,7 +16,7 @@ use crate::v2::{event::EventRefinement, metric::MetricRefinement, span::SpanRefi /// Note: Refinements will always include a "base" refinement for every signal definition. /// For example, if a Metric signal named `my_metric` is defined, there will be /// a metric refinement named `my_metric` as well. -/// This allows codegen to *only* interact with refinements, if desired, to +/// This allows code generation to *only* interact with refinements, if desired, to /// provide optimised methods for generating telemetry signals. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] diff --git a/src/registry/resolve.rs b/src/registry/resolve.rs index ba2bdf04d..643e431db 100644 --- a/src/registry/resolve.rs +++ b/src/registry/resolve.rs @@ -39,7 +39,7 @@ pub struct RegistryResolveArgs { #[arg(short, long, default_value = "yaml")] format: Format, - // TODO - Figure out long term plan for verisons here. + // TODO - Figure out long term plan for versions here. /// Whether or not to output version 2 of the schema. #[arg(long, default_value = "false")] v2: bool, From c56256f22af8e415431fd46b62fd842100a3b41d Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 13:58:23 -0500 Subject: [PATCH 29/46] Fix tests. --- crates/weaver_forge/src/registry.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/weaver_forge/src/registry.rs b/crates/weaver_forge/src/registry.rs index 938924467..01612647d 100644 --- a/crates/weaver_forge/src/registry.rs +++ b/crates/weaver_forge/src/registry.rs @@ -297,6 +297,7 @@ mod tests { body: None, entity_associations: vec![], annotations: None, + visibility: None, }, Group { id: "apple.group".to_owned(), @@ -319,6 +320,7 @@ mod tests { body: None, entity_associations: vec![], annotations: None, + visibility: None, }, Group { id: "middle.group".to_owned(), @@ -341,6 +343,7 @@ mod tests { body: None, entity_associations: vec![], annotations: None, + visibility: None, }, ], }; From 374c493a48f8124d3a37c85c2736fb81a2ed98e4 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 15:14:01 -0500 Subject: [PATCH 30/46] Fix format. --- crates/weaver_resolved_schema/src/v2/mod.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 560c92ab9..3c4a27175 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -12,16 +12,14 @@ use weaver_semconv::{ }, }; -use crate::{ - v2::{ - attribute_group::AttributeGroup, - catalog::Catalog, - entity::Entity, - metric::Metric, - refinements::Refinements, - registry::Registry, - span::{Span, SpanRefinement}, - }, +use crate::v2::{ + attribute_group::AttributeGroup, + catalog::Catalog, + entity::Entity, + metric::Metric, + refinements::Refinements, + registry::Registry, + span::{Span, SpanRefinement}, }; pub mod attribute; From d1a61fa7c017767e58b2c45a5716c703ad8cd26a Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 15:43:41 -0500 Subject: [PATCH 31/46] Clippy fixes. --- crates/weaver_forge/src/v2/registry.rs | 2 +- crates/weaver_resolved_schema/src/v2/mod.rs | 30 ++++++++++++++++----- src/registry/generate.rs | 4 +-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 47857486a..f17f36241 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -380,7 +380,7 @@ impl ForgeResolvedRegistry { .attributes .iter() .filter_map(|ar| { - let attr = attribute_lookup(&ar).map(|a| Attribute { + let attr = attribute_lookup(ar).map(|a| Attribute { key: a.key.clone(), r#type: a.r#type.clone(), examples: a.examples.clone(), diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 3c4a27175..6bce1c53a 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -185,7 +185,7 @@ pub fn convert_v1_to_v2( .as_ref() .and_then(|l| l.extends_group.as_ref()) .map(|id| fix_span_group_id(id)) - .unwrap(); + .expect("Refinement extraction issue - this is a logic bug"); span_refinements.push(SpanRefinement { id: fix_span_group_id(&g.id), span: Span { @@ -234,7 +234,11 @@ pub fn convert_v1_to_v2( } } let event = event::Event { - name: g.name.clone().unwrap().into(), + name: g + .name + .clone() + .expect("Name must exist on events prior to translation to v2") + .into(), attributes: event_attributes, entity_associations: g.entity_associations.clone(), common: CommonFields { @@ -283,9 +287,19 @@ pub fn convert_v1_to_v2( } // TODO - deal with unwrap errors. let metric = Metric { - name: g.metric_name.clone().unwrap().into(), - instrument: g.instrument.clone().unwrap(), - unit: g.unit.clone().unwrap(), + name: g + .metric_name + .clone() + .expect("metric_name must exist on metrics prior to translation to v2") + .into(), + instrument: g + .instrument + .clone() + .expect("instrument must exist on metrics prior to translation to v2"), + unit: g + .unit + .clone() + .expect("unit must exist on metrics prior to translation to v2"), attributes: metric_attributes, entity_associations: g.entity_associations.clone(), common: CommonFields { @@ -515,7 +529,8 @@ mod tests { ], }; - let (v2_registry, v2_refinements) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); + let (v2_registry, v2_refinements) = + convert_v1_to_v2(v1_catalog, v1_registry).expect("Failed to convert v1 to v2"); // assert only ONE attribute due to sharing. assert_eq!(v2_registry.attributes.len(), 1); // assert attribute fields not shared show up on ref in span. @@ -638,7 +653,8 @@ mod tests { ], }; - let (v2_registry, v2_refinements) = convert_v1_to_v2(v1_catalog, v1_registry).unwrap(); + let (v2_registry, v2_refinements) = + convert_v1_to_v2(v1_catalog, v1_registry).expect("Failed to convert v1 to v2"); // assert only ONE attribute due to sharing. assert_eq!(v2_registry.attributes.len(), 1); // assert attribute fields not shared show up on ref in span. diff --git a/src/registry/generate.rs b/src/registry/generate.rs index b29531549..618ba8cd4 100644 --- a/src/registry/generate.rs +++ b/src/registry/generate.rs @@ -124,7 +124,7 @@ pub(crate) fn command(args: &RegistryGenerateArgs) -> Result { - engine.generate(®istry, args.output.as_path(), &OutputDirective::File)? + engine.generate(®istry, args.output.as_path(), &OutputDirective::File)?; } None => engine.generate(&v1, args.output.as_path(), &OutputDirective::File)?, } @@ -519,7 +519,7 @@ mod tests { // Delete all the files in the observed_output/target directory // before generating the new files. - std::fs::remove_dir_all(&temp_output).unwrap_or_default(); + std::fs::remove_dir_all(temp_output).unwrap_or_default(); let cli = Cli { debug: 0, From 4d22341af05ab6b3c337e9ceff9e4f278c1fdfaa Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 16:04:55 -0500 Subject: [PATCH 32/46] Fix last clippy issue. --- crates/weaver_resolved_schema/src/v2/catalog.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index b656051e1..9be2479bd 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -52,7 +52,7 @@ impl Catalog { self.lookup .get(&attribute.name)? .iter() - .filter_map(|idx| { + .find_map(|idx| { self.attributes .get(*idx) .filter(|a| { @@ -75,6 +75,5 @@ impl Catalog { }) .map(|_| AttributeRef(*idx as u32)) }) - .next() } } From bd681a6f772201af0c2dcb12895b8c3e2d565ff9 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 16:07:07 -0500 Subject: [PATCH 33/46] Fix spelling issues. --- crates/weaver_resolved_schema/src/v2/event.rs | 2 +- crates/weaver_resolved_schema/src/v2/metric.rs | 2 +- crates/weaver_resolved_schema/src/v2/refinements.rs | 2 +- crates/weaver_resolved_schema/src/v2/span.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/event.rs b/crates/weaver_resolved_schema/src/v2/event.rs index f64d392c3..0bb7b4698 100644 --- a/crates/weaver_resolved_schema/src/v2/event.rs +++ b/crates/weaver_resolved_schema/src/v2/event.rs @@ -60,7 +60,7 @@ pub struct EventRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `name` to refer - // to the underlying event defintiion, but override all fields here. + // to the underlying event definition, but override all fields here. // We probably should copy-paste all the "event" attributes here // including the `ty` /// The definition of the event refinement. diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs index b13bf4908..d98bd3dec 100644 --- a/crates/weaver_resolved_schema/src/v2/metric.rs +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -71,7 +71,7 @@ pub struct MetricRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `name` to refer - // to the underlying metric defintiion, but override all fields here. + // to the underlying metric definition, but override all fields here. // We probably should copy-paste all the "metric" attributes here // including the `ty` /// The definition of the metric refinement. diff --git a/crates/weaver_resolved_schema/src/v2/refinements.rs b/crates/weaver_resolved_schema/src/v2/refinements.rs index 29f01f58b..d93f53d15 100644 --- a/crates/weaver_resolved_schema/src/v2/refinements.rs +++ b/crates/weaver_resolved_schema/src/v2/refinements.rs @@ -10,7 +10,7 @@ use crate::v2::{event::EventRefinement, metric::MetricRefinement, span::SpanRefi /// Refinements are a specialization of a signal that can be used to optimise documentation, /// or code generation. A refinement will *always* match the conventions defined by the /// signal it refines. Refinements cannot be inferred from signals over the wire (e.g. OTLP). -/// This is because any identifying feature of a refinement is used purely for codgen but has +/// This is because any identifying feature of a refinement is used purely for codegen but has /// no storage location in OTLP. /// /// Note: Refinements will always include a "base" refinement for every signal definition. diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs index b61c55f81..ddd190aea 100644 --- a/crates/weaver_resolved_schema/src/v2/span.rs +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -70,7 +70,7 @@ pub struct SpanRefinement { pub id: SignalId, // TODO - This is a lazy way of doing this. We use `type` to refer - // to the underlying span defintiion, but override all fields here. + // to the underlying span definition, but override all fields here. // We probably should copy-paste all the "span" attributes here // including the `ty` /// The definition of the span refinement. From ce701d248ae9acc29ce28e2344531650935cc43e Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 3 Nov 2025 16:07:56 -0500 Subject: [PATCH 34/46] Cargo fmt --- .../weaver_resolved_schema/src/v2/catalog.rs | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index 9be2479bd..c13ec77fb 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -49,31 +49,28 @@ impl Catalog { // Note - we do a fast lookup to contentious attributes, // then linear scan of attributes with same key but different // other aspects. - self.lookup - .get(&attribute.name)? - .iter() - .find_map(|idx| { - self.attributes - .get(*idx) - .filter(|a| { - a.key == attribute.name - && a.r#type == attribute.r#type - && a.examples == attribute.examples - && a.common.brief == attribute.brief - && a.common.note == attribute.note - && a.common.deprecated == attribute.deprecated - && attribute - .stability - .as_ref() - .map(|s| a.common.stability == *s) - .unwrap_or(false) - && attribute - .annotations - .as_ref() - .map(|ans| a.common.annotations == *ans) - .unwrap_or(false) - }) - .map(|_| AttributeRef(*idx as u32)) - }) + self.lookup.get(&attribute.name)?.iter().find_map(|idx| { + self.attributes + .get(*idx) + .filter(|a| { + a.key == attribute.name + && a.r#type == attribute.r#type + && a.examples == attribute.examples + && a.common.brief == attribute.brief + && a.common.note == attribute.note + && a.common.deprecated == attribute.deprecated + && attribute + .stability + .as_ref() + .map(|s| a.common.stability == *s) + .unwrap_or(false) + && attribute + .annotations + .as_ref() + .map(|ans| a.common.annotations == *ans) + .unwrap_or(false) + }) + .map(|_| AttributeRef(*idx as u32)) + }) } } From c69a92adf049fb0cbb52c5fefd9f2f01e0b6e86f Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 4 Nov 2025 08:52:58 -0500 Subject: [PATCH 35/46] Fix logic error in comparing annotations. --- Cargo.lock | 1 + crates/weaver_resolved_schema/Cargo.toml | 1 + .../weaver_resolved_schema/src/v2/catalog.rs | 78 ++++++++++++++++++- crates/weaver_resolved_schema/src/v2/mod.rs | 3 + 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 922175881..14f7b11cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5466,6 +5466,7 @@ dependencies = [ name = "weaver_resolved_schema" version = "0.18.0" dependencies = [ + "log", "ordered-float", "schemars", "serde", diff --git a/crates/weaver_resolved_schema/Cargo.toml b/crates/weaver_resolved_schema/Cargo.toml index 72f75feab..edb842f42 100644 --- a/crates/weaver_resolved_schema/Cargo.toml +++ b/crates/weaver_resolved_schema/Cargo.toml @@ -19,6 +19,7 @@ thiserror.workspace = true serde.workspace = true ordered-float.workspace = true schemars.workspace = true +log.workspace = true [dev-dependencies] serde_json.workspace = true diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index c13ec77fb..299914479 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -68,9 +68,85 @@ impl Catalog { .annotations .as_ref() .map(|ans| a.common.annotations == *ans) - .unwrap_or(false) + .unwrap_or(a.common.annotations.is_empty()) }) .map(|_| AttributeRef(*idx as u32)) }) } } + + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + + use weaver_semconv::attribute::{BasicRequirementLevelSpec, RequirementLevel}; + use weaver_semconv::{attribute::AttributeType, stability::Stability}; + + use crate::v2::attribute::Attribute; + use crate::v2::CommonFields; + use super::Catalog; + + #[test] + fn test_lookup_works() { + let key = "test.key".to_owned(); + let atype = AttributeType::PrimitiveOrArray(weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String); + let brief = "brief".to_owned(); + let note = "note".to_owned(); + let stability = Stability::Stable; + let annotations = BTreeMap::new(); + let catalog = Catalog::from_attributes(vec![ + Attribute { + key: key.clone(), + r#type: atype.clone(), + examples: None, + common: CommonFields { + brief: brief.clone(), + note: note.clone(), + stability: stability.clone(), + deprecated: None, + annotations: annotations.clone(), + }, + }, + ]); + + let result = catalog.convert_ref(&crate::attribute::Attribute { + name: key.clone(), + r#type: atype.clone(), + brief: brief.clone(), + examples: None, + tag: None, + requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required), + sampling_relevant: Some(true), + note: note.clone(), + stability: Some(stability.clone()), + deprecated: None, + prefix: false, + tags: None, + annotations: Some(annotations.clone()), + value: None, + role: None, + }); + assert_eq!(result.is_some(), true); + + // Make sure "none" annotations is the same as empty annotations. + let result2 = catalog.convert_ref(&crate::attribute::Attribute { + name: key.clone(), + r#type: atype.clone(), + brief: brief.clone(), + examples: None, + tag: None, + requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required), + sampling_relevant: Some(true), + note: note.clone(), + stability: Some(stability.clone()), + deprecated: None, + prefix: false, + tags: None, + annotations: None, + value: None, + role: None, + }); + assert_eq!(result2.is_some(), true); + } +} \ No newline at end of file diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 6bce1c53a..657f275de 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -147,6 +147,7 @@ pub fn convert_v1_to_v2( }); } else { // TODO logic error! + log::info!("Logic failue - unable to convert attribute {attr:?}"); } } if !is_refinement { @@ -231,6 +232,7 @@ pub fn convert_v1_to_v2( }); } else { // TODO logic error! + log::info!("Logic failue - unable to convert attribute {attr:?}"); } } let event = event::Event { @@ -283,6 +285,7 @@ pub fn convert_v1_to_v2( }); } else { // TODO logic error! + log::info!("Logic failue - unable to convert attribute {attr:?}"); } } // TODO - deal with unwrap errors. From aa533dbb23ec035647961c38217860ddce70af91 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 12 Nov 2025 08:38:32 -0500 Subject: [PATCH 36/46] Rust fmt. --- .../weaver_resolved_schema/src/v2/catalog.rs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index 299914479..bab870038 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -75,7 +75,6 @@ impl Catalog { } } - #[cfg(test)] mod test { use std::collections::BTreeMap; @@ -83,32 +82,32 @@ mod test { use weaver_semconv::attribute::{BasicRequirementLevelSpec, RequirementLevel}; use weaver_semconv::{attribute::AttributeType, stability::Stability}; + use super::Catalog; use crate::v2::attribute::Attribute; use crate::v2::CommonFields; - use super::Catalog; #[test] fn test_lookup_works() { let key = "test.key".to_owned(); - let atype = AttributeType::PrimitiveOrArray(weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String); + let atype = AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ); let brief = "brief".to_owned(); let note = "note".to_owned(); let stability = Stability::Stable; let annotations = BTreeMap::new(); - let catalog = Catalog::from_attributes(vec![ - Attribute { - key: key.clone(), - r#type: atype.clone(), - examples: None, - common: CommonFields { - brief: brief.clone(), - note: note.clone(), - stability: stability.clone(), - deprecated: None, - annotations: annotations.clone(), - }, + let catalog = Catalog::from_attributes(vec![Attribute { + key: key.clone(), + r#type: atype.clone(), + examples: None, + common: CommonFields { + brief: brief.clone(), + note: note.clone(), + stability: stability.clone(), + deprecated: None, + annotations: annotations.clone(), }, - ]); + }]); let result = catalog.convert_ref(&crate::attribute::Attribute { name: key.clone(), @@ -149,4 +148,4 @@ mod test { }); assert_eq!(result2.is_some(), true); } -} \ No newline at end of file +} From 9492fe1ac0e767c702702c246db535555f196e82 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 12 Nov 2025 08:41:48 -0500 Subject: [PATCH 37/46] Fix clippy. --- crates/weaver_resolved_schema/src/v2/catalog.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/catalog.rs b/crates/weaver_resolved_schema/src/v2/catalog.rs index bab870038..18210e723 100644 --- a/crates/weaver_resolved_schema/src/v2/catalog.rs +++ b/crates/weaver_resolved_schema/src/v2/catalog.rs @@ -126,7 +126,7 @@ mod test { value: None, role: None, }); - assert_eq!(result.is_some(), true); + assert!(result.is_some()); // Make sure "none" annotations is the same as empty annotations. let result2 = catalog.convert_ref(&crate::attribute::Attribute { @@ -146,6 +146,6 @@ mod test { value: None, role: None, }); - assert_eq!(result2.is_some(), true); + assert!(result2.is_some()); } } From e6e2918c2be57e22f0e97d3c67d7183584cf8a6f Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 12 Nov 2025 08:47:12 -0500 Subject: [PATCH 38/46] Fix docs. --- crates/weaver_forge/src/v2/entity.rs | 2 +- crates/weaver_forge/src/v2/event.rs | 2 +- crates/weaver_forge/src/v2/metric.rs | 2 +- crates/weaver_forge/src/v2/span.rs | 2 +- crates/weaver_resolved_schema/src/v2/entity.rs | 2 +- crates/weaver_resolved_schema/src/v2/event.rs | 2 +- crates/weaver_resolved_schema/src/v2/metric.rs | 2 +- crates/weaver_resolved_schema/src/v2/span.rs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/weaver_forge/src/v2/entity.rs b/crates/weaver_forge/src/v2/entity.rs index ea26f8ae4..249e7f713 100644 --- a/crates/weaver_forge/src/v2/entity.rs +++ b/crates/weaver_forge/src/v2/entity.rs @@ -41,7 +41,7 @@ pub struct EntityAttribute { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition`` MUST /// specify the conditions under which the attribute is required. pub requirement_level: RequirementLevel, } diff --git a/crates/weaver_forge/src/v2/event.rs b/crates/weaver_forge/src/v2/event.rs index a453e7ac5..bd264110e 100644 --- a/crates/weaver_forge/src/v2/event.rs +++ b/crates/weaver_forge/src/v2/event.rs @@ -44,7 +44,7 @@ pub struct EventAttribute { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition` MUST /// specify the conditions under which the attribute is required. pub requirement_level: RequirementLevel, } diff --git a/crates/weaver_forge/src/v2/metric.rs b/crates/weaver_forge/src/v2/metric.rs index c173b8d56..abc2d0b79 100644 --- a/crates/weaver_forge/src/v2/metric.rs +++ b/crates/weaver_forge/src/v2/metric.rs @@ -52,7 +52,7 @@ pub struct MetricAttribute { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition`` MUST /// specify the conditions under which the attribute is required. /// /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will diff --git a/crates/weaver_forge/src/v2/span.rs b/crates/weaver_forge/src/v2/span.rs index 48d2ba89b..4f180ec1c 100644 --- a/crates/weaver_forge/src/v2/span.rs +++ b/crates/weaver_forge/src/v2/span.rs @@ -47,7 +47,7 @@ pub struct SpanAttribute { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition` MUST /// specify the conditions under which the attribute is required. pub requirement_level: RequirementLevel, diff --git a/crates/weaver_resolved_schema/src/v2/entity.rs b/crates/weaver_resolved_schema/src/v2/entity.rs index a37ab406f..ce0ef4468 100644 --- a/crates/weaver_resolved_schema/src/v2/entity.rs +++ b/crates/weaver_resolved_schema/src/v2/entity.rs @@ -37,7 +37,7 @@ pub struct EntityAttributeRef { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition` MUST /// specify the conditions under which the attribute is required. pub requirement_level: RequirementLevel, } diff --git a/crates/weaver_resolved_schema/src/v2/event.rs b/crates/weaver_resolved_schema/src/v2/event.rs index 0bb7b4698..6aa361be1 100644 --- a/crates/weaver_resolved_schema/src/v2/event.rs +++ b/crates/weaver_resolved_schema/src/v2/event.rs @@ -44,7 +44,7 @@ pub struct EventAttributeRef { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition` MUST /// specify the conditions under which the attribute is required. pub requirement_level: RequirementLevel, } diff --git a/crates/weaver_resolved_schema/src/v2/metric.rs b/crates/weaver_resolved_schema/src/v2/metric.rs index d98bd3dec..78446af07 100644 --- a/crates/weaver_resolved_schema/src/v2/metric.rs +++ b/crates/weaver_resolved_schema/src/v2/metric.rs @@ -51,7 +51,7 @@ pub struct MetricAttributeRef { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition` MUST /// specify the conditions under which the attribute is required. /// /// Note: For attributes that are "recommended" or "opt-in" - not all metric source will diff --git a/crates/weaver_resolved_schema/src/v2/span.rs b/crates/weaver_resolved_schema/src/v2/span.rs index ddd190aea..86e87ea47 100644 --- a/crates/weaver_resolved_schema/src/v2/span.rs +++ b/crates/weaver_resolved_schema/src/v2/span.rs @@ -50,7 +50,7 @@ pub struct SpanAttributeRef { /// Specifies if the attribute is mandatory. Can be "required", /// "conditionally_required", "recommended" or "opt_in". When omitted, /// the attribute is "recommended". When set to - /// "conditionally_required", the string provided as MUST + /// "conditionally_required", the string provided as `condition` MUST /// specify the conditions under which the attribute is required. pub requirement_level: RequirementLevel, /// Specifies if the attribute is (especially) relevant for sampling From c1f47767b3c108f032db42b5999f9c87509d45fe Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 12 Nov 2025 08:49:05 -0500 Subject: [PATCH 39/46] Fix typo. --- crates/weaver_resolved_schema/src/v2/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index 657f275de..e786cf758 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -147,7 +147,7 @@ pub fn convert_v1_to_v2( }); } else { // TODO logic error! - log::info!("Logic failue - unable to convert attribute {attr:?}"); + log::info!("Logic failure - unable to convert attribute {attr:?}"); } } if !is_refinement { @@ -232,7 +232,7 @@ pub fn convert_v1_to_v2( }); } else { // TODO logic error! - log::info!("Logic failue - unable to convert attribute {attr:?}"); + log::info!("Logic failure - unable to convert attribute {attr:?}"); } } let event = event::Event { @@ -285,7 +285,7 @@ pub fn convert_v1_to_v2( }); } else { // TODO logic error! - log::info!("Logic failue - unable to convert attribute {attr:?}"); + log::info!("Logic failure - unable to convert attribute {attr:?}"); } } // TODO - deal with unwrap errors. From d742086d2b5cb631104051a20fd54d4aa1114583 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 12 Nov 2025 09:14:29 -0500 Subject: [PATCH 40/46] Add more tests for conversions. --- crates/weaver_resolved_schema/src/v2/mod.rs | 145 ++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/crates/weaver_resolved_schema/src/v2/mod.rs b/crates/weaver_resolved_schema/src/v2/mod.rs index e786cf758..5f73d8fe5 100644 --- a/crates/weaver_resolved_schema/src/v2/mod.rs +++ b/crates/weaver_resolved_schema/src/v2/mod.rs @@ -678,4 +678,149 @@ mod tests { vec!["http".to_owned(), "http.custom".to_owned()] ); } + + #[test] + fn test_convert_event_v1_to_v2() { + let mut v1_catalog = crate::catalog::Catalog::from_attributes(vec![]); + let test_refs = v1_catalog.add_attributes([Attribute { + name: "test.key".to_owned(), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), + brief: "".to_owned(), + examples: None, + tag: None, + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + sampling_relevant: None, + note: "".to_owned(), + stability: Some(Stability::Stable), + deprecated: None, + prefix: false, + tags: None, + annotations: None, + value: None, + role: None, + }]); + let v1_registry = crate::registry::Registry { + registry_url: "my.schema.url".to_owned(), + groups: vec![Group { + id: "event.my-event".to_owned(), + r#type: GroupType::Event, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[0]], + span_kind: None, + events: vec![], + metric_name: None, + instrument: None, + unit: None, + name: Some("my-event".to_owned()), + lineage: None, + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + visibility: None, + }], + }; + + let (v2_registry, _) = + convert_v1_to_v2(v1_catalog, v1_registry).expect("Failed to convert v1 to v2"); + assert_eq!(v2_registry.events.len(), 1); + if let Some(event) = v2_registry.events.first() { + assert_eq!(event.name, "my-event".to_owned().into()); + } + } + + #[test] + fn test_convert_entity_v1_to_v2() { + let mut v1_catalog = crate::catalog::Catalog::from_attributes(vec![]); + let test_refs = v1_catalog.add_attributes([Attribute { + name: "test.key".to_owned(), + r#type: weaver_semconv::attribute::AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), + brief: "".to_owned(), + examples: None, + tag: None, + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + sampling_relevant: None, + note: "".to_owned(), + stability: Some(Stability::Stable), + deprecated: None, + prefix: false, + tags: None, + annotations: None, + value: None, + role: Some(weaver_semconv::attribute::AttributeRole::Identifying), + }]); + let v1_registry = crate::registry::Registry { + registry_url: "my.schema.url".to_owned(), + groups: vec![Group { + id: "entity.my-entity".to_owned(), + r#type: GroupType::Entity, + brief: "".to_owned(), + note: "".to_owned(), + prefix: "".to_owned(), + extends: None, + stability: Some(Stability::Stable), + deprecated: None, + attributes: vec![test_refs[0]], + span_kind: None, + events: vec![], + metric_name: None, + instrument: None, + unit: None, + name: Some("my-entity".to_owned()), + lineage: None, + display_name: None, + body: None, + annotations: None, + entity_associations: vec![], + visibility: None, + }], + }; + + let (v2_registry, _) = + convert_v1_to_v2(v1_catalog, v1_registry).expect("Failed to convert v1 to v2"); + assert_eq!(v2_registry.entities.len(), 1); + if let Some(entity) = v2_registry.entities.first() { + assert_eq!(entity.r#type, "my-entity".to_owned().into()); + assert_eq!(entity.identity.len(), 1); + } + } + + #[test] + fn test_try_from_v1_to_v2() { + let v1_schema = crate::ResolvedTelemetrySchema { + file_format: "1.0.0".to_owned(), + schema_url: "my.schema.url".to_owned(), + registry_id: "my-registry".to_owned(), + catalog: crate::catalog::Catalog::from_attributes(vec![]), + registry: crate::registry::Registry { + registry_url: "my.schema.url".to_owned(), + groups: vec![], + }, + instrumentation_library: None, + resource: None, + dependencies: vec![], + versions: None, + registry_manifest: None, + }; + + let v2_schema: Result = v1_schema.try_into(); + assert!(v2_schema.is_ok()); + let v2_schema = v2_schema.unwrap(); + assert_eq!(v2_schema.file_format, "1.0.0"); + assert_eq!(v2_schema.schema_url, "my.schema.url"); + assert_eq!(v2_schema.registry_id, "my-registry"); + } } From e420112bdddf6b75db82ce5f6296df00db9f71e9 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 13 Nov 2025 08:09:09 -0500 Subject: [PATCH 41/46] Add test in forge for forge code instead of relying on outer crate for testing (which codecov doesn't see). --- crates/weaver_forge/src/v2/registry.rs | 154 +++++++++++++++++++++++++ crates/weaver_semconv/src/v2/mod.rs | 12 ++ 2 files changed, 166 insertions(+) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index f17f36241..43822fff0 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -427,3 +427,157 @@ impl ForgeResolvedRegistry { }) } } + +#[cfg(test)] +mod tests { + use weaver_resolved_schema::{ + v2::{ + attribute, event, metric, span, ResolvedTelemetrySchema, + {self}, + }, + }; + use weaver_semconv::{ + attribute::{AttributeType, PrimitiveOrArrayTypeSpec}, + group::{InstrumentSpec, SpanKindSpec}, + v2::{signal_id::SignalId, span::SpanName, CommonFields}, + }; + + use super::*; + + #[test] + fn test_try_from_resolved_schema() { + let resolved_schema = ResolvedTelemetrySchema { + file_format: "2.0.0".to_owned(), + schema_url: "https://example.com/schema".to_owned(), + registry_id: "my-registry".to_owned(), + registry: v2::registry::Registry { + registry_url: "https://example.com/registry".to_owned(), + attributes: vec![attribute::Attribute { + key: "test.attr".to_owned(), + r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String), + examples: None, + common: CommonFields::default(), + }], + spans: vec![span::Span { + r#type: SignalId::from("my-span".to_owned()), + kind: SpanKindSpec::Internal, + name: SpanName { + note: "My Span".to_owned(), + }, + attributes: vec![span::SpanAttributeRef { + base: attribute::AttributeRef(0), + requirement_level: + weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + sampling_relevant: Some(true), + }], + entity_associations: vec![], + common: CommonFields::default(), + }], + metrics: vec![metric::Metric { + name: SignalId::from("my-metric".to_owned()), + instrument: InstrumentSpec::Counter, + unit: "1".to_owned(), + attributes: vec![metric::MetricAttributeRef { + base: attribute::AttributeRef(0), + requirement_level: + weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + }], + entity_associations: vec![], + common: CommonFields::default(), + }], + events: vec![event::Event { + name: SignalId::from("my-event".to_owned()), + attributes: vec![event::EventAttributeRef { + base: attribute::AttributeRef(0), + requirement_level: + weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + }], + entity_associations: vec![], + common: CommonFields::default(), + }], + entities: vec![], + attribute_groups: vec![], + }, + refinements: v2::refinements::Refinements { + spans: vec![], + metrics: vec![], + events: vec![], + }, + }; + + let forge_registry = + ForgeResolvedRegistry::try_from(resolved_schema).expect("Conversion failed"); + + assert_eq!(forge_registry.attributes.len(), 1); + assert_eq!(forge_registry.signals.spans.len(), 1); + assert_eq!(forge_registry.signals.metrics.len(), 1); + assert_eq!(forge_registry.signals.events.len(), 1); + + let span = &forge_registry.signals.spans[0]; + assert_eq!(span.r#type, "my-span".to_owned().into()); + assert_eq!(span.attributes.len(), 1); + assert_eq!(span.attributes[0].base.key, "test.attr"); + } + + // This should never happen, but we want a test where "try_from" fails, so we + // purposely construct a bad registry in case of a logic bug further up in the stack. + #[test] + fn test_try_from_resolved_schema_with_missing_attribute() { + let resolved_schema = ResolvedTelemetrySchema { + file_format: "2.0.0".to_owned(), + schema_url: "https://example.com/schema".to_owned(), + registry_id: "my-registry".to_owned(), + registry: v2::registry::Registry { + registry_url: "https://example.com/registry".to_owned(), + attributes: vec![], // No attributes - This is the logic bug. + spans: vec![span::Span { + r#type: SignalId::from("my-span".to_owned()), + kind: SpanKindSpec::Internal, + name: SpanName { + note: "My Span".to_owned(), + }, + attributes: vec![span::SpanAttributeRef { + base: attribute::AttributeRef(0), // Refers to bad attribute. + requirement_level: + weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + sampling_relevant: Some(true), + }], + entity_associations: vec![], + common: CommonFields::default(), + }], + metrics: vec![], + events: vec![], + entities: vec![], + attribute_groups: vec![], + }, + refinements: v2::refinements::Refinements { + spans: vec![], + metrics: vec![], + events: vec![], + }, + }; + + let result = ForgeResolvedRegistry::try_from(resolved_schema); + assert!(result.is_err()); + + if let Err(Error::CompoundError(errors)) = result { + assert_eq!(errors.len(), 1); + if let Some(Error::AttributeNotFound { group_id, attr_ref }) = errors.get(0) { + assert_eq!(group_id, "span.my-span"); + assert_eq!(*attr_ref, AttributeRef(0)); + } else { + panic!("Expected AttributeNotFound error"); + } + } else { + panic!("Expected CompoundError"); + } + } +} diff --git a/crates/weaver_semconv/src/v2/mod.rs b/crates/weaver_semconv/src/v2/mod.rs index 4e3362f91..218750d0a 100644 --- a/crates/weaver_semconv/src/v2/mod.rs +++ b/crates/weaver_semconv/src/v2/mod.rs @@ -127,6 +127,18 @@ impl SemConvSpecV2 { } } +impl Default for CommonFields { + fn default() -> Self { + Self { + brief: Default::default(), + note: Default::default(), + stability: Stability::Alpha, + deprecated: Default::default(), + annotations: Default::default() + } + } +} + #[cfg(test)] mod tests { use super::*; From cd3942909b6758d29a8c4c68bc8ab5923f66f996 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 13 Nov 2025 08:09:29 -0500 Subject: [PATCH 42/46] Cargo fmt --- crates/weaver_forge/src/v2/registry.rs | 35 +++++++++++--------------- crates/weaver_semconv/src/v2/mod.rs | 12 ++++----- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 43822fff0..0025772de 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -430,11 +430,8 @@ impl ForgeResolvedRegistry { #[cfg(test)] mod tests { - use weaver_resolved_schema::{ - v2::{ - attribute, event, metric, span, ResolvedTelemetrySchema, - {self}, - }, + use weaver_resolved_schema::v2::{ + attribute, event, metric, span, ResolvedTelemetrySchema, {self}, }; use weaver_semconv::{ attribute::{AttributeType, PrimitiveOrArrayTypeSpec}, @@ -466,10 +463,9 @@ mod tests { }, attributes: vec![span::SpanAttributeRef { base: attribute::AttributeRef(0), - requirement_level: - weaver_semconv::attribute::RequirementLevel::Basic( - weaver_semconv::attribute::BasicRequirementLevelSpec::Required, - ), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), sampling_relevant: Some(true), }], entity_associations: vec![], @@ -481,10 +477,9 @@ mod tests { unit: "1".to_owned(), attributes: vec![metric::MetricAttributeRef { base: attribute::AttributeRef(0), - requirement_level: - weaver_semconv::attribute::RequirementLevel::Basic( - weaver_semconv::attribute::BasicRequirementLevelSpec::Required, - ), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), }], entity_associations: vec![], common: CommonFields::default(), @@ -493,10 +488,9 @@ mod tests { name: SignalId::from("my-event".to_owned()), attributes: vec![event::EventAttributeRef { base: attribute::AttributeRef(0), - requirement_level: - weaver_semconv::attribute::RequirementLevel::Basic( - weaver_semconv::attribute::BasicRequirementLevelSpec::Required, - ), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), }], entity_associations: vec![], common: CommonFields::default(), @@ -544,10 +538,9 @@ mod tests { }, attributes: vec![span::SpanAttributeRef { base: attribute::AttributeRef(0), // Refers to bad attribute. - requirement_level: - weaver_semconv::attribute::RequirementLevel::Basic( - weaver_semconv::attribute::BasicRequirementLevelSpec::Required, - ), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), sampling_relevant: Some(true), }], entity_associations: vec![], diff --git a/crates/weaver_semconv/src/v2/mod.rs b/crates/weaver_semconv/src/v2/mod.rs index 218750d0a..e6ac836ef 100644 --- a/crates/weaver_semconv/src/v2/mod.rs +++ b/crates/weaver_semconv/src/v2/mod.rs @@ -129,12 +129,12 @@ impl SemConvSpecV2 { impl Default for CommonFields { fn default() -> Self { - Self { - brief: Default::default(), - note: Default::default(), - stability: Stability::Alpha, - deprecated: Default::default(), - annotations: Default::default() + Self { + brief: Default::default(), + note: Default::default(), + stability: Stability::Alpha, + deprecated: Default::default(), + annotations: Default::default(), } } } From 2f0c2cf6167b601e035e3a1bbec6705c6c917752 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 13 Nov 2025 08:19:51 -0500 Subject: [PATCH 43/46] Fix clippy. --- crates/weaver_forge/src/v2/registry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 0025772de..82579168a 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -563,7 +563,7 @@ mod tests { if let Err(Error::CompoundError(errors)) = result { assert_eq!(errors.len(), 1); - if let Some(Error::AttributeNotFound { group_id, attr_ref }) = errors.get(0) { + if let Some(Error::AttributeNotFound { group_id, attr_ref }) = errors.first() { assert_eq!(group_id, "span.my-span"); assert_eq!(*attr_ref, AttributeRef(0)); } else { From 3dd518a084662a7efc3d6e30a82aadbd044ee08b Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 17 Nov 2025 08:55:59 -0500 Subject: [PATCH 44/46] Add more test coverage. --- crates/weaver_forge/src/v2/registry.rs | 103 ++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index 82579168a..f05b9fb50 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -495,13 +495,69 @@ mod tests { entity_associations: vec![], common: CommonFields::default(), }], - entities: vec![], + entities: vec![v2::entity::Entity { + r#type: SignalId::from("my-entity".to_owned()), + identity: vec![v2::entity::EntityAttributeRef { + base: attribute::AttributeRef(0), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + }], + description: vec![], + common: CommonFields::default(), + }], attribute_groups: vec![], }, refinements: v2::refinements::Refinements { - spans: vec![], - metrics: vec![], - events: vec![], + spans: vec![span::SpanRefinement { + id: SignalId::from("my-refined-span".to_owned()), + span: span::Span { + r#type: SignalId::from("my-span".to_owned()), + kind: SpanKindSpec::Client, + name: SpanName { + note: "My Refined Span".to_owned(), + }, + attributes: vec![span::SpanAttributeRef { + base: attribute::AttributeRef(0), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Required, + ), + sampling_relevant: Some(false), + }], + entity_associations: vec![], + common: CommonFields::default(), + }, + }], + metrics: vec![metric::MetricRefinement { + id: SignalId::from("my-refined-metric".to_owned()), + metric: metric::Metric { + name: SignalId::from("my-metric".to_owned()), + instrument: InstrumentSpec::Histogram, + unit: "ms".to_owned(), + attributes: vec![metric::MetricAttributeRef { + base: attribute::AttributeRef(0), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::Recommended, + ), + }], + entity_associations: vec![], + common: CommonFields::default(), + }, + }], + events: vec![event::EventRefinement { + id: SignalId::from("my-refined-event".to_owned()), + event: event::Event { + name: SignalId::from("my-event".to_owned()), + attributes: vec![event::EventAttributeRef { + base: attribute::AttributeRef(0), + requirement_level: weaver_semconv::attribute::RequirementLevel::Basic( + weaver_semconv::attribute::BasicRequirementLevelSpec::OptIn, + ), + }], + entity_associations: vec![], + common: CommonFields::default(), + }, + }], }, }; @@ -512,11 +568,50 @@ mod tests { assert_eq!(forge_registry.signals.spans.len(), 1); assert_eq!(forge_registry.signals.metrics.len(), 1); assert_eq!(forge_registry.signals.events.len(), 1); + assert_eq!(forge_registry.signals.entities.len(), 1); + assert_eq!(forge_registry.refinements.spans.len(), 1); + assert_eq!(forge_registry.refinements.metrics.len(), 1); + assert_eq!(forge_registry.refinements.events.len(), 1); let span = &forge_registry.signals.spans[0]; assert_eq!(span.r#type, "my-span".to_owned().into()); assert_eq!(span.attributes.len(), 1); assert_eq!(span.attributes[0].base.key, "test.attr"); + + let entity = &forge_registry.signals.entities[0]; + assert_eq!(entity.r#type, "my-entity".to_owned().into()); + assert_eq!(entity.identity.len(), 1); + assert_eq!(entity.identity[0].base.key, "test.attr"); + + let refined_span = &forge_registry.refinements.spans[0]; + assert_eq!(refined_span.id, "my-refined-span".to_owned().into()); + assert_eq!(refined_span.span.r#type, "my-span".to_owned().into()); + assert_eq!(refined_span.span.attributes.len(), 1); + assert_eq!(refined_span.span.attributes[0].base.key, "test.attr"); + + let refined_metric = &forge_registry.refinements.metrics[0]; + assert_eq!( + refined_metric.id, + "my-refined-metric".to_owned().into() + ); + assert_eq!( + refined_metric.metric.name, + "my-metric".to_owned().into() + ); + assert_eq!(refined_metric.metric.attributes.len(), 1); + assert_eq!( + refined_metric.metric.attributes[0].base.key, + "test.attr" + ); + + let refined_event = &forge_registry.refinements.events[0]; + assert_eq!(refined_event.id, "my-refined-event".to_owned().into()); + assert_eq!(refined_event.event.name, "my-event".to_owned().into()); + assert_eq!(refined_event.event.attributes.len(), 1); + assert_eq!( + refined_event.event.attributes[0].base.key, + "test.attr" + ); } // This should never happen, but we want a test where "try_from" fails, so we From 55e68aca8942daeeb91ec771d9cb4ecd72421029 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 17 Nov 2025 13:46:08 -0500 Subject: [PATCH 45/46] Rust fmt. --- crates/weaver_forge/src/v2/registry.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/crates/weaver_forge/src/v2/registry.rs b/crates/weaver_forge/src/v2/registry.rs index f05b9fb50..baf1054a7 100644 --- a/crates/weaver_forge/src/v2/registry.rs +++ b/crates/weaver_forge/src/v2/registry.rs @@ -590,28 +590,16 @@ mod tests { assert_eq!(refined_span.span.attributes[0].base.key, "test.attr"); let refined_metric = &forge_registry.refinements.metrics[0]; - assert_eq!( - refined_metric.id, - "my-refined-metric".to_owned().into() - ); - assert_eq!( - refined_metric.metric.name, - "my-metric".to_owned().into() - ); + assert_eq!(refined_metric.id, "my-refined-metric".to_owned().into()); + assert_eq!(refined_metric.metric.name, "my-metric".to_owned().into()); assert_eq!(refined_metric.metric.attributes.len(), 1); - assert_eq!( - refined_metric.metric.attributes[0].base.key, - "test.attr" - ); + assert_eq!(refined_metric.metric.attributes[0].base.key, "test.attr"); let refined_event = &forge_registry.refinements.events[0]; assert_eq!(refined_event.id, "my-refined-event".to_owned().into()); assert_eq!(refined_event.event.name, "my-event".to_owned().into()); assert_eq!(refined_event.event.attributes.len(), 1); - assert_eq!( - refined_event.event.attributes[0].base.key, - "test.attr" - ); + assert_eq!(refined_event.event.attributes[0].base.key, "test.attr"); } // This should never happen, but we want a test where "try_from" fails, so we From a4da645e0af7c7680ebaa707d3fc13848a4180a3 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 17 Nov 2025 13:47:50 -0500 Subject: [PATCH 46/46] Update crates/weaver_forge/src/v2/entity.rs Co-authored-by: Jeremy Blythe --- crates/weaver_forge/src/v2/entity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/weaver_forge/src/v2/entity.rs b/crates/weaver_forge/src/v2/entity.rs index 249e7f713..658ea1738 100644 --- a/crates/weaver_forge/src/v2/entity.rs +++ b/crates/weaver_forge/src/v2/entity.rs @@ -31,7 +31,7 @@ pub struct Entity { pub common: CommonFields, } -/// A special type of reference to attributes that remembers entity-specicific information. +/// A special type of reference to attributes that remembers entity-specific information. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct EntityAttribute {