diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e5785fae..7768877b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## [TBD] - TBD + +What's changed + +* Add support for annotations on attributes and groups. ([#645](https://github.com/open-telemetry/weaver/pull/645) by @lquerel). * 💥 BREAKING CHANGE 💥 - Upgrade to version 0.4.0 of regorus [requires all v0 policies to be modified](https://github.com/microsoft/regorus/pull/373). Policy upgrade instructions [here](https://www.openpolicyagent.org/docs/latest/v0-upgrade/#upgrading-rego) may help. ([#651](https://github.com/open-telemetry/weaver/pull/651) by @jerbly). ## [0.13.2] - 2025-02-13 diff --git a/Cargo.lock b/Cargo.lock index 90faa3105..5a1291f0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5364,6 +5364,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "serde_yaml", "thiserror 2.0.12", "weaver_semconv", "weaver_version", diff --git a/crates/weaver_emit/src/lib.rs b/crates/weaver_emit/src/lib.rs index 99fcd4886..65561f7b7 100644 --- a/crates/weaver_emit/src/lib.rs +++ b/crates/weaver_emit/src/lib.rs @@ -163,6 +163,7 @@ mod tests { prefix: false, tags: None, value: None, + annotations: None, }], span_kind: Some(SpanKindSpec::Internal), events: vec![], diff --git a/crates/weaver_emit/src/spans.rs b/crates/weaver_emit/src/spans.rs index c4e1d5703..57af935e4 100644 --- a/crates/weaver_emit/src/spans.rs +++ b/crates/weaver_emit/src/spans.rs @@ -183,6 +183,7 @@ mod tests { prefix: false, tags: None, value: None, + annotations: None, } } diff --git a/crates/weaver_forge/src/extensions/otel.rs b/crates/weaver_forge/src/extensions/otel.rs index c64dfbb96..a84d57697 100644 --- a/crates/weaver_forge/src/extensions/otel.rs +++ b/crates/weaver_forge/src/extensions/otel.rs @@ -789,6 +789,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }; otel::add_filters(&mut env); @@ -817,6 +818,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }; assert_eq!( @@ -1003,6 +1005,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "rec.b".into(), @@ -1018,6 +1021,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "crec.a".into(), @@ -1033,6 +1037,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "crec.b".into(), @@ -1048,6 +1053,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "rec.c".into(), @@ -1063,6 +1069,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "rec.d".into(), @@ -1078,6 +1085,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "opt.a".into(), @@ -1093,6 +1101,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "opt.b".into(), @@ -1108,6 +1117,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "req.a".into(), @@ -1123,6 +1133,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "req.b".into(), @@ -1138,6 +1149,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, ]; let json = @@ -1196,6 +1208,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "attr2".to_owned(), @@ -1211,6 +1224,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, Attribute { name: "attr3".to_owned(), @@ -1226,6 +1240,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }, ]; @@ -1516,6 +1531,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }; otel::add_filters(&mut env); @@ -1544,6 +1560,7 @@ mod tests { tags: None, value: None, prefix: false, + annotations: None, }; otel::add_filters(&mut env); diff --git a/crates/weaver_resolved_schema/Cargo.toml b/crates/weaver_resolved_schema/Cargo.toml index 72f75feab..cb291fb80 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 +serde_yaml.workspace = true [dev-dependencies] serde_json.workspace = true diff --git a/crates/weaver_resolved_schema/src/attribute.rs b/crates/weaver_resolved_schema/src/attribute.rs index ebd76dddb..a0b158bdc 100644 --- a/crates/weaver_resolved_schema/src/attribute.rs +++ b/crates/weaver_resolved_schema/src/attribute.rs @@ -8,6 +8,7 @@ use crate::tags::Tags; use crate::value::Value; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use std::fmt::Display; use std::ops::Not; #[cfg(test)] @@ -15,6 +16,7 @@ use weaver_semconv::attribute::PrimitiveOrArrayTypeSpec; use weaver_semconv::attribute::{AttributeSpec, AttributeType, Examples, RequirementLevel}; use weaver_semconv::deprecated::Deprecated; use weaver_semconv::stability::Stability; +use weaver_semconv::YamlValue; /// An attribute definition. #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] @@ -76,6 +78,10 @@ pub struct Attribute { /// A set of tags for the attribute. #[serde(skip_serializing_if = "Option::is_none")] pub tags: Option, + /// Annotations for the group. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option>, /// The value of the attribute. /// Note: This is only used in a telemetry schema specification. @@ -121,6 +127,7 @@ impl Attribute { prefix: false, tags: None, value: None, + annotations: None, } } @@ -142,6 +149,7 @@ impl Attribute { prefix: false, tags: None, value: None, + annotations: None, } } @@ -163,6 +171,7 @@ impl Attribute { prefix: false, tags: None, value: None, + annotations: None, } } @@ -188,6 +197,7 @@ impl Attribute { prefix: false, tags: None, value: None, + annotations: None, } } diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs index 0f5cc5860..6384f5259 100644 --- a/crates/weaver_resolved_schema/src/lib.rs +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -136,6 +136,7 @@ impl ResolvedTelemetrySchema { constraints: vec![], unit: None, body: None, + annotations: None, }); } diff --git a/crates/weaver_resolved_schema/src/lineage.rs b/crates/weaver_resolved_schema/src/lineage.rs index 36a806bba..85a942a2a 100644 --- a/crates/weaver_resolved_schema/src/lineage.rs +++ b/crates/weaver_resolved_schema/src/lineage.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use weaver_semconv::attribute::{AttributeSpec, Examples, RequirementLevel}; use weaver_semconv::deprecated::Deprecated; use weaver_semconv::stability::Stability; +use weaver_semconv::YamlValue; /// Attribute lineage (at the field level). #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] @@ -374,6 +375,32 @@ impl AttributeLineage { } } + /// Determines the value of the annotations field by evaluating the presence of + /// a local value. If a local value is provided, it is used, and the annotations + /// field's lineage is marked as local. Otherwise, the specified parent + /// value is used, and the tag field's lineage is marked as inherited + /// from the parent. + /// This method updates the lineage information for the annotations field to + /// reflect the source of its value. + pub fn annotations( + &mut self, + local_value: &Option>, + parent_value: &Option>, + ) -> Option> { + if local_value.is_some() { + _ = self + .locally_overridden_fields + .insert("annotations".to_owned()); + _ = self.inherited_fields.remove("annotations"); + local_value.clone() + } else { + if parent_value.is_some() { + _ = self.inherited_fields.insert("annotations".to_owned()); + } + parent_value.clone() + } + } + /// Determines the value of the tags field by evaluating the presence of /// a local value. If a local value is provided, it is used, and the tags /// field's lineage is marked as local. Otherwise, the specified parent diff --git a/crates/weaver_resolved_schema/src/registry.rs b/crates/weaver_resolved_schema/src/registry.rs index af3bc230d..f97cefa38 100644 --- a/crates/weaver_resolved_schema/src/registry.rs +++ b/crates/weaver_resolved_schema/src/registry.rs @@ -8,11 +8,6 @@ use schemars::JsonSchema; use std::collections::{BTreeMap, HashMap, HashSet}; use weaver_semconv::any_value::AnyValueSpec; -use serde::{Deserialize, Serialize}; -use weaver_semconv::deprecated::Deprecated; -use weaver_semconv::group::{GroupType, InstrumentSpec, SpanKindSpec}; -use weaver_semconv::stability::Stability; - use crate::attribute::{Attribute, AttributeRef}; use crate::catalog::Catalog; use crate::error::{handle_errors, Error}; @@ -20,6 +15,11 @@ use crate::lineage::GroupLineage; use crate::registry::GroupStats::{ AttributeGroup, Event, Metric, MetricGroup, Resource, Scope, Span, }; +use serde::{Deserialize, Serialize}; +use weaver_semconv::deprecated::Deprecated; +use weaver_semconv::group::{GroupType, InstrumentSpec, SpanKindSpec}; +use weaver_semconv::stability::Stability; +use weaver_semconv::YamlValue; /// A semantic convention registry. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] @@ -131,6 +131,10 @@ pub struct Group { /// This fields is only used for event groups. #[serde(skip_serializing_if = "Option::is_none")] pub body: Option, + /// Annotations for the group. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option>, } /// Common statistics for a group. diff --git a/crates/weaver_resolver/data/registry-test-14-annotations/README.md b/crates/weaver_resolver/data/registry-test-14-annotations/README.md new file mode 100644 index 000000000..60683f094 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-14-annotations/README.md @@ -0,0 +1,2 @@ +Test that annotations end up in the resolved registry. +This test must success. diff --git a/crates/weaver_resolver/data/registry-test-14-annotations/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-14-annotations/expected-attribute-catalog.json new file mode 100644 index 000000000..085c43649 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-14-annotations/expected-attribute-catalog.json @@ -0,0 +1,47 @@ +[ + { + "name": "attr1", + "type": "string", + "brief": "Brief", + "examples": "eu-central-1", + "requirement_level": "required", + "note": "Note", + "stability": "stable", + "annotations": { + "code_generation": { + "exclude": true + }, + "privacy_sensitivity": "PII" + } + }, + { + "name": "attr1", + "type": "string", + "brief": "Brief", + "examples": "eu-central-1", + "requirement_level": "required", + "note": "Note", + "stability": "stable", + "annotations": { + "code_generation": { + "exclude": true + }, + "privacy_sensitivity": "PII + PHI", + "complex": { + "key1": "string", + "key2": 234, + "key3": true, + "key4": { + "key4.1": "string", + "key4.2": 234 + }, + "key5": [ + 12, + 45, + 67 + ], + "key6": null + } + } + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-14-annotations/expected-registry.json b/crates/weaver_resolver/data/registry-test-14-annotations/expected-registry.json new file mode 100644 index 000000000..c6949dc42 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-14-annotations/expected-registry.json @@ -0,0 +1,48 @@ +{ + "registry_url": "https://127.0.0.1", + "groups": [ + { + "id": "attrs", + "type": "attribute_group", + "brief": "Attributes", + "attributes": [ + 0 + ], + "lineage": { + "source_file": "data/registry-test-14-annotations/registry/group-with-annotations.yaml" + }, + "annotations": { + "privacy_sensitivity": "PII", + "code_generation": { + "exclude": true + } + } + }, + { + "id": "attrs_ext", + "type": "attribute_group", + "brief": "Attributes", + "attributes": [ + 1 + ], + "lineage": { + "source_file": "data/registry-test-14-annotations/registry/group-with-annotations.yaml", + "attributes": { + "attr1": { + "source_group": "attrs", + "inherited_fields": [ + "brief", + "examples", + "note", + "requirement_level", + "stability" + ], + "locally_overridden_fields": [ + "annotations" + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-14-annotations/registry/group-with-annotations.yaml b/crates/weaver_resolver/data/registry-test-14-annotations/registry/group-with-annotations.yaml new file mode 100644 index 000000000..82062a4b0 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-14-annotations/registry/group-with-annotations.yaml @@ -0,0 +1,38 @@ +groups: + - id: attrs + type: attribute_group + brief: "Attributes" + attributes: + - id: attr1 + stability: stable + type: string + requirement_level: required + brief: Brief + note: Note + examples: 'eu-central-1' + annotations: + code_generation: + exclude: true + privacy_sensitivity: PII + annotations: + code_generation: + exclude: true + privacy_sensitivity: PII + - id: attrs_ext + type: attribute_group + brief: "Attributes" + attributes: + - ref: attr1 + annotations: + code_generation: + exclude: true + privacy_sensitivity: PII + PHI + complex: + key1: string + key2: 234 + key3: true + key4: + key4.1: string + key4.2: 234 + key5: [12, 45, 67] + key6: \ No newline at end of file diff --git a/crates/weaver_resolver/src/attribute.rs b/crates/weaver_resolver/src/attribute.rs index ec4fe3f4e..3c6b18cd1 100644 --- a/crates/weaver_resolver/src/attribute.rs +++ b/crates/weaver_resolver/src/attribute.rs @@ -91,6 +91,7 @@ impl AttributeCatalog { stability, deprecated, prefix, + annotations, } => { let name; let root_attr = self.root_attributes.get(r#ref); @@ -129,6 +130,8 @@ impl AttributeCatalog { tags: root_attr.attribute.tags.clone(), value: root_attr.attribute.value.clone(), prefix: *prefix, + annotations: attr_lineage + .annotations(annotations, &root_attr.attribute.annotations), }; let attr_ref = self.attribute_ref(resolved_attr.clone()); @@ -167,6 +170,7 @@ impl AttributeCatalog { note, stability, deprecated, + annotations, } => { // Create a fully resolved attribute from an attribute spec (id), // and check if it already exists in the catalog. @@ -186,6 +190,7 @@ impl AttributeCatalog { tags: None, value: None, prefix: false, + annotations: annotations.clone(), }; _ = self.root_attributes.insert( diff --git a/crates/weaver_resolver/src/registry.rs b/crates/weaver_resolver/src/registry.rs index b9e71baa4..db2199c63 100644 --- a/crates/weaver_resolver/src/registry.rs +++ b/crates/weaver_resolver/src/registry.rs @@ -2,6 +2,10 @@ //! Functions to resolve a semantic convention registry. +use crate::attribute::AttributeCatalog; +use crate::constraint::resolve_constraints; +use crate::Error::{DuplicateGroupId, DuplicateGroupName, DuplicateMetricName}; +use crate::{Error, UnsatisfiedAnyOfConstraint}; use itertools::Itertools; use serde::Deserialize; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -16,11 +20,6 @@ use weaver_semconv::attribute::AttributeSpec; use weaver_semconv::group::GroupSpecWithProvenance; use weaver_semconv::registry::SemConvRegistry; -use crate::attribute::AttributeCatalog; -use crate::constraint::resolve_constraints; -use crate::Error::{DuplicateGroupId, DuplicateGroupName, DuplicateMetricName}; -use crate::{Error, UnsatisfiedAnyOfConstraint}; - /// A registry containing unresolved groups. #[derive(Debug, Deserialize)] pub struct UnresolvedRegistry { @@ -406,6 +405,7 @@ fn group_from_spec(group: GroupSpecWithProvenance) -> UnresolvedGroup { lineage: Some(GroupLineage::new(&group.provenance)), display_name: group.spec.display_name, body: group.spec.body, + annotations: group.spec.annotations, }, attributes: attrs, provenance: group.provenance, @@ -766,6 +766,7 @@ fn resolve_inheritance_attr( stability, deprecated, prefix, + annotations, } => { match parent_attr { AttributeSpec::Ref { @@ -778,6 +779,7 @@ fn resolve_inheritance_attr( stability: parent_stability, deprecated: parent_deprecated, prefix: parent_prefix, + annotations: parent_annotations, .. } => { // attr and attr_parent are both references. @@ -796,6 +798,7 @@ fn resolve_inheritance_attr( stability: lineage.stability(stability, parent_stability), deprecated: lineage.deprecated(deprecated, parent_deprecated), prefix: lineage.prefix(prefix, parent_prefix), + annotations: lineage.annotations(annotations, parent_annotations), } } AttributeSpec::Id { @@ -808,6 +811,7 @@ fn resolve_inheritance_attr( note: parent_note, stability: parent_stability, deprecated: parent_deprecated, + annotations: parent_annotations, .. } => { // attr is a reference and attr_parent is an id. @@ -825,6 +829,7 @@ fn resolve_inheritance_attr( note: lineage.note(note, parent_note), stability: lineage.stability(stability, parent_stability), deprecated: lineage.deprecated(deprecated, parent_deprecated), + annotations: lineage.annotations(annotations, parent_annotations), } } } diff --git a/crates/weaver_semconv/allowed-external-types.toml b/crates/weaver_semconv/allowed-external-types.toml index 543eb1887..d8f2acdc2 100644 --- a/crates/weaver_semconv/allowed-external-types.toml +++ b/crates/weaver_semconv/allowed-external-types.toml @@ -11,4 +11,5 @@ allowed_external_types = [ "ordered_float::OrderedFloat", # ToDo: Remove this dependency before version 1.0 "miette::protocol::Diagnostic", "schemars::JsonSchema", + "serde_yaml::value::Value", ] \ No newline at end of file diff --git a/crates/weaver_semconv/src/attribute.rs b/crates/weaver_semconv/src/attribute.rs index dc2dc9d1f..8c8995650 100644 --- a/crates/weaver_semconv/src/attribute.rs +++ b/crates/weaver_semconv/src/attribute.rs @@ -7,10 +7,11 @@ use crate::any_value::AnyValueSpec; use crate::deprecated::Deprecated; use crate::stability::Stability; -use crate::Error; +use crate::{Error, YamlValue}; use ordered_float::OrderedFloat; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; use std::ops::Not; use weaver_common::result::WResult; @@ -81,6 +82,8 @@ pub enum AttributeSpec { #[serde(default)] #[serde(skip_serializing_if = "<&bool>::not")] prefix: bool, + /// Annotations for the attribute. + annotations: Option>, }, /// Attribute definition. Id { @@ -133,6 +136,8 @@ pub enum AttributeSpec { default )] deprecated: Option, + /// Annotations for the attribute. + annotations: Option>, }, } @@ -892,6 +897,7 @@ mod tests { deprecated: Some(Deprecated::Obsoleted { note: "".to_owned(), }), + annotations: None, }; assert_eq!(attr.id(), "id"); assert_eq!(attr.brief(), "brief"); @@ -912,6 +918,7 @@ mod tests { note: "".to_owned(), }), prefix: false, + annotations: None, }; assert_eq!(attr.id(), "ref"); assert_eq!(attr.brief(), "brief"); diff --git a/crates/weaver_semconv/src/group.rs b/crates/weaver_semconv/src/group.rs index 730624abd..11041800a 100644 --- a/crates/weaver_semconv/src/group.rs +++ b/crates/weaver_semconv/src/group.rs @@ -5,7 +5,7 @@ //! A group specification. use schemars::JsonSchema; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize}; @@ -15,7 +15,7 @@ use crate::attribute::{AttributeSpec, AttributeType, PrimitiveOrArrayTypeSpec}; use crate::deprecated::Deprecated; use crate::group::InstrumentSpec::{Counter, Gauge, Histogram, UpDownCounter}; use crate::stability::Stability; -use crate::Error; +use crate::{Error, YamlValue}; use weaver_common::result::WResult; /// Group Spec contain the list of semantic conventions for attributes, @@ -97,6 +97,8 @@ pub struct GroupSpec { /// Note: only valid if type is event #[serde(skip_serializing_if = "Option::is_none")] pub body: Option, + /// Annotations for the group. + pub annotations: Option>, } impl GroupSpec { @@ -593,6 +595,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }], constraints: vec![], span_kind: Some(SpanKindSpec::Client), @@ -603,6 +606,7 @@ mod tests { name: None, display_name: None, body: None, + annotations: None, }; assert!(group .validate("") @@ -726,6 +730,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }], constraints: vec![], span_kind: Some(SpanKindSpec::Client), @@ -736,6 +741,7 @@ mod tests { name: None, display_name: None, body: None, + annotations: None, }; assert!(group .validate("") @@ -756,6 +762,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }]; let result = group.validate("").into_result_failing_non_fatal(); assert_eq!( @@ -783,6 +790,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }]; let result = group.validate("").into_result_failing_non_fatal(); assert_eq!( @@ -810,6 +818,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }]; let result = group.validate("").into_result_failing_non_fatal(); assert_eq!( @@ -846,6 +855,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }]; let result = group.validate("").into_result_failing_non_fatal(); assert_eq!( @@ -888,6 +898,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }], constraints: vec![], span_kind: Some(SpanKindSpec::Client), @@ -898,6 +909,7 @@ mod tests { name: None, display_name: None, body: None, + annotations: None, }; let result = group.validate("").into_result_failing_non_fatal(); assert_eq!( @@ -928,6 +940,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }]; let result = group.validate("").into_result_failing_non_fatal(); assert!(result.is_ok()); @@ -967,6 +980,7 @@ mod tests { ), }, }), + annotations: None, }; assert!(group .validate("") @@ -1181,6 +1195,7 @@ mod tests { ), }, }), + annotations: None, }; assert!(group .validate("") @@ -1311,6 +1326,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }], constraints: vec![], span_kind: None, @@ -1321,6 +1337,7 @@ mod tests { name: None, display_name: None, body: None, + annotations: None, }; assert!(group .validate("") @@ -1455,6 +1472,7 @@ mod tests { requirement_level: Default::default(), sampling_relevant: None, note: "".to_owned(), + annotations: None, }]; let mut group = GroupSpec { id: "test".to_owned(), @@ -1475,6 +1493,7 @@ mod tests { name: None, display_name: None, body: None, + annotations: None, }; // Attribute Group must have extends or attributes. @@ -1589,6 +1608,7 @@ mod tests { stability: None, deprecated: None, prefix: false, + annotations: None, }, AttributeSpec::Ref { r#ref: "attribute".to_owned(), @@ -1601,6 +1621,7 @@ mod tests { stability: None, deprecated: None, prefix: false, + annotations: None, }, ]; let mut group = GroupSpec { @@ -1622,6 +1643,7 @@ mod tests { name: None, display_name: None, body: None, + annotations: None, }; // Check group with duplicate attributes. diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs index d40361960..c9db2718c 100644 --- a/crates/weaver_semconv/src/lib.rs +++ b/crates/weaver_semconv/src/lib.rs @@ -4,7 +4,10 @@ use crate::Error::CompoundError; use miette::Diagnostic; -use serde::Serialize; +use schemars::schema::{InstanceType, Schema}; +use schemars::{JsonSchema, SchemaGenerator}; +use serde::{Deserialize, Serialize}; +use std::hash::Hasher; use std::path::PathBuf; use weaver_common::diagnostic::{DiagnosticMessage, DiagnosticMessages}; use weaver_common::error::{format_errors, WeaverError}; @@ -299,6 +302,143 @@ impl From for DiagnosticMessages { } } +/// Create a newtype wrapper for serde_yaml::value::Value in order to implement +/// JsonSchema for it. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(transparent)] +pub struct YamlValue(pub serde_yaml::value::Value); + +impl JsonSchema for YamlValue { + fn schema_name() -> String { + "YamlValue".to_owned() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + // Create a schema that accepts any type + let schema = schemars::schema::SchemaObject { + instance_type: Some( + vec![ + InstanceType::Null, + InstanceType::Boolean, + InstanceType::Object, + InstanceType::Array, + InstanceType::Number, + InstanceType::String, + ] + .into(), + ), + ..Default::default() + }; + + Schema::Object(schema) + } +} + +/// Implement Hash for YamlValue. +/// Keys are sorted for consistent hashing in the case of mappings/objects. +impl std::hash::Hash for YamlValue { + fn hash(&self, state: &mut H) { + // Convert the YAML value to a string representation for hashing + // This is a simplification that works for most cases + match &self.0 { + serde_yaml::Value::Null => { + 0_u8.hash(state); + "null".hash(state); + } + serde_yaml::Value::Bool(b) => { + 1_u8.hash(state); + b.hash(state); + } + serde_yaml::Value::Number(n) => { + 2_u8.hash(state); + // Convert number to string for hashing as Number itself doesn't implement Hash + n.to_string().hash(state); + } + serde_yaml::Value::String(s) => { + 3_u8.hash(state); + s.hash(state); + } + serde_yaml::Value::Sequence(seq) => { + 4_u8.hash(state); + // Hash each element's string representation + for item in seq { + YamlValue(item.clone()).hash(state); + } + } + serde_yaml::Value::Mapping(map) => { + 5_u8.hash(state); + // Sort keys for consistent hashing + let mut keys: Vec<_> = map.keys().cloned().collect(); + + // Custom sort function that doesn't rely on to_string() + keys.sort_by(|a, b| { + // Compare keys based on their variant first + let type_order = |v: &serde_yaml::Value| -> u8 { + match v { + serde_yaml::Value::Null => 0, + serde_yaml::Value::Bool(_) => 1, + serde_yaml::Value::Number(_) => 2, + serde_yaml::Value::String(_) => 3, + serde_yaml::Value::Sequence(_) => 4, + serde_yaml::Value::Mapping(_) => 5, + serde_yaml::Value::Tagged(_) => 6, + } + }; + + let a_order = type_order(a); + let b_order = type_order(b); + + if a_order != b_order { + return a_order.cmp(&b_order); + } + + // If same type, do a specialized comparison + match (a, b) { + (serde_yaml::Value::Null, serde_yaml::Value::Null) => { + std::cmp::Ordering::Equal + } + (serde_yaml::Value::Bool(a_val), serde_yaml::Value::Bool(b_val)) => { + a_val.cmp(b_val) + } + (serde_yaml::Value::Number(a_val), serde_yaml::Value::Number(b_val)) => { + // Compare as strings since we can't directly compare numbers + a_val.to_string().cmp(&b_val.to_string()) + } + (serde_yaml::Value::String(a_val), serde_yaml::Value::String(b_val)) => { + a_val.cmp(b_val) + } + // For complex types, we'll use a hash-based comparison + // This isn't ideal for sorting but ensures consistency + _ => { + // Create a hasher and hash both values + let mut a_hasher = std::collections::hash_map::DefaultHasher::new(); + let mut b_hasher = std::collections::hash_map::DefaultHasher::new(); + + YamlValue(a.clone()).hash(&mut a_hasher); + YamlValue(b.clone()).hash(&mut b_hasher); + + a_hasher.finish().cmp(&b_hasher.finish()) + } + } + }); + + // Hash each key-value pair + for key in keys { + YamlValue(key.clone()).hash(state); + if let Some(value) = map.get(&key) { + YamlValue(value.clone()).hash(state); + } + } + } + serde_yaml::Value::Tagged(tag) => { + 6_u8.hash(state); + tag.tag.hash(state); + YamlValue(tag.value.clone()).hash(state); + } + } + } +} + #[cfg(test)] mod tests { use crate::registry::SemConvRegistry; diff --git a/crates/weaver_semconv/src/registry.rs b/crates/weaver_semconv/src/registry.rs index 6f7085d65..8815fe7ae 100644 --- a/crates/weaver_semconv/src/registry.rs +++ b/crates/weaver_semconv/src/registry.rs @@ -313,6 +313,7 @@ mod tests { note: "note".to_owned(), stability: None, deprecated: None, + annotations: None, }], constraints: vec![], span_kind: None, @@ -329,6 +330,7 @@ mod tests { name: None, display_name: Some("Group 1".to_owned()), body: None, + annotations: None, }], }, ), @@ -354,6 +356,7 @@ mod tests { name: None, display_name: Some("Group 2".to_owned()), body: None, + annotations: None, }], }, ), diff --git a/schemas/semconv-syntax.md b/schemas/semconv-syntax.md index eb7668ed9..d25447c93 100644 --- a/schemas/semconv-syntax.md +++ b/schemas/semconv-syntax.md @@ -42,7 +42,7 @@ All attributes are lower case. groups ::= semconv | semconv groups -semconv ::= id [convtype] brief [note] [extends] [stability] [deprecated] [display_name] [attributes] specificfields +semconv ::= id [convtype] brief [note] [extends] [stability] [deprecated] [display_name] [attributes] [annotations] specificfields extends_or_attributes ::= (extends | attributes | (extends attributes)) @@ -74,7 +74,9 @@ renamed_to ::= string display_name ::= string -attributes ::= (id type brief examples | ref [brief] [examples]) [tag] stability [deprecated] [requirement_level] [sampling_relevant] [note] +annotations ::= string yaml + +attributes ::= (id type brief examples | ref [brief] [examples]) [tag] stability [deprecated] [requirement_level] [sampling_relevant] [note] [annotations] # ref MUST point to an existing attribute id ref ::= id @@ -180,6 +182,8 @@ The field `semconv` represents a semantic convention and it is made by: - `deprecated`, optional, when present marks the semantic convention as deprecated. The string provided as `` MUST specify why it's deprecated and/or what to use instead. - `attributes`, list of attributes that belong to the semantic convention. +- `annotations`, optional map of annotations. Annotations are key-value pairs that provide additional information about + the group. The keys are strings and the values are any YAML value. #### Span semantic convention @@ -353,6 +357,8 @@ An attribute is defined by: 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. See [below](#examples-for-examples). +- `annotations`, optional map of annotations. Annotations are key-value pairs that provide additional information about + the attribute. The keys are strings and the values are any YAML value. #### Examples (for examples) diff --git a/schemas/semconv.schema.json b/schemas/semconv.schema.json index d4ce4078d..5f4d9b0d7 100644 --- a/schemas/semconv.schema.json +++ b/schemas/semconv.schema.json @@ -41,6 +41,10 @@ } }, "$defs": { + "annotations": { + "type": "object", + "additionalProperties": true + }, "Deprecated": { "description": "Specifies if an attribute or a signal is deprecated.", "oneOf": [ @@ -199,6 +203,9 @@ "$ref": "#/$defs/StabilityLevel" } ] + }, + "annotations": { + "$ref": "#/$defs/annotations" } } }, @@ -668,6 +675,9 @@ } ], "description": "sequence/dictionary of example values for the attribute. They are optional for boolean, int, double, and enum 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." + }, + "annotations": { + "$ref": "#/$defs/annotations" } } },