diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf25ee0..048355b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ What's changed * Fixed bug in `check` if `--skip-policies` was specified then it would not fail for any validation errors. * Semantic Conventions Issue [#1513](https://github.com/open-telemetry/semantic-conventions/issues/1513) - Make span_kind required in yaml and break down multi-kind span definitions - ([#542](https://github.com/open-telemetry/weaver/pull/542) by @jerbly). * Updated the EBNF and JSON schema to define `span_kind` as mandatory for `span` group types. Added a group validity check as a warning. +* First iteration of the new command: `registry emit`. Emits a semantic convention registry as example spans to your OTLP receiver. This may be useful in testing/simulation scenarios. Defaults to `http://localhost:4317` if the OpenTelemetry standard `OTEL_EXPORTER_OTLP_ENDPOINT` env var is unset. ([#549](https://github.com/open-telemetry/weaver/pull/549) by @jerbly) + + ## [0.12.0] - 2024-12-09 diff --git a/Cargo.lock b/Cargo.lock index 8d660a38..62ca3f5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,28 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.85" @@ -215,6 +237,53 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -274,9 +343,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -736,9 +805,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "deflate" @@ -1894,13 +1963,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -2029,6 +2104,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2054,6 +2130,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -2260,6 +2349,16 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.7.0" @@ -2363,13 +2462,13 @@ dependencies = [ [[package]] name = "jaq-json" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf2b52304419d7bf5ec32891884c65274a3eedc0b5834b84627099901a1176" +checksum = "01d103d9e961e2d60c31c57b31869351ba3fa01717f2dde499eaf20da9cedd96" dependencies = [ "foldhash", "hifijson", - "indexmap", + "indexmap 2.7.0", "jaq-core", "jaq-std", "serde_json", @@ -2377,9 +2476,9 @@ dependencies = [ [[package]] name = "jaq-std" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e2c65cceafd4c0019f15a0dac7c0dd659b0fcf5182fc3a10d15b89d89ac6e8" +checksum = "df355eccf9f27755ebc5d0b220d4878b7017349b904acde126909155ba33fba1" dependencies = [ "aho-corasick", "base64 0.22.1", @@ -2578,6 +2677,12 @@ dependencies = [ "unicode-id", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "maybe-async" version = "0.2.10" @@ -2880,6 +2985,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-otlp" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" +dependencies = [ + "async-trait", + "futures-core", + "http", + "opentelemetry", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "thiserror 1.0.69", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + [[package]] name = "opentelemetry-stdout" version = "0.27.0" @@ -2913,6 +3049,8 @@ dependencies = [ "rand 0.8.5", "serde_json", "thiserror 1.0.69", + "tokio", + "tokio-stream", "tracing", ] @@ -2936,9 +3074,9 @@ dependencies = [ [[package]] name = "outref" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "owo-colors" @@ -3054,6 +3192,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3159,6 +3317,29 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3503,7 +3684,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-rustls", - "tower", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -3783,7 +3964,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.7.0", "itoa", "ryu", "serde", @@ -4237,11 +4418,25 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.26.1" @@ -4252,6 +4447,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -4292,13 +4498,63 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", "winnow", ] +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -4333,9 +4589,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.33" @@ -4688,9 +4956,14 @@ dependencies = [ "assert_cmd", "clap", "crossterm", + "futures-util", "include_dir", "itertools 0.13.0", "miette", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-stdout", + "opentelemetry_sdk", "ratatui", "rayon", "schemars", @@ -4699,6 +4972,7 @@ dependencies = [ "serde_yaml", "tempdir", "thiserror 2.0.11", + "tokio", "tui-textarea", "walkdir", "weaver_cache", @@ -4793,7 +5067,7 @@ dependencies = [ "dirs", "globset", "include_dir", - "indexmap", + "indexmap 2.7.0", "itertools 0.13.0", "jaq-core", "jaq-json", @@ -5245,7 +5519,7 @@ dependencies = [ "displaydoc", "flate2", "hmac", - "indexmap", + "indexmap 2.7.0", "lzma-rs", "memchr", "pbkdf2", diff --git a/Cargo.toml b/Cargo.toml index e7834d5e..c33a7e39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ publish = false resolver = "2" [package.metadata.dist] -dist=true +dist = true [package.metadata.wix] upgrade-guid = "C9F6BF20-4C32-4AF3-8550-79653C00886C" @@ -23,9 +23,7 @@ eula = false # Workspace definition ======================================================== [workspace] -members = [ - "crates/*", -] +members = ["crates/*"] [workspace.package] version = "0.12.0" @@ -39,7 +37,7 @@ rust-version = "1.81.0" [workspace.dependencies] serde = { version = "1.0.217", features = ["derive"] } serde_yaml = "0.9.34" -serde_json = { version = "1.0.135"} +serde_json = { version = "1.0.135" } thiserror = "2.0.9" url = "2.5.4" ureq = "2.12.1" @@ -56,8 +54,17 @@ tempdir = "0.3.7" schemars = "0.8.21" dirs = "6.0.0" once_cell = "1.20.2" -opentelemetry = { version = "0.27.1", features = ["trace", "metrics", "logs", "otel_unstable"] } +opentelemetry = { version = "0.27.1", features = [ + "trace", + "metrics", + "logs", + "otel_unstable", +] } +opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] } +opentelemetry-otlp = "0.27.0" +opentelemetry-stdout = { version = "0.27.0", features = ["trace"] } rouille = "3.6.2" +tokio = { version = "1.43.0", features = ["full"] } # Features definition ========================================================= [features] @@ -82,7 +89,7 @@ weaver_checker = { path = "crates/weaver_checker" } clap = { version = "4.5.24", features = ["derive"] } rayon = "1.10.0" -ratatui = { version = "0.29.0", features=["serde"] } +ratatui = { version = "0.29.0", features = ["serde"] } crossterm = { version = "0.28.1", features = ["serde"] } tui-textarea = "0.7.0" @@ -96,11 +103,17 @@ thiserror.workspace = true miette.workspace = true schemars.workspace = true itertools.workspace = true +opentelemetry.workspace = true +opentelemetry_sdk.workspace = true +opentelemetry-otlp.workspace = true +opentelemetry-stdout.workspace = true +tokio.workspace = true [dev-dependencies] weaver_diff = { path = "crates/weaver_diff" } tempdir.workspace = true assert_cmd = "2.0.16" +futures-util = { version = "0.3", default-features = false } [profile.release] lto = true @@ -214,7 +227,12 @@ ci = "github" # The installers to generate for each app installers = ["shell", "powershell", "msi"] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", +] # Which actions to run on pull requests pr-run-mode = "plan" # Whether to install an updater program diff --git a/data/emit/spans.yaml b/data/emit/spans.yaml new file mode 100644 index 00000000..31906f25 --- /dev/null +++ b/data/emit/spans.yaml @@ -0,0 +1,185 @@ +# This file defines a set of spans to test emission covering all possible attribute types and span kinds. +groups: + - id: test.comprehensive.attr + type: attribute_group + brief: > + A comprehensive set of test attributes exercising all possible types. + note: > + This group contains examples of every possible attribute type supported by the schema. + attributes: + # Test basic attribute types + - id: test.string + type: string + brief: "Test string attribute" + note: "Additional notes about the string attribute" + requirement_level: required + stability: stable + sampling_relevant: true + examples: ["value1", "value2"] + + - id: test.integer + type: int + brief: "Test integer attribute" + stability: stable + examples: [42, 123] + + - id: test.double + type: double + brief: "Test double attribute" + stability: stable + examples: [3.13, 2.718] + + - id: test.boolean + type: boolean + brief: "Test boolean attribute" + stability: stable + examples: [true, false] + + # Test all array types + - id: test.string_array + type: string[] + brief: "Test string array attribute" + stability: stable + examples: [["val1", "val2"], ["val3", "val4"]] + + - id: test.int_array + type: int[] + brief: "Test integer array attribute" + stability: stable + examples: [[1, 2], [3, 4]] + + - id: test.double_array + type: double[] + brief: "Test double array attribute" + stability: stable + examples: [[1.1, 2.2], [3.3, 4.4]] + + - id: test.boolean_array + type: boolean[] + brief: "Test boolean array attribute" + stability: stable + examples: [[true, false], [false, true]] + + # Test all template types + - id: test.template_string + type: template[string] + brief: "Test string template attribute" + stability: stable + examples: ["test.template_string.key=\"hello\""] + + # - id: test.template_int + # type: template[int] + # brief: "Test int template attribute" + # stability: stable + # examples: ["test.template_int.key=42"] + + # - id: test.template_double + # type: template[double] + # brief: "Test double template attribute" + # stability: stable + # examples: ["test.template_double.key=3.14"] + + # - id: test.template_boolean + # type: template[boolean] + # brief: "Test boolean template attribute" + # stability: stable + # examples: ["test.template_boolean.key=true"] + + # Test template array types + - id: test.template_string_array + type: template[string[]] + brief: "Test string array template attribute" + stability: stable + examples: ["test.template_string_array.key=[\"val1\",\"val2\"]"] + + # - id: test.template_int_array + # type: template[int[]] + # brief: "Test int array template attribute" + # stability: stable + # examples: ["test.template_int_array.key=[1,2,3]"] + + # - id: test.template_double_array + # type: template[double[]] + # brief: "Test double array template attribute" + # stability: stable + # examples: ["test.template_double_array.key=[1.1,2.2,3.3]"] + + # - id: test.template_boolean_array + # type: template[boolean[]] + # brief: "Test boolean array template attribute" + # stability: stable + # examples: ["test.template_boolean_array.key=[true,false,true]"] + + # Test enum type + - id: test.enum + brief: "Test enum attribute" + stability: stable + type: + members: + - id: value1 + value: "VALUE_1" + brief: "First enum value" + note: "Detailed description of first value" + stability: stable + + - id: value2 + value: "VALUE_2" + brief: "Second enum value" + stability: beta + + # Now define spans for each span_kind with attribute references + - id: test.comprehensive.client + type: span + stability: development + brief: "Test span with client span_kind" + span_kind: client + attributes: + - ref: test.string + - ref: test.string_array + - ref: test.int_array + - ref: test.double_array + - ref: test.boolean_array + - ref: test.integer + - ref: test.double + - ref: test.boolean + - ref: test.template_string + # - ref: test.template_int + # - ref: test.template_double + # - ref: test.template_boolean + - ref: test.template_string_array + # - ref: test.template_int_array + # - ref: test.template_double_array + # - ref: test.template_boolean_array + - ref: test.enum + + - id: test.comprehensive.server + type: span + stability: development + brief: "Test span with server span_kind" + span_kind: server + attributes: + - ref: test.string + + - id: test.comprehensive.producer + type: span + stability: development + brief: "Test span with producer span_kind" + span_kind: producer + attributes: + - ref: test.string + + - id: test.comprehensive.consumer + type: span + stability: development + brief: "Test span with consumer span_kind" + span_kind: consumer + attributes: + - ref: test.string + + - id: test.comprehensive.internal + type: span + stability: development + brief: "Test span with internal span_kind" + span_kind: internal + attributes: + - ref: test.string \ No newline at end of file diff --git a/src/registry/emit.rs b/src/registry/emit.rs new file mode 100644 index 00000000..58653bc8 --- /dev/null +++ b/src/registry/emit.rs @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Emit a semantic convention registry to an OTLP receiver. + +use clap::Args; + +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::Resource; +use weaver_common::diagnostic::DiagnosticMessages; +use weaver_common::Logger; +use weaver_forge::registry::ResolvedRegistry; +use weaver_resolved_schema::attribute::Attribute; +use weaver_semconv::attribute::{ + AttributeType, Examples, PrimitiveOrArrayTypeSpec, TemplateTypeSpec, +}; +use weaver_semconv::group::{GroupType, SpanKindSpec}; + +use crate::registry::{PolicyArgs, RegistryArgs}; +use crate::util::prepare_main_registry; +use crate::{DiagnosticArgs, ExitDirectives}; + +use opentelemetry::global; +use opentelemetry::trace::{SpanKind, TraceContextExt, TraceError, Tracer}; +use opentelemetry::{Array, KeyValue, Value}; +use opentelemetry_sdk::trace as sdktrace; + +/// Parameters for the `registry emit` sub-command +#[derive(Debug, Args)] +pub struct RegistryEmitArgs { + /// Parameters to specify the semantic convention registry + #[command(flatten)] + registry: RegistryArgs, + + /// Policy parameters + #[command(flatten)] + policy: PolicyArgs, + + /// Parameters to specify the diagnostic format. + #[command(flatten)] + pub diagnostic: DiagnosticArgs, + + /// Write the telemetry to standard output + #[arg(long)] + stdout: bool, +} + +// For the given attribute, return a name/value pair. +// Values are generated based on the attribute type and examples where possible. +fn get_attribute_name_value(attribute: &Attribute) -> KeyValue { + let name = attribute.name.clone(); + match &attribute.r#type { + AttributeType::PrimitiveOrArray(primitive_or_array) => { + let value = match primitive_or_array { + PrimitiveOrArrayTypeSpec::Boolean => Value::Bool(true), + PrimitiveOrArrayTypeSpec::Int => match &attribute.examples { + Some(Examples::Int(i)) => Value::I64(*i), + Some(Examples::Ints(ints)) => Value::I64(*ints.first().unwrap_or(&42)), + _ => Value::I64(42), + }, + PrimitiveOrArrayTypeSpec::Double => match &attribute.examples { + Some(Examples::Double(d)) => Value::F64(f64::from(*d)), + Some(Examples::Doubles(doubles)) => { + Value::F64(f64::from(*doubles.first().unwrap_or((&3.13).into()))) + } + _ => Value::F64(3.15), + }, + PrimitiveOrArrayTypeSpec::String => match &attribute.examples { + Some(Examples::String(s)) => Value::String(s.clone().into()), + Some(Examples::Strings(strings)) => Value::String( + strings + .first() + .unwrap_or(&"value".to_owned()) + .clone() + .into(), + ), + _ => Value::String("value".into()), + }, + PrimitiveOrArrayTypeSpec::Booleans => Value::Array(Array::Bool(vec![true, false])), + PrimitiveOrArrayTypeSpec::Ints => match &attribute.examples { + Some(Examples::Ints(ints)) => Value::Array(Array::I64(ints.to_vec())), + Some(Examples::ListOfInts(list_of_ints)) => Value::Array(Array::I64( + list_of_ints.first().unwrap_or(&vec![42, 43]).to_vec(), + )), + _ => Value::Array(Array::I64(vec![42, 43])), + }, + PrimitiveOrArrayTypeSpec::Doubles => match &attribute.examples { + Some(Examples::Doubles(doubles)) => { + Value::Array(Array::F64(doubles.iter().map(|d| f64::from(*d)).collect())) + } + Some(Examples::ListOfDoubles(list_of_doubles)) => Value::Array(Array::F64( + list_of_doubles + .first() + .unwrap_or(&vec![(3.13).into(), (3.15).into()]) + .iter() + .map(|d| f64::from(*d)) + .collect(), + )), + _ => Value::Array(Array::F64(vec![3.13, 3.15])), + }, + PrimitiveOrArrayTypeSpec::Strings => match &attribute.examples { + Some(Examples::Strings(strings)) => Value::Array(Array::String( + strings.iter().map(|s| s.clone().into()).collect(), + )), + Some(Examples::ListOfStrings(list_of_strings)) => Value::Array(Array::String( + list_of_strings + .first() + .unwrap_or(&vec!["value1".to_owned(), "value2".to_owned()]) + .iter() + .map(|s| s.clone().into()) + .collect(), + )), + _ => Value::Array(Array::String(vec!["value1".into(), "value2".into()])), + }, + }; + KeyValue::new(name, value) + } + AttributeType::Enum { members, .. } => { + KeyValue::new(name, Value::String(members[0].value.to_string().into())) + } + AttributeType::Template(template_type_spec) => { + // TODO Support examples when https://github.com/open-telemetry/semantic-conventions/issues/1740 is complete + let value = match template_type_spec { + TemplateTypeSpec::String => Value::String("template_value".into()), + TemplateTypeSpec::Int => Value::I64(42), + TemplateTypeSpec::Double => Value::F64(3.13), + TemplateTypeSpec::Boolean => Value::Bool(true), + TemplateTypeSpec::Strings => Value::Array(Array::String(vec![ + "template_value1".into(), + "template_value2".into(), + ])), + TemplateTypeSpec::Ints => Value::Array(Array::I64(vec![42, 43])), + TemplateTypeSpec::Doubles => Value::Array(Array::F64(vec![3.13, 3.15])), + TemplateTypeSpec::Booleans => Value::Array(Array::Bool(vec![true, false])), + }; + KeyValue::new(format!("{name}.key"), value) + } + } +} + +/// Convert the span kind to an OTLP span kind. +/// If the span kind is not specified, return `SpanKind::Internal`. +fn otel_span_kind(span_kind: Option<&SpanKindSpec>) -> SpanKind { + match span_kind { + Some(SpanKindSpec::Client) => SpanKind::Client, + Some(SpanKindSpec::Server) => SpanKind::Server, + Some(SpanKindSpec::Producer) => SpanKind::Producer, + Some(SpanKindSpec::Consumer) => SpanKind::Consumer, + Some(SpanKindSpec::Internal) | None => SpanKind::Internal, + } +} + +/// Initialise a grpc OTLP exporter, sends to by default http://localhost:4317 +/// but can be overridden with the standard OTEL_EXPORTER_OTLP_ENDPOINT env var. +fn init_tracer_provider() -> Result { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint("http://localhost:4317") + .build()?; + Ok(sdktrace::TracerProvider::builder() + .with_resource(Resource::new(vec![KeyValue::new("service.name", "weaver")])) // TODO meta semconv! + .with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio) + .build()) +} + +/// Initialise a stdout exporter for debug +fn init_stdout_tracer_provider() -> sdktrace::TracerProvider { + sdktrace::TracerProvider::builder() + .with_resource(Resource::new(vec![KeyValue::new("service.name", "weaver")])) // TODO meta semconv! + .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) + .build() +} + +/// Uses the global tracer_provider to emit a single trace for all the defined +/// spans in the registry +fn emit_trace_for_registry(registry: &ResolvedRegistry, registry_path: &str) { + let tracer = global::tracer("weaver"); + // Start a parent span here and use this context to create child spans + tracer.in_span("weaver.emit", |cx| { + let span = cx.span(); + span.set_attribute(KeyValue::new( + "weaver.registry_path", // TODO meta semconv! + registry_path.to_owned(), + )); + + // Emit each span to the OTLP receiver. + for group in registry.groups.iter() { + if group.r#type == GroupType::Span { + let _span = tracer + .span_builder(group.id.clone()) + .with_kind(otel_span_kind(group.span_kind.as_ref())) + .with_attributes(group.attributes.iter().map(get_attribute_name_value)) + .start_with_context(&tracer, &cx); + } + } + }); +} + +/// Emit all spans in the resolved registry. +pub(crate) fn command( + logger: impl Logger + Sync + Clone, + args: &RegistryEmitArgs, +) -> Result { + logger.log("Weaver Registry Emit"); + logger.loading(&format!("Resolving registry `{}`", args.registry.registry)); + + let mut diag_msgs = DiagnosticMessages::empty(); + + let (registry, _) = + prepare_main_registry(&args.registry, &args.policy, logger.clone(), &mut diag_msgs)?; + + logger.loading(&format!("Emitting registry `{}`", args.registry.registry)); + let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime"); + rt.block_on(async { + let tracer_provider = if args.stdout { + logger.mute(); + init_stdout_tracer_provider() + } else { + init_tracer_provider().expect("OTLP Tracer Provider must be created") + }; + let _ = global::set_tracer_provider(tracer_provider.clone()); + + emit_trace_for_registry(®istry, &args.registry.registry.to_string()); + + global::shutdown_tracer_provider(); + }); + logger.success(&format!("Emitted registry `{}`", args.registry.registry)); + + if !diag_msgs.is_empty() { + return Err(diag_msgs); + } + + Ok(ExitDirectives { + exit_code: 0, + quiet_mode: args.stdout, + }) +} + +#[cfg(test)] +mod tests { + use opentelemetry::{global, Array, Value}; + use opentelemetry_sdk::{trace as sdktrace, Resource}; + use weaver_common::diagnostic::DiagnosticMessages; + use weaver_common::TestLogger; + + use crate::cli::{Cli, Commands}; + use crate::registry::emit::RegistryEmitArgs; + use crate::registry::{ + PolicyArgs, RegistryArgs, RegistryCommand, RegistryPath, RegistrySubCommand, + }; + use crate::run_command; + use opentelemetry::KeyValue; + + use futures_util::future::BoxFuture; + use opentelemetry::trace::{SpanKind, TraceError}; + use opentelemetry_sdk::export::{self, trace::ExportResult}; + use std::sync::{atomic, Arc, Mutex}; + + #[test] + fn test_registry_emit() { + let logger = TestLogger::new(); + + let cli = Cli { + debug: 1, + quiet: false, + future: false, + command: Some(Commands::Registry(RegistryCommand { + command: RegistrySubCommand::Emit(RegistryEmitArgs { + registry: RegistryArgs { + registry: RegistryPath::LocalFolder { + path: "data/emit/".to_owned(), + }, + follow_symlinks: false, + }, + policy: PolicyArgs { + policies: vec![], + skip_policies: true, + display_policy_coverage: false, + }, + diagnostic: Default::default(), + stdout: true, + }), + })), + }; + + let exit_directive = run_command(&cli, logger.clone()); + // The command should succeed. + assert_eq!(exit_directive.exit_code, 0); + } + + #[derive(Debug)] + pub struct SpanExporter { + resource: Resource, + is_shutdown: atomic::AtomicBool, + spans: Arc>>, + } + + impl export::trace::SpanExporter for SpanExporter { + fn export( + &mut self, + batch: Vec, + ) -> BoxFuture<'static, ExportResult> { + if self.is_shutdown.load(atomic::Ordering::SeqCst) { + Box::pin(std::future::ready(Err(TraceError::from( + "exporter is shut down", + )))) + } else { + self.spans.lock().unwrap().extend(batch); + Box::pin(std::future::ready(Ok(()))) + } + } + + fn shutdown(&mut self) { + self.is_shutdown.store(true, atomic::Ordering::SeqCst); + } + + fn set_resource(&mut self, res: &Resource) { + self.resource = res.clone(); + } + } + + #[test] + fn test_emit_trace_for_registry() { + let arg_registry = RegistryArgs { + registry: RegistryPath::LocalFolder { + path: "data/emit/".to_owned(), + }, + follow_symlinks: false, + }; + let arg_policy = PolicyArgs { + policies: vec![], + skip_policies: true, + display_policy_coverage: false, + }; + + let logger = TestLogger::new(); + let mut diag_msgs = DiagnosticMessages::empty(); + + let spans = Arc::new(Mutex::new(Vec::new())); + let span_exporter = SpanExporter { + resource: Resource::empty(), + is_shutdown: atomic::AtomicBool::new(false), + spans: spans.clone(), + }; + let tracer_provider = sdktrace::TracerProvider::builder() + .with_resource(Resource::new(vec![KeyValue::new("service.name", "weaver")])) + .with_simple_exporter(span_exporter) + .build(); + + let _ = global::set_tracer_provider(tracer_provider.clone()); + + let (registry, _) = super::prepare_main_registry( + &arg_registry, + &arg_policy, + logger.clone(), + &mut diag_msgs, + ) + .expect("Test registry must be prepared"); + + super::emit_trace_for_registry(®istry, &arg_registry.registry.to_string()); + + global::shutdown_tracer_provider(); + + // Now check the spans stored in the span exporter + assert_eq!(spans.lock().unwrap().len(), 6); + + let expected = vec![ + ( + "test.comprehensive.client", + SpanKind::Client, + vec![ + KeyValue::new("test.string", "value1".to_owned()), + KeyValue::new("test.integer", Value::I64(42)), + KeyValue::new("test.double", Value::F64(3.13)), + KeyValue::new("test.boolean", Value::Bool(true)), + KeyValue::new( + "test.string_array", + Value::Array(Array::String(vec!["val1".into(), "val2".into()])), + ), + KeyValue::new("test.int_array", Value::Array(Array::I64(vec![1, 2]))), + KeyValue::new( + "test.double_array", + Value::Array(Array::F64(vec![1.1, 2.2])), + ), + KeyValue::new( + "test.boolean_array", + Value::Array(Array::Bool(vec![true, false])), + ), + KeyValue::new( + "test.template_string.key", + Value::String("template_value".into()), + ), + KeyValue::new( + "test.template_string_array.key", + Value::Array(Array::String(vec![ + "template_value1".into(), + "template_value2".into(), + ])), + ), + KeyValue::new("test.enum", Value::String("VALUE_1".into())), + ], + ), + ( + "test.comprehensive.server", + SpanKind::Server, + vec![KeyValue::new("test.string", Value::String("value1".into()))], + ), + ( + "test.comprehensive.producer", + SpanKind::Producer, + vec![KeyValue::new("test.string", Value::String("value1".into()))], + ), + ( + "test.comprehensive.consumer", + SpanKind::Consumer, + vec![KeyValue::new("test.string", Value::String("value1".into()))], + ), + ( + "test.comprehensive.internal", + SpanKind::Internal, + vec![KeyValue::new("test.string", Value::String("value1".into()))], + ), + ( + "weaver.emit", + SpanKind::Internal, + vec![KeyValue::new( + "weaver.registry_path", + Value::String("data/emit/".into()), + )], + ), + ]; + for (i, span_data) in spans.lock().unwrap().iter().enumerate() { + assert_eq!(span_data.name, expected[i].0); + assert_eq!(span_data.span_kind, expected[i].1); + for (j, attr) in span_data.attributes.iter().enumerate() { + assert_eq!(attr.key, expected[i].2[j].key); + assert_eq!(attr.value, expected[i].2[j].value); + } + } + } +} diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 50ad8257..b6bd7943 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use clap::{Args, Subcommand}; +use emit::RegistryEmitArgs; use miette::Diagnostic; use serde::Serialize; @@ -21,6 +22,7 @@ use weaver_common::diagnostic::{DiagnosticMessage, DiagnosticMessages}; use weaver_common::Logger; mod check; +mod emit; mod generate; mod json_schema; mod resolve; @@ -101,6 +103,12 @@ pub enum RegistrySubCommand { /// The produced JSON Schema can be used to generate documentation of the resolved registry format or to generate code in your language of choice if you need to interact with the resolved registry format for any reason. #[clap(verbatim_doc_comment)] JsonSchema(RegistryJsonSchemaArgs), + /// Emits a semantic convention registry as example spans to your OTLP receiver. + /// + /// This uses the standard OpenTelemetry SDK, defaulting to OTLP gRPC on localhost:4317. + /// Use the standard env vars e.g. `OTEL_EXPORTER_OTLP_ENDPOINT` to override. + #[clap(verbatim_doc_comment)] + Emit(RegistryEmitArgs), } /// Set of parameters used to specify a semantic convention registry. @@ -171,5 +179,9 @@ pub fn semconv_registry(log: impl Logger + Sync + Clone, command: &RegistryComma json_schema::command(log.clone(), args), Some(args.diagnostic.clone()), ), + RegistrySubCommand::Emit(args) => CmdResult::new( + emit::command(log.clone(), args), + Some(args.diagnostic.clone()), + ), } }