diff --git a/.github/codecov.yaml b/.github/codecov.yaml index f39c5bbe..b7264fe0 100644 --- a/.github/codecov.yaml +++ b/.github/codecov.yaml @@ -13,4 +13,5 @@ coverage: ignore: - "crates/xtask" # Part of the build system - - "src" # CLI (not tested yet) \ No newline at end of file + - "src" # CLI (not tested yet) + - "crates/weaver_forge/codegen_examples/expected_codegen" # Generated code \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d8047e6..6abc8c4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,7 +175,7 @@ jobs: - name: Install cargo-tarpaulin run: cargo install cargo-tarpaulin - name: Gather coverage - run: cargo tarpaulin --workspace --output-dir coverage --out lcov -e xtask -e weaver + run: cargo tarpaulin --workspace --output-dir coverage --out lcov -e xtask -e weaver --exclude-files 'crates/weaver_forge/codegen_examples/expected_codegen/*' - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 with: diff --git a/Cargo.lock b/Cargo.lock index f82b7a01..845a8073 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,17 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -675,12 +686,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -701,6 +734,7 @@ checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -2124,11 +2158,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2141,9 +2174,9 @@ checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] @@ -2229,6 +2262,59 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opentelemetry" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d57987be3f2aeb70d385fff9b27fb74c5723cc9a52d904d4f9c807a0667bf" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", + "urlencoding", +] + +[[package]] +name = "opentelemetry-stdout" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bdf28b381f23afcd150afc0b38a4183dd321fc96320c1554752b6b761648f78" +dependencies = [ + "async-trait", + "chrono", + "futures-util", + "opentelemetry", + "opentelemetry_sdk", + "ordered-float", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e90c7113be649e31e9a0f8b5ee24ed7a16923b322c3c5ab6367469c049d6b7e" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "ordered-float", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2399,9 +2485,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -2676,9 +2762,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -2719,9 +2805,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -2736,9 +2822,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2783,27 +2869,27 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -2812,9 +2898,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2991,9 +3077,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -3072,18 +3158,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", @@ -3547,6 +3633,19 @@ dependencies = [ "weaver_common", ] +[[package]] +name = "weaver_codegen_test" +version = "0.1.0" +dependencies = [ + "opentelemetry", + "walkdir", + "weaver_cache", + "weaver_common", + "weaver_forge", + "weaver_resolver", + "weaver_semconv", +] + [[package]] name = "weaver_common" version = "0.1.0" @@ -3576,6 +3675,9 @@ dependencies = [ "jaq-parse", "jaq-std", "minijinja", + "opentelemetry", + "opentelemetry-stdout", + "opentelemetry_sdk", "rayon", "regex", "serde", @@ -3886,18 +3988,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 71f12fad..86401e73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ categories = ["command-line-utilities"] license = "Apache-2.0" readme = "README.md" publish = false +resolver = "2" # Workspace definition ======================================================== [workspace] diff --git a/README.md b/README.md index de8e5fb4..fc6aede9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![build](https://github.com/open-telemetry/weaver/actions/workflows/audit.yml/badge.svg)](https://github.com/open-telemetry/weaver/actions/workflows/audit.yml) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ---- -(CONTRIBUTING.md) | [Links](#links) | + [Getting started](#getting-started) | [Main commands](#main-commands) | [Generate Doc & Code](crates/weaver_forge/README.md) | [Architecture](docs/architecture.md) | [Change log](CHANGELOG.md) | [Contributing](CONTRIBUTING.md) | [Links](#links) | ## What is OpenTelemetry Weaver? diff --git a/crates/weaver_codegen_test/Cargo.toml b/crates/weaver_codegen_test/Cargo.toml new file mode 100644 index 00000000..15643f12 --- /dev/null +++ b/crates/weaver_codegen_test/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "weaver_codegen_test" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[build-dependencies] +weaver_common = { path = "../weaver_common" } +weaver_cache = { path = "../weaver_cache" } +weaver_forge = { path = "../weaver_forge" } +weaver_resolver = { path = "../weaver_resolver" } +weaver_semconv = { path = "../weaver_semconv" } +walkdir.workspace = true + +[dependencies] +opentelemetry = { version = "0.22.0", features = ["trace", "metrics", "logs", "otel_unstable"] } \ No newline at end of file diff --git a/crates/weaver_codegen_test/README.md b/crates/weaver_codegen_test/README.md new file mode 100644 index 00000000..b67a0da7 --- /dev/null +++ b/crates/weaver_codegen_test/README.md @@ -0,0 +1,7 @@ +# Weaver CodeGen Test + +This crate is used to test the generation of an unofficial Rust OpenTelemetry Client API derived from a semantic +convention registry. This crate is not intended to be published. It is used solely for testing and validation purposes. + +The generated Rust API client exposes a type-safe API (i.e., one that cannot be misused) that adheres to the signal +specification defined in the semantic convention registry located in the semconv_registry directory. \ No newline at end of file diff --git a/crates/weaver_codegen_test/allowed-external-types.toml b/crates/weaver_codegen_test/allowed-external-types.toml new file mode 100644 index 00000000..f186b677 --- /dev/null +++ b/crates/weaver_codegen_test/allowed-external-types.toml @@ -0,0 +1,6 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# This is used with cargo-check-external-types to reduce the surface area of downstream crates from +# the public API. Ideally this can have a few exceptions as possible. +allowed_external_types = [ +] \ No newline at end of file diff --git a/crates/weaver_codegen_test/build.rs b/crates/weaver_codegen_test/build.rs new file mode 100644 index 00000000..f4873166 --- /dev/null +++ b/crates/weaver_codegen_test/build.rs @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! This `build.rs` generates code for the local semantic convention registry located in the +//! `semconv_registry` directory. The templates used for code generation are located in the +//! `templates/registry/rust` directory. The generated code is produced in the `OUT_DIR` directory +//! specified by Cargo. This generation step occurs before the standard build of the crate. The +//! generated code, along with the standard crate code, will be compiled together. + +use std::collections::HashMap; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; +use std::process::exit; +use weaver_cache::Cache; +use weaver_common::in_memory::LogMessage; +use weaver_common::{in_memory, Logger}; +use weaver_forge::registry::TemplateRegistry; +use weaver_forge::{GeneratorConfig, TemplateEngine}; +use weaver_resolver::SchemaResolver; +use weaver_semconv::path::RegistryPath; +use weaver_semconv::registry::SemConvRegistry; + +const SEMCONV_REGISTRY_PATH: &str = "./semconv_registry/"; +const TEMPLATES_PATH: &str = "./templates/"; +const REGISTRY_ID: &str = "test"; +const TARGET: &str = "rust"; + +fn main() { + // Tell Cargo when to rerun this build script + println!("cargo:rerun-if-changed=templates/registry/rust"); + println!("cargo:rerun-if-changed=semconv_registry"); + println!("cargo:rerun-if-changed=build.rs"); + + // Get the output directory from Cargo + let target_dir = std::env::var("OUT_DIR").expect("Failed to get OUT_DIR from Cargo"); + + // Create an in-memory logger as stdout and stderr are not "classically" available in build.rs. + let logger = in_memory::Logger::new(0); + + // Load and resolve the semantic convention registry + let cache = Cache::try_new().unwrap_or_else(|e| process_error(&logger, e)); + let registry_path = RegistryPath::Local { + path_pattern: SEMCONV_REGISTRY_PATH.into(), + }; + let semconv_specs = SchemaResolver::load_semconv_specs(®istry_path, &cache) + .unwrap_or_else(|e| process_error(&logger, e)); + let mut registry = SemConvRegistry::from_semconv_specs(REGISTRY_ID, semconv_specs); + let schema = SchemaResolver::resolve_semantic_convention_registry(&mut registry) + .unwrap_or_else(|e| process_error(&logger, e)); + + let config = GeneratorConfig::new(TEMPLATES_PATH.into()); + let engine = TemplateEngine::try_new(&format!("registry/{}", TARGET), config) + .unwrap_or_else(|e| process_error(&logger, e)); + let template_registry = TemplateRegistry::try_from_resolved_registry( + schema + .registry(REGISTRY_ID) + .expect("Failed to get the registry from the resolved schema"), + schema.catalog(), + ) + .unwrap_or_else(|e| process_error(&logger, e)); + let target_dir: PathBuf = target_dir.into(); + engine + .generate(logger.clone(), &template_registry, target_dir.as_path()) + .unwrap_or_else(|e| process_error(&logger, e)); + + print_logs(&logger); + + // For the purpose of the integration test located in `tests/codegen.rs`, we need to: + // - Combine all generated files to form a single file containing all the generated code + // organized in nested modules. + // - Replace `//!` with `//` + // - Remove `pub mod` lines + create_single_generated_rs_file(target_dir.as_path()); +} + +/// Create a single generated.rs file containing all the generated code organized in nested modules. +fn create_single_generated_rs_file(root: &Path) { + let mut root_module = Module { + name: "".to_owned(), + content: "".to_owned(), + sub_modules: HashMap::new(), + }; + + // Traverse the directory and add the content of each file to the root module + for entry in walkdir::WalkDir::new(root) { + let entry = entry.expect("Failed to read entry"); + let path = entry.path(); + + // Only process files with the .rs extension that contain the generated comment + if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { + let file_content = std::fs::read_to_string(path).expect("Failed to read file"); + + // Only process files that have been generated + // This test expects the comment `DO NOT EDIT, THIS FILE HAS BEEN GENERATED BY WEAVER` + // to be present in each generated file. + if file_content.contains("DO NOT EDIT, THIS FILE HAS BEEN GENERATED BY WEAVER") { + let relative_path = path.strip_prefix(root).expect("Failed to strip prefix"); + let file_name = path + .file_stem() + .expect("Failed to extract the file name") + .to_str() + .expect("Failed to convert to string"); + let parent_modules = relative_path.parent().map_or(vec![], |parent| { + parent + .components() + .filter_map(|component| match component { + Component::Normal(part) => Some(part.to_string_lossy().into_owned()), + _ => None, + }) + .collect() + }); + + // Skip generated.rs + if file_name == "generated" { + continue; + } + + // Replace //! with // + // Nested modules doesn't support //! comments + let new_file_content = file_content.replace("//!", "//"); + + // Remove lines starting with `pub mod` because we are going to nest the modules + // manually in the generated.rs file + let new_file = new_file_content + .lines() + .filter(|line| !line.starts_with("pub mod")) + .collect::>() + .join("\n"); + + add_modules( + &mut root_module, + parent_modules.clone(), + file_name.to_owned(), + new_file, + ); + } + } + } + + // Generate `generated.rs` from the hierarchy of modules + let mut output = + std::fs::File::create(root.join("generated.rs")).expect("Failed to create file"); + output + .write_all(root_module.generate().as_bytes()) + .expect("Failed to write to file"); +} + +struct Module { + name: String, + content: String, + sub_modules: HashMap, +} + +impl Module { + /// Generate the content of the module and its sub-modules + pub fn generate(&self) -> String { + let mut content = String::new(); + + content.push_str(&self.content); + + for module in self.sub_modules.values() { + content.push_str(&format!("\npub mod {} {{\n", module.name)); + content.push_str(&module.generate()); + content.push_str("\n}\n"); + } + content + } +} + +/// Add the given module to the hierarchy of modules represented by the root module. +fn add_modules( + root_module: &mut Module, + parent_modules: Vec, + module_name: String, + module_content: String, +) { + let mut current_module = root_module; + for module_name in parent_modules.iter() { + let module = current_module + .sub_modules + .entry(module_name.clone()) + .or_insert(Module { + name: module_name.clone(), + content: "".to_owned(), + sub_modules: HashMap::new(), + }); + current_module = module; + } + + if module_name == "mod" || module_name == "lib" { + current_module.content = module_content; + } else { + _ = current_module.sub_modules.insert( + module_name.clone(), + Module { + name: module_name.clone(), + content: module_content, + sub_modules: HashMap::new(), + }, + ); + } +} + +/// Print logs to stdout by following the Cargo's build script output format. +fn print_logs(logger: &in_memory::Logger) { + for log_message in logger.messages() { + match &log_message { + LogMessage::Warn(msg) => println!("cargo:warning={}", msg), + LogMessage::Error(err) => println!("cargo:warning=Error: {}", err), + _ => { /* Ignore */ } + } + } +} + +/// Process the error message and exit the build script with a non-zero exit code. +fn process_error(logger: &in_memory::Logger, error: impl std::fmt::Display) -> ! { + logger.error(&error.to_string()); + print_logs(logger); + #[allow(clippy::exit)] // Expected behavior + exit(1) +} diff --git a/crates/weaver_codegen_test/semconv_registry/http-common.yaml b/crates/weaver_codegen_test/semconv_registry/http-common.yaml new file mode 100644 index 00000000..f175215f --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/http-common.yaml @@ -0,0 +1,87 @@ +groups: + - id: attributes.http.common + type: attribute_group + brief: "Describes HTTP attributes." + attributes: + - ref: http.request.method + requirement_level: required + - ref: http.response.status_code + requirement_level: + conditionally_required: If and only if one was received/sent. + - ref: error.type + requirement_level: + conditionally_required: If request has ended with an error. + examples: ['timeout', 'java.net.UnknownHostException', 'server_certificate_invalid', '500'] + note: | + If the request fails with an error before response status code was sent or received, + `error.type` SHOULD be set to exception type (its fully-qualified class name, if applicable) + or a component-specific low cardinality error identifier. + + If response status code was sent or received and status indicates an error according to [HTTP span status definition](/docs/http/http-spans.md), + `error.type` SHOULD be set to the status code number (represented as a string), an exception type (if thrown) or a component-specific error identifier. + + The `error.type` value SHOULD be predictable and SHOULD have low cardinality. + Instrumentations SHOULD document the list of errors they report. + + The cardinality of `error.type` within one instrumentation library SHOULD be low, but + telemetry consumers that aggregate data from multiple instrumentation libraries and applications + should be prepared for `error.type` to have high cardinality at query time, when no + additional filters are applied. + + If the request has completed successfully, instrumentations SHOULD NOT set `error.type`. + - ref: network.protocol.name + examples: ['http', 'spdy'] + requirement_level: + conditionally_required: If not `http` and `network.protocol.version` is set. + - ref: network.protocol.version + examples: ['1.0', '1.1', '2', '3'] + + - id: attributes.http.client + type: attribute_group + brief: 'HTTP Client attributes' + extends: attributes.http.common + attributes: + - ref: server.address + requirement_level: required + brief: > + Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + note: > + If an HTTP client request is explicitly made to an IP address, e.g. `http://x.x.x.x:8080`, then + `server.address` SHOULD be the IP address `x.x.x.x`. A DNS lookup SHOULD NOT be used. + - ref: server.port + requirement_level: required + brief: > + Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + - ref: url.scheme + requirement_level: opt_in + examples: ["http", "https"] + + - id: attributes.http.server + type: attribute_group + brief: 'HTTP Server attributes' + extends: attributes.http.common + attributes: + - ref: http.route + requirement_level: + conditionally_required: If and only if it's available + - ref: server.address + brief: > + Name of the local HTTP server that received the request. + note: > + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + - ref: server.port + brief: > + Port of the local HTTP server that received the request. + note: > + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + requirement_level: + conditionally_required: If `server.address` is set. + - ref: url.scheme + requirement_level: required + examples: ["http", "https"] + note: > + The scheme of the original client request, if known + (e.g. from [Forwarded#proto](https://developer.mozilla.org/docs/Web/HTTP/Headers/Forwarded#proto), + [X-Forwarded-Proto](https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Forwarded-Proto), + or a similar header). + Otherwise, the scheme of the immediate peer request. \ No newline at end of file diff --git a/crates/weaver_codegen_test/semconv_registry/metrics/http.yaml b/crates/weaver_codegen_test/semconv_registry/metrics/http.yaml new file mode 100644 index 00000000..9d24d884 --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/metrics/http.yaml @@ -0,0 +1,62 @@ +groups: + - id: metric_attributes.http.server + type: attribute_group + brief: 'HTTP server attributes' + extends: attributes.http.server + attributes: + - ref: server.address + requirement_level: opt_in + note: | + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + > **Warning** + > Since this attribute is based on HTTP headers, opting in to it may allow an attacker + > to trigger cardinality limits, degrading the usefulness of the metric. + - ref: server.port + requirement_level: opt_in + note: | + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + > **Warning** + > Since this attribute is based on HTTP headers, opting in to it may allow an attacker + > to trigger cardinality limits, degrading the usefulness of the metric. + - id: metric_attributes.http.client + type: attribute_group + brief: 'HTTP client attributes' + extends: attributes.http.client + + - id: metric.http.server.request.duration + type: metric + metric_name: http.server.request.duration + brief: "Duration of HTTP server requests." + instrument: histogram + unit: "s" + stability: stable + extends: metric_attributes.http.server + + - id: metric.http.client.request.duration + type: metric + metric_name: http.client.request.duration + brief: "Duration of HTTP client requests." + instrument: histogram + unit: "s" + stability: stable + extends: metric_attributes.http.client + + - id: metric.http.client.active_requests + type: metric + metric_name: http.client.active_requests + stability: experimental + brief: "Number of active HTTP requests." + instrument: updowncounter + unit: "{request}" + attributes: + - ref: http.request.method + requirement_level: recommended + - ref: server.address + requirement_level: required + - ref: server.port + requirement_level: required + brief: > + Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + - ref: url.scheme + requirement_level: opt_in + examples: ["http", "https"] \ No newline at end of file diff --git a/crates/weaver_codegen_test/semconv_registry/metrics/system-metrics.yaml b/crates/weaver_codegen_test/semconv_registry/metrics/system-metrics.yaml new file mode 100644 index 00000000..12073142 --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/metrics/system-metrics.yaml @@ -0,0 +1,38 @@ +groups: + # system.cpu.* metrics + - id: metric.system.cpu.time + type: metric + metric_name: system.cpu.time + stability: experimental + brief: "Seconds each logical CPU spent on each mode" + instrument: counter + unit: "s" + attributes: + - ref: system.cpu.state + brief: "The CPU state for this data point. A system's CPU SHOULD be characterized *either* by data points with no `state` labels, *or only* data points with `state` labels." + - ref: system.cpu.logical_number + + - id: metric.system.cpu.utilization + type: metric + metric_name: system.cpu.utilization + stability: stable + brief: "Difference in system.cpu.time since the last measurement, divided by the elapsed time and number of logical CPUs" + instrument: gauge + unit: "1" + attributes: + - ref: system.cpu.state + brief: "The CPU state for this data point. A system's CPU SHOULD be characterized *either* by data points with no `state` labels, *or only* data points with `state` labels." + - ref: system.cpu.logical_number + + - id: metric.system.memory.usage + type: metric + metric_name: system.memory.usage + stability: stable + brief: "Reports memory in use by state." + note: | + The sum over all `system.memory.state` values SHOULD equal the total memory + available on the system, that is `system.memory.limit`. + instrument: updowncounter + unit: "By" + attributes: + - ref: system.memory.state \ No newline at end of file diff --git a/crates/weaver_codegen_test/semconv_registry/registry/client.yaml b/crates/weaver_codegen_test/semconv_registry/registry/client.yaml new file mode 100644 index 00000000..3b17ed8b --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/client.yaml @@ -0,0 +1,28 @@ +groups: + - id: registry.client + prefix: client + type: attribute_group + brief: > + These attributes may be used to describe the client in a connection-based network interaction + where there is one side that initiates the connection (the client is the side that initiates the connection). + This covers all TCP network interactions since TCP is connection-based and one side initiates the + connection (an exception is made for peer-to-peer communication over TCP where the "user-facing" surface of the + protocol / API doesn't expose a clear notion of client and server). + This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS. + attributes: + - id: address + stability: stable + type: string + brief: "Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name." + note: > + When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent + the client address behind any intermediaries, for example proxies, if it's available. + examples: ['client.example.com', '10.1.2.80', '/tmp/my.sock'] + - id: port + stability: stable + type: int + brief: Client port number. + examples: [65123] + note: > + When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent + the client port behind any intermediaries, for example proxies, if it's available. diff --git a/crates/weaver_codegen_test/semconv_registry/registry/deprecated/network.yaml b/crates/weaver_codegen_test/semconv_registry/registry/deprecated/network.yaml new file mode 100644 index 00000000..66144671 --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/deprecated/network.yaml @@ -0,0 +1,121 @@ +groups: + - id: registry.network.deprecated + prefix: net + type: attribute_group + brief: > + These attributes may be used for any network related operation. + attributes: + - id: sock.peer.name + type: string + deprecated: "Removed." + stability: experimental + brief: Deprecated, no replacement at this time. + examples: ['/var/my.sock'] + - id: sock.peer.addr + type: string + deprecated: "Replaced by `network.peer.address`." + stability: experimental + brief: Deprecated, use `network.peer.address`. + examples: ['192.168.0.1'] + - id: sock.peer.port + type: int + deprecated: "Replaced by `network.peer.port`." + stability: experimental + examples: [65531] + brief: Deprecated, use `network.peer.port`. + - id: peer.name + type: string + deprecated: "Replaced by `server.address` on client spans and `client.address` on server spans." + stability: experimental + brief: Deprecated, use `server.address` on client spans and `client.address` on server spans. + examples: ['example.com'] + - id: peer.port + type: int + deprecated: "Replaced by `server.port` on client spans and `client.port` on server spans." + stability: experimental + brief: Deprecated, use `server.port` on client spans and `client.port` on server spans. + examples: [8080] + - id: host.name + type: string + deprecated: "Replaced by `server.address`." + stability: experimental + brief: Deprecated, use `server.address`. + examples: ['example.com'] + - id: host.port + type: int + deprecated: "Replaced by `server.port`." + stability: experimental + brief: Deprecated, use `server.port`. + examples: [8080] + - id: sock.host.addr + type: string + deprecated: "Replaced by `network.local.address`." + stability: experimental + brief: Deprecated, use `network.local.address`. + examples: ['/var/my.sock'] + - id: sock.host.port + type: int + deprecated: "Replaced by `network.local.port`." + stability: experimental + brief: Deprecated, use `network.local.port`. + examples: [8080] + - id: transport + type: + allow_custom_values: true + members: + - id: ip_tcp + value: "ip_tcp" + stability: experimental + - id: ip_udp + value: "ip_udp" + stability: experimental + - id: pipe + value: "pipe" + brief: 'Named or anonymous pipe.' + stability: experimental + - id: inproc + value: "inproc" + brief: 'In-process communication.' + stability: experimental + note: > + Signals that there is only in-process communication not using a "real" network protocol + in cases where network attributes would normally be expected. Usually all other network + attributes can be left out in that case. + - id: other + value: "other" + stability: experimental + brief: 'Something else (non IP-based).' + deprecated: "Replaced by `network.transport`." + stability: experimental + brief: Deprecated, use `network.transport`. + - id: protocol.name + type: string + deprecated: "Replaced by `network.protocol.name`." + stability: experimental + brief: Deprecated, use `network.protocol.name`. + examples: ['amqp', 'http', 'mqtt'] + - id: protocol.version + type: string + deprecated: "Replaced by `network.protocol.version`." + stability: experimental + brief: Deprecated, use `network.protocol.version`. + examples: '3.1.1' + - id: sock.family + type: + allow_custom_values: true + members: + - id: inet + value: 'inet' + brief: "IPv4 address" + stability: experimental + - id: inet6 + value: 'inet6' + brief: "IPv6 address" + stability: experimental + - id: unix + value: 'unix' + brief: "Unix domain socket path" + stability: experimental + deprecated: "Split to `network.transport` and `network.type`." + stability: experimental + brief: Deprecated, use `network.transport` and `network.type`. diff --git a/crates/weaver_codegen_test/semconv_registry/registry/error.yaml b/crates/weaver_codegen_test/semconv_registry/registry/error.yaml new file mode 100644 index 00000000..621022f6 --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/error.yaml @@ -0,0 +1,40 @@ +groups: + - id: registry.error + type: attribute_group + prefix: error + brief: > + This document defines the shared attributes used to report an error. + attributes: + - id: type + stability: stable + brief: > + Describes a class of error the operation ended with. + type: + allow_custom_values: true + members: + - id: other + value: "_OTHER" + stability: stable + brief: > + A fallback error value to be used when the instrumentation doesn't define a custom value. + examples: ['timeout', 'java.net.UnknownHostException', 'server_certificate_invalid', '500'] + note: | + The `error.type` SHOULD be predictable, and SHOULD have low cardinality. + + When `error.type` is set to a type (e.g., an exception type), its + canonical class name identifying the type within the artifact SHOULD be used. + + Instrumentations SHOULD document the list of errors they report. + + The cardinality of `error.type` within one instrumentation library SHOULD be low. + Telemetry consumers that aggregate data from multiple instrumentation libraries and applications + should be prepared for `error.type` to have high cardinality at query time when no + additional filters are applied. + + If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. + + If a specific domain defines its own set of error identifiers (such as HTTP or gRPC status codes), + it's RECOMMENDED to: + + * Use a domain-specific attribute + * Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. \ No newline at end of file diff --git a/crates/weaver_codegen_test/semconv_registry/registry/exception.yaml b/crates/weaver_codegen_test/semconv_registry/registry/exception.yaml new file mode 100644 index 00000000..7e1b0118 --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/exception.yaml @@ -0,0 +1,54 @@ +groups: + - id: registry.exception + type: attribute_group + prefix: exception + brief: > + This document defines the shared attributes used to + report a single exception associated with a span or log. + attributes: + - id: type + type: string + stability: stable + brief: > + The type of the exception (its fully-qualified class name, if applicable). + The dynamic type of the exception should be preferred over the static type + in languages that support it. + examples: ["java.net.ConnectException", "OSError"] + - id: message + type: string + stability: stable + brief: The exception message. + examples: ["Division by zero", "Can't convert 'int' object to str implicitly"] + - id: stacktrace + type: string + stability: stable + brief: > + A stacktrace as a string in the natural representation for the language runtime. + The representation is to be determined and documented by each language SIG. + examples: 'Exception in thread "main" java.lang.RuntimeException: Test exception\n + at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\n + at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\n + at com.example.GenerateTrace.main(GenerateTrace.java:5)' + - id: escaped + type: boolean + stability: stable + brief: > + SHOULD be set to true if the exception event is recorded at a point where + it is known that the exception is escaping the scope of the span. + note: |- + An exception is considered to have escaped (or left) the scope of a span, + if that span is ended while the exception is still logically "in flight". + This may be actually "in flight" in some languages (e.g. if the exception + is passed to a Context manager's `__exit__` method in Python) but will + usually be caught at the point of recording the exception in most languages. + + It is usually not possible to determine at the point where an exception is thrown + whether it will escape the scope of a span. + However, it is trivial to know that an exception + will escape, if one checks for an active exception just before ending the span, + as done in the [example for recording span exceptions](#recording-an-exception). + + It follows that an exception may still escape the scope of the span + even if the `exception.escaped` attribute was not set or set to false, + since the event might have been recorded at a time where it was not + clear whether the exception will escape. diff --git a/crates/weaver_codegen_test/semconv_registry/registry/http.yaml b/crates/weaver_codegen_test/semconv_registry/registry/http.yaml new file mode 100644 index 00000000..e013704a --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/http.yaml @@ -0,0 +1,174 @@ +groups: + - id: registry.http + prefix: http + type: attribute_group + brief: 'This document defines semantic convention attributes in the HTTP namespace.' + attributes: + - id: request.body.size + type: int + brief: > + The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and + is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) + header. For requests using transport encoding, this should be the compressed size. + examples: 3495 + stability: experimental # this should not be marked stable with other HTTP attributes + - id: request.header + stability: stable + type: template[string[]] + brief: > + HTTP request headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. + note: > + Instrumentations SHOULD require an explicit configuration of which headers are to be captured. + Including all request headers can be a security risk - explicit configuration helps avoid leaking sensitive information. + + The `User-Agent` header is already captured in the `user_agent.original` attribute. + Users MAY explicitly configure instrumentations to capture them even though it is not recommended. + + The attribute value MUST consist of either multiple header values as an array of strings + or a single-item array containing a possibly comma-concatenated string, depending on the way + the HTTP library provides access to headers. + examples: ['http.request.header.content-type=["application/json"]', 'http.request.header.x-forwarded-for=["1.2.3.4", "1.2.3.5"]'] + - id: request.method + stability: stable + type: + allow_custom_values: true + members: + - id: connect + value: "CONNECT" + brief: 'CONNECT method.' + stability: stable + - id: delete + value: "DELETE" + brief: 'DELETE method.' + stability: stable + - id: get + value: "GET" + brief: 'GET method.' + stability: stable + - id: head + value: "HEAD" + brief: 'HEAD method.' + stability: stable + - id: options + value: "OPTIONS" + brief: 'OPTIONS method.' + stability: stable + - id: patch + value: "PATCH" + brief: 'PATCH method.' + stability: stable + - id: post + value: "POST" + brief: 'POST method.' + stability: stable + - id: put + value: "PUT" + brief: 'PUT method.' + stability: stable + - id: trace + value: "TRACE" + brief: 'TRACE method.' + stability: stable + - id: other + value: "_OTHER" + brief: 'Any HTTP method that the instrumentation has no prior knowledge of.' + stability: stable + brief: 'HTTP request method.' + examples: ["GET", "POST", "HEAD"] + note: | + HTTP request method value SHOULD be "known" to the instrumentation. + By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods) + and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). + + If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`. + + If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override + the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named + OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods + (this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults). + + HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. + Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. + Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. + - id: request.method_original + stability: stable + type: string + brief: Original HTTP method sent by the client in the request line. + examples: ["GeT", "ACL", "foo"] + - id: request.resend_count + stability: stable + type: int + brief: > + The ordinal number of request resending attempt (for any reason, including redirects). + note: > + The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what + was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, + or any other). + examples: 3 + - id: request.size + type: int + brief: > + The total size of the request in bytes. This should be the total number of bytes sent over the wire, including the request line (HTTP/1.1), + framing (HTTP/2 and HTTP/3), headers, and request body if any. + examples: 1437 + stability: experimental + - id: response.body.size + type: int + brief: > + The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and + is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) + header. For requests using transport encoding, this should be the compressed size. + examples: 3495 + stability: experimental # this should not be marked stable with other HTTP attributes + - id: response.header + stability: stable + type: template[string[]] + brief: > + HTTP response headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. + note: > + Instrumentations SHOULD require an explicit configuration of which headers are to be captured. + Including all response headers can be a security risk - explicit configuration helps avoid leaking sensitive information. + + Users MAY explicitly configure instrumentations to capture them even though it is not recommended. + + The attribute value MUST consist of either multiple header values as an array of strings + or a single-item array containing a possibly comma-concatenated string, depending on the way + the HTTP library provides access to headers. + examples: ['http.response.header.content-type=["application/json"]', 'http.response.header.my-custom-header=["abc", "def"]'] + - id: response.size + type: int + brief: > + The total size of the response in bytes. This should be the total number of bytes sent over the wire, including the status line (HTTP/1.1), + framing (HTTP/2 and HTTP/3), headers, and response body and trailers if any. + examples: 1437 + stability: experimental + - id: response.status_code + stability: stable + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: [200] + - id: route + stability: stable + type: string + brief: > + The matched route, that is, the path template in the format used by the respective server framework. + examples: ['/users/:userID?', '{controller}/{action}/{id?}'] + note: > + MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. + + SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. + - id: connection.state + type: + allow_custom_values: true + members: + - id: active + value: "active" + brief: 'active state.' + stability: experimental + - id: idle + value: "idle" + brief: 'idle state.' + stability: experimental + brief: State of the HTTP connection in the HTTP connection pool. + stability: experimental + examples: ["active", "idle"] diff --git a/crates/weaver_codegen_test/semconv_registry/registry/network.yaml b/crates/weaver_codegen_test/semconv_registry/registry/network.yaml new file mode 100644 index 00000000..591a0188 --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/network.yaml @@ -0,0 +1,235 @@ +groups: + - id: registry.network + prefix: network + type: attribute_group + brief: > + These attributes may be used for any network related operation. + attributes: + - id: carrier.icc + type: string + stability: experimental + brief: "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network." + examples: "DE" + - id: carrier.mcc + type: string + stability: experimental + brief: "The mobile carrier country code." + examples: "310" + - id: carrier.mnc + type: string + stability: experimental + brief: "The mobile carrier network code." + examples: "001" + - id: carrier.name + type: string + stability: experimental + brief: "The name of the mobile carrier." + examples: "sprint" + - id: connection.subtype + type: + allow_custom_values: true + members: + - id: gprs + brief: GPRS + value: "gprs" + stability: experimental + - id: edge + brief: EDGE + value: "edge" + stability: experimental + - id: umts + brief: UMTS + value: "umts" + stability: experimental + - id: cdma + brief: CDMA + value: "cdma" + stability: experimental + - id: evdo_0 + brief: EVDO Rel. 0 + value: "evdo_0" + stability: experimental + - id: evdo_a + brief: "EVDO Rev. A" + value: "evdo_a" + stability: experimental + - id: cdma2000_1xrtt + brief: CDMA2000 1XRTT + value: "cdma2000_1xrtt" + stability: experimental + - id: hsdpa + brief: HSDPA + value: "hsdpa" + stability: experimental + - id: hsupa + brief: HSUPA + value: "hsupa" + stability: experimental + - id: hspa + brief: HSPA + value: "hspa" + stability: experimental + - id: iden + brief: IDEN + value: "iden" + stability: experimental + - id: evdo_b + brief: "EVDO Rev. B" + value: "evdo_b" + stability: experimental + - id: lte + brief: LTE + value: "lte" + stability: experimental + - id: ehrpd + brief: EHRPD + value: "ehrpd" + stability: experimental + - id: hspap + brief: HSPAP + value: "hspap" + stability: experimental + - id: gsm + brief: GSM + value: "gsm" + stability: experimental + - id: td_scdma + brief: TD-SCDMA + value: "td_scdma" + stability: experimental + - id: iwlan + brief: IWLAN + value: "iwlan" + stability: experimental + - id: nr + brief: "5G NR (New Radio)" + value: "nr" + stability: experimental + - id: nrnsa + brief: "5G NRNSA (New Radio Non-Standalone)" + value: "nrnsa" + stability: experimental + - id: lte_ca + brief: LTE CA + value: "lte_ca" + stability: experimental + stability: experimental + brief: 'This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection.' + examples: 'LTE' + - id: connection.type + type: + allow_custom_values: true + members: + - id: wifi + value: "wifi" + stability: experimental + - id: wired + value: "wired" + stability: experimental + - id: cell + value: "cell" + stability: experimental + - id: unavailable + value: "unavailable" + stability: experimental + - id: unknown + value: "unknown" + stability: experimental + stability: experimental + brief: 'The internet connection type.' + examples: 'wifi' + - id: local.address + stability: stable + type: string + brief: Local address of the network connection - IP address or Unix domain socket name. + examples: ['10.1.2.80', '/tmp/my.sock'] + - id: local.port + stability: stable + type: int + brief: Local port number of the network connection. + examples: [65123] + - id: peer.address + stability: stable + type: string + brief: Peer address of the network connection - IP address or Unix domain socket name. + examples: ['10.1.2.80', '/tmp/my.sock'] + - id: peer.port + stability: stable + type: int + brief: Peer port number of the network connection. + examples: [65123] + - id: protocol.name + stability: stable + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + note: The value SHOULD be normalized to lowercase. + examples: ['amqp', 'http', 'mqtt'] + - id: protocol.version + stability: stable + type: string + brief: The actual version of the protocol used for network communication. + examples: ['1.1', '2'] + note: > + If protocol version is subject to negotiation (for example using [ALPN](https://www.rfc-editor.org/rfc/rfc7301.html)), + this attribute SHOULD be set to the negotiated version. If the actual protocol version is not known, + this attribute SHOULD NOT be set. + - id: transport + stability: stable + type: + allow_custom_values: true + members: + - id: tcp + value: 'tcp' + brief: "TCP" + stability: stable + - id: udp + value: 'udp' + brief: "UDP" + stability: stable + - id: pipe + value: "pipe" + brief: 'Named or anonymous pipe.' + stability: stable + - id: unix + value: 'unix' + brief: "Unix domain socket" + stability: stable + brief: > + [OSI transport layer](https://osi-model.com/transport-layer/) or + [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication). + note: | + The value SHOULD be normalized to lowercase. + + Consider always setting the transport when setting a port number, since + a port number is ambiguous without knowing the transport. For example + different processes could be listening on TCP port 12345 and UDP port 12345. + examples: ['tcp', 'udp'] + - id: type + stability: stable + type: + allow_custom_values: true + members: + - id: ipv4 + value: 'ipv4' + brief: "IPv4" + stability: stable + - id: ipv6 + value: 'ipv6' + brief: "IPv6" + stability: stable + brief: '[OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent.' + note: The value SHOULD be normalized to lowercase. + examples: ['ipv4', 'ipv6'] + - id: io.direction + type: + allow_custom_values: false + members: + - id: transmit + value: 'transmit' + stability: experimental + - id: receive + value: 'receive' + stability: experimental + stability: experimental + brief: "The network IO operation direction." + examples: ["transmit"] \ No newline at end of file diff --git a/crates/weaver_codegen_test/semconv_registry/registry/server.yaml b/crates/weaver_codegen_test/semconv_registry/registry/server.yaml new file mode 100644 index 00000000..0afe3fab --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/server.yaml @@ -0,0 +1,28 @@ +groups: + - id: registry.server + prefix: server + type: attribute_group + brief: > + These attributes may be used to describe the server in a connection-based network interaction + where there is one side that initiates the connection (the client is the side that initiates the connection). + This covers all TCP network interactions since TCP is connection-based and one side initiates the + connection (an exception is made for peer-to-peer communication over TCP where the "user-facing" surface of the + protocol / API doesn't expose a clear notion of client and server). + This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS. + attributes: + - id: address + stability: stable + type: string + brief: "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name." + note: > + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries, for example proxies, if it's available. + examples: ['example.com', '10.1.2.80', '/tmp/my.sock'] + - id: port + stability: stable + type: int + brief: Server port number. + note: > + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent + the server port behind any intermediaries, for example proxies, if it's available. + examples: [80, 8080, 443] \ No newline at end of file diff --git a/crates/weaver_codegen_test/semconv_registry/registry/system.yaml b/crates/weaver_codegen_test/semconv_registry/registry/system.yaml new file mode 100644 index 00000000..1f6bc175 --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/system.yaml @@ -0,0 +1,65 @@ +groups: + # system.cpu.* attribute group + - id: registry.system.cpu + prefix: system.cpu + type: attribute_group + brief: "Describes System CPU attributes" + attributes: + - id: state + type: + allow_custom_values: true + members: + - id: user + value: 'user' + - id: system + value: 'system' + - id: nice + value: 'nice' + - id: idle + value: 'idle' + - id: iowait + value: 'iowait' + stability: experimental + - id: interrupt + value: 'interrupt' + stability: experimental + - id: steal + value: 'steal' + stability: experimental + brief: "The state of the CPU" + stability: stable + examples: ["idle", "interrupt"] + - id: logical_number + type: int + stability: stable + brief: "The logical CPU number [0..n-1]" + examples: [1] + # sytem.memory.* attribute group + - id: registry.system.memory + prefix: system.memory + type: attribute_group + brief: "Describes System Memory attributes" + attributes: + - id: state + type: + allow_custom_values: true + members: + - id: used + value: 'used' + stability: experimental + - id: free + value: 'free' + stability: experimental + - id: shared + value: 'shared' + stability: experimental + deprecated: 'Removed, report shared memory usage with `metric.system.memory.shared` metric' + - id: buffers + value: 'buffers' + stability: experimental + - id: cached + value: 'cached' + stability: experimental + stability: stable + brief: "The memory state" + examples: ["free", "cached"] \ No newline at end of file diff --git a/crates/weaver_codegen_test/semconv_registry/registry/url.yaml b/crates/weaver_codegen_test/semconv_registry/registry/url.yaml new file mode 100644 index 00000000..7c132dae --- /dev/null +++ b/crates/weaver_codegen_test/semconv_registry/registry/url.yaml @@ -0,0 +1,116 @@ +groups: + - id: registry.url + brief: Attributes describing URL. + type: attribute_group + prefix: url + attributes: + - id: domain + type: string + stability: experimental + brief: > + Domain extracted from the `url.full`, such as "opentelemetry.io". + note: > + In some cases a URL may refer to an IP and/or port directly, + without a domain name. In this case, the IP address would go to the domain field. + If the URL contains a [literal IPv6 address](https://www.rfc-editor.org/rfc/rfc2732#section-2) + enclosed by `[` and `]`, the `[` and `]` characters should also be captured in the domain field. + examples: ["www.foo.bar", "opentelemetry.io", "3.12.167.2", "[1080:0:0:0:8:800:200C:417A]"] + - id: extension + type: string + stability: experimental + brief: > + The file extension extracted from the `url.full`, excluding the leading dot. + note: > + The file extension is only set if it exists, as not every url has a file extension. + When the file name has multiple extensions `example.tar.gz`, only the last one should be captured `gz`, not `tar.gz`. + examples: [ "png", "gz" ] + - id: fragment + stability: stable + type: string + brief: > + The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component + examples: ["SemConv"] + - id: full + stability: stable + type: string + brief: Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) + note: > + For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment + is not transmitted over HTTP, but if it is known, it SHOULD be included nevertheless. + + `url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. + In such case username and password SHOULD be redacted and attribute's value SHOULD be `https://REDACTED:REDACTED@www.example.com/`. + + `url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed). + Sensitive content provided in `url.full` SHOULD be scrubbed when instrumentations can identify it. + examples: ['https://www.foo.bar/search?q=OpenTelemetry#SemConv', '//localhost'] + - id: original + type: string + stability: experimental + brief: > + Unmodified original URL as seen in the event source. + note: > + In network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often + just represented as a path. This field is meant to represent the URL as it was observed, complete or not. + + `url.original` might contain credentials passed via URL in form of `https://username:password@www.example.com/`. + In such case password and username SHOULD NOT be redacted and attribute's value SHOULD remain the same. + examples: ["https://www.foo.bar/search?q=OpenTelemetry#SemConv", "search?q=OpenTelemetry"] + - id: path + stability: stable + type: string + brief: > + The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component + examples: ["/search"] + note: > + Sensitive content provided in `url.path` SHOULD be scrubbed when instrumentations can identify it. + - id: port + type: int + stability: experimental + brief: > + Port extracted from the `url.full` + examples: [443] + - id: query + stability: stable + type: string + brief: > + The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component + examples: ["q=OpenTelemetry"] + note: > + Sensitive content provided in `url.query` SHOULD be scrubbed when instrumentations can identify it. + - id: registered_domain + type: string + stability: experimental + brief: > + The highest registered url domain, stripped of the subdomain. + examples: ["example.com", "foo.co.uk"] + note: > + This value can be determined precisely with the [public suffix list](http://publicsuffix.org). + For example, the registered domain for `foo.example.com` is `example.com`. + Trying to approximate this by simply taking the last two labels will not work well for TLDs such as `co.uk`. + - id: scheme + stability: stable + type: string + brief: > + The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: ["https", "ftp", "telnet"] + - id: subdomain + type: string + stability: experimental + brief: > + The subdomain portion of a fully qualified domain name includes all of the names except the host name + under the registered_domain. In a partially qualified domain, or if the qualification level of the + full name cannot be determined, subdomain contains all of the names below the registered domain. + examples: ["east", "sub2.sub1"] + note: > + The subdomain portion of `www.east.mydomain.co.uk` is `east`. If the domain has multiple levels of subdomain, + such as `sub2.sub1.example.com`, the subdomain field should contain `sub2.sub1`, with no trailing period. + - id: top_level_domain + type: string + stability: experimental + brief: > + The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. + For example, the top level domain for example.com is `com`. + examples: ["com", "co.uk"] + note: > + This value can be determined precisely with the [public suffix list](http://publicsuffix.org). \ No newline at end of file diff --git a/crates/weaver_codegen_test/src/lib.rs b/crates/weaver_codegen_test/src/lib.rs new file mode 100644 index 00000000..8bb850f1 --- /dev/null +++ b/crates/weaver_codegen_test/src/lib.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! This lib file is used to test the code generation of the `weaver_codegen_test` crate. +//! This file only exists to satisfy the `cargo` build process. The actual code generation +//! is done in the `build.rs` file. +//! See the integration test `tests/codegen.rs` for more information. diff --git a/crates/weaver_codegen_test/templates/registry/rust/attribute_macros.j2 b/crates/weaver_codegen_test/templates/registry/rust/attribute_macros.j2 new file mode 100644 index 00000000..447c6b74 --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/attribute_macros.j2 @@ -0,0 +1,60 @@ +{%- macro comments(attribute, prefix) -%} +{%- if attribute.brief %} +{{ attribute.brief | comment_with_prefix(prefix ~ " ") }} +{%- endif %} +{%- if attribute.note %} +{{ prefix }} +{{ prefix }} Notes: +{{ attribute.note | comment_with_prefix(prefix ~ " ") }} +{%- endif %} +{%- if attribute.examples %} +{%- if attribute.examples is sequence %} +{{ prefix }} +{{ prefix }} Examples: +{%- for example in attribute.examples %} +{{ example | comment_with_prefix(prefix ~ " - ") }} +{%- endfor %} +{%- else %} +{{ prefix }} +{{ prefix }} Example: {{ attribute.examples | trim }} +{%- endif %} +{%- endif %} +{%- endmacro %} + +{%- macro attributes_to_key_values(required_attributes, not_required_attributes) -%} + let mut attributes = vec![ + {%- for attribute in required_attributes | attribute_sort %} + {%- if attribute is experimental %} + #[cfg(feature = "semconv_experimental")] + {%- endif %} + {%- if attribute.type.members is defined %} + crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | screaming_snake_case }}.value(&required_attributes.{{ attribute.name | snake_case }}), + {%- elif attribute.type == "string" %} + crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | screaming_snake_case }}.value(required_attributes.{{ attribute.name | snake_case }}.to_owned().into()), + {%- else %} + crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | screaming_snake_case }}.value(required_attributes.{{ attribute.name | snake_case }}), + {%- endif %} + {%- endfor %} + ]; + + if let Some(value) = ¬_required_attributes { + {%- for attribute in not_required_attributes | attribute_sort %} + {%- if attribute is experimental %} + #[cfg(feature = "semconv_experimental")] + {%- endif %} + {%- if attribute.type.members is defined %} + if let Some({{ attribute.name | snake_case }}) = &value.{{ attribute.name | snake_case }} { + attributes.push(crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | screaming_snake_case }}.value({{ attribute.name | snake_case }})); + } + {%- elif attribute.type == "string" %} + if let Some({{ attribute.name | snake_case }}) = &value.{{ attribute.name | snake_case }} { + attributes.push(crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | screaming_snake_case }}.value({{ attribute.name | snake_case }}.to_owned().into())); + } + {%- else %} + if let Some({{ attribute.name | snake_case }}) = value.{{ attribute.name | snake_case }} { + attributes.push(crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | screaming_snake_case }}.value({{ attribute.name | snake_case }})); + } + {%- endif %} + {%- endfor %} + } +{%- endmacro %} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/attributes/attributes.rs.j2 b/crates/weaver_codegen_test/templates/registry/rust/attributes/attributes.rs.j2 new file mode 100644 index 00000000..b14e3465 --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/attributes/attributes.rs.j2 @@ -0,0 +1,85 @@ +{%- set file_name = ctx.id | attribute_registry_namespace | snake_case -%} +{{- template.set_file_name("attributes/" ~ file_name ~ ".rs") -}} +{%- import 'attribute_macros.j2' as attribute_macros -%} + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +{{ ctx.brief | comment_with_prefix("//! ") }} +{%- if ctx.note %} +//! +//! Notes: +{{ ctx.note | comment_with_prefix("//! ") }} +{%- endif %} +//! DO NOT EDIT, THIS FILE HAS BEEN GENERATED BY WEAVER + +{%- for attribute in ctx.attributes | attribute_sort %} +{{ attribute_macros.comments(attribute, "///") }} +{%- if attribute is experimental %} +#[cfg(feature = "semconv_experimental")] +{%- endif %} +{%- if attribute is deprecated %} +#[deprecated(note="{{ attribute.deprecated }}")] +{%- endif %} +{%- if attribute.type.allow_custom_values is defined %} +pub const {{ attribute.name | screaming_snake_case }}: crate::attributes::AttributeKey<{{ attribute.name | pascal_case }}> = crate::attributes::AttributeKey::new("{{ attribute.name }}"); +{%- elif attribute.type == "string" %} +pub const {{ attribute.name | screaming_snake_case }}: crate::attributes::AttributeKey = crate::attributes::AttributeKey::new("{{ attribute.name }}"); +{%- else %} +pub const {{ attribute.name | screaming_snake_case }}: crate::attributes::AttributeKey<{{ attribute.type | type_mapping }}> = crate::attributes::AttributeKey::new("{{ attribute.name }}"); +{%- endif %} +{%- if attribute.type.members is defined %} + +{% if attribute.brief %}{{ attribute.brief | comment_with_prefix("/// ") }}{%- endif %} +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum {{ attribute.name | pascal_case }} { +{%- for variant in attribute.type.members %} + {{ variant.brief | default("No brief") | comment_with_prefix(" /// ") }} + {%- if variant.note %}{{ variant.note | comment_with_prefix(" /// ") }}{% endif %} + {%- if variant is experimental %} + #[cfg(feature = "semconv_experimental")] {% endif %} + {{ variant.id | pascal_case }}, +{%- endfor %} + /// This variant allows defining a custom entry in the enum. + _Custom(String), +} + +impl {{ attribute.name | pascal_case }} { + /// Returns the string representation of the [`{{ attribute.name | pascal_case }}`]. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + {%- for variant in attribute.type.members %} + {%- if variant is experimental %} + #[cfg(feature = "semconv_experimental")] {% endif %} + {{ attribute.name | pascal_case }}::{{ variant.id | pascal_case }} => "{{ variant.value }}", + {%- endfor %} + {{ attribute.name | pascal_case }}::_Custom(v) => v.as_str(), + // Without this default case, the match expression would not + // contain any variants if all variants are annotated with the + // 'semconv_experimental' feature and the feature is not enabled. + #[allow(unreachable_patterns)] + _ => unreachable!(), + } + } +} + +impl core::fmt::Display for {{ attribute.name | pascal_case }} { + /// Formats the value using the given formatter. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl crate::attributes::AttributeKey<{{ attribute.name | pascal_case }}> { + /// Returns a [`KeyValue`] pair for the given value. + #[must_use] + pub fn value(&self, v: &{{ attribute.name | pascal_case }}) -> opentelemetry::KeyValue { + opentelemetry::KeyValue::new(self.key.clone(), v.to_string()) + } +} +{%- endif %} +{%- endfor %} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/attributes/mod.rs.j2 b/crates/weaver_codegen_test/templates/registry/rust/attributes/mod.rs.j2 new file mode 100644 index 00000000..064c9ff7 --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/attributes/mod.rs.j2 @@ -0,0 +1,71 @@ +{{- template.set_file_name("attributes/mod.rs") -}} + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +//! OpenTelemetry Semantic Convention Attributes +//! DO NOT EDIT, THIS FILE HAS BEEN GENERATED BY WEAVER + +use opentelemetry::{Key, KeyValue, StringValue}; + +{% for group in ctx %} +/// Attributes for the `{{ group.id | attribute_registry_namespace }}` namespace. +pub mod {{ group.id | attribute_registry_namespace | snake_case }}; +{%- endfor %} + +/// A typed attribute key. +pub struct AttributeKey { + key: Key, + phantom: std::marker::PhantomData +} + +impl AttributeKey { + /// Returns a new [`AttributeKey`] with the given key. + #[must_use] + pub(crate) const fn new(key: &'static str) -> AttributeKey { + Self { + key: Key::from_static_str(key), + phantom: std::marker::PhantomData + } + } + + /// Returns the key of the attribute. + #[must_use] + pub fn key(&self) -> &Key { + &self.key + } +} + +impl AttributeKey { + /// Returns a [`KeyValue`] pair for the given value. + #[must_use] + pub fn value(&self, v: StringValue) -> KeyValue { + KeyValue::new(self.key.clone(), v) + } +} + +impl AttributeKey { + /// Returns a [`KeyValue`] pair for the given value. + #[must_use] + pub fn value(&self, v: i64) -> KeyValue { + KeyValue::new(self.key.clone(), v) + } +} + +impl AttributeKey { + /// Returns a [`KeyValue`] pair for the given value. + #[must_use] + pub fn value(&self, v: f64) -> KeyValue { + KeyValue::new(self.key.clone(), v) + } +} + +impl AttributeKey { + /// Returns a [`KeyValue`] pair for the given value. + #[must_use] + pub fn value(&self, v: bool) -> KeyValue { + KeyValue::new(self.key.clone(), v) + } +} diff --git a/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/counter.j2 b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/counter.j2 new file mode 100644 index 00000000..8a32cc75 --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/counter.j2 @@ -0,0 +1,71 @@ +{%- import 'attribute_macros.j2' as attribute_macros %} +{%- if metric.brief %} +{{ metric.brief | comment_with_prefix("/// ") }} +{%- endif %} +{%- if metric is experimental %} +#[cfg(feature = "semconv_experimental")] +{%- endif %} +#[must_use] +pub fn create_{{ metric.metric_name | snake_case }}(meter: &opentelemetry::metrics::Meter) -> opentelemetry::metrics::Counter + where opentelemetry::metrics::Meter: crate::metrics::CounterProvider { + crate::metrics::CounterProvider::create_counter(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}") +} + +/// Metric: {{ metric.metric_name }} +/// Brief: {{ metric.brief }} +/// Unit: {{ metric.unit }} +#[derive(Debug)] +pub struct {{ metric.metric_name | pascal_case }}(opentelemetry::metrics::Counter); + +{%- set required_attributes = metric.attributes | required %} +{%- set not_required_attributes = metric.attributes | not_required %} + +{% if required_attributes %} +/// Required attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone)] +pub struct {{ metric.metric_name | pascal_case }}ReqAttributes { + {%- for attribute in required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | pascal_case }}, + {%- else %} + pub {{ attribute.name | snake_case }}: {{ attribute.type | type_mapping }}, + {%- endif %} + {%- endfor %} +} +{% endif %} + +{% if not_required_attributes %} +/// Not required attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone, Default)] +pub struct {{ metric.metric_name | pascal_case }}OptAttributes { + {%- for attribute in not_required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: Option, + {%- else %} + pub {{ attribute.name | snake_case }}: Option<{{ attribute.type | type_mapping }}>, + {%- endif %} + {%- endfor %} +} +{% endif %} + +impl {{ metric.metric_name | pascal_case }} { + /// Creates a new `{{ metric.metric_name }}` metric. + #[must_use] + pub fn new(meter: &opentelemetry::metrics::Meter) -> Self + where opentelemetry::metrics::Meter: crate::metrics::CounterProvider{ + Self(crate::metrics::CounterProvider::create_counter(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}")) + } + + /// Adds an additional value to the counter. + pub fn add( + &self, + value: T, + {% if required_attributes %}required_attributes: &{{ metric.metric_name | pascal_case }}ReqAttributes,{% endif %} + {% if not_required_attributes %}not_required_attributes: Option<&{{ metric.metric_name | pascal_case }}OptAttributes>,{% endif %} + ) { + {{ attribute_macros.attributes_to_key_values(required_attributes, not_required_attributes) }} + self.0.add(value, &attributes); + } +} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/gauge.j2 b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/gauge.j2 new file mode 100644 index 00000000..930cbc6f --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/gauge.j2 @@ -0,0 +1,71 @@ +{%- import 'attribute_macros.j2' as attribute_macros %} +{%- if metric.brief %} +{{ metric.brief | comment_with_prefix("/// ") }} +{%- endif %} +{%- if metric is experimental %} +#[cfg(feature = "semconv_experimental")] +{%- endif %} +#[must_use] +pub fn create_{{ metric.metric_name | snake_case }}(meter: &opentelemetry::metrics::Meter) -> opentelemetry::metrics::Gauge + where opentelemetry::metrics::Meter: crate::metrics::GaugeProvider { + crate::metrics::GaugeProvider::create_gauge(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}") +} + +/// Metric: {{ metric.metric_name }} +/// Brief: {{ metric.brief }} +/// Unit: {{ metric.unit }} +#[derive(Debug)] +pub struct {{ metric.metric_name | pascal_case }}(opentelemetry::metrics::Gauge); + +{%- set required_attributes = metric.attributes | required %} +{%- set not_required_attributes = metric.attributes | not_required %} + +{% if required_attributes %} +/// Required attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone)] +pub struct {{ metric.metric_name | pascal_case }}ReqAttributes { + {%- for attribute in required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | pascal_case }}, + {%- else %} + pub {{ attribute.name | snake_case }}: {{ attribute.type | type_mapping }}, + {%- endif %} + {%- endfor %} +} +{% endif %} + +{% if not_required_attributes %} +/// Not required attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone, Default)] +pub struct {{ metric.metric_name | pascal_case }}OptAttributes { + {%- for attribute in not_required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: Option, + {%- else %} + pub {{ attribute.name | snake_case }}: Option<{{ attribute.type | type_mapping }}>, + {%- endif %} + {%- endfor %} +} +{% endif %} + +impl {{ metric.metric_name | pascal_case }} { + /// Creates a new `{{ metric.metric_name }}` metric. + #[must_use] + pub fn new(meter: &opentelemetry::metrics::Meter) -> Self + where opentelemetry::metrics::Meter: crate::metrics::GaugeProvider{ + Self(crate::metrics::GaugeProvider::create_gauge(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}")) + } + + /// Records an additional value to the gauge. + pub fn record( + &self, + value: T, + {% if required_attributes %}required_attributes: &{{ metric.metric_name | pascal_case }}ReqAttributes,{% endif %} + {% if not_required_attributes %}not_required_attributes: Option<&{{ metric.metric_name | pascal_case }}OptAttributes>,{% endif %} + ) { + {{ attribute_macros.attributes_to_key_values(required_attributes, not_required_attributes) }} + self.0.record(value, &attributes); + } +} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/histogram.j2 b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/histogram.j2 new file mode 100644 index 00000000..ffe08a10 --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/histogram.j2 @@ -0,0 +1,71 @@ +{%- import 'attribute_macros.j2' as attribute_macros %} +{%- if metric.brief %} +{{ metric.brief | comment_with_prefix("/// ") }} +{%- endif %} +{%- if metric is experimental %} +#[cfg(feature = "semconv_experimental")] +{%- endif %} +#[must_use] +pub fn create_{{ metric.metric_name | snake_case }}(meter: &opentelemetry::metrics::Meter) -> opentelemetry::metrics::Histogram + where opentelemetry::metrics::Meter: crate::metrics::HistogramProvider { + crate::metrics::HistogramProvider::create_histogram(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}") +} + +/// Metric: {{ metric.metric_name }} +/// Brief: {{ metric.brief }} +/// Unit: {{ metric.unit }} +#[derive(Debug)] +pub struct {{ metric.metric_name | pascal_case }}(opentelemetry::metrics::Histogram); + +{%- set required_attributes = metric.attributes | required %} +{%- set not_required_attributes = metric.attributes | not_required %} + +{% if required_attributes %} +/// Required attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone)] +pub struct {{ metric.metric_name | pascal_case }}ReqAttributes { + {%- for attribute in required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | pascal_case }}, + {%- else %} + pub {{ attribute.name | snake_case }}: {{ attribute.type | type_mapping }}, + {%- endif %} + {%- endfor %} +} +{% endif %} + +{% if not_required_attributes %} +/// Not required attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone, Default)] +pub struct {{ metric.metric_name | pascal_case }}OptAttributes { + {%- for attribute in not_required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: Option, + {%- else %} + pub {{ attribute.name | snake_case }}: Option<{{ attribute.type | type_mapping }}>, + {%- endif %} + {%- endfor %} +} +{% endif %} + +impl {{ metric.metric_name | pascal_case }} { + /// Creates a new instance of the `{{ metric.metric_name }}` metric. + #[must_use] + pub fn new(meter: &opentelemetry::metrics::Meter) -> Self + where opentelemetry::metrics::Meter: crate::metrics::HistogramProvider{ + Self(crate::metrics::HistogramProvider::create_histogram(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}")) + } + + /// Adds an additional value to the distribution. + pub fn record( + &self, + value: T, + {% if required_attributes %}required_attributes: &{{ metric.metric_name | pascal_case }}ReqAttributes,{% endif %} + {% if not_required_attributes %}not_required_attributes: Option<&{{ metric.metric_name | pascal_case }}OptAttributes>,{% endif %} + ) { + {{ attribute_macros.attributes_to_key_values(required_attributes, not_required_attributes) }} + self.0.record(value, &attributes); + } +} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/updowncounter.j2 b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/updowncounter.j2 new file mode 100644 index 00000000..3276cb6f --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/metrics/instruments/updowncounter.j2 @@ -0,0 +1,72 @@ +{%- import 'attribute_macros.j2' as attribute_macros %} +{%- if metric.brief %} +{{ metric.brief | comment_with_prefix("/// ") }} +{%- endif %} +{%- if metric is experimental %} +#[cfg(feature = "semconv_experimental")] +{%- endif %} +#[must_use] +pub fn create_{{ metric.metric_name | snake_case }}(meter: &opentelemetry::metrics::Meter) -> opentelemetry::metrics::UpDownCounter + where opentelemetry::metrics::Meter: crate::metrics::UpDownCounterProvider { + crate::metrics::UpDownCounterProvider::create_up_down_counter(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}") +} + +/// Metric: {{ metric.metric_name }} +/// Brief: {{ metric.brief }} +/// Unit: {{ metric.unit }} +#[derive(Debug)] +pub struct {{ metric.metric_name | pascal_case }}(opentelemetry::metrics::UpDownCounter); + +{%- set required_attributes = metric.attributes | required %} +{%- set not_required_attributes = metric.attributes | not_required %} + +{% if required_attributes %} +/// Required attributes for the `{{ metric.metric_name }}` metric. +/// Attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone)] +pub struct {{ metric.metric_name | pascal_case }}ReqAttributes { + {%- for attribute in required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: crate::attributes::{{ attribute.name | attribute_namespace }}::{{ attribute.name | pascal_case }}, + {%- else %} + pub {{ attribute.name | snake_case }}: {{ attribute.type | type_mapping }}, + {%- endif %} + {%- endfor %} +} +{% endif %} + +{% if not_required_attributes %} +/// Not required attributes for the `{{ metric.metric_name }}` metric. +#[derive(Debug, Clone, Default)] +pub struct {{ metric.metric_name | pascal_case }}OptAttributes { + {%- for attribute in not_required_attributes | attribute_sort %} + {{ attribute_macros.comments(attribute, " ///") }} + {%- if attribute.type.members is defined %} + pub {{ attribute.name | snake_case }}: Option, + {%- else %} + pub {{ attribute.name | snake_case }}: Option<{{ attribute.type | type_mapping }}>, + {%- endif %} + {%- endfor %} +} +{% endif %} + +impl {{ metric.metric_name | pascal_case }} { + /// Creates a new instance of the `{{ metric.metric_name }}` metric. + #[must_use] + pub fn new(meter: &opentelemetry::metrics::Meter) -> Self + where opentelemetry::metrics::Meter: crate::metrics::UpDownCounterProvider{ + Self(crate::metrics::UpDownCounterProvider::create_up_down_counter(meter, "{{ metric.metric_name }}", "{{ metric.brief }}", "{{ metric.unit }}")) + } + + /// Adds an additional value to the up-down-counter. + pub fn add( + &self, + value: T, + {% if required_attributes %}required_attributes: &{{ metric.metric_name | pascal_case }}ReqAttributes,{% endif %} + {% if not_required_attributes %}not_required_attributes: Option<&{{ metric.metric_name | pascal_case }}OptAttributes>,{% endif %} + ) { + {{ attribute_macros.attributes_to_key_values(required_attributes, not_required_attributes) }} + self.0.add(value, &attributes); + } +} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/metrics/metrics.rs.j2 b/crates/weaver_codegen_test/templates/registry/rust/metrics/metrics.rs.j2 new file mode 100644 index 00000000..0b23820a --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/metrics/metrics.rs.j2 @@ -0,0 +1,13 @@ +{%- set file_name = ctx.prefix | snake_case -%} +{{- template.set_file_name("metrics/" ~ file_name ~ ".rs") -}} + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +//! DO NOT EDIT, THIS FILE HAS BEEN GENERATED BY WEAVER + +{%- for metric in ctx.groups %} +{% include "metrics/instruments/" ~ metric.instrument ~ ".j2" %} +{%- endfor %} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/metrics/mod.rs.j2 b/crates/weaver_codegen_test/templates/registry/rust/metrics/mod.rs.j2 new file mode 100644 index 00000000..eec7f03f --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/metrics/mod.rs.j2 @@ -0,0 +1,137 @@ +{{- template.set_file_name("metrics/mod.rs") -}} + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +//! OpenTelemetry Semantic Convention Metrics +//! DO NOT EDIT, THIS FILE HAS BEEN GENERATED BY WEAVER + +{% for group in ctx %} +/// Metrics for the `{{ group.id | metric_namespace }}` namespace. +pub mod {{ group.id | metric_namespace | snake_case }}; +{%- endfor %} + +/// A trait implemented by histogram providers (e.g. `Meter`). +pub trait HistogramProvider { + /// Creates a new histogram with the given name, description, and unit. + fn create_histogram(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Histogram; +} + +/// This implementation specifies that a Meter is able to create u64 histograms. +impl HistogramProvider for opentelemetry::metrics::Meter { + /// Creates a new u64 histogram with the given name, description, and unit. + fn create_histogram(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Histogram { + self.u64_histogram(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// This implementation specifies that a Meter is able to create u64 histograms. +impl HistogramProvider for opentelemetry::metrics::Meter { + /// Creates a new f64 histogram with the given name, description, and unit. + fn create_histogram(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Histogram { + self.f64_histogram(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// A trait implemented by up-down-counter providers (e.g. `Meter`). +pub trait UpDownCounterProvider { + /// Creates a new up-down-counter with the given name, description, and unit. + fn create_up_down_counter(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::UpDownCounter; +} + +/// This implementation specifies that a Meter is able to create i64 up-down-counters. +impl UpDownCounterProvider for opentelemetry::metrics::Meter { + /// Creates a new i64 up-down-counter with the given name, description, and unit. + fn create_up_down_counter(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::UpDownCounter { + self.i64_up_down_counter(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// This implementation specifies that a Meter is able to create f64 up-down-counters. +impl UpDownCounterProvider for opentelemetry::metrics::Meter { + /// Creates a new f64 up-down-counter with the given name, description, and unit. + fn create_up_down_counter(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::UpDownCounter { + self.f64_up_down_counter(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// A trait implemented by counter providers (e.g. `Meter`). +pub trait CounterProvider { + /// Creates a new counter with the given name, description, and unit. + fn create_counter(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Counter; +} + +/// This implementation specifies that a Meter is able to create u64 counters. +impl CounterProvider for opentelemetry::metrics::Meter { + /// Creates a new u64 counter with the given name, description, and unit. + fn create_counter(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Counter { + self.u64_counter(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// This implementation specifies that a Meter is able to create f64 counters. +impl CounterProvider for opentelemetry::metrics::Meter { + /// Creates a new f64 counter with the given name, description, and unit. + fn create_counter(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Counter { + self.f64_counter(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// A trait implemented by gauge providers (e.g. `Meter`). +pub trait GaugeProvider { + /// Creates a new gauge with the given name, description, and unit. + fn create_gauge(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Gauge; +} + +/// This implementation specifies that a Meter is able to create u64 gauges. +impl GaugeProvider for opentelemetry::metrics::Meter { + /// Creates a new u64 gauge with the given name, description, and unit. + fn create_gauge(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Gauge { + self.u64_gauge(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// This implementation specifies that a Meter is able to create i64 gauges. +impl GaugeProvider for opentelemetry::metrics::Meter { + /// Creates a new i64 gauge with the given name, description, and unit. + fn create_gauge(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Gauge { + self.i64_gauge(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} + +/// This implementation specifies that a Meter is able to create f64 gauges. +impl GaugeProvider for opentelemetry::metrics::Meter { + /// Creates a new f64 gauge with the given name, description, and unit. + fn create_gauge(&self, name: &'static str, description: &'static str, unit: &'static str) -> opentelemetry::metrics::Gauge { + self.f64_gauge(name) + .with_description(description) + .with_unit(opentelemetry::metrics::Unit::new(unit)) + .init() + } +} \ No newline at end of file diff --git a/crates/weaver_codegen_test/templates/registry/rust/weaver.yaml b/crates/weaver_codegen_test/templates/registry/rust/weaver.yaml new file mode 100644 index 00000000..eeb0fc6b --- /dev/null +++ b/crates/weaver_codegen_test/templates/registry/rust/weaver.yaml @@ -0,0 +1,156 @@ +type_mapping: + int: i64 + double: f64 + boolean: bool + string: String + string[]: Vec + template[string]: String # Not yet properly handled in codegen + template[string[]]: Vec # Not yet properly handled in codegen + +templates: + - pattern: README.md + filter: . + application_mode: single + - pattern: lib.rs + filter: . + application_mode: single + + # Templates for the `attribute_group` group type + - pattern: attributes/mod.rs.j2 + # The following JQ filter extracts the id, type, brief, and prefix of groups matching the following criteria: + # - groups with an id starting with the prefix `registry.` + # - groups of the type `attribute_group`. + # - groups are deduplicated by namespace. + # - groups are sorted by namespace. + filter: > + .groups + | map(select(.id | startswith("registry."))) + | map(select(.type == "attribute_group") + | { + id, + type, + brief, + prefix}) + | unique_by(.id | split(".") | .[1]) + | sort_by(.id | split(".") | .[1]) + application_mode: single + - pattern: attributes/attributes.rs.j2 + # The following JQ filter extracts the id, type, brief, prefix, and attributes of groups matching the following + # criteria: + # - groups with an id starting with the prefix `registry.` + # - groups of the type `attribute_group`. + # - groups are sorted by namespace. + filter: > + .groups + | map(select(.id | startswith("registry."))) + | map(select(.type == "attribute_group") + | { + id, + type, + brief, + prefix, + attributes}) + | group_by(.id | split(".") | .[1]) + | map({ + id: (map(select(.id | endswith(".deprecated") | not)) | first).id, + type: (map(select(.id | endswith(".deprecated") | not)) | first).type, + brief: (map(select(.id | endswith(".deprecated") | not)) | first).brief, + prefix: (map(select(.id | endswith(".deprecated") | not)) | first).prefix, + attributes: map(.attributes) | add + }) + | sort_by(.id | split(".") | .[1]) + application_mode: each + + # Templates for the `metric` group type + - pattern: metrics/mod.rs.j2 + # The following JQ filter extracts the id, type, brief, and prefix of groups matching the following criteria: + # - groups with an id starting with the prefix `metric.` + # - groups of the type `metric`. + # - groups are deduplicated by namespace. + # - groups are sorted by prefix. + filter: > + .groups + | map(select(.id | startswith("metric."))) + | map(select(.type == "metric") + | { + id, + type, + brief, + prefix}) + | unique_by(.id | split(".") | .[1]) + | sort_by(.id | split(".") | .[1]) + application_mode: single + - pattern: metrics/metrics.rs.j2 + # The following JQ filter extracts the id, type, brief, prefix, and attributes of groups matching the following + # criteria: + # - groups with an id starting with the prefix `metric.` + # - groups of the type `metric`. + # - groups are sorted by namespace. + filter: > + .groups + | map(select(.id | startswith("metric."))) + | group_by(.id | split(".") | .[1]) + | map({ + prefix: .[0].id | split(".") | .[1], + groups: . + }) + application_mode: each + + +# .groups +# | map(select(.type == "attribute_group")) +# | map(select(.id | startswith("registry"))) +# | group_by(.id | split(".") | .[1]) +# | map({id: .[0].id | split(".") | .[1], groups: .}) + +# Other examples of filters + +# The following JQ filter extracts the id, type, brief, and prefix of groups matching the following criteria: +# - groups with an id starting with the prefix `registry.` +# - groups of the type `attribute_group`. +# - groups with a well-defined prefix. +# - groups with a non-empty list of attributes that are neither deprecated nor experimental. +# - groups are deduplicated by prefix. +# - groups are sorted by prefix. +# filter: > +# .groups +# | map(select(.id | startswith("registry."))) +# | map(select(.type == "attribute_group" and .prefix != null and .prefix != "") +# | { +# id, +# type, +# brief, +# prefix, +# attributes: (.attributes +# | map(select(.stability == "experimental" and .deprecated | not)))}) +# | map(select(.attributes | length > 0)) +# | map( +# { +# id, +# type, +# brief, +# prefix +# } +# ) +# | unique_by(.prefix) +# | sort_by(.prefix) + + +# The following JQ filter extracts the id, type, brief, prefix, and attributes of groups matching the following +# criteria: +# - groups with an id starting with the prefix `registry.` +# - groups of the type `attribute_group`. +# - groups with a well-defined prefix. +# - groups with a non-empty list of attributes that are neither deprecated nor experimental. +# - groups are sorted by prefix. +# filter: > +# .groups +# | map(select(.id | startswith("registry."))) +# | map(select(.type == "attribute_group" and .prefix != null and .prefix != "") +# | { +# id, +# type, +# brief, +# prefix, +# attributes: (.attributes | map(select(.stability == "experimental" and .deprecated | not)))}) +# | sort_by(.prefix // empty) \ No newline at end of file diff --git a/crates/weaver_codegen_test/tests/codegen.rs b/crates/weaver_codegen_test/tests/codegen.rs new file mode 100644 index 00000000..daa62be2 --- /dev/null +++ b/crates/weaver_codegen_test/tests/codegen.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! This integration test aims to validate the code generation from a semantic convention registry. +//! By using an integration test, we confirm that the interface of the generated code is public. We +//! also verify that the entirety of the generated code is compilable and exposes the expected +//! constants, structs, enums, and functions. + +// Include the generated code +include!(concat!(env!("OUT_DIR"), "/generated.rs")); + +use crate::attributes::client; +use crate::attributes::http::HttpRequestMethod; +use crate::attributes::http::HTTP_REQUEST_METHOD; +use crate::attributes::system::SystemCpuState; +use crate::metrics::http::create_http_client_request_duration; +use crate::metrics::http::HttpClientActiveRequests; +use crate::metrics::http::HttpClientActiveRequestsReqAttributes; +use crate::metrics::http::HttpServerRequestDuration; +use crate::metrics::http::HttpServerRequestDurationOptAttributes; +use crate::metrics::http::HttpServerRequestDurationReqAttributes; +use crate::metrics::system::SystemCpuTime; +use crate::metrics::system::SystemCpuTimeOptAttributes; +use crate::metrics::system::SystemCpuUtilization; +use crate::metrics::system::SystemCpuUtilizationOptAttributes; +use opentelemetry::metrics::Histogram; +use opentelemetry::{global, KeyValue}; + +#[test] +fn test_codegen() { + // Test the constants generated for the attributes + // In the generated API the attributes are typed, so the compiler will catch type errors + assert_eq!(client::CLIENT_ADDRESS.key().as_str(), "client.address"); + assert_eq!( + client::CLIENT_ADDRESS.value("145.34.23.56".into()), + KeyValue::new("client.address", "145.34.23.56") + ); + assert_eq!(client::CLIENT_PORT.key().as_str(), "client.port"); + assert_eq!( + client::CLIENT_PORT.value(8080), + KeyValue::new("client.port", 8080) + ); + + // Enum values are also generated + assert_eq!(HttpRequestMethod::Connect.as_str(), "CONNECT"); + assert_eq!(HttpRequestMethod::Delete.as_str(), "DELETE"); + assert_eq!( + HttpRequestMethod::_Custom("UNKNOWN_METHOD".to_owned()).as_str(), + "UNKNOWN_METHOD" + ); + + // Create an OpenTelemetry meter + let meter = global::meter("my_meter"); + + // Create a u64 http.client.request.duration metric and record a data point. + // This is the low-level API, where: + // - the required attributes are not enforced by the compiler. + // - the attributes provided are not checked for correctness by the compiler (i.e. the + // attributes specified in the original semantic convention + let http_client_request_duration: Histogram = create_http_client_request_duration(&meter); + http_client_request_duration.record( + 100, + &[HTTP_REQUEST_METHOD.value(&HttpRequestMethod::Connect)], + ); + + // Create a f64 http.client.request.duration metric and record a data point. + let http_client_request_duration: Histogram = create_http_client_request_duration(&meter); + http_client_request_duration.record( + 100.0, + &[HTTP_REQUEST_METHOD.value(&HttpRequestMethod::Connect)], + ); + + // ==== A TYPE-SAFE HISTOGRAM API ==== + // Create a u64 http.server.request.duration metric (as defined in the OpenTelemetry HTTP + // semantic conventions). + // The API is type-safe, so the compiler will catch type errors. The required attributes are + // enforced by the compiler. All the attributes provided are checked for correctness by the + // compiler in relation to the original semantic convention. + let http_request_duration = HttpServerRequestDuration::::new(&meter); + // Records a new data point and provide the required and some optional attributes + http_request_duration.record( + 100, + &HttpServerRequestDurationReqAttributes { + http_request_method: HttpRequestMethod::Connect, + url_scheme: "http".to_owned(), + }, + Some(&HttpServerRequestDurationOptAttributes { + http_response_status_code: Some(200), + ..Default::default() + }), + ); + + // ==== A TYPE-SAFE UP-DOWN-COUNTER API ==== + // Create a f64 http.server.request.duration metric (as defined in the OpenTelemetry HTTP + // semantic conventions) + let http_client_active_requests = HttpClientActiveRequests::::new(&meter); + // Adds a new data point and provide the required attributes. Optional attributes are not + // provided in this example. + http_client_active_requests.add( + 10.0, + &HttpClientActiveRequestsReqAttributes { + server_address: "10.0.0.1".to_owned(), + server_port: 8080, + }, + None, + ); + + // ==== A TYPE-SAFE COUNTER API ==== + // Create a f64 system.cpu.time metric (as defined in the OpenTelemetry System semantic + // conventions) + let system_cpu_time = SystemCpuTime::::new(&meter); + // Adds a new data point and provide some optional attributes. + // Note: In the method signature, there is no required attribute. + system_cpu_time.add( + 10.0, + Some(&SystemCpuTimeOptAttributes { + system_cpu_logical_number: Some(0), + system_cpu_state: Some(SystemCpuState::Idle), + }), + ); + // Adds a new data point with a custom CPU state. + system_cpu_time.add( + 20.0, + Some(&SystemCpuTimeOptAttributes { + system_cpu_logical_number: Some(0), + system_cpu_state: Some(SystemCpuState::_Custom("custom".to_owned())), + }), + ); + + // ==== A TYPE-SAFE GAUGE API ==== + // Create a i64 system.cpu.utilization metric (as defined in the OpenTelemetry System semantic + // conventions) + let system_cpu_utilization = SystemCpuUtilization::::new(&meter); + // Adds a new data point with no optional attributes. + system_cpu_utilization.record(-5, None); + // Adds a new data point with some optional attributes. + system_cpu_utilization.record( + 10, + Some(&SystemCpuUtilizationOptAttributes { + system_cpu_logical_number: Some(0), + system_cpu_state: Some(SystemCpuState::Idle), + }), + ); +} diff --git a/crates/weaver_common/src/in_memory.rs b/crates/weaver_common/src/in_memory.rs new file mode 100644 index 00000000..19d8a6c7 --- /dev/null +++ b/crates/weaver_common/src/in_memory.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! In-memory logger implementation. +//! Can be used in tests and build.rs scripts. + +use std::sync::{Arc, Mutex}; + +/// An in-memory log message. +#[derive(Debug, Clone)] +pub enum LogMessage { + /// A trace message. + Trace(String), + /// An info message. + Info(String), + /// A warning message. + Warn(String), + /// An error message. + Error(String), + /// A success message. + Success(String), + /// A loading message. + Loading(String), + /// A log message. + Log(String), +} + +/// A logger that can be used in tests or build.rs scripts. +/// This logger is thread-safe and can be cloned. +#[derive(Default, Clone)] +pub struct Logger { + messages: Arc>>, + debug_level: u8, +} + +impl Logger { + /// Creates a new logger. + #[must_use] + pub fn new(debug_level: u8) -> Self { + Logger { + messages: Arc::new(Mutex::new(Vec::new())), + debug_level, + } + } + + /// Returns the number of warning messages logged. + #[must_use] + pub fn warn_count(&self) -> usize { + self.messages + .lock() + .expect("Failed to lock messages") + .iter() + .filter(|m| matches!(m, LogMessage::Warn(_))) + .count() + } + + /// Returns the number of error messages logged. + #[must_use] + pub fn error_count(&self) -> usize { + self.messages + .lock() + .expect("Failed to lock messages") + .iter() + .filter(|m| matches!(m, LogMessage::Error(_))) + .count() + } + + /// Returns the recorded log messages. + #[must_use] + pub fn messages(&self) -> Vec { + self.messages + .lock() + .expect("Failed to lock messages") + .clone() + } +} + +impl crate::Logger for Logger { + /// Logs a trace message (only with debug enabled). + fn trace(&self, message: &str) { + if self.debug_level > 0 { + self.messages + .lock() + .expect("Failed to lock messages") + .push(LogMessage::Trace(message.to_owned())); + } + } + + /// Logs an info message. + fn info(&self, message: &str) { + self.messages + .lock() + .expect("Failed to lock messages") + .push(LogMessage::Info(message.to_owned())); + } + + /// Logs a warning message. + fn warn(&self, message: &str) { + self.messages + .lock() + .expect("Failed to lock messages") + .push(LogMessage::Warn(message.to_owned())); + } + + /// Logs an error message. + fn error(&self, message: &str) { + self.messages + .lock() + .expect("Failed to lock messages") + .push(LogMessage::Error(message.to_owned())); + } + + /// Logs a success message. + fn success(&self, message: &str) { + self.messages + .lock() + .expect("Failed to lock messages") + .push(LogMessage::Success(message.to_owned())); + } + + /// Logs a newline. + fn newline(&self, _count: usize) {} + + /// Indents the logger. + fn indent(&self, _count: usize) {} + + /// Stops a loading message. + fn done(&self) {} + + /// 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.messages + .lock() + .expect("Failed to lock messages") + .push(LogMessage::Loading(message.to_owned())); + } + + /// 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.messages + .lock() + .expect("Failed to lock messages") + .push(LogMessage::Log(message.to_owned())); + } +} diff --git a/crates/weaver_common/src/lib.rs b/crates/weaver_common/src/lib.rs index 796c639f..1f6c1581 100644 --- a/crates/weaver_common/src/lib.rs +++ b/crates/weaver_common/src/lib.rs @@ -4,6 +4,7 @@ pub mod diag; pub mod error; +pub mod in_memory; pub mod quiet; use std::sync::atomic::AtomicUsize; diff --git a/crates/weaver_forge/Cargo.toml b/crates/weaver_forge/Cargo.toml index 5b2ada1d..fe9be384 100644 --- a/crates/weaver_forge/Cargo.toml +++ b/crates/weaver_forge/Cargo.toml @@ -36,3 +36,8 @@ serde_json.workspace = true rayon.workspace = true walkdir.workspace = true +[dev-dependencies] +opentelemetry = { version = "0.22.0", features = ["trace", "metrics", "logs", "otel_unstable"] } +opentelemetry_sdk = { version = "0.22.1", features = ["trace", "metrics", "logs"] } +opentelemetry-stdout = { version = "0.3.0", features = ["trace", "metrics", "logs"] } + diff --git a/crates/weaver_forge/src/debug.rs b/crates/weaver_forge/src/debug.rs index ae3fc590..0941b145 100644 --- a/crates/weaver_forge/src/debug.rs +++ b/crates/weaver_forge/src/debug.rs @@ -2,14 +2,34 @@ //! Utility functions to help with debugging. -use crate::error::Error; use crate::error::Error::{CompoundError, TemplateEvaluationFailed}; use indexmap::IndexMap; +use std::error::Error; use weaver_common::Logger; -/// Return a nice summary of the error. +/// Return a nice summary of the error including the chain of causes. +/// Only the last error in the chain is displayed with a full stack trace. pub(crate) fn error_summary(error: minijinja::Error) -> String { - format!("{:#}", error) + let mut errors = Vec::new(); + let mut curr_error: &dyn Error = &error; + + errors.push(curr_error); + + while let Some(e) = curr_error.source() { + errors.push(e); + curr_error = e; + } + + let mut error_msg = String::new(); + for (i, e) in errors.iter().enumerate() { + if i == errors.len() - 1 { + // Display the last error with all the referenced variables + error_msg.push_str(&format!("{:#}\n", e)); + } else { + error_msg.push_str(&format!("{}\nCaused by:\n", e)); + } + } + error_msg } /// Print deduplicated errors. @@ -26,7 +46,7 @@ pub(crate) fn error_summary(error: minijinja::Error) -> String { /// /// * `logger` - The logger to use for logging. /// * `error` - The error to print. -pub fn print_dedup_errors(logger: impl Logger + Sync + Clone, error: Error) { +pub fn print_dedup_errors(logger: impl Logger + Sync + Clone, error: crate::error::Error) { struct DedupError { pub error: String, pub occurrences: usize, diff --git a/crates/weaver_forge/src/extensions/otel.rs b/crates/weaver_forge/src/extensions/otel.rs index 6f5e107d..c27729f8 100644 --- a/crates/weaver_forge/src/extensions/otel.rs +++ b/crates/weaver_forge/src/extensions/otel.rs @@ -2,12 +2,11 @@ //! Set of filters, tests, and functions that are specific to the OpenTelemetry project. +use crate::config::CaseConvention; use itertools::Itertools; use minijinja::{ErrorKind, Value}; use serde::de::Error; -use crate::config::CaseConvention; - /// Filters the input value to only include the required "object". /// A required object is one that has a field named "requirement_level" with the value "required". /// An object that is "conditionally_required" is not returned by this filter. diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index c69e91a1..f8502460 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -4,13 +4,13 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; -use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use std::{fs, io}; use minijinja::syntax::SyntaxConfig; use minijinja::value::{from_args, Object}; -use minijinja::{path_loader, Environment, State, Value}; +use minijinja::{Environment, ErrorKind, State, Value}; use rayon::iter::IntoParallelIterator; use rayon::iter::ParallelIterator; use serde::Serialize; @@ -100,7 +100,7 @@ impl Object for TemplateObject { Ok(Value::from("")) } else { Err(minijinja::Error::new( - minijinja::ErrorKind::UnknownMethod, + ErrorKind::UnknownMethod, format!("template has no method named {name}"), )) } @@ -341,12 +341,18 @@ impl TemplateEngine { engine.add_global("template", Value::from_object(template_object.clone())); log.loading(&format!("Generating file {}", template_file)); - let template = engine - .get_template(template_file) - .map_err(|e| InvalidTemplateFile { - template: template_path.to_path_buf(), - error: e.to_string(), - })?; + + let template = engine.get_template(template_file).map_err(|e| { + let templates = engine + .templates() + .map(|(name, _)| name.to_owned()) + .collect::>(); + let error = format!("{}. Available templates: {:?}", e, templates); + InvalidTemplateFile { + template: template_file.into(), + error, + } + })?; let output = template .render(ctx.clone()) @@ -385,7 +391,7 @@ impl TemplateEngine { error: e.to_string(), })?; - env.set_loader(path_loader(&self.path)); + env.set_loader(cross_platform_loader(&self.path)); env.set_syntax(syntax); // Register code-oriented filters @@ -468,12 +474,6 @@ impl TemplateEngine { // env.add_filter("without_value", extensions::without_value); // env.add_filter("with_enum", extensions::with_enum); // env.add_filter("without_enum", extensions::without_enum); - // env.add_filter( - // "type_mapping", - // extensions::TypeMapping { - // type_mapping: target_config.type_mapping, - // }, - // ); Ok(env) } @@ -507,6 +507,59 @@ impl TemplateEngine { } } +// The template loader provided by MiniJinja is not cross-platform. +fn cross_platform_loader<'x, P: AsRef + 'x>( + dir: P, +) -> impl for<'a> Fn(&'a str) -> Result, minijinja::Error> + Send + Sync + 'static { + let dir = dir.as_ref().to_path_buf(); + move |name| { + let path = safe_join(&dir, name)?; + match fs::read_to_string(path) { + Ok(result) => Ok(Some(result)), + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(minijinja::Error::new( + ErrorKind::InvalidOperation, + "could not read template", + ) + .with_source(err)), + } + } +} + +// Combine a root path and a template name, ensuring that the combined path is +// a subdirectory of the base path. +fn safe_join(root: &Path, template: &str) -> Result { + let mut path = root.to_path_buf(); + path.push(template); + + // Canonicalize the paths to resolve any `..` or `.` components + let canonical_root = root.canonicalize().map_err(|e| { + minijinja::Error::new( + ErrorKind::InvalidOperation, + format!("Failed to canonicalize root path: {}", e), + ) + })?; + let canonical_combined = path.canonicalize().map_err(|e| { + minijinja::Error::new( + ErrorKind::InvalidOperation, + format!("Failed to canonicalize combined path: {}", e), + ) + })?; + + // Verify that the canonical combined path starts with the canonical root path + if canonical_combined.starts_with(&canonical_root) { + Ok(canonical_combined) + } else { + Err(minijinja::Error::new( + ErrorKind::InvalidOperation, + format!( + "The combined path is not a subdirectory of the root path: {:?} -> {:?}", + canonical_root, canonical_combined + ), + )) + } +} + // Helper filter to work around lack of `list.append()` support in minijinja. // Will take a list of lists and return a new list containing only elements of sublists. fn flatten(value: Value) -> Result { @@ -530,7 +583,7 @@ fn split_id(value: Value) -> Result, minijinja::Error> { Ok(values) } None => Err(minijinja::Error::new( - minijinja::ErrorKind::InvalidOperation, + ErrorKind::InvalidOperation, format!("Expected string, found: {value}"), )), } @@ -538,19 +591,19 @@ fn split_id(value: Value) -> Result, minijinja::Error> { #[cfg(test)] mod tests { - use globset::Glob; use std::collections::HashSet; use std::fs; use std::path::Path; + use globset::Glob; use walkdir::WalkDir; - use crate::config::{ApplicationMode, TemplateConfig}; use weaver_common::TestLogger; use weaver_diff::diff_output; use weaver_resolver::SchemaResolver; use weaver_semconv::registry::SemConvRegistry; + use crate::config::{ApplicationMode, TemplateConfig}; use crate::debug::print_dedup_errors; use crate::filter::Filter; use crate::registry::TemplateRegistry; diff --git a/docs/images/dependencies.svg b/docs/images/dependencies.svg index 36f9c51e..4dcdd6d6 100644 --- a/docs/images/dependencies.svg +++ b/docs/images/dependencies.svg @@ -35,8 +35,8 @@ 3 - -weaver_diff + +weaver_codegen_test @@ -44,105 +44,111 @@ weaver_forge - - -8 + + +9 weaver_resolver - + -4->8 +4->9 5 - -weaver_resolved_schema + +weaver_diff 6 + +weaver_resolved_schema + + + +7 weaver_semconv - + -5->6 +6->7 - - -7 + + +8 weaver_version - + -5->7 +6->8 - + -6->2 +7->2 - + -8->0 +9->0 - + -8->3 +9->5 - + -8->5 +9->6 - - -9 + + +10 weaver_semconv_gen - + -9->4 +10->4 - - -10 - -xtask - 11 + +xtask + + + +12 weaver - + -11->1 +12->1 - + -11->9 +12->10