diff --git a/Cargo.lock b/Cargo.lock index 09c361a2..a0a6bfd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -157,6 +158,15 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -302,6 +312,15 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown", +] + [[package]] name = "clap" version = "4.5.2" @@ -601,6 +620,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.10.0" @@ -1570,6 +1595,7 @@ dependencies = [ "log", "regex-automata 0.4.6", "regex-syntax 0.8.2", + "serde", ] [[package]] @@ -1624,6 +1650,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hifijson" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ef6b41c333e6dd2a4aaa59125a19b633cd17e7aaf372b2260809777bcdef4a" + [[package]] name = "home" version = "0.5.9" @@ -1849,6 +1881,68 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jaq-core" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d6a5713b8f33675abfac79d1db0022a3f28764b2a6b96a185c199ad8dab86d" +dependencies = [ + "aho-corasick", + "base64", + "hifijson", + "jaq-interpret", + "libm", + "log", + "regex", + "time", + "urlencoding", +] + +[[package]] +name = "jaq-interpret" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569e38e5fc677db8dfda89ee0b4c25b3f53e811b16434fd14bdc5b43fc362ac" +dependencies = [ + "ahash", + "dyn-clone", + "hifijson", + "indexmap", + "jaq-syn", + "once_cell", + "serde_json", +] + +[[package]] +name = "jaq-parse" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6f8beb9f9922546419e774e24199e8a968f54c63a5a2323c8f3ef3321ace14" +dependencies = [ + "chumsky", + "jaq-syn", +] + +[[package]] +name = "jaq-std" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7871c59297cbfdd18f6f1bbbafaad24e97fd555ee1e2a1be7a40a5a20f551a" +dependencies = [ + "bincode", + "jaq-parse", + "jaq-syn", +] + +[[package]] +name = "jaq-syn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d60101fb791b20c982731d848ed6e7d25363656497647c2093b68bd88398d6" +dependencies = [ + "serde", +] + [[package]] name = "jobserver" version = "0.1.28" @@ -2391,9 +2485,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -2601,9 +2695,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" dependencies = [ "base64", "bytes", @@ -3026,18 +3120,18 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck", "proc-macro2", @@ -3298,18 +3392,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -3384,7 +3478,6 @@ dependencies = [ "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "windows-sys 0.48.0", @@ -3655,6 +3748,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8-ranges" version = "1.0.5" @@ -3818,6 +3917,7 @@ dependencies = [ "crossterm", "ratatui", "serde", + "serde_json", "serde_yaml", "tantivy", "tui-textarea", @@ -3845,7 +3945,12 @@ name = "weaver_forge" version = "0.1.0" dependencies = [ "convert_case", - "glob", + "globset", + "indexmap", + "jaq-core", + "jaq-interpret", + "jaq-parse", + "jaq-std", "minijinja", "rayon", "serde", diff --git a/Cargo.toml b/Cargo.toml index e520a6bd..7cba349e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,13 +25,13 @@ license = "Apache-2.0" publish = false [workspace.dependencies] -serde = { version = "1.0.196", features = ["derive"] } +serde = { version = "1.0.197", features = ["derive"] } serde_yaml = "0.9.32" -serde_json = "1.0.113" -thiserror = "1.0.56" +serde_json = "1.0.114" +thiserror = "1.0.58" ureq = "2.9.6" regex = "1.10.3" -rayon = "1.8.1" +rayon = "1.9.0" ordered-float = { version = "4.2.0", features = ["serde"] } walkdir = "2.5.0" @@ -64,6 +64,7 @@ tantivy = "0.21.1" # workspace dependencies serde.workspace = true serde_yaml.workspace = true +serde_json.workspace = true [package.metadata.cargo-machete] # force cargo machete to ignore the following crates diff --git a/README.md b/README.md index adafd45d..5bf7df76 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Commands: Options: -d, --debug... Turn debugging information on + -q, --quiet Turn the quiet mode on (i.e., minimal output) -h, --help Print help -V, --version Print version ``` @@ -61,76 +62,86 @@ Manage Semantic Convention Registry Usage: weaver registry Commands: - check Validates a registry (i.e., parsing, resolution of references, extends clauses, and constraints) - generate Generates documentation or code for a registry (not yet implemented) - resolve Resolves a registry (not yet implemented) - search Searches a registry (not yet implemented) - stats Calculate and display a set of general statistics on a registry (not yet implemented) - help Print this message or the help of the given subcommand(s) + check Validates a registry (i.e., parsing, resolution of references, extends clauses, and constraints) + generate Generates artifacts from a registry + resolve Resolves a registry + search Searches a registry (not yet implemented) + stats Calculate and display a set of general statistics on a registry (not yet implemented) + update-markdown Update markdown files that contain markers indicating the templates used to update the specified sections Options: -h, --help Print help ``` -### Command `search` (Experimental) +### Sub-Command `registry check` -This command provides an interactive terminal UI, allowing users to search for -attributes and metrics specified within a given semantic convention registry or -a telemetry schema (including dependencies). +``` +Validates a registry (i.e., parsing, resolution of references, extends clauses, and constraints) -To search into the OpenTelemetry Semantic Convention Registry, run the following -command: +Usage: weaver registry check [OPTIONS] -```bash -weaver search registry https://github.com/open-telemetry/semantic-conventions.git model +Options: + -r, --registry + Local path or Git URL of the semantic convention registry to check [default: https://github.com/open-telemetry/semantic-conventions.git] + -d, --registry-git-sub-dir + Optional path in the Git repository where the semantic convention registry is located [default: model] ``` -To search into a telemetry schema, run the following command: +### Sub-Command `registry generate` -```bash -weaver search schema demo/app-telemetry-schema.yaml ``` +Generates artifacts from a registry -This search engine leverages [Tantivy](https://github.com/quickwit-oss/tantivy) -and supports a simple [search syntax](https://docs.rs/tantivy/latest/tantivy/query/struct.QueryParser.html) -in the search bar. +Usage: weaver registry generate [OPTIONS] [OUTPUT] -### Command `resolve` (Experimental) +Arguments: + Target to generate the artifacts for + [OUTPUT] Path to the directory where the generated artifacts will be saved. Default is the `output` directory [default: output] + +Options: + -t, --templates + Path to the directory where the templates are located. Default is the `templates` directory [default: templates] + -r, --registry + Local path or Git URL of the semantic convention registry [default: https://github.com/open-telemetry/semantic-conventions.git] + -d, --registry-git-sub-dir + Optional path in the Git repository where the semantic convention registry is located [default: model] +``` -This command resolves a schema or a semantic convention registry (not yet -implemented) and displays the result on the standard output. -Alternatively, the result can be written to a file if specified using the -`--output` option. This command is primarily used for validating and debugging -telemetry schemas and semantic convention registries. +### Sub-Command `registry resolve` -```bash -weaver resolve schema telemetry-schema.yaml --output telemetry-schema-resolved.yaml ``` +Resolves a registry -A "resolved schema" is one where: -- All references have been resolved and expanded. -- All overrides have been applied. -- This resolved schema is what the code generator and upcoming plugins utilize. +Usage: weaver registry resolve [OPTIONS] -### Command `gen-client` (Experimental) +Options: + -r, --registry + Local path or Git URL of the semantic convention registry -This command generates a client SDK from a telemetry schema for a given language -specified with the `--language` option. + [default: https://github.com/open-telemetry/semantic-conventions.git] -```bash -weaver gen-client --schema telemetry-schema.yaml --language go -``` + -d, --registry-git-sub-dir + Optional path in the Git repository where the semantic convention registry is located + + [default: model] + + --catalog + Flag to indicate if the shared catalog should be included in the resolved schema + + --lineage + Flag to indicate if lineage information should be included in the resolved schema (not yet implemented) -In the future, users will be able to specify the protocol to use for the generated -client SDK (i.e. OTLP or OTel Arrow Protocol) and few others options. + -o, --output + Output file to write the resolved schema to If not specified, the resolved schema is printed to stdout -### Command `languages` (Experimental) + -f, --format + Output format for the resolved schema If not specified, the resolved schema is printed in YAML format Supported formats: yaml, json Default format: yaml Example: `--format json` -This command displays all the languages for which a client SDK/API can -be generated. + [default: yaml] -```bash -weaver languages + Possible values: + - yaml: YAML format + - json: JSON format ``` ### Crates Layout @@ -153,16 +164,16 @@ crates.io. The following is a list of crates in the workspace, along with a brief description and the current status of each crate: -| Crate | Description | Status | -|-------------------------------------------------------------------|---------------------------------------------------------|------------------------| -| [weaver_semconv](crates/weaver_semconv/README.md) | Semantic Convention Registry Data Model | Alpha; Need more tests | -| [weaver_version](crates/weaver_version/README.md) | OpenTelemetry Schema Versioning Data Model | Alpha; Need more tests | -| [weaver_resolved_schema](crates/weaver_resolved_schema/README.md) | Resolved Schema Data Model | Work-In-Progress | -| [weaver_schema](crates/weaver_schema/README.md) | Telemetry Schema Data Model | Work-In-Progress | -| [weaver_resolver](crates/weaver_resolver/README.md) | Telemetry Schema Resolution Process | Work-In-Progress | -| [weaver_cache](crates/weaver_cache/README.md) | Telemetry Schema and Semantic Convention Registry Cache | Work-In-Progress | -| [weaver_logger](crates/weaver_logger/README.md) | Generic logger supported colorized output | Alpha | -| [weaver_template](crates/weaver_template/README.md) | Functions and Filters used in the template engine | Work-In-Progress | +| Crate | Description | Status | +|-------------------------------------------------------------------|----------------------------------------------------------------------|-------------------------| +| [weaver_semconv](crates/weaver_semconv/README.md) | Semantic Convention Registry Data Model | Alpha; Need more tests | +| [weaver_version](crates/weaver_version/README.md) | OpenTelemetry Schema Versioning Data Model | Alpha; Need more tests | +| [weaver_resolved_schema](crates/weaver_resolved_schema/README.md) | Resolved Schema Data Model | Work-In-Progress | +| [weaver_schema](crates/weaver_schema/README.md) | Telemetry Schema Data Model | Work-In-Progress | +| [weaver_resolver](crates/weaver_resolver/README.md) | Telemetry Schema Resolution Process | Work-In-Progress | +| [weaver_cache](crates/weaver_cache/README.md) | Telemetry Schema and Semantic Convention Registry Cache | Work-In-Progress | +| [weaver_logger](crates/weaver_logger/README.md) | Generic logger supported colorized output | Alpha | +| [weaver_forge](crates/weaver_forge/README.md) | Template engine used to generate artifacts from any serde json value | Alpha; Need more tests | Note 1: Alpha status means that the crate is in a usable state but may have limited functionality and/or may not be fully tested. diff --git a/crates/weaver_forge/Cargo.toml b/crates/weaver_forge/Cargo.toml index 733a8bd6..d46b6ffc 100644 --- a/crates/weaver_forge/Cargo.toml +++ b/crates/weaver_forge/Cargo.toml @@ -13,9 +13,14 @@ weaver_resolver = { path = "../weaver_resolver" } weaver_resolved_schema = { path = "../weaver_resolved_schema" } weaver_semconv = { path = "../weaver_semconv" } -minijinja = { version = "1.0.12", features = ["loader", "custom_syntax"] } +minijinja = { version = "1.0.12", features = ["loader", "custom_syntax", "debug"] } convert_case = "0.6.0" -glob = "0.3.1" +globset = { version = "0.4.14", features = ["serde1"] } +jaq-core = "1.2.1" +jaq-std = "1.2.1" +jaq-interpret = "1.2.1" +jaq-parse = "1.0.2" +indexmap = "2.2.5" thiserror.workspace = true serde.workspace = true diff --git a/crates/weaver_forge/allowed-external-types.toml b/crates/weaver_forge/allowed-external-types.toml index 658b7951..ba4fbdb3 100644 --- a/crates/weaver_forge/allowed-external-types.toml +++ b/crates/weaver_forge/allowed-external-types.toml @@ -5,6 +5,8 @@ allowed_external_types = [ "serde::ser::Serialize", "serde::de::Deserialize", + "serde_json::value::Value", "weaver_logger::*", "weaver_resolved_schema::*", + "weaver_semconv::*", ] \ No newline at end of file diff --git a/crates/weaver_forge/expected_output/metric_groups.md b/crates/weaver_forge/expected_output/metric_groups.md deleted file mode 100644 index 28eae3a2..00000000 --- a/crates/weaver_forge/expected_output/metric_groups.md +++ /dev/null @@ -1,2 +0,0 @@ -# Semantic Convention Metric Group Groups - diff --git a/crates/weaver_forge/src/config.rs b/crates/weaver_forge/src/config.rs index f21209f2..07c36453 100644 --- a/crates/weaver_forge/src/config.rs +++ b/crates/weaver_forge/src/config.rs @@ -6,10 +6,13 @@ use std::collections::HashMap; use std::path::Path; use convert_case::{Case, Casing}; +use globset::{Glob, GlobSet, GlobSetBuilder}; use serde::Deserialize; -use crate::Error; -use crate::Error::InvalidConfigFile; +use crate::error::Error; +use crate::error::Error::InvalidConfigFile; +use crate::filter::Filter; +use crate::WEAVER_YAML; /// Case convention for naming of functions and structs. #[derive(Deserialize, Clone, Debug)] @@ -57,6 +60,146 @@ pub struct TargetConfig { /// Configuration for the template syntax. #[serde(default)] pub template_syntax: TemplateSyntax, + + /// Configuration for the templates. + #[serde(default = "default_templates")] + pub templates: Vec, +} + +fn default_templates() -> Vec { + vec![ + TemplateConfig { + pattern: Glob::new("**/registry.md").unwrap(), + filter: Filter::try_new(".").expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/attribute_group.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"attribute_group\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/attribute_groups.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"attribute_group\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/event.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"event\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/events.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"event\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/group.md").unwrap(), + filter: Filter::try_new(".groups").expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/groups.md").unwrap(), + filter: Filter::try_new(".groups").expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/metric.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"metric\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/metrics.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"metric\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/resource.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"resource\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/resources.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"resource\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/scope.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"scope\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/scopes.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"scope\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/span.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"span\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/spans.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"span\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + ] +} + +/// Application mode defining how to apply a template on the result of a +/// filter applied on a registry. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ApplicationMode { + /// Apply the template to the output of the filter as a whole. + Single, + /// Apply the template to each item of the list returned by the filter. + Each, +} + +/// A template configuration. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub struct TemplateConfig { + /// The pattern used to identify when this template configuration must be + /// applied to a specific template file. + pub pattern: Glob, + /// The filter to apply to the registry before applying the template. + /// Applying a filter to a registry will return a list of elements from the + /// registry that satisfy the filter. + pub filter: Filter, + /// The mode to apply the template. + /// `single`: Apply the template to the output of the filter as a whole. + /// `each`: Apply the template to each item of the list returned by the filter. + pub application_mode: ApplicationMode, +} + +/// A template matcher. +pub struct TemplateMatcher<'a> { + templates: &'a [TemplateConfig], + glob_set: GlobSet, +} + +impl<'a> TemplateMatcher<'a> { + pub fn matches>(&self, path: P) -> Vec<&'a TemplateConfig> { + self.glob_set + .matches(path) + .into_iter() + .map(|i| &self.templates[i]) + .collect() + } } /// Syntax configuration for the template engine. @@ -163,7 +306,7 @@ impl CaseConvention { impl TargetConfig { pub fn try_new(lang_path: &Path) -> Result { - let config_file = lang_path.join("weaver.yaml"); + let config_file = lang_path.join(WEAVER_YAML); if config_file.exists() { let reader = std::fs::File::open(config_file.clone()).map_err(|e| InvalidConfigFile { @@ -178,4 +321,23 @@ impl TargetConfig { Ok(TargetConfig::default()) } } + + /// Return a template matcher for the target configuration. + pub fn template_matcher(&self) -> Result { + let mut builder = GlobSetBuilder::new(); + + self.templates.iter().for_each(|template| { + _ = builder.add(template.pattern.clone()); + }); + + builder + .build() + .map_err(|e| Error::InvalidTemplatePattern { + error: e.to_string(), + }) + .map(|glob_set| TemplateMatcher { + templates: &self.templates, + glob_set, + }) + } } diff --git a/crates/weaver_forge/src/debug.rs b/crates/weaver_forge/src/debug.rs new file mode 100644 index 00000000..88ea731f --- /dev/null +++ b/crates/weaver_forge/src/debug.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to help with debugging. + +use crate::error::Error; +use crate::error::Error::{CompoundError, TemplateEvaluationFailed}; +use indexmap::IndexMap; +use weaver_logger::Logger; + +/// Return a nice summary of the error. +pub(crate) fn error_summary(error: minijinja::Error) -> String { + format!("{:#}", error) +} + +/// Print deduplicated errors. +/// +/// This function prints the error message and the number of occurrences of +/// each error. If an error occurs only once, the error message is printed +/// as is. If an error occurs more than once, the error message is printed +/// once and the number of occurrences is printed as "and n more similar +/// errors". +/// +/// The order of the errors is preserved. +/// +/// # Arguments +/// +/// * `logger` - The logger to use for logging. +/// * `error` - The error to print. +pub fn print_dedup_errors(logger: impl Logger + Sync + Clone, error: Error) { + struct DedupError { + pub error: String, + pub occurrences: usize, + } + + let mut dedup_errs = IndexMap::new(); + match error { + CompoundError(errs) => { + for err in errs { + match err.clone() { + TemplateEvaluationFailed { + error_id, error, .. + } => { + _ = dedup_errs + .entry(error_id) + .and_modify(|e: &mut DedupError| e.occurrences += 1) + .or_insert(DedupError { + error, + occurrences: 1, + }); + } + _ => { + _ = dedup_errs + .entry(err.to_string()) + .and_modify(|e: &mut DedupError| e.occurrences += 1) + .or_insert(DedupError { + error: err.to_string(), + occurrences: 1, + }); + } + } + } + } + _ => { + _ = dedup_errs + .entry(error.to_string()) + .and_modify(|e| e.occurrences += 1) + .or_insert(DedupError { + error: error.to_string(), + occurrences: 1, + }); + } + } + dedup_errs.iter().for_each(|(_, err)| { + let output = match err.occurrences { + 1 => err.error.to_string(), + 2 => format!("{}\n\nFound 1 similar error", err.error), + _ => format!( + "{}\n\nFound {} similar errors", + err.error, + err.occurrences - 1 + ), + }; + _ = logger.error(&output); + }); +} diff --git a/crates/weaver_forge/src/error.rs b/crates/weaver_forge/src/error.rs new file mode 100644 index 00000000..2a65b918 --- /dev/null +++ b/crates/weaver_forge/src/error.rs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Error types and utilities. + +use crate::error::Error::CompoundError; +use std::path::PathBuf; +use weaver_resolved_schema::attribute::AttributeRef; + +/// Errors emitted by this crate. +#[derive(thiserror::Error, Debug, Clone)] +pub enum Error { + /// Invalid config file. + #[error("Invalid config file `{config_file}`: {error}")] + InvalidConfigFile { + /// Config file. + config_file: PathBuf, + /// Error message. + error: String, + }, + + /// Target not found. + #[error( + "Target `{target}` not found in `{root_path}`. Use the command `targets` to list supported targets." + )] + TargetNotSupported { + /// Root path. + root_path: String, + /// Target name. + target: String, + }, + + /// Invalid template directory. + #[error("Invalid template directory {template_dir}: {error}")] + InvalidTemplateDir { + /// Template directory. + template_dir: PathBuf, + /// Error message. + error: String, + }, + + /// Invalid telemetry schema. + #[error("Invalid telemetry schema {schema}: {error}")] + InvalidTelemetrySchema { + /// Schema file. + schema: PathBuf, + /// Error message. + error: String, + }, + + /// Invalid template file. + #[error("Invalid template file '{template}': {error}")] + InvalidTemplateFile { + /// Template path. + template: PathBuf, + /// Error message. + error: String, + }, + + /// Template evaluation failed. + #[error("Template evaluation error -> {error}")] + TemplateEvaluationFailed { + /// Template path. + template: PathBuf, + /// Error id used to deduplicate the error. + error_id: String, + /// Error message. + error: String, + }, + + /// Invalid template directory. + #[error("Invalid template directory: {0}")] + InvalidTemplateDirectory(PathBuf), + + /// Template file name undefined. + #[error("File name undefined in the template `{template}`. To resolve this, use the function `config(file_name = )` to set the file name.")] + TemplateFileNameUndefined { + /// Template path. + template: PathBuf, + }, + + /// Write generated code failed. + #[error("Writing of the generated code {template} failed: {error}")] + WriteGeneratedCodeFailed { + /// Template path. + template: PathBuf, + /// Error message. + error: String, + }, + + /// Attribute reference not found in the catalog. + #[error("Attribute reference {attr_ref} (group: {group_id}) not found in the catalog")] + AttributeNotFound { + /// Group id. + group_id: String, + /// Attribute reference. + attr_ref: AttributeRef, + }, + + /// Filter error. + #[error("Filter '{filter}' failed: {error}")] + FilterError { + /// Filter that caused the error. + filter: String, + /// Error message. + error: String, + }, + + /// Invalid template pattern. + #[error("Invalid template pattern: {error}")] + InvalidTemplatePattern { + /// Error message. + error: String, + }, + + /// The serialization of the context failed. + #[error("The serialization of the context failed: {error}")] + ContextSerializationFailed { + /// Error message. + error: String, + }, + + /// A generic container for multiple errors. + #[error("Errors:\n{0:#?}")] + CompoundError(Vec), +} + +/// Handles a list of errors and returns a compound error if the list is not +/// empty or () if the list is empty. +pub fn handle_errors(errors: Vec) -> Result<(), Error> { + if errors.is_empty() { + Ok(()) + } else { + Err(Error::compound_error(errors)) + } +} + +impl Error { + /// Creates a compound error from a list of errors. + /// Note: All compound errors are flattened. + pub fn compound_error(errors: Vec) -> Self { + CompoundError( + errors + .into_iter() + .flat_map(|e| match e { + CompoundError(errors) => errors, + e => vec![e], + }) + .collect(), + ) + } +} diff --git a/crates/weaver_forge/src/filter.rs b/crates/weaver_forge/src/filter.rs new file mode 100644 index 00000000..ff24b5fe --- /dev/null +++ b/crates/weaver_forge/src/filter.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Filter JSON values using a simple expression language. + +use crate::error::Error; +use core::fmt; +use jaq_interpret::{Ctx, FilterT, RcIter, Val}; +use serde::de; +use std::fmt::Debug; + +/// A filter that can be applied to a JSON value. +pub struct Filter { + filter_expr: String, + filter: jaq_interpret::Filter, +} + +impl Filter { + /// Create a new filter from a string expression or return an error if the + /// expression is invalid. + pub fn try_new(filter_expr: &str) -> Result { + let vars = Vec::new(); + let mut ctx = jaq_interpret::ParseCtx::new(vars); + ctx.insert_natives(jaq_core::core()); + ctx.insert_defs(jaq_std::std()); + + let (parsed_expr, errs) = jaq_parse::parse(filter_expr, jaq_parse::main()); + + // If there are any errors, return them + if !errs.is_empty() { + return Err(Error::CompoundError( + errs.into_iter() + .map(|e| Error::FilterError { + filter: filter_expr.to_string(), + error: e.to_string(), + }) + .collect(), + )); + } + + let parsed_expr = parsed_expr.ok_or_else(|| Error::FilterError { + filter: filter_expr.to_string(), + error: "No parsed expression".to_string(), + })?; + + Ok(Self { + filter_expr: filter_expr.to_string(), + filter: ctx.compile(parsed_expr), + }) + } + + /// Apply the filter to a JSON value and return the result as a JSON value. + pub fn apply(&self, ctx: serde_json::Value) -> Result { + let inputs = RcIter::new(core::iter::empty()); + let filter_result = self.filter.run((Ctx::new([], &inputs), Val::from(ctx))); + let mut errs = Vec::new(); + let mut values = Vec::new(); + + for r in filter_result { + match r { + Ok(v) => values.push(serde_json::Value::from(v)), + Err(e) => errs.push(e), + } + } + + if values.len() == 1 { + return Ok(values.pop().unwrap()); + } + + Ok(serde_json::Value::Array(values)) + } +} + +impl Debug for Filter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Filter({})", self.filter_expr) + } +} + +struct FilterVisitor; + +impl<'de> de::Visitor<'de> for FilterVisitor { + type Value = Filter; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a filter string") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Filter::try_new(value).map_err(E::custom) + } +} + +impl<'de> de::Deserialize<'de> for Filter { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + deserializer.deserialize_str(FilterVisitor) + } +} diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 08d00ef1..7f5a8228 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -19,118 +19,35 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use glob::{glob, Paths}; use minijinja::value::{from_args, Object}; use minijinja::{path_loader, Environment, State, Value}; use rayon::iter::IntoParallelIterator; use rayon::iter::ParallelIterator; use serde::Serialize; +use walkdir::{DirEntry, WalkDir}; +use error::Error; +use error::Error::{ + ContextSerializationFailed, InvalidTemplateDir, InvalidTemplateFile, TargetNotSupported, + TemplateEvaluationFailed, WriteGeneratedCodeFailed, +}; use weaver_logger::Logger; -use weaver_resolved_schema::attribute::AttributeRef; -use weaver_resolved_schema::catalog::Catalog; -use weaver_resolved_schema::registry::Registry; -use weaver_semconv::group::GroupType; -use crate::config::TargetConfig; +use crate::config::{ApplicationMode, TargetConfig}; +use crate::debug::error_summary; +use crate::error::Error::InvalidConfigFile; use crate::extensions::case_converter::case_converter; use crate::registry::{TemplateGroup, TemplateRegistry}; -use crate::Error::{ - InternalError, InvalidTemplateDir, InvalidTemplateDirectory, InvalidTemplateFile, - TargetNotSupported, WriteGeneratedCodeFailed, -}; mod config; +pub mod debug; +pub mod error; mod extensions; -mod registry; - -/// Errors emitted by this crate. -#[derive(thiserror::Error, Debug)] -pub enum Error { - /// Invalid config file. - #[error("Invalid config file `{config_file}`: {error}")] - InvalidConfigFile { - /// Config file. - config_file: PathBuf, - /// Error message. - error: String, - }, - - /// Target not found. - #[error( - "Target `{target}` not found in `{root_path}`. Use the command `targets` to list supported targets." - )] - TargetNotSupported { - /// Root path. - root_path: String, - /// Target name. - target: String, - }, - - /// Invalid template directory. - #[error("Invalid template directory {template_dir}: {error}")] - InvalidTemplateDir { - /// Template directory. - template_dir: PathBuf, - /// Error message. - error: String, - }, - - /// Invalid telemetry schema. - #[error("Invalid telemetry schema {schema}: {error}")] - InvalidTelemetrySchema { - /// Schema file. - schema: PathBuf, - /// Error message. - error: String, - }, - - /// Invalid template file. - #[error("Invalid template file '{template}': {error}")] - InvalidTemplateFile { - /// Template path. - template: PathBuf, - /// Error message. - error: String, - }, - - /// Invalid template directory. - #[error("Invalid template directory: {0}")] - InvalidTemplateDirectory(PathBuf), - - /// Internal error. - #[error("Internal error: {0}")] - InternalError(String), - - /// Template file name undefined. - #[error("File name undefined in the template `{template}`. To resolve this, use the function `config(file_name = )` to set the file name.")] - TemplateFileNameUndefined { - /// Template path. - template: PathBuf, - }, - - /// Write generated code failed. - #[error("Writing of the generated code {template} failed: {error}")] - WriteGeneratedCodeFailed { - /// Template path. - template: PathBuf, - /// Error message. - error: String, - }, - - /// Attribute reference not found in the catalog. - #[error("Attribute reference {attr_ref} (group: {group_id}) not found in the catalog")] - AttributeNotFound { - /// Group id. - group_id: String, - /// Attribute reference. - attr_ref: AttributeRef, - }, - - /// A generic container for multiple errors. - #[error("Errors:\n{0:#?}")] - CompoundError(Vec), -} +mod filter; +pub mod registry; + +/// Name of the Weaver configuration file. +pub const WEAVER_YAML: &str = "weaver.yaml"; /// General configuration for the generator. pub struct GeneratorConfig { @@ -147,23 +64,6 @@ impl Default for GeneratorConfig { } } -/// A pair {template, object} to generate code for. -#[derive(Debug)] -enum TemplateObjectPair<'a> { - Group { - template_path: PathBuf, - group: &'a TemplateGroup, - }, - Groups { - template_path: PathBuf, - groups: Vec<&'a TemplateGroup>, - }, - Registry { - template_path: PathBuf, - registry: &'a TemplateRegistry, - }, -} - /// A template object accessible from the template. #[derive(Debug, Clone)] struct TemplateObject { @@ -227,6 +127,24 @@ pub struct Context<'a> { pub groups: Option>, } +/// Global context for the template engine. +#[derive(Serialize, Debug)] +pub struct NewContext<'a> { + /// The semantic convention registry. + pub ctx: &'a serde_json::Value, +} + +/// Convert a context into a serde_json::Value. +impl TryInto for NewContext<'_> { + type Error = Error; + + fn try_into(self) -> Result { + serde_json::to_value(self).map_err(|e| ContextSerializationFailed { + error: e.to_string(), + }) + } +} + impl TemplateEngine { /// Create a new template engine for the given target or return an error if /// the target does not exist or is not a directory. @@ -248,80 +166,138 @@ impl TemplateEngine { }) } - // ToDo Refactor InternalError - // ToDo Use compound error - - /// Generate assets from a semantic convention registry. - pub fn generate_registry( + /// Generate artifacts from a serializable context and a template directory, + /// in parallel. + /// + /// # Arguments + /// + /// * `log` - The logger to use for logging. + /// * `context` - The context to use for generating the artifacts. + /// * `output_dir` - The directory where the generated artifacts will be saved. + /// + /// # Returns + /// + /// * `Ok(())` if the artifacts were generated successfully. + /// * `Err(error)` if an error occurred during the generation of the artifacts. + pub fn generate( &self, log: impl Logger + Clone + Sync, - registry: &Registry, - catalog: &Catalog, + context: &T, output_dir: &Path, ) -> Result<(), Error> { - // Process recursively all files in the template directory - let mut lang_path = self.path.to_str().unwrap_or_default().to_string(); - let paths = if lang_path.is_empty() { - glob("**/*").map_err(|e| InternalError(e.to_string()))? - } else { - lang_path.push_str("/**/*"); - glob(lang_path.as_str()).map_err(|e| InternalError(e.to_string()))? - }; - - let template_registry = TemplateRegistry::try_from_resolved_registry(registry, catalog) - .map_err(|e| InternalError(e.to_string()))?; + // List all files in the target directory and its subdirectories + let files: Vec = WalkDir::new(self.path.clone()) + .into_iter() + .filter_map(|e| { + // Skip directories that the owner of the running process does not + // have permission to access + e.ok() + }) + .filter(|dir_entry| dir_entry.path().is_file()) + .collect(); + + let config = TargetConfig::try_new(&self.path)?; + let tmpl_matcher = config.template_matcher()?; + + // Create a read-only context for the filter evaluations + let context = serde_json::to_value(context).map_err(|e| ContextSerializationFailed { + error: e.to_string(), + })?; - // List all {template, object} pairs to run in parallel the template - // engine as all pairs are independent. - self.list_registry_templates(&template_registry, paths)? + // Process all files in parallel + // - Filter the files that match the template pattern + // - Apply the filter to the context + // - Evaluate the template with the filtered context based on the + // application mode. + // - If the application mode is single, the filtered context is + // evaluated as a single object. + // - If the application mode is each, the filtered context is + // evaluated as an array of objects and each object is evaluated + // independently and in parallel with the same template. + let errs = files .into_par_iter() - .try_for_each(|pair| match pair { - TemplateObjectPair::Group { - template_path: relative_template_path, - group, - } => { - let ctx: serde_json::Value = serde_json::to_value(Context { - registry: &template_registry, - group: Some(group), - groups: None, - }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path, output_dir) - } - TemplateObjectPair::Groups { - template_path: relative_template_path, - groups, - } => { - let ctx: serde_json::Value = serde_json::to_value(Context { - registry: &template_registry, - group: None, - groups: Some(groups), - }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path, output_dir) - } - TemplateObjectPair::Registry { - template_path: relative_template_path, - registry, - } => { - let ctx: serde_json::Value = serde_json::to_value(Context { - registry, - group: None, - groups: None, - }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path, output_dir) + .filter_map(|file| { + let relative_path = match file.path().strip_prefix(&self.path) { + Ok(relative_path) => relative_path, + Err(e) => { + return Some(InvalidTemplateDir { + template_dir: self.path.clone(), + error: e.to_string(), + }); + } + }; + + for template in tmpl_matcher.matches(relative_path) { + let filtered_result = match template.filter.apply(context.clone()) { + Ok(result) => result, + Err(e) => return Some(e), + }; + + match template.application_mode { + // The filtered result is evaluated as a single object + ApplicationMode::Single => { + if let Err(e) = self.evaluate_template( + log.clone(), + NewContext { + ctx: &filtered_result, + } + .try_into() + .ok()?, + relative_path, + output_dir, + ) { + return Some(e); + } + } + // The filtered result is evaluated as an array of objects + // and each object is evaluated independently and in parallel + // with the same template. + ApplicationMode::Each => { + if let Some(values) = filtered_result.as_array() { + let errs = values + .into_par_iter() + .filter_map(|result| { + if let Err(e) = self.evaluate_template( + log.clone(), + NewContext { ctx: result }.try_into().ok()?, + relative_path, + output_dir, + ) { + return Some(e); + } + None + }) + .collect::>(); + if !errs.is_empty() { + return Some(Error::compound_error(errs)); + } + } else if let Err(e) = self.evaluate_template( + log.clone(), + NewContext { + ctx: &filtered_result, + } + .try_into() + .ok()?, + relative_path, + output_dir, + ) { + return Some(e); + } + } + } } - })?; + None + }) + .collect::>(); - Ok(()) + error::handle_errors(errs) } fn evaluate_template( &self, log: impl Logger + Clone + Sync, ctx: serde_json::Value, - template_path: PathBuf, + template_path: &Path, output_dir: &Path, ) -> Result<(), Error> { let template_object = TemplateObject { @@ -331,18 +307,27 @@ impl TemplateEngine { }; let mut engine = self.template_engine()?; let template_file = template_path.to_str().ok_or(InvalidTemplateFile { - template: template_path.clone(), + template: template_path.to_path_buf(), error: "".to_string(), })?; engine.add_global("template", Value::from_object(template_object.clone())); _ = log.loading(&format!("Generating file {}", template_file)); - let output = engine + let template = engine .get_template(template_file) - .map_err(|e| InternalError(e.to_string()))? - .render(ctx) - .map_err(|e| InternalError(e.to_string()))?; + .map_err(|e| InvalidTemplateFile { + template: template_path.to_path_buf(), + error: e.to_string(), + })?; + + let output = template + .render(ctx.clone()) + .map_err(|e| TemplateEvaluationFailed { + template: template_path.to_path_buf(), + error_id: e.to_string(), + error: error_summary(e), + })?; let generated_file = Self::save_generated_code(output_dir, template_object.file_name(), output)?; _ = log.success(&format!("Generated file {:?}", generated_file)); @@ -354,7 +339,10 @@ impl TemplateEngine { let mut env = Environment::new(); env.set_loader(path_loader(&self.path)); env.set_syntax(self.target_config.template_syntax.clone().into()) - .map_err(|e| InternalError(e.to_string()))?; + .map_err(|e| InvalidConfigFile { + config_file: self.path.join(WEAVER_YAML), + error: e.to_string(), + })?; // Register case conversion filters based on the target configuration env.add_filter( @@ -401,239 +389,6 @@ impl TemplateEngine { Ok(env) } - /// Lists all {template, object} pairs derived from a template directory and a given - /// semantic convention registry. - fn list_registry_templates<'a>( - &self, - registry: &'a TemplateRegistry, - paths: Paths, - ) -> Result>, Error> { - let mut templates = Vec::new(); - - for entry in paths { - if let Ok(tmpl_file_path) = entry { - if tmpl_file_path.is_dir() { - continue; - } - let relative_path = - tmpl_file_path - .strip_prefix(&self.path) - .map_err(|e| InvalidTemplateDir { - template_dir: self.path.clone(), - error: e.to_string(), - })?; - let tmpl_file = tmpl_file_path.to_str().ok_or(InvalidTemplateFile { - template: tmpl_file_path.clone(), - error: "".to_string(), - })?; - - if tmpl_file.ends_with(".j2") { - // Files with .j2 are either macros or included files - // imported from the template files, so we skip them. - continue; - } - - if tmpl_file.ends_with("weaver.yaml") { - // Skip weaver configuration file. - continue; - } - - match tmpl_file_path.file_stem().and_then(|s| s.to_str()) { - Some("attribute_group") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::AttributeGroup)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("event") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Event)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("group") => { - registry.groups.iter().for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("metric") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Metric)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("metric_group") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::MetricGroup)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("registry") => { - templates.push(TemplateObjectPair::Registry { - template_path: relative_path.to_path_buf(), - registry, - }); - } - Some("resource") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Resource)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("scope") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Scope)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("span") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Span)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("attribute_groups") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::AttributeGroup)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("events") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Event)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("groups") => { - let groups = registry.groups.iter().collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("metrics") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Metric)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("metric_groups") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::MetricGroup)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("resources") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Resource)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("scopes") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Scope)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("spans") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Span)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - _ => templates.push(TemplateObjectPair::Registry { - template_path: relative_path.to_path_buf(), - registry, - }), - } - } else { - return Err(InvalidTemplateDirectory(self.path.clone())); - } - } - - Ok(templates) - } - /// Save the generated code to the output directory. fn save_generated_code( output_dir: &Path, @@ -668,11 +423,15 @@ mod tests { use std::collections::HashSet; use std::fs; use std::path::Path; + use walkdir::WalkDir; + use weaver_logger::TestLogger; use weaver_resolver::SchemaResolver; use weaver_semconv::SemConvRegistry; + use crate::registry::TemplateRegistry; + #[test] fn test() { let logger = TestLogger::default(); @@ -685,13 +444,17 @@ mod tests { SchemaResolver::resolve_semantic_convention_registry(&mut registry, logger.clone()) .expect("Failed to resolve registry"); + let template_registry = + TemplateRegistry::try_from_resolved_registry(&schema.registries[0], &schema.catalog) + .unwrap_or_else(|e| { + panic!( + "Failed to create the context for the template evaluation: {:?}", + e + ) + }); + engine - .generate_registry( - logger, - &schema.registries[0], - &schema.catalog, - Path::new("observed_output"), - ) + .generate(logger, &template_registry, Path::new("observed_output")) .expect("Failed to generate registry assets"); assert!(cmp_dir("expected_output", "observed_output").unwrap()); diff --git a/crates/weaver_forge/src/registry.rs b/crates/weaver_forge/src/registry.rs index 80b7f972..e57e7d77 100644 --- a/crates/weaver_forge/src/registry.rs +++ b/crates/weaver_forge/src/registry.rs @@ -4,7 +4,7 @@ //! catalog are resolved to the actual catalog entries to ease the template //! evaluation. -use crate::Error; +use crate::error::Error; use serde::{Deserialize, Serialize}; use weaver_resolved_schema::attribute::Attribute; use weaver_resolved_schema::catalog::Catalog; diff --git a/crates/weaver_forge/templates/test/attribute_group.md b/crates/weaver_forge/templates/test/attribute_group.md index 24c5023d..1fed8404 100644 --- a/crates/weaver_forge/templates/test/attribute_group.md +++ b/crates/weaver_forge/templates/test/attribute_group.md @@ -1,17 +1,17 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("attribute_group/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -prefix: {{ group.prefix }} +prefix: {{ ctx.prefix }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/attribute_groups.md b/crates/weaver_forge/templates/test/attribute_groups.md index ae59a378..5cd2168b 100644 --- a/crates/weaver_forge/templates/test/attribute_groups.md +++ b/crates/weaver_forge/templates/test/attribute_groups.md @@ -1,6 +1,6 @@ # Semantic Convention Attribute Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/event.md b/crates/weaver_forge/templates/test/event.md index 2752b07f..43736aa7 100644 --- a/crates/weaver_forge/templates/test/event.md +++ b/crates/weaver_forge/templates/test/event.md @@ -1,18 +1,18 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("event/" ~ file_name ~ ".md") -}} -# Group `{{ group.id }}` ({{ group.type }}) +# Group `{{ ctx.id }}` ({{ ctx.type }}) ## Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -Prefix: {{ group.prefix }} -Name: {{ group.name }} +Prefix: {{ ctx.prefix }} +Name: {{ ctx.name }} ## Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} ### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/events.md b/crates/weaver_forge/templates/test/events.md index bcc09d59..eae78bc1 100644 --- a/crates/weaver_forge/templates/test/events.md +++ b/crates/weaver_forge/templates/test/events.md @@ -1,6 +1,6 @@ # Semantic Convention Event Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/group.md b/crates/weaver_forge/templates/test/group.md index c58c8e3c..802ccb77 100644 --- a/crates/weaver_forge/templates/test/group.md +++ b/crates/weaver_forge/templates/test/group.md @@ -1,17 +1,17 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("group/" ~ file_name ~ ".md") -}} -# Group `{{ group.id }}` ({{ group.type }}) +# Group `{{ ctx.id }}` ({{ ctx.type }}) ## Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -prefix: {{ group.prefix }} +prefix: {{ ctx.prefix }} ## Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} ### Attribute `{{ attribute.name }}` {{ attribute.brief }} @@ -47,8 +47,8 @@ prefix: {{ group.prefix }} ## Provenance -Source: {{ group.lineage.provenance }} +Source: {{ ctx.lineage.provenance }} -{% for item in group.lineage.attributes -%} -item: {{ group.lineage.attributes[item] }} +{% for item in ctx.lineage.attributes -%} +item: {{ ctx.lineage.attributes[item] }} {% endfor -%} \ No newline at end of file diff --git a/crates/weaver_forge/templates/test/groups.md b/crates/weaver_forge/templates/test/groups.md index 3e8188f9..8b072179 100644 --- a/crates/weaver_forge/templates/test/groups.md +++ b/crates/weaver_forge/templates/test/groups.md @@ -1,6 +1,6 @@ # Semantic Convention Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/metric.md b/crates/weaver_forge/templates/test/metric.md index fc58ca10..d45df277 100644 --- a/crates/weaver_forge/templates/test/metric.md +++ b/crates/weaver_forge/templates/test/metric.md @@ -1,23 +1,23 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("metric/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -{{ group.note | trim }} +{{ ctx.note | trim }} -Prefix: {{ group.prefix }} -Metric: {{ group.metric_name }} -Instrument: {{ group.instrument }} -Unit: {{ group.unit }} -Stability: {{ group.stability | capitalize }} +Prefix: {{ ctx.prefix }} +Metric: {{ ctx.metric_name }} +Instrument: {{ ctx.instrument }} +Unit: {{ ctx.unit }} +Stability: {{ ctx.stability | capitalize }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/metric_group.md b/crates/weaver_forge/templates/test/metric_group.md deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/weaver_forge/templates/test/metrics.md b/crates/weaver_forge/templates/test/metrics.md index df460b78..641805cb 100644 --- a/crates/weaver_forge/templates/test/metrics.md +++ b/crates/weaver_forge/templates/test/metrics.md @@ -1,6 +1,6 @@ # Semantic Convention Metric Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/registry.md b/crates/weaver_forge/templates/test/registry.md index e1c26c56..3bd4b2c5 100644 --- a/crates/weaver_forge/templates/test/registry.md +++ b/crates/weaver_forge/templates/test/registry.md @@ -3,49 +3,49 @@ Url: {{ registry_url }} # Attribute Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "attribute_group" %} - [{{ group.id }}](attribute_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Events -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "event" %} - [{{ group.id }}](event/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metrics -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric" %} - [{{ group.id }}](metric/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metric Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric_group" %} - [{{ group.id }}](metric_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Resource -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "resource" %} - [{{ group.id }}](resource/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Scope -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "scope" %} - [{{ group.id }}](scope/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Span -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "span" %} - [{{ group.id }}](span/{{ group.id | file_name }}.md) {%- endif %} diff --git a/crates/weaver_forge/templates/test/resource.md b/crates/weaver_forge/templates/test/resource.md index 391d364b..5fb7efa6 100644 --- a/crates/weaver_forge/templates/test/resource.md +++ b/crates/weaver_forge/templates/test/resource.md @@ -1,17 +1,17 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("resource/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -prefix: {{ group.prefix }} +prefix: {{ ctx.prefix }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/resources.md b/crates/weaver_forge/templates/test/resources.md index 8a33dfbc..955b76fc 100644 --- a/crates/weaver_forge/templates/test/resources.md +++ b/crates/weaver_forge/templates/test/resources.md @@ -1,6 +1,6 @@ # Semantic Convention Resource Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/span.md b/crates/weaver_forge/templates/test/span.md index 432bc8e9..02b58a3f 100644 --- a/crates/weaver_forge/templates/test/span.md +++ b/crates/weaver_forge/templates/test/span.md @@ -1,20 +1,20 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("span/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -{{ group.note | trim }} +{{ ctx.note | trim }} -Prefix: {{ group.prefix }} -Kind: {{ group.span_kind }} +Prefix: {{ ctx.prefix }} +Kind: {{ ctx.span_kind }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/spans.md b/crates/weaver_forge/templates/test/spans.md index ebe2f969..28083c2a 100644 --- a/crates/weaver_forge/templates/test/spans.md +++ b/crates/weaver_forge/templates/test/spans.md @@ -1,6 +1,6 @@ # Semantic Convention Span Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_logger/src/lib.rs b/crates/weaver_logger/src/lib.rs index 7c5edfcb..2a489173 100644 --- a/crates/weaver_logger/src/lib.rs +++ b/crates/weaver_logger/src/lib.rs @@ -6,6 +6,8 @@ #![deny(clippy::print_stdout)] #![deny(clippy::print_stderr)] +pub mod quiet; + use std::sync::atomic::AtomicUsize; use std::sync::{Arc, Mutex}; diff --git a/crates/weaver_logger/src/quiet.rs b/crates/weaver_logger/src/quiet.rs new file mode 100644 index 00000000..086468fb --- /dev/null +++ b/crates/weaver_logger/src/quiet.rs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Logger in quiet mode. +//! This logger only logs errors and warnings. + +use crate::Logger; +use std::sync::{Arc, Mutex}; + +/// A quient logger that can be used to log messages to the console. +/// This logger is thread-safe and can be cloned. +/// Only logs errors and warnings are logged to the console. +#[derive(Default, Clone)] +pub struct QuietLogger { + logger: Arc>>, +} + +impl QuietLogger { + /// Creates a new logger. + pub fn new() -> Self { + Self { + logger: Arc::new(Mutex::new(paris::Logger::new())), + } + } +} + +impl Logger for QuietLogger { + /// Logs a trace message (only with debug enabled). + fn trace(&self, _message: &str) -> &Self { + self + } + + /// Logs an info message. + fn info(&self, _message: &str) -> &Self { + self + } + + /// Logs a warning message. + fn warn(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .warn(message); + self + } + + /// Logs an error message. + fn error(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .error(message); + self + } + + /// Logs a success message. + fn success(&self, _message: &str) -> &Self { + self + } + + /// Logs a newline. + fn newline(&self, _count: usize) -> &Self { + self + } + + /// Indents the logger. + fn indent(&self, _count: usize) -> &Self { + self + } + + /// Stops a loading message. + fn done(&self) { + self.logger.lock().expect("Failed to lock logger").done(); + } + + /// Adds a style to the logger. + fn add_style(&self, _name: &str, _styles: Vec<&'static str>) -> &Self { + self + } + + /// Logs a loading message with a spinner. + fn loading(&self, _message: &str) -> &Self { + self + } + + /// Forces the logger to not print a newline for the next message. + fn same(&self) -> &Self { + self + } + + /// Logs a message without icon. + fn log(&self, _message: &str) -> &Self { + self + } +} diff --git a/crates/weaver_resolved_schema/Cargo.toml b/crates/weaver_resolved_schema/Cargo.toml index 34b00b87..3b02528e 100644 --- a/crates/weaver_resolved_schema/Cargo.toml +++ b/crates/weaver_resolved_schema/Cargo.toml @@ -15,4 +15,4 @@ serde.workspace = true ordered-float.workspace = true [dev-dependencies] -serde_json = "1.0.113" \ No newline at end of file +serde_json = "1.0.114" \ No newline at end of file diff --git a/crates/weaver_resolver/src/metrics.rs b/crates/weaver_resolver/src/metrics.rs index 0a584555..8f8b8dfe 100644 --- a/crates/weaver_resolver/src/metrics.rs +++ b/crates/weaver_resolver/src/metrics.rs @@ -93,7 +93,7 @@ pub fn resolve_metrics( // Initialize all/required_shared_attributes only if first metric. if i == 0 { - all_shared_attributes = inherited_attrs.clone(); + all_shared_attributes.clone_from(&inherited_attrs); all_shared_attributes .iter() .filter(|attr| attr.is_required()) diff --git a/crates/weaver_schema/src/attribute.rs b/crates/weaver_schema/src/attribute.rs index 3d786685..96764119 100644 --- a/crates/weaver_schema/src/attribute.rs +++ b/crates/weaver_schema/src/attribute.rs @@ -266,27 +266,31 @@ impl Attribute { pub fn set_tags(&mut self, tags: &Option) { match self { Attribute::Ref { tags: tags_ref, .. } => { - *tags_ref = tags.clone(); + tags_ref.clone_from(tags); } Attribute::Id { tags: tags_id, .. } => { - *tags_id = tags.clone(); + tags_id.clone_from(tags); } Attribute::AttributeGroupRef { tags: tags_group, .. } => { - *tags_group = tags.clone(); + tags_group.clone_from(tags); } Attribute::ResourceRef { tags: tags_resource, .. } => { - *tags_resource = tags.clone(); + tags_resource.clone_from(tags); } - Attribute::SpanRef { tags, .. } => { - *tags = tags.clone(); + Attribute::SpanRef { + tags: span_tags, .. + } => { + span_tags.clone_from(tags); } - Attribute::EventRef { tags, .. } => { - *tags = tags.clone(); + Attribute::EventRef { + tags: event_tags, .. + } => { + event_tags.clone_from(tags); } } } @@ -339,7 +343,7 @@ impl Attribute { // Override process. // Use the field values from the reference when defined in the reference. if let Some(brief_from_ref) = brief_from_ref { - brief = brief_from_ref.clone(); + brief.clone_from(brief_from_ref); } if let Some(requirement_level_from_ref) = requirement_level_from_ref { requirement_level = requirement_level_from_ref.clone(); @@ -354,7 +358,7 @@ impl Attribute { sampling_relevant = Some(*sampling_from_ref); } if let Some(note_from_ref) = note_from_ref { - note = note_from_ref.clone(); + note.clone_from(note_from_ref); } if let Some(stability_from_ref) = stability_from_ref { stability = Some(stability_from_ref.clone()); diff --git a/crates/weaver_schema/src/lib.rs b/crates/weaver_schema/src/lib.rs index 8268ec3c..eb3e2b1b 100644 --- a/crates/weaver_schema/src/lib.rs +++ b/crates/weaver_schema/src/lib.rs @@ -233,7 +233,7 @@ impl TelemetrySchema { } } None => { - self.versions = parent_schema.versions.clone(); + self.versions.clone_from(&parent_schema.versions); } } } diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs index 9cdb2f09..8d646e8b 100644 --- a/crates/weaver_semconv/src/lib.rs +++ b/crates/weaver_semconv/src/lib.rs @@ -732,7 +732,7 @@ impl SemConvRegistry { format!("{}.{}", prefix, id) }; if let AttributeSpec::Id { id, .. } = &mut attr { - *id = fq_attr_id.clone(); + id.clone_from(&fq_attr_id) } let prev_val = self.all_attributes.insert( fq_attr_id.clone(), diff --git a/docs/template-engine.md b/docs/template-engine.md index 734ebdcc..72ecb904 100644 --- a/docs/template-engine.md +++ b/docs/template-engine.md @@ -66,10 +66,6 @@ produced from the template: {{- template.set_file_name("span/" ~ file_name ~ ".md") -}} ``` -> Note: Other naming conventions might be introduced in the future to facilitate -the generation of assets (e.g., groups_per_prefix.md could be used to call the -template with a list of groups sharing the same prefix). - ## Configuration File The configuration file `weaver.yaml` is optional. It allows configuring the @@ -107,6 +103,70 @@ template_syntax: variable_end: "}}" comment_start: "{#" comment_end: "#}" + +# Please uncomment the following templates to override the default template +# mapping. Each template mapping specifies a jaq filter (compatible with jq) +# to apply to every file matching the pattern. The application_mode specifies +# how the template should be applied. The application_mode can be `each` or +# `single`. The `each` mode will evaluate the template for each object selected +# by the jaq filter. The `single` mode will evaluate the template once with all +# the objects selected by the jq filter. +# +# Note: jaq is a Rust reimplementation of jq. Most of the jq filters are +# supported. For more information, see https://github.com/01mf02/jaq +# +# templates: +# - pattern: "**/registry.md" +# filter: "." +# application_mode: single +# - pattern: "**/attribute_group.md" +# filter: ".groups[] | select(.type == \"attribute_group\")" +# application_mode: each +# - pattern: "**/attribute_groups.md" +# filter: ".groups[] | select(.type == \"attribute_group\")" +# application_mode: single +# - pattern: "**/event.md" +# filter: ".groups[] | select(.type == \"event\")" +# application_mode: each +# - pattern: "**/events.md" +# filter: ".groups[] | select(.type == \"event\")" +# application_mode: single +# - pattern: "**/group.md" +# filter: ".groups[] | select(.type == \"group\")" +# application_mode: each +# - pattern: "**/groups.md" +# filter: ".groups[] | select(.type == \"group\")" +# application_mode: single +# - pattern: "**/metric.md" +# filter: ".groups[] | select(.type == \"metric\")" +# application_mode: each +# - pattern: "**/metrics.md" +# filter: ".groups[] | select(.type == \"metric\")" +# application_mode: single +# - pattern: "**/metric_group.md" +# filter: ".groups[] | select(.type == \"metric_group\")" +# application_mode: each +# - pattern: "**/metric_groups.md" +# filter: ".groups[] | select(.type == \"metric_group\")" +# application_mode: single +# - pattern: "**/resource.md" +# filter: ".groups[] | select(.type == \"resource\")" +# application_mode: each +# - pattern: "**/resources.md" +# filter: ".groups[] | select(.type == \"resource\")" +# application_mode: single +# - pattern: "**/scope.md" +# filter: ".groups[] | select(.type == \"scope\")" +# application_mode: each +# - pattern: "**/scopes.md" +# filter: ".groups[] | select(.type == \"scope\")" +# application_mode: single +# - pattern: "**/span.md" +# filter: ".groups[] | select(.type == \"span\")" +# application_mode: each +# - pattern: "**/spans.md" +# filter: ".groups[] | select(.type == \"span\")" +# application_mode: single ``` Supported case converters: diff --git a/justfile b/justfile index e6455bef..c8965b8e 100644 --- a/justfile +++ b/justfile @@ -8,6 +8,8 @@ install: cargo install cargo-check-external-types pre-push-check: + rustup update + cargo clean cargo update cargo machete cargo fmt --all diff --git a/src/cli.rs b/src/cli.rs index 40d8cd79..61781de9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,6 +21,10 @@ pub struct Cli { #[arg(short, long, action = clap::ArgAction::Count)] pub debug: u8, + /// Turn the quiet mode on (i.e., minimal output) + #[arg(short, long)] + pub quiet: bool, + /// List of supported commands #[command(subcommand)] pub command: Option, diff --git a/src/main.rs b/src/main.rs index cae45e63..77dacb80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use clap::Parser; use registry::semconv_registry; -use weaver_logger::ConsoleLogger; +use weaver_logger::quiet::QuietLogger; +use weaver_logger::{ConsoleLogger, Logger}; use crate::cli::{Cli, Commands}; #[cfg(feature = "experimental")] @@ -22,8 +23,20 @@ mod search; fn main() { let cli = Cli::parse(); - let log = ConsoleLogger::new(cli.debug); + let start = std::time::Instant::now(); + if cli.quiet { + let log = QuietLogger::new(); + run_command(&cli, log); + } else { + let log = ConsoleLogger::new(cli.debug); + run_command(&cli, log); + }; + let elapsed = start.elapsed(); + println!("Total execution time: {:?}s", elapsed.as_secs_f64()); +} + +fn run_command(cli: &Cli, log: impl Logger + Sync + Clone) { match &cli.command { #[cfg(feature = "experimental")] Some(Commands::Resolve(params)) => { diff --git a/src/registry/generate.rs b/src/registry/generate.rs index 97ab55b0..15e94211 100644 --- a/src/registry/generate.rs +++ b/src/registry/generate.rs @@ -6,6 +6,8 @@ use clap::Args; use std::path::PathBuf; use weaver_cache::Cache; +use weaver_forge::debug::print_dedup_errors; +use weaver_forge::registry::TemplateRegistry; use weaver_forge::{GeneratorConfig, TemplateEngine}; use weaver_logger::Logger; use weaver_resolver::SchemaResolver; @@ -73,14 +75,20 @@ pub(crate) fn command( ) .expect("Failed to create template engine"); - engine - .generate_registry( - logger.clone(), - &schema.registries[0], - &schema.catalog, - args.output.as_path(), - ) - .expect("Failed to generate registry assets"); + let template_registry = + TemplateRegistry::try_from_resolved_registry(&schema.registries[0], &schema.catalog) + .unwrap_or_else(|e| { + panic!( + "Failed to create the context for the template evaluation: {:?}", + e + ) + }); - logger.success("Artifacts generated successfully"); + match engine.generate(logger.clone(), &template_registry, args.output.as_path()) { + Ok(_) => logger.success("Artifacts generated successfully"), + Err(e) => { + print_dedup_errors(logger.clone(), e); + std::process::exit(1); + } + }; } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 72629268..062fc23f 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -36,7 +36,7 @@ pub enum RegistrySubCommand { Check(RegistryCheckArgs), /// Generates artifacts from a registry. Generate(RegistryGenerateArgs), - /// Resolves a registry (not yet implemented). + /// Resolves a registry. Resolve(RegistryResolveArgs), /// Searches a registry (not yet implemented). Search(RegistrySearchArgs), @@ -46,6 +46,23 @@ pub enum RegistrySubCommand { UpdateMarkdown(RegistryUpdateMarkdownArgs), } +/// Set of parameters used to specify a semantic convention registry. +#[derive(Args, Debug)] +pub struct RegistryArgs { + /// Local path or Git URL of the semantic convention registry. + #[arg( + short = 'r', + long, + default_value = "https://github.com/open-telemetry/semantic-conventions.git" + )] + pub registry: String, + + /// Optional path in the Git repository where the semantic convention + /// registry is located + #[arg(short = 'd', long, default_value = "model")] + pub registry_git_sub_dir: Option, +} + /// Manage a semantic convention registry. pub fn semconv_registry(log: impl Logger + Sync + Clone, command: &RegistryCommand) { let cache = Cache::try_new().unwrap_or_else(|e| { @@ -57,9 +74,7 @@ pub fn semconv_registry(log: impl Logger + Sync + Clone, command: &RegistryComma RegistrySubCommand::Check(args) => check::command(log, &cache, args), RegistrySubCommand::Generate(args) => generate::command(log, &cache, args), RegistrySubCommand::Stats(args) => stats::command(log, &cache, args), - RegistrySubCommand::Resolve(_) => { - unimplemented!() - } + RegistrySubCommand::Resolve(args) => resolve::command(log, &cache, args), RegistrySubCommand::Search(_) => { unimplemented!() } diff --git a/src/registry/resolve.rs b/src/registry/resolve.rs index 9c60fddd..78f59491 100644 --- a/src/registry/resolve.rs +++ b/src/registry/resolve.rs @@ -2,26 +2,123 @@ //! Resolve a semantic convention registry. -use clap::Args; use std::path::PathBuf; +use clap::{Args, ValueEnum}; +use serde::Serialize; + +use weaver_cache::Cache; +use weaver_forge::registry::TemplateRegistry; +use weaver_logger::Logger; +use weaver_resolver::SchemaResolver; + +use crate::registry::RegistryArgs; + +/// Supported output formats for the resolved schema +#[derive(Debug, Clone, ValueEnum)] +enum Format { + /// YAML format + Yaml, + /// JSON format + Json, +} + /// Parameters for the `registry resolve` sub-command #[derive(Debug, Args)] pub struct RegistryResolveArgs { - /// Local path or Git URL of the semantic convention registry. - #[arg( - short = 'r', - long, - default_value = "https://github.com/open-telemetry/semantic-conventions.git" - )] - pub registry: String, - - /// Optional path in the Git repository where the semantic convention - /// registry is located - #[arg(short = 'd', long, default_value = "model")] - pub registry_git_sub_dir: Option, + /// Parameters to specify the semantic convention registry + #[command(flatten)] + registry: RegistryArgs, + + /// Flag to indicate if the shared catalog should be included in the resolved schema + #[arg(long, default_value = "false")] + catalog: bool, + + /// Flag to indicate if lineage information should be included in the + /// resolved schema (not yet implemented) + #[arg(long, default_value = "false")] + lineage: bool, /// Output file to write the resolved schema to /// If not specified, the resolved schema is printed to stdout - pub output: Option, + #[arg(short, long)] + output: Option, + + /// Output format for the resolved schema + /// If not specified, the resolved schema is printed in YAML format + /// Supported formats: yaml, json + /// Default format: yaml + /// Example: `--format json` + #[arg(short, long, default_value = "yaml")] + format: Format, +} + +/// Resolve a semantic convention registry and write the resolved schema to a +/// file or print it to stdout. +pub(crate) fn command( + logger: impl Logger + Sync + Clone, + cache: &Cache, + args: &RegistryResolveArgs, +) { + logger.loading(&format!("Resolving registry `{}`", args.registry.registry)); + + // Load the semantic convention registry into a local cache. + let mut registry = SchemaResolver::load_semconv_registry( + args.registry.registry.to_string(), + args.registry.registry_git_sub_dir.clone(), + cache, + logger.clone(), + ) + .unwrap_or_else(|e| { + panic!("Failed to load and parse the semantic convention registry, error: {e}"); + }); + + // Resolve the semantic convention registry. + let schema = + SchemaResolver::resolve_semantic_convention_registry(&mut registry, logger.clone()) + .expect("Failed to resolve registry"); + + // Serialize the resolved schema and write it + // to a file or print it to stdout. + match args.catalog { + // The original resolved schema already includes the catalog. + // So, we just need to serialize it. + true => apply_format(&args.format, &schema) + .map_err(|e| format!("Failed to serialize the registry: {e:?}")), + // Build a template registry from the resolved schema and serialize it. + // The template registry does not include any reference to a shared + // catalog of attributes. + false => { + let registry = TemplateRegistry::try_from_resolved_registry( + &schema.registries[0], + &schema.catalog, + ) + .unwrap_or_else(|e| panic!("Failed to create the registry without catalog: {e:?}")); + apply_format(&args.format, ®istry) + .map_err(|e| format!("Failed to serialize the registry: {e:?}")) + } + } + .and_then(|s| match args.output { + // Write the resolved registry to a file. + Some(ref path) => std::fs::write(path, s) + .map_err(|e| format!("Failed to write the resolved registry to file: {e:?}")), + // Print the resolved registry to stdout. + None => { + println!("{}", s); + Ok(()) + } + }) + .unwrap_or_else(|e| { + // Capture all the errors + panic!("{}", e); + }); +} + +fn apply_format(format: &Format, object: &T) -> Result { + match format { + Format::Yaml => serde_yaml::to_string(object) + .map_err(|e| format!("Failed to serialize in Yaml the resolved registry: {:?}", e)), + Format::Json => serde_json::to_string_pretty(object) + .map_err(|e| format!("Failed to serialize in Json the resolved registry: {:?}", e)), + } } diff --git a/templates/registry/markdown/attribute_group.md b/templates/registry/markdown/attribute_group.md new file mode 100644 index 00000000..1fed8404 --- /dev/null +++ b/templates/registry/markdown/attribute_group.md @@ -0,0 +1,46 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("attribute_group/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +prefix: {{ ctx.prefix }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/crates/weaver_forge/templates/test/metric_groups.md b/templates/registry/markdown/attribute_groups.md similarity index 94% rename from crates/weaver_forge/templates/test/metric_groups.md rename to templates/registry/markdown/attribute_groups.md index 6e721a57..5cd2168b 100644 --- a/crates/weaver_forge/templates/test/metric_groups.md +++ b/templates/registry/markdown/attribute_groups.md @@ -1,6 +1,6 @@ -# Semantic Convention Metric Group Groups +# Semantic Convention Attribute Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/templates/registry/markdown/attribute_type.j2 b/templates/registry/markdown/attribute_type.j2 new file mode 100644 index 00000000..1c9147ba --- /dev/null +++ b/templates/registry/markdown/attribute_type.j2 @@ -0,0 +1,5 @@ +{%- if attribute.type is mapping %} +- Type: Enum [{{ attribute.type.members | map(attribute="value") | join(", ") }}] +{%- else %} +- Type: {{ attribute.type }} +{%- endif %} \ No newline at end of file diff --git a/templates/registry/markdown/event.md b/templates/registry/markdown/event.md new file mode 100644 index 00000000..43736aa7 --- /dev/null +++ b/templates/registry/markdown/event.md @@ -0,0 +1,47 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("event/" ~ file_name ~ ".md") -}} + +# Group `{{ ctx.id }}` ({{ ctx.type }}) + +## Brief + +{{ ctx.brief | trim }} + +Prefix: {{ ctx.prefix }} +Name: {{ ctx.name }} + +## Attributes + +{% for attribute in ctx.attributes %} +### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/templates/registry/markdown/events.md b/templates/registry/markdown/events.md new file mode 100644 index 00000000..eae78bc1 --- /dev/null +++ b/templates/registry/markdown/events.md @@ -0,0 +1,48 @@ +# Semantic Convention Event Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +Prefix: {{ group.prefix }} +Name: {{ group.name }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} diff --git a/templates/registry/markdown/examples.j2 b/templates/registry/markdown/examples.j2 new file mode 100644 index 00000000..337fc479 --- /dev/null +++ b/templates/registry/markdown/examples.j2 @@ -0,0 +1,7 @@ +{%- if attribute.examples %} +{%- if attribute.examples is sequence %} +- Examples: {{ attribute.examples | pprint }} +{%- else %} +- Examples: {{ attribute.examples }} +{%- endif %} +{%- endif %} diff --git a/templates/registry/markdown/group.md b/templates/registry/markdown/group.md new file mode 100644 index 00000000..802ccb77 --- /dev/null +++ b/templates/registry/markdown/group.md @@ -0,0 +1,54 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("group/" ~ file_name ~ ".md") -}} + +# Group `{{ ctx.id }}` ({{ ctx.type }}) + +## Brief + +{{ ctx.brief | trim }} + +prefix: {{ ctx.prefix }} + +## Attributes + +{% for attribute in ctx.attributes %} +### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required +{%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} +{%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended +{%- else %} +- Requirement Level: Optional +{%- endif %} +{% if attribute.tag %} +- Tag: {{ attribute.tag }} +{% endif %} +{%- include "attribute_type.j2" %} +{%- include "examples.j2" -%} +{%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} +{%- endif %} +{%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} +{%- endif %} +{% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} +{% endif %} +{% endfor %} + +## Provenance + +Source: {{ ctx.lineage.provenance }} + +{% for item in ctx.lineage.attributes -%} +item: {{ ctx.lineage.attributes[item] }} +{% endfor -%} \ No newline at end of file diff --git a/templates/registry/markdown/groups.md b/templates/registry/markdown/groups.md new file mode 100644 index 00000000..8b072179 --- /dev/null +++ b/templates/registry/markdown/groups.md @@ -0,0 +1,47 @@ +# Semantic Convention Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +prefix: {{ group.prefix }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} +{% endfor %} diff --git a/templates/registry/markdown/groups_per_prefix.md b/templates/registry/markdown/groups_per_prefix.md new file mode 100644 index 00000000..1639ef1c --- /dev/null +++ b/templates/registry/markdown/groups_per_prefix.md @@ -0,0 +1,50 @@ +{%- set file_name = ctx.prefix | file_name -%} +{{- template.set_file_name("prefix/" ~ file_name ~ ".md") -}} + +# Semantic Convention Groups with Prefix `{{ ctx.prefix }}` + +{% for group in ctx.groups %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +prefix: {{ group.prefix }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} +{% endfor %} diff --git a/templates/registry/markdown/metric.md b/templates/registry/markdown/metric.md new file mode 100644 index 00000000..d45df277 --- /dev/null +++ b/templates/registry/markdown/metric.md @@ -0,0 +1,52 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("metric/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +{{ ctx.note | trim }} + +Prefix: {{ ctx.prefix }} +Metric: {{ ctx.metric_name }} +Instrument: {{ ctx.instrument }} +Unit: {{ ctx.unit }} +Stability: {{ ctx.stability | capitalize }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/templates/registry/markdown/metrics.md b/templates/registry/markdown/metrics.md new file mode 100644 index 00000000..641805cb --- /dev/null +++ b/templates/registry/markdown/metrics.md @@ -0,0 +1,53 @@ +# Semantic Convention Metric Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +{{ group.note | trim }} + +Prefix: {{ group.prefix }} +Metric: {{ group.metric_name }} +Instrument: {{ group.instrument }} +Unit: {{ group.unit }} +Stability: {{ group.stability | capitalize }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} diff --git a/templates/registry/markdown/registry.md b/templates/registry/markdown/registry.md index e1c26c56..3bd4b2c5 100644 --- a/templates/registry/markdown/registry.md +++ b/templates/registry/markdown/registry.md @@ -3,49 +3,49 @@ Url: {{ registry_url }} # Attribute Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "attribute_group" %} - [{{ group.id }}](attribute_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Events -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "event" %} - [{{ group.id }}](event/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metrics -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric" %} - [{{ group.id }}](metric/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metric Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric_group" %} - [{{ group.id }}](metric_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Resource -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "resource" %} - [{{ group.id }}](resource/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Scope -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "scope" %} - [{{ group.id }}](scope/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Span -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "span" %} - [{{ group.id }}](span/{{ group.id | file_name }}.md) {%- endif %} diff --git a/templates/registry/markdown/resource.md b/templates/registry/markdown/resource.md new file mode 100644 index 00000000..5fb7efa6 --- /dev/null +++ b/templates/registry/markdown/resource.md @@ -0,0 +1,46 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("resource/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +prefix: {{ ctx.prefix }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/templates/registry/markdown/resources.md b/templates/registry/markdown/resources.md new file mode 100644 index 00000000..955b76fc --- /dev/null +++ b/templates/registry/markdown/resources.md @@ -0,0 +1,47 @@ +# Semantic Convention Resource Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +prefix: {{ group.prefix }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} \ No newline at end of file diff --git a/crates/weaver_forge/templates/test/scope.md b/templates/registry/markdown/scope.md similarity index 100% rename from crates/weaver_forge/templates/test/scope.md rename to templates/registry/markdown/scope.md diff --git a/templates/registry/markdown/span.md b/templates/registry/markdown/span.md new file mode 100644 index 00000000..02b58a3f --- /dev/null +++ b/templates/registry/markdown/span.md @@ -0,0 +1,49 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("span/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +{{ ctx.note | trim }} + +Prefix: {{ ctx.prefix }} +Kind: {{ ctx.span_kind }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} \ No newline at end of file diff --git a/templates/registry/markdown/spans.md b/templates/registry/markdown/spans.md new file mode 100644 index 00000000..28083c2a --- /dev/null +++ b/templates/registry/markdown/spans.md @@ -0,0 +1,50 @@ +# Semantic Convention Span Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +{{ group.note | trim }} + +Prefix: {{ group.prefix }} +Kind: {{ group.span_kind }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} \ No newline at end of file diff --git a/templates/registry/markdown/weaver.yaml b/templates/registry/markdown/weaver.yaml new file mode 100644 index 00000000..b0996d19 --- /dev/null +++ b/templates/registry/markdown/weaver.yaml @@ -0,0 +1,43 @@ +templates: + - pattern: registry.md + filter: . + application_mode: single + - pattern: attribute_group.md + filter: .groups[] | select(.type == "attribute_group") + application_mode: each + - pattern: attribute_groups.md + filter: .groups[] | select(.type == "attribute_group") + application_mode: single + - pattern: event.md + filter: .groups[] | select(.type == "event") + application_mode: each + - pattern: events.md + filter: .groups[] | select(.type == "event") + application_mode: single + - pattern: group.md + filter: .groups + application_mode: each + - pattern: groups.md + filter: .groups + application_mode: single + - pattern: metric.md + filter: .groups[] | select(.type == "metric") + application_mode: each + - pattern: metrics.md + filter: .groups[] | select(.type == "metric") + application_mode: single + - pattern: resource.md + filter: .groups[] | select(.type == "resource") + application_mode: each + - pattern: resources.md + filter: .groups[] | select(.type == "resource") + application_mode: single + - pattern: span.md + filter: .groups[] | select(.type == "span") + application_mode: each + - pattern: spans.md + filter: .groups[] | select(.type == "span") + application_mode: single + - pattern: groups_per_prefix.md + filter: '.groups | map(select(.prefix != null and .prefix != "")) | group_by(.prefix) | map({prefix: .[0].prefix, groups: .})' + application_mode: each \ No newline at end of file