Skip to content

Commit

Permalink
(feat): Add attribute_sort filter to weaver_forge (open-telemetry#144)
Browse files Browse the repository at this point in the history
* (feat): Add attribute_sort filter to weaver_forge

Add a filter that lets us perform attribute sorting the way we need to for semconv templates.

Also document other filters/tests added previously.

* Remove poor expect in code

* Fixes from review

* Fix spelling
  • Loading branch information
jsuereth authored May 3, 2024
1 parent 261e4f4 commit 4d0b8bd
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ rayon = "1.10.0"
ordered-float = { version = "4.2.0", features = ["serde"] }
walkdir = "2.5.0"
anyhow = "1.0.81"
itertools = "0.12.1"

# Features definition =========================================================
[features]
Expand Down
1 change: 1 addition & 0 deletions crates/weaver_forge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jaq-parse = "1.0.2"
indexmap = "2.2.6"
regex = "1.10.4"

itertools.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_yaml.workspace = true
Expand Down
13 changes: 11 additions & 2 deletions crates/weaver_forge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,12 @@ in the `acronyms` section of the `weaver.yaml` configuration file.
- `split_ids`: Splits a string by '.' creating a list of nested ids.
- `flatten`: Converts a List of Lists into a single list with all elements.
e.g. \[\[a,b\],\[c\]\] => \[a,b,c\]

- `attribute_sort`: Sorts a list of `Attribute`s by requirement level, then name.
- `metric_namespace`: Converts registry.{namespace}.{other}.{components} to {namespace}.
- `attribute_registry_file`: Converts registry.{namespace}.{other}.{components} to attributes-registry/{namespace}.md (kebab-case namespace).
- `attribute_registry_title`: Converts registry.{namespace}.{other}.{components} to {Namespace} (title case the namespace).
- `attribute_registry_namespace`: Converts metric.{namespace}.{other}.{components} to {namespace}.
- `attribute_namespace`: Converts {namespace}.{attribute_id} to {namespace}.

> Note 1: This project uses the [convert_case](https://crates.io/crates/convert_case)
> crate to convert strings to different cases.
Expand All @@ -236,4 +241,8 @@ All the tests available in the MiniJinja template engine are available. In
addition, OTel Weaver provides a set of custom tests to facilitate the
generation of assets.

Not yet implemented.
- `stable`: Tests if an `Attribute` is stable.
- `experimental`: Tests if an `Attribute` is experimental.
- `deprecated`: Tests if an `Attribute` is deprecated.

> Note: Other tests might be introduced in the future.
271 changes: 270 additions & 1 deletion crates/weaver_forge/src/extensions/otel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
//! Set of filters, tests, and functions that are specific to the OpenTelemetry project.
use crate::config::CaseConvention;
use itertools::Itertools;
use minijinja::{ErrorKind, Value};
use serde::de::Error;

/// Converts registry.{namespace}.{other}.{components} to {namespace}.
///
Expand Down Expand Up @@ -83,6 +85,82 @@ pub(crate) fn attribute_namespace(input: &str) -> Result<String, minijinja::Erro
Ok(parts[0].to_owned())
}

/// Compares two attributes by their requirement_level, then name.
fn compare_requirement_level(
lhs: &Value,
rhs: &Value,
) -> Result<std::cmp::Ordering, minijinja::Error> {
fn sort_ordinal_for_requirement(attribute: &Value) -> Result<i32, minijinja::Error> {
let level = attribute.get_attr("requirement_level")?;
if level
.get_attr("conditionally_required")
.is_ok_and(|v| !v.is_undefined())
{
Ok(2)
} else if level
.get_attr("recommended")
.is_ok_and(|v| !v.is_undefined())
{
Ok(3)
} else {
match level.as_str() {
Some("required") => Ok(1),
Some("recommended") => Ok(3),
Some("opt_in") => Ok(4),
_ => Err(minijinja::Error::custom(format!(
"Expected requirement level, found {}",
level
))),
}
}
}
match sort_ordinal_for_requirement(lhs)?.cmp(&sort_ordinal_for_requirement(rhs)?) {
std::cmp::Ordering::Equal => {
let lhs_name = lhs.get_attr("name")?;
let rhs_name = rhs.get_attr("name")?;
if lhs_name.lt(&rhs_name) {
Ok(std::cmp::Ordering::Less)
} else if lhs_name.eq(&rhs_name) {
Ok(std::cmp::Ordering::Equal)
} else {
Ok(std::cmp::Ordering::Greater)
}
}
other => Ok(other),
}
}

/// Sorts a sequence of attributes by their requirement_level, then name.
pub(crate) fn attribute_sort(input: Value) -> Result<Value, minijinja::Error> {
let mut errors: Vec<minijinja::Error> = vec![];
let opt_result = input.as_seq().map(|values| {
let result: Vec<Value> = values
.iter()
.sorted_by(|lhs, rhs| {
// Sorted doesn't allow us to keep errors, so we sneak them into
// a mutable vector.
match compare_requirement_level(lhs, rhs) {
Ok(result) => result,
Err(error) => {
errors.push(error);
std::cmp::Ordering::Less
}
}
})
.to_owned()
.collect();
Value::from(result)
});
// If we had an internal error, return the first.
match errors.pop() {
Some(err) => Err(err),
None => opt_result.ok_or(minijinja::Error::custom(format!(
"Expected sequence of attributes, found: {}",
input
))),
}
}

/// Checks if the input value is an object with a field named "stability" that has the value "stable".
/// Otherwise, it returns false.
#[must_use]
Expand Down Expand Up @@ -129,10 +207,15 @@ pub(crate) fn is_deprecated(input: Value) -> bool {
mod tests {
use crate::extensions::otel::{
attribute_registry_file, attribute_registry_namespace, attribute_registry_title,
is_deprecated, is_experimental, is_stable, metric_namespace,
attribute_sort, is_deprecated, is_experimental, is_stable, metric_namespace,
};
use minijinja::value::StructObject;
use minijinja::Value;
use weaver_resolved_schema::attribute::Attribute;
use weaver_semconv::attribute::AttributeType;
use weaver_semconv::attribute::BasicRequirementLevelSpec;
use weaver_semconv::attribute::PrimitiveOrArrayTypeSpec;
use weaver_semconv::attribute::RequirementLevel;

struct DynAttr {
id: String,
Expand Down Expand Up @@ -338,4 +421,190 @@ mod tests {
});
assert!(!is_deprecated(attr));
}

#[test]
fn test_attribute_sort() {
// Attributes in no particular order.
let attributes: Vec<Attribute> = vec![
Attribute {
name: "rec.a".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Recommended),
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "rec.b".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Recommended),
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "crec.a".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::ConditionallyRequired { text: "hi".into() },
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "crec.b".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::ConditionallyRequired { text: "hi".into() },
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "rec.c".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Recommended { text: "hi".into() },
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "rec.d".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Recommended { text: "hi".into() },
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "opt.a".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::OptIn),
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "opt.b".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::OptIn),
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "req.a".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required),
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
Attribute {
name: "req.b".into(),
r#type: AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String),
brief: "".into(),
examples: None,
tag: None,
requirement_level: RequirementLevel::Basic(BasicRequirementLevelSpec::Required),
sampling_relevant: None,
note: "".into(),
stability: None,
deprecated: None,
tags: None,
value: None,
},
];
let json =
serde_json::to_value(attributes).expect("Failed to serialize attributes to json.");
let value = Value::from_serialize(json);
let result = attribute_sort(value).expect("Failed to sort attributes");
let result_seq = result.as_seq().expect("Result was not a sequence!");
// Assert that requirement level takes precedence over anything else.
assert_eq!(
result_seq.item_count(),
10,
"Expected 10 items, found {}",
result
);
let names: Vec<String> = result_seq
.iter()
.map(|item| item.get_attr("name").unwrap().as_str().unwrap().to_owned())
.collect();
let expected_names: Vec<String> = vec![
// Required First
"req.a".to_owned(),
"req.b".to_owned(),
// Conditionally Required Second
"crec.a".to_owned(),
"crec.b".to_owned(),
// Conditionally Recommended + Recommended Third
"rec.a".to_owned(),
"rec.b".to_owned(),
"rec.c".to_owned(),
"rec.d".to_owned(),
// OptIn last
"opt.a".to_owned(),
"opt.b".to_owned(),
];

for (idx, (result, expected)) in names.iter().zip(expected_names.iter()).enumerate() {
assert_eq!(
result, expected,
"Expected item @ {idx} to have name {expected}, found {names:?}"
);
}
}
}
1 change: 1 addition & 0 deletions crates/weaver_forge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ impl TemplateEngine {
"attribute_registry_file",
extensions::otel::attribute_registry_file,
);
env.add_filter("attribute_sort", extensions::otel::attribute_sort);
env.add_filter("metric_namespace", extensions::otel::metric_namespace);
// ToDo Implement more filters: required, not_required, stable, experimental, deprecated
env.add_test("stable", extensions::otel::is_stable);
Expand Down
2 changes: 1 addition & 1 deletion crates/weaver_semconv_gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ weaver_forge = { path = "../weaver_forge" }
weaver_resolver = { path = "../weaver_resolver" }
weaver_resolved_schema = { path = "../weaver_resolved_schema" }
weaver_semconv = { path = "../weaver_semconv" }
itertools = "0.12.1"
nom = "7.1.3"

itertools.workspace = true
thiserror.workspace = true
serde.workspace = true

Expand Down

0 comments on commit 4d0b8bd

Please sign in to comment.