From 930156b8d46ac58d43dc579f5a659a2192d1be4b Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Fri, 22 Mar 2024 12:22:48 -0600 Subject: [PATCH 01/16] feat(wasi-observe): A WASI Observe host component Signed-off-by: Caleb Schoepp --- Cargo.lock | 196 +++++- Cargo.toml | 1 + crates/factor-key-value/Cargo.toml | 1 + crates/factor-key-value/src/host.rs | 24 +- crates/factor-key-value/src/lib.rs | 8 +- crates/factor-llm/Cargo.toml | 1 + crates/factor-llm/src/host.rs | 4 + crates/factor-llm/src/lib.rs | 6 +- crates/factor-observe/Cargo.toml | 23 + crates/factor-observe/src/host.rs | 244 +++++++ crates/factor-observe/src/lib.rs | 177 +++++ crates/factor-outbound-http/Cargo.toml | 1 + crates/factor-outbound-http/src/lib.rs | 4 + crates/factor-outbound-http/src/spin.rs | 2 + crates/factor-outbound-http/src/wasi.rs | 2 + crates/factor-outbound-mqtt/Cargo.toml | 1 + crates/factor-outbound-mqtt/src/host.rs | 13 +- crates/factor-outbound-mqtt/src/lib.rs | 4 + crates/factor-outbound-mysql/Cargo.toml | 1 + crates/factor-outbound-mysql/src/host.rs | 3 + crates/factor-outbound-mysql/src/lib.rs | 5 + crates/factor-outbound-pg/Cargo.toml | 1 + crates/factor-outbound-pg/src/host.rs | 4 + crates/factor-outbound-pg/src/lib.rs | 5 + crates/factor-outbound-redis/Cargo.toml | 1 + crates/factor-outbound-redis/src/host.rs | 23 +- crates/factor-outbound-redis/src/lib.rs | 4 + crates/factor-sqlite/Cargo.toml | 1 + crates/factor-sqlite/src/host.rs | 8 + crates/factor-sqlite/src/lib.rs | 5 +- crates/factor-variables/Cargo.toml | 1 + crates/factor-variables/src/host.rs | 1 + crates/factor-variables/src/lib.rs | 6 +- crates/runtime-config/Cargo.toml | 1 + crates/runtime-config/src/lib.rs | 7 + crates/runtime-factors/Cargo.toml | 1 + crates/runtime-factors/src/lib.rs | 3 + crates/trigger-http/Cargo.toml | 1 + crates/trigger-http/src/wasi.rs | 12 +- crates/world/Cargo.toml | 2 + crates/world/src/conversions.rs | 176 +++++ tests/integration.rs | 644 ++++++++++++++++-- tests/test-components/components/Cargo.lock | 30 + .../components/otel-smoke-test/Cargo.toml | 12 + .../components/otel-smoke-test/src/lib.rs | 22 + .../wasi-observe-tracing/Cargo.toml | 13 + .../wasi-observe-tracing/src/lib.rs | 147 ++++ tests/testcases/otel-smoke-test/spin.toml | 11 +- .../testcases/wasi-observe-tracing/spin.toml | 19 + wit/deps/observe/tracer.wit | 151 ++++ wit/deps/observe/world.wit | 8 + wit/world.wit | 1 + 52 files changed, 1914 insertions(+), 128 deletions(-) create mode 100644 crates/factor-observe/Cargo.toml create mode 100644 crates/factor-observe/src/host.rs create mode 100644 crates/factor-observe/src/lib.rs create mode 100644 tests/test-components/components/otel-smoke-test/Cargo.toml create mode 100644 tests/test-components/components/otel-smoke-test/src/lib.rs create mode 100644 tests/test-components/components/wasi-observe-tracing/Cargo.toml create mode 100644 tests/test-components/components/wasi-observe-tracing/src/lib.rs create mode 100644 tests/testcases/wasi-observe-tracing/spin.toml create mode 100644 wit/deps/observe/tracer.wit create mode 100644 wit/deps/observe/world.wit diff --git a/Cargo.lock b/Cargo.lock index 751221affc..cafe919b36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1418,7 +1418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" dependencies = [ "serde", - "toml", + "toml 0.8.19", ] [[package]] @@ -1692,7 +1692,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml", + "toml 0.8.19", "yaml-rust2", ] @@ -2636,6 +2636,25 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "fake-opentelemetry-collector" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2711530ca4a8dd909e447cf61f299d4492ec5a046d5982e1c06694c8953756" +dependencies = [ + "futures", + "hex", + "opentelemetry 0.25.0", + "opentelemetry-otlp 0.25.0", + "opentelemetry-proto 0.25.0", + "opentelemetry_sdk 0.25.0", + "serde", + "tokio", + "tokio-stream", + "tonic", + "tracing", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -5465,6 +5484,20 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803801d3d3b71cd026851a53f974ea03df3d179cb758b260136a6c9e22e196af" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror 1.0.69", +] + [[package]] name = "opentelemetry" version = "0.27.0" @@ -5485,7 +5518,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5feffc321035ad94088a7e5333abb4d84a8726e54a802e736ce9dd7237e85b" dependencies = [ - "opentelemetry", + "opentelemetry 0.27.0", "tracing", "tracing-core", "tracing-subscriber", @@ -5500,10 +5533,28 @@ dependencies = [ "async-trait", "bytes", "http 1.1.0", - "opentelemetry", + "opentelemetry 0.27.0", "reqwest 0.12.9", ] +[[package]] +name = "opentelemetry-otlp" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "596b1719b3cab83addb20bcbffdf21575279d9436d9ccccfe651a3bf0ab5ab06" +dependencies = [ + "async-trait", + "futures-core", + "http 1.1.0", + "opentelemetry 0.25.0", + "opentelemetry-proto 0.25.0", + "opentelemetry_sdk 0.25.0", + "prost 0.13.3", + "thiserror 1.0.69", + "tokio", + "tonic", +] + [[package]] name = "opentelemetry-otlp" version = "0.27.0" @@ -5513,10 +5564,10 @@ dependencies = [ "async-trait", "futures-core", "http 1.1.0", - "opentelemetry", + "opentelemetry 0.27.0", "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", + "opentelemetry-proto 0.27.0", + "opentelemetry_sdk 0.27.0", "prost 0.13.3", "reqwest 0.12.9", "thiserror 1.0.69", @@ -5525,18 +5576,52 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-proto" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c43620e8f93359eb7e627a3b16ee92d8585774986f24f2ab010817426c5ce61" +dependencies = [ + "opentelemetry 0.25.0", + "opentelemetry_sdk 0.25.0", + "prost 0.13.3", + "tonic", +] + [[package]] name = "opentelemetry-proto" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" dependencies = [ - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.27.0", + "opentelemetry_sdk 0.27.0", "prost 0.13.3", "tonic", ] +[[package]] +name = "opentelemetry_sdk" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0da0d6b47a3dbc6e9c9e36a0520e25cf943e046843818faaa3f87365a548c82" +dependencies = [ + "async-std", + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry 0.25.0", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", +] + [[package]] name = "opentelemetry_sdk" version = "0.27.0" @@ -5549,7 +5634,7 @@ dependencies = [ "futures-util", "glob", "once_cell", - "opentelemetry", + "opentelemetry 0.27.0", "percent-encoding", "rand 0.8.5", "serde_json", @@ -7587,7 +7672,7 @@ dependencies = [ "spin-factors-test", "spin-locked-app", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -7601,7 +7686,7 @@ dependencies = [ "subprocess", "terminal", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -7620,6 +7705,7 @@ dependencies = [ "conformance-tests", "ctrlc", "dialoguer", + "fake-opentelemetry-collector", "futures", "hex", "http 1.1.0", @@ -7667,7 +7753,7 @@ dependencies = [ "test-environment", "testing-framework", "tokio", - "toml", + "toml 0.8.19", "tracing", "url", "uuid", @@ -7702,7 +7788,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", - "toml", + "toml 0.8.19", "tracing", "wasm-encoder 0.217.0", "wasm-metadata 0.217.0", @@ -7764,7 +7850,7 @@ dependencies = [ "tempfile", "terminal", "tokio", - "toml", + "toml 0.8.19", "toml_edit 0.22.22", "tracing", "ui-testing", @@ -7780,7 +7866,7 @@ dependencies = [ "spin-locked-app", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -7790,6 +7876,7 @@ dependencies = [ "anyhow", "serde", "spin-core", + "spin-factor-observe", "spin-factors", "spin-factors-test", "spin-key-value-redis", @@ -7800,7 +7887,7 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", "tracing", ] @@ -7811,6 +7898,7 @@ dependencies = [ "anyhow", "async-trait", "serde", + "spin-factor-observe", "spin-factors", "spin-factors-test", "spin-llm-local", @@ -7819,11 +7907,28 @@ dependencies = [ "spin-telemetry", "spin-world", "tokio", - "toml", + "toml 0.8.19", "tracing", "url", ] +[[package]] +name = "spin-factor-observe" +version = "3.2.0-pre0" +dependencies = [ + "anyhow", + "indexmap 2.7.1", + "opentelemetry 0.27.0", + "opentelemetry_sdk 0.27.0", + "spin-core", + "spin-factors", + "spin-resource-table", + "spin-world", + "toml 0.5.11", + "tracing", + "tracing-opentelemetry", +] + [[package]] name = "spin-factor-outbound-http" version = "3.2.0-pre0" @@ -7835,6 +7940,7 @@ dependencies = [ "ip_network", "reqwest 0.12.9", "rustls 0.23.18", + "spin-factor-observe", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -7856,6 +7962,7 @@ dependencies = [ "anyhow", "rumqttc", "spin-core", + "spin-factor-observe", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -7873,6 +7980,7 @@ dependencies = [ "anyhow", "mysql_async", "spin-core", + "spin-factor-observe", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -7906,7 +8014,7 @@ dependencies = [ "spin-serde", "tempfile", "tokio", - "toml", + "toml 0.8.19", "tracing", "url", "urlencoding", @@ -7923,6 +8031,7 @@ dependencies = [ "native-tls", "postgres-native-tls", "spin-core", + "spin-factor-observe", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -7941,6 +8050,7 @@ dependencies = [ "anyhow", "redis 0.25.4", "spin-core", + "spin-factor-observe", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -7956,6 +8066,7 @@ name = "spin-factor-sqlite" version = "3.2.0-pre0" dependencies = [ "async-trait", + "spin-factor-observe", "spin-factors", "spin-factors-test", "spin-locked-app", @@ -7970,6 +8081,7 @@ name = "spin-factor-variables" version = "3.2.0-pre0" dependencies = [ "spin-expressions", + "spin-factor-observe", "spin-factors", "spin-factors-test", "spin-world", @@ -8000,7 +8112,7 @@ dependencies = [ "spin-app", "spin-factors-derive", "thiserror 1.0.69", - "toml", + "toml 0.8.19", "wasmtime", ] @@ -8035,7 +8147,7 @@ dependencies = [ "spin-factors", "spin-loader", "tempfile", - "toml", + "toml 0.8.19", ] [[package]] @@ -8051,7 +8163,7 @@ dependencies = [ "routefinder", "serde", "spin-app", - "toml", + "toml 0.8.19", "tracing", "wasmtime", "wasmtime-wasi-http", @@ -8166,7 +8278,7 @@ dependencies = [ "spin-serde", "tempfile", "tokio", - "toml", + "toml 0.8.19", "tracing", "ui-testing", "wasm-pkg-client", @@ -8197,7 +8309,7 @@ dependencies = [ "spin-serde", "terminal", "thiserror 1.0.69", - "toml", + "toml 0.8.19", "ui-testing", "url", "wasm-pkg-common", @@ -8272,6 +8384,7 @@ dependencies = [ "spin-common", "spin-factor-key-value", "spin-factor-llm", + "spin-factor-observe", "spin-factor-outbound-http", "spin-factor-outbound-mqtt", "spin-factor-outbound-mysql", @@ -8293,7 +8406,7 @@ dependencies = [ "spin-world", "tempfile", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -8305,6 +8418,7 @@ dependencies = [ "spin-common", "spin-factor-key-value", "spin-factor-llm", + "spin-factor-observe", "spin-factor-outbound-http", "spin-factor-outbound-mqtt", "spin-factor-outbound-mysql", @@ -8342,7 +8456,7 @@ dependencies = [ "spin-factors", "spin-sqlite-inproc", "spin-sqlite-libsql", - "toml", + "toml 0.8.19", ] [[package]] @@ -8376,10 +8490,10 @@ dependencies = [ "anyhow", "http 0.2.12", "http 1.1.0", - "opentelemetry", + "opentelemetry 0.27.0", "opentelemetry-appender-tracing", - "opentelemetry-otlp", - "opentelemetry_sdk", + "opentelemetry-otlp 0.27.0", + "opentelemetry_sdk 0.27.0", "terminal", "tracing", "tracing-opentelemetry", @@ -8413,7 +8527,7 @@ dependencies = [ "tar", "tempfile", "tokio", - "toml", + "toml 0.8.19", "toml_edit 0.22.22", "url", "walkdir", @@ -8464,6 +8578,7 @@ dependencies = [ "serde_json", "spin-app", "spin-core", + "spin-factor-observe", "spin-factor-outbound-http", "spin-factor-outbound-networking", "spin-factor-wasi", @@ -8519,7 +8634,9 @@ dependencies = [ name = "spin-world" version = "3.2.0-pre0" dependencies = [ + "anyhow", "async-trait", + "opentelemetry 0.27.0", "wasmtime", ] @@ -9196,6 +9313,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.19" @@ -9370,8 +9496,8 @@ checksum = "97a971f6058498b5c0f1affa23e7ea202057a7301dbff68e968b2d578bcbd053" dependencies = [ "js-sys", "once_cell", - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.27.0", + "opentelemetry_sdk 0.27.0", "smallvec", "tracing", "tracing-core", @@ -10121,7 +10247,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-util", - "toml", + "toml 0.8.19", "tracing", "tracing-subscriber", "url", @@ -10151,7 +10277,7 @@ dependencies = [ "sha2", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", "tracing", ] @@ -10344,7 +10470,7 @@ dependencies = [ "serde", "serde_derive", "sha2", - "toml", + "toml 0.8.19", "windows-sys 0.59.0", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index 92ebbd7dbc..cfdd390d09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ openssl = { version = "0.10" } anyhow = { workspace = true, features = ["backtrace"] } conformance = { path = "tests/conformance-tests" } conformance-tests = { workspace = true } +fake-opentelemetry-collector = "0.21.1" hex = "0.4" http-body-util = { workspace = true } hyper = { workspace = true } diff --git a/crates/factor-key-value/Cargo.toml b/crates/factor-key-value/Cargo.toml index d414a68390..88d4f347f2 100644 --- a/crates/factor-key-value/Cargo.toml +++ b/crates/factor-key-value/Cargo.toml @@ -8,6 +8,7 @@ edition = { workspace = true } anyhow = { workspace = true } serde = { workspace = true } spin-core = { path = "../core" } +spin-factor-observe = { path = "../factor-observe" } spin-factors = { path = "../factors" } spin-locked-app = { path = "../locked-app" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-key-value/src/host.rs b/crates/factor-key-value/src/host.rs index 08e2df2a0e..aae50cdf05 100644 --- a/crates/factor-key-value/src/host.rs +++ b/crates/factor-key-value/src/host.rs @@ -1,6 +1,7 @@ use super::{Cas, SwapError}; use anyhow::{Context, Result}; use spin_core::{async_trait, wasmtime::component::Resource}; +use spin_factor_observe::ObserveContext; use spin_resource_table::Table; use spin_world::v2::key_value; use spin_world::wasi::keyvalue as wasi_keyvalue; @@ -48,23 +49,26 @@ pub struct KeyValueDispatch { manager: Arc, stores: Table>, compare_and_swaps: Table>, + observe_context: Option, } impl KeyValueDispatch { pub fn new(allowed_stores: HashSet, manager: Arc) -> Self { - Self::new_with_capacity(allowed_stores, manager, DEFAULT_STORE_TABLE_CAPACITY) + Self::new_with_capacity(allowed_stores, manager, DEFAULT_STORE_TABLE_CAPACITY, None) } pub fn new_with_capacity( allowed_stores: HashSet, manager: Arc, capacity: u32, + observe_context: Option, ) -> Self { Self { allowed_stores, manager, stores: Table::new(capacity), compare_and_swaps: Table::new(capacity), + observe_context, } } @@ -108,6 +112,9 @@ impl key_value::Host for KeyValueDispatch {} impl key_value::HostStore for KeyValueDispatch { #[instrument(name = "spin_key_value.open", skip(self), err(level = Level::INFO), fields(otel.kind = "client", kv.backend=self.manager.summary(&name).unwrap_or("unknown".to_string())))] async fn open(&mut self, name: String) -> Result, Error>> { + if let Some(observe_context) = self.observe_context.as_ref() { + observe_context.reparent_tracing_span() + } Ok(async { if self.allowed_stores.contains(&name) { let store = self.manager.get(&name).await?; @@ -130,6 +137,9 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result>, Error>> { + if let Some(observe_context) = self.observe_context.as_ref() { + observe_context.reparent_tracing_span() + } let store = self.get_store(store)?; Ok(store.get(&key).await) } @@ -141,6 +151,9 @@ impl key_value::HostStore for KeyValueDispatch { key: String, value: Vec, ) -> Result> { + if let Some(observe_context) = self.observe_context.as_ref() { + observe_context.reparent_tracing_span() + } let store = self.get_store(store)?; Ok(store.set(&key, &value).await) } @@ -151,6 +164,9 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result> { + if let Some(observe_context) = self.observe_context.as_ref() { + observe_context.reparent_tracing_span() + } let store = self.get_store(store)?; Ok(store.delete(&key).await) } @@ -161,6 +177,9 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result> { + if let Some(observe_context) = self.observe_context.as_ref() { + observe_context.reparent_tracing_span() + } let store = self.get_store(store)?; Ok(store.exists(&key).await) } @@ -170,6 +189,9 @@ impl key_value::HostStore for KeyValueDispatch { &mut self, store: Resource, ) -> Result, Error>> { + if let Some(observe_context) = self.observe_context.as_ref() { + observe_context.reparent_tracing_span() + } let store = self.get_store(store)?; Ok(store.get_keys().await) } diff --git a/crates/factor-key-value/src/lib.rs b/crates/factor-key-value/src/lib.rs index f893263af9..5fdff3ef22 100644 --- a/crates/factor-key-value/src/lib.rs +++ b/crates/factor-key-value/src/lib.rs @@ -8,6 +8,7 @@ use std::{ }; use anyhow::ensure; +use spin_factor_observe::ObserveContext; use spin_factors::{ ConfigureAppContext, Factor, FactorInstanceBuilder, InitContext, PrepareContext, RuntimeFactors, }; @@ -84,7 +85,7 @@ impl Factor for KeyValueFactor { fn prepare( &self, - ctx: PrepareContext, + mut ctx: PrepareContext, ) -> anyhow::Result { let app_state = ctx.app_state(); let allowed_stores = app_state @@ -92,9 +93,11 @@ impl Factor for KeyValueFactor { .get(ctx.app_component().id()) .expect("component should be in component_stores") .clone(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; Ok(InstanceBuilder { store_manager: app_state.store_manager.clone(), allowed_stores, + observe_context, }) } } @@ -174,6 +177,7 @@ pub struct InstanceBuilder { store_manager: Arc, /// The allowed stores for this component instance. allowed_stores: HashSet, + observe_context: ObserveContext, } impl FactorInstanceBuilder for InstanceBuilder { @@ -183,11 +187,13 @@ impl FactorInstanceBuilder for InstanceBuilder { let Self { store_manager, allowed_stores, + observe_context, } = self; Ok(KeyValueDispatch::new_with_capacity( allowed_stores, store_manager, u32::MAX, + Some(observe_context), )) } } diff --git a/crates/factor-llm/Cargo.toml b/crates/factor-llm/Cargo.toml index e069dbbfd8..a1563d565d 100644 --- a/crates/factor-llm/Cargo.toml +++ b/crates/factor-llm/Cargo.toml @@ -17,6 +17,7 @@ llm-cublas = ["llm", "spin-llm-local/cublas"] anyhow = { workspace = true } async-trait = { workspace = true } serde = { workspace = true } +spin-factor-observe = { path = "../factor-observe" } spin-factors = { path = "../factors" } spin-llm-local = { path = "../llm-local", optional = true } spin-llm-remote-http = { path = "../llm-remote-http" } diff --git a/crates/factor-llm/src/host.rs b/crates/factor-llm/src/host.rs index 63a5525146..00e82813f7 100644 --- a/crates/factor-llm/src/host.rs +++ b/crates/factor-llm/src/host.rs @@ -13,6 +13,8 @@ impl v2::Host for InstanceState { prompt: String, params: Option, ) -> Result { + self.observe_context.reparent_tracing_span(); + if !self.allowed_models.contains(&model) { return Err(access_denied_error(&model)); } @@ -40,6 +42,8 @@ impl v2::Host for InstanceState { model: v1::EmbeddingModel, data: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); + if !self.allowed_models.contains(&model) { return Err(access_denied_error(&model)); } diff --git a/crates/factor-llm/src/lib.rs b/crates/factor-llm/src/lib.rs index 6491d9afcd..34f9faaf19 100644 --- a/crates/factor-llm/src/lib.rs +++ b/crates/factor-llm/src/lib.rs @@ -5,6 +5,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use async_trait::async_trait; +use spin_factor_observe::ObserveContext; use spin_factors::{ ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, }; @@ -76,7 +77,7 @@ impl Factor for LlmFactor { fn prepare( &self, - ctx: PrepareContext, + mut ctx: PrepareContext, ) -> anyhow::Result { let allowed_models = ctx .app_state() @@ -85,10 +86,12 @@ impl Factor for LlmFactor { .cloned() .unwrap_or_default(); let engine = ctx.app_state().engine.clone(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { engine, allowed_models, + observe_context, }) } } @@ -103,6 +106,7 @@ pub struct AppState { pub struct InstanceState { engine: Arc>, pub allowed_models: Arc>, + observe_context: ObserveContext, } /// The runtime configuration for the LLM factor. diff --git a/crates/factor-observe/Cargo.toml b/crates/factor-observe/Cargo.toml new file mode 100644 index 0000000000..dfde761caa --- /dev/null +++ b/crates/factor-observe/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "spin-factor-observe" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +indexmap = "2.2.6" +opentelemetry = { workspace = true } +opentelemetry_sdk = { workspace = true } +spin-core = { path = "../core" } +spin-factors = { path = "../factors" } +spin-resource-table = { path = "../table" } +spin-world = { path = "../world" } +tracing = { workspace = true } +tracing-opentelemetry = { workspace = true } + +[dev-dependencies] +toml = "0.5" + +[lints] +workspace = true diff --git a/crates/factor-observe/src/host.rs b/crates/factor-observe/src/host.rs new file mode 100644 index 0000000000..b4a401ebd0 --- /dev/null +++ b/crates/factor-observe/src/host.rs @@ -0,0 +1,244 @@ +use std::time::SystemTime; + +use anyhow::anyhow; +use anyhow::Result; +use opentelemetry::global::ObjectSafeSpan; +use opentelemetry::trace::TraceContextExt; +use opentelemetry::trace::Tracer; +use opentelemetry::Context; +use spin_core::async_trait; +use spin_core::wasmtime::component::Resource; +use spin_world::wasi::observe::tracer; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +use crate::{GuestSpan, InstanceState}; + +#[async_trait] +impl tracer::Host for InstanceState { + async fn start( + &mut self, + name: String, + options: Option, + ) -> Result> { + let mut state = self.state.write().unwrap(); + let options = options.unwrap_or_default(); + + // Before we ever create any new spans make sure we track the original host span ID + if state.original_host_span_id.is_none() { + state.original_host_span_id = Some( + tracing::Span::current() + .context() + .span() + .span_context() + .span_id(), + ); + } + + // Get span's parent based on whether it's a new root and whether there are any active spans + let parent_context = match (options.new_root, state.active_spans.is_empty()) { + // Not a new root && Active spans -> Last active guest span is parent + (false, false) => { + let span_context = state + .guest_spans + .get(*state.active_spans.last().unwrap()) + .unwrap() + .inner + .span_context() + .clone(); + Context::new().with_remote_span_context(span_context) + } + // Not a new root && No active spans -> Current host span is parent + (false, true) => tracing::Span::current().context(), + // New root && n/a -> No parent + (true, _) => Context::new(), + }; + + // Create the underlying opentelemetry span + let mut builder = self.tracer.span_builder(name); + if let Some(kind) = options.span_kind { + builder = builder.with_kind(kind.into()); + } + if let Some(attributes) = options.attributes { + builder = builder.with_attributes(attributes.into_iter().map(Into::into)); + } + if let Some(links) = options.links { + builder = builder.with_links(links.into_iter().map(Into::into).collect()); + } + if let Some(timestamp) = options.timestamp { + builder = builder.with_start_time(timestamp); + } + let otel_span = builder.start_with_context(&self.tracer, &parent_context); + + // Wrap it in a GuestSpan for our own bookkeeping purposes + let guest_span = GuestSpan { inner: otel_span }; + + // Put the GuestSpan in our resource table and push it on to our stack of active spans + let resource_id = state.guest_spans.push(guest_span).unwrap(); + state.active_spans.insert(resource_id); + + Ok(Resource::new_own(resource_id)) + } +} + +#[async_trait] +impl tracer::HostSpan for InstanceState { + async fn span_context( + &mut self, + resource: Resource, + ) -> Result { + if let Some(guest_span) = self.state.read().unwrap().guest_spans.get(resource.rep()) { + Ok(guest_span.inner.span_context().clone().into()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn is_recording(&mut self, resource: Resource) -> Result { + if let Some(guest_span) = self.state.read().unwrap().guest_spans.get(resource.rep()) { + Ok(guest_span.inner.is_recording()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn set_attributes( + &mut self, + resource: Resource, + attributes: Vec, + ) -> Result<()> { + if let Some(guest_span) = self + .state + .write() + .unwrap() + .guest_spans + .get_mut(resource.rep()) + { + for attribute in attributes { + guest_span.inner.set_attribute(attribute.into()); + } + Ok(()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn add_event( + &mut self, + resource: Resource, + name: String, + timestamp: Option, + attributes: Option>, + ) -> Result<()> { + if let Some(guest_span) = self + .state + .write() + .unwrap() + .guest_spans + .get_mut(resource.rep()) + { + let timestamp = timestamp.map(Into::into).unwrap_or_else(SystemTime::now); + let attributes = if let Some(attributes) = attributes { + attributes.into_iter().map(Into::into).collect() + } else { + vec![] + }; + + guest_span + .inner + .add_event_with_timestamp(name.into(), timestamp, attributes); + + Ok(()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn add_link( + &mut self, + resource: Resource, + link: tracer::Link, + ) -> Result<()> { + if let Some(guest_span) = self + .state + .write() + .unwrap() + .guest_spans + .get_mut(resource.rep()) + { + guest_span.inner.add_link( + link.span_context.into(), + link.attributes.into_iter().map(Into::into).collect(), + ); + Ok(()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn set_status( + &mut self, + resource: Resource, + status: tracer::Status, + ) -> Result<()> { + if let Some(guest_span) = self + .state + .write() + .unwrap() + .guest_spans + .get_mut(resource.rep()) + { + guest_span.inner.set_status(status.into()); + Ok(()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn update_name(&mut self, resource: Resource, name: String) -> Result<()> { + if let Some(guest_span) = self + .state + .write() + .unwrap() + .guest_spans + .get_mut(resource.rep()) + { + guest_span.inner.update_name(name.into()); + Ok(()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn end( + &mut self, + resource: Resource, + timestamp: Option, + ) -> Result<()> { + let mut state = self.state.write().unwrap(); + if let Some(guest_span) = state.guest_spans.get_mut(resource.rep()) { + if let Some(timestamp) = timestamp { + guest_span.inner.end_with_timestamp(timestamp.into()); + } else { + guest_span.inner.end(); + } + + // Remove the span from active_spans + state.active_spans.shift_remove(&resource.rep()); + + Ok(()) + } else { + Err(anyhow!("BUG: cannot find resource in table")) + } + } + + async fn drop(&mut self, resource: Resource) -> Result<()> { + // Dropping the resource automatically calls drop on the Span which ends itself with the + // current timestamp if the Span is not already ended. + + // Ensure that the span has been removed from active_spans + let mut state = self.state.write().unwrap(); + state.active_spans.shift_remove(&resource.rep()); + + Ok(()) + } +} diff --git a/crates/factor-observe/src/lib.rs b/crates/factor-observe/src/lib.rs new file mode 100644 index 0000000000..9a8a5a4dc8 --- /dev/null +++ b/crates/factor-observe/src/lib.rs @@ -0,0 +1,177 @@ +mod host; + +use std::sync::{Arc, RwLock}; + +use indexmap::IndexSet; +use opentelemetry::{ + global::{self, BoxedTracer, ObjectSafeSpan}, + trace::{SpanId, TraceContextExt}, + Context, +}; +use spin_factors::{Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +#[derive(Default)] +pub struct ObserveFactor {} + +impl Factor for ObserveFactor { + type RuntimeConfig = (); + type AppState = (); + type InstanceBuilder = InstanceState; + + fn init( + &mut self, + mut ctx: spin_factors::InitContext, + ) -> anyhow::Result<()> { + ctx.link_bindings(spin_world::wasi::observe::tracer::add_to_linker)?; + Ok(()) + } + + fn configure_app( + &self, + _ctx: spin_factors::ConfigureAppContext, + ) -> anyhow::Result { + Ok(()) + } + + fn prepare( + &self, + ctx: spin_factors::PrepareContext, + ) -> anyhow::Result { + let tracer = global::tracer(ctx.app_component().app.id().to_string()); + Ok(InstanceState { + state: Arc::new(RwLock::new(State { + guest_spans: Default::default(), + active_spans: Default::default(), + original_host_span_id: None, + })), + tracer, + }) + } +} + +impl ObserveFactor { + pub fn new() -> Self { + Self::default() + } +} + +pub struct InstanceState { + pub(crate) state: Arc>, + pub(crate) tracer: BoxedTracer, +} + +impl SelfInstanceBuilder for InstanceState {} + +/// Internal state of the ObserveFactor instance state. +/// +/// This data lives here rather than directly on InstanceState so that we can have multiple things +/// take Arc references to it. +pub(crate) struct State { + /// A resource table that holds the guest spans. + pub(crate) guest_spans: spin_resource_table::Table, + + /// A stack of resource ids for all the active guest spans. The topmost span is the active span. + /// + /// When a guest span is ended it is removed from this stack (regardless of whether is the + /// active span) and all other spans are shifted back to retain relative order. + pub(crate) active_spans: IndexSet, + + /// Id of the last span emitted from within the host before entering the guest. + /// + /// We use this to avoid accidentally reparenting the original host span as a child of a guest + /// span. + pub(crate) original_host_span_id: Option, +} + +/// The WIT resource Span. Effectively wraps an [opentelemetry::global::BoxedSpan]. +pub struct GuestSpan { + /// The [opentelemetry::global::BoxedSpan] we use to do the actual tracing work. + pub inner: opentelemetry::global::BoxedSpan, +} + +/// Manages access to the ObserveFactor state for the purpose of maintaining proper span +/// parent/child relationships when WASI Observe spans are being created. +pub struct ObserveContext { + pub(crate) state: Option>>, +} + +impl ObserveContext { + /// Creates an [`ObserveContext`] from a [`PrepareContext`]. + /// + /// If [`RuntimeFactors`] does not contain an [`ObserveFactor`], then calling + /// [`ObserveContext::reparent_tracing_span`] will be a no-op. + pub fn from_prepare_context( + prepare_context: &mut PrepareContext, + ) -> anyhow::Result { + let state = match prepare_context.instance_builder::() { + Ok(instance_state) => Some(instance_state.state.clone()), + Err(spin_factors::Error::NoSuchFactor(_)) => None, + Err(e) => return Err(e.into()), + }; + Ok(Self { state }) + } + + /// Reparents the current [tracing] span to be a child of the last active guest span. + /// + /// The observe factor enables guests to emit spans that should be part of the same trace as the + /// host is producing for a request. Below is an example trace. A request is made to an app, a + /// guest span is created and then the host is re-entered to fetch a key value. + /// + /// ```text + /// | GET /... _________________________________| + /// | execute_wasm_component foo ___________| + /// | my_guest_span ___________________| + /// | spin_key_value.get | + /// ``` + /// + /// Setting the guest spans parent as the host is trivially done. However, the more difficult + /// task is having the host factor spans be children of the guest span. + /// [`ObserveContext::reparent_tracing_span`] handles this by reparenting the current span to be + /// a child of the last active guest span (which is tracked internally in the observe factor). + /// + /// Note that if the observe factor is not in your [`RuntimeFactors`] than this is effectively a + /// no-op. + /// + /// This MUST only be called from a factor host implementation function that is instrumented. + /// + /// This MUST be called at the very start of the function before any awaits. + pub fn reparent_tracing_span(&self) { + // If state is None then we want to return early b/c the factor doesn't depend on the + // Observe factor and therefore there is nothing to do + let state = if let Some(state) = self.state.as_ref() { + state.read().unwrap() + } else { + return; + }; + + // If there are no active guest spans then there is nothing to do + let Some(current_span_id) = state.active_spans.last() else { + return; + }; + + // Ensure that we are not reparenting the original host span + if let Some(original_host_span_id) = state.original_host_span_id { + if tracing::Span::current() + .context() + .span() + .span_context() + .span_id() + .eq(&original_host_span_id) + { + panic!("Incorrectly attempting to reparent the original host span. Likely `reparent_tracing_span` was called in an incorrect location.") + } + } + + // Now reparent the current span to the last active guest span + let span_context = state + .guest_spans + .get(*current_span_id) + .unwrap() + .inner + .span_context() + .clone(); + let parent_context = Context::new().with_remote_span_context(span_context); + tracing::Span::current().set_parent(parent_context); + } +} diff --git a/crates/factor-outbound-http/Cargo.toml b/crates/factor-outbound-http/Cargo.toml index 1377e464fa..49ffc3a020 100644 --- a/crates/factor-outbound-http/Cargo.toml +++ b/crates/factor-outbound-http/Cargo.toml @@ -12,6 +12,7 @@ hyper = { workspace = true } ip_network = "0.4" reqwest = { version = "0.12", features = ["gzip"] } rustls = { workspace = true } +spin-factor-observe = { path = "../factor-observe" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-telemetry = { path = "../telemetry" } diff --git a/crates/factor-outbound-http/src/lib.rs b/crates/factor-outbound-http/src/lib.rs index cfcb167d5e..714ec56bc3 100644 --- a/crates/factor-outbound-http/src/lib.rs +++ b/crates/factor-outbound-http/src/lib.rs @@ -12,6 +12,7 @@ use http::{ HeaderValue, Uri, }; use intercept::OutboundHttpInterceptor; +use spin_factor_observe::ObserveContext; use spin_factor_outbound_networking::{ ComponentTlsConfigs, OutboundAllowedHosts, OutboundNetworkingFactor, }; @@ -75,6 +76,7 @@ impl Factor for OutboundHttpFactor { let outbound_networking = ctx.instance_builder::()?; let allowed_hosts = outbound_networking.allowed_hosts(); let component_tls_configs = outbound_networking.component_tls_configs().clone(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { wasi_http_ctx: WasiHttpCtx::new(), allowed_hosts, @@ -83,6 +85,7 @@ impl Factor for OutboundHttpFactor { self_request_origin: None, request_interceptor: None, spin_http_client: None, + observe_context, }) } } @@ -96,6 +99,7 @@ pub struct InstanceState { request_interceptor: Option>, // Connection-pooling client for 'fermyon:spin/http' interface spin_http_client: Option, + observe_context: ObserveContext, } impl InstanceState { diff --git a/crates/factor-outbound-http/src/spin.rs b/crates/factor-outbound-http/src/spin.rs index 85f5384db1..52b5fd8684 100644 --- a/crates/factor-outbound-http/src/spin.rs +++ b/crates/factor-outbound-http/src/spin.rs @@ -12,6 +12,8 @@ impl spin_http::Host for crate::InstanceState { fields(otel.kind = "client", url.full = Empty, http.request.method = Empty, http.response.status_code = Empty, otel.name = Empty, server.address = Empty, server.port = Empty))] async fn send_request(&mut self, req: Request) -> Result { + self.observe_context.reparent_tracing_span(); + let span = Span::current(); record_request_fields(&span, &req); diff --git a/crates/factor-outbound-http/src/wasi.rs b/crates/factor-outbound-http/src/wasi.rs index efe9aad6aa..00b86bee68 100644 --- a/crates/factor-outbound-http/src/wasi.rs +++ b/crates/factor-outbound-http/src/wasi.rs @@ -87,6 +87,8 @@ impl WasiHttpView for WasiHttpImplInner<'_> { request: Request, config: wasmtime_wasi_http::types::OutgoingRequestConfig, ) -> wasmtime_wasi_http::HttpResult { + self.state.observe_context.reparent_tracing_span(); + Ok(HostFutureIncomingResponse::Pending( wasmtime_wasi::runtime::spawn( send_request_impl( diff --git a/crates/factor-outbound-mqtt/Cargo.toml b/crates/factor-outbound-mqtt/Cargo.toml index bd5222881c..8d087dc2bb 100644 --- a/crates/factor-outbound-mqtt/Cargo.toml +++ b/crates/factor-outbound-mqtt/Cargo.toml @@ -8,6 +8,7 @@ edition = { workspace = true } anyhow = { workspace = true } rumqttc = { version = "0.24", features = ["url"] } spin-core = { path = "../core" } +spin-factor-observe = { path = "../factor-observe" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-mqtt/src/host.rs b/crates/factor-outbound-mqtt/src/host.rs index 1546c23472..d22ba8541a 100644 --- a/crates/factor-outbound-mqtt/src/host.rs +++ b/crates/factor-outbound-mqtt/src/host.rs @@ -2,6 +2,7 @@ use std::{sync::Arc, time::Duration}; use anyhow::Result; use spin_core::{async_trait, wasmtime::component::Resource}; +use spin_factor_observe::ObserveContext; use spin_factor_outbound_networking::OutboundAllowedHosts; use spin_world::v2::mqtt::{self as v2, Connection, Error, Qos}; use tracing::{instrument, Level}; @@ -12,14 +13,20 @@ pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table>, create_client: Arc, + observe_context: ObserveContext, } impl InstanceState { - pub fn new(allowed_hosts: OutboundAllowedHosts, create_client: Arc) -> Self { + pub fn new( + allowed_hosts: OutboundAllowedHosts, + create_client: Arc, + observe_context: ObserveContext, + ) -> Self { Self { allowed_hosts, create_client, connections: spin_resource_table::Table::new(1024), + observe_context, } } } @@ -72,6 +79,8 @@ impl v2::HostConnection for InstanceState { password: String, keep_alive_interval: u64, ) -> Result, Error> { + self.observe_context.reparent_tracing_span(); + if !self .is_address_allowed(&address) .await @@ -105,6 +114,8 @@ impl v2::HostConnection for InstanceState { payload: Vec, qos: Qos, ) -> Result<(), Error> { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; conn.publish_bytes(topic, qos, payload).await?; diff --git a/crates/factor-outbound-mqtt/src/lib.rs b/crates/factor-outbound-mqtt/src/lib.rs index 79c5138e5c..d5b8fc0867 100644 --- a/crates/factor-outbound-mqtt/src/lib.rs +++ b/crates/factor-outbound-mqtt/src/lib.rs @@ -7,6 +7,7 @@ use host::other_error; use host::InstanceState; use rumqttc::{AsyncClient, Event, Incoming, Outgoing, QoS}; use spin_core::async_trait; +use spin_factor_observe::ObserveContext; use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factors::{ ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -53,9 +54,12 @@ impl Factor for OutboundMqttFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState::new( allowed_hosts, self.create_client.clone(), + observe_context, )) } } diff --git a/crates/factor-outbound-mysql/Cargo.toml b/crates/factor-outbound-mysql/Cargo.toml index 2d87fa38c5..ad73b37cba 100644 --- a/crates/factor-outbound-mysql/Cargo.toml +++ b/crates/factor-outbound-mysql/Cargo.toml @@ -14,6 +14,7 @@ mysql_async = { version = "0.34", default-features = false, features = [ "native-tls-tls", ] } spin-core = { path = "../core" } +spin-factor-observe = { path = "../factor-observe" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-mysql/src/host.rs b/crates/factor-outbound-mysql/src/host.rs index e562d4d741..4435c57ae9 100644 --- a/crates/factor-outbound-mysql/src/host.rs +++ b/crates/factor-outbound-mysql/src/host.rs @@ -38,6 +38,7 @@ impl v2::Host for InstanceState {} impl v2::HostConnection for InstanceState { #[instrument(name = "spin_outbound_mysql.open", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "mysql", db.address = Empty, server.port = Empty, db.namespace = Empty))] async fn open(&mut self, address: String) -> Result, v2::Error> { + self.observe_context.reparent_tracing_span(); spin_factor_outbound_networking::record_address_fields(&address); if !self @@ -59,6 +60,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result<(), v2::Error> { + self.observe_context.reparent_tracing_span(); self.get_client(connection) .await? .execute(statement, params) @@ -72,6 +74,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); self.get_client(connection) .await? .query(statement, params) diff --git a/crates/factor-outbound-mysql/src/lib.rs b/crates/factor-outbound-mysql/src/lib.rs index e666fff56c..d8c7d33a82 100644 --- a/crates/factor-outbound-mysql/src/lib.rs +++ b/crates/factor-outbound-mysql/src/lib.rs @@ -3,6 +3,7 @@ mod host; use client::Client; use mysql_async::Conn as MysqlClient; +use spin_factor_observe::ObserveContext; use spin_factor_outbound_networking::{OutboundAllowedHosts, OutboundNetworkingFactor}; use spin_factors::{Factor, InitContext, RuntimeFactors, SelfInstanceBuilder}; use spin_world::v1::mysql as v1; @@ -37,9 +38,12 @@ impl Factor for OutboundMysqlFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState { allowed_hosts, connections: Default::default(), + observe_context, }) } } @@ -61,6 +65,7 @@ impl OutboundMysqlFactor { pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table, + observe_context: ObserveContext, } impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/factor-outbound-pg/Cargo.toml b/crates/factor-outbound-pg/Cargo.toml index 47899aee91..b6217b1ca0 100644 --- a/crates/factor-outbound-pg/Cargo.toml +++ b/crates/factor-outbound-pg/Cargo.toml @@ -10,6 +10,7 @@ chrono = "0.4" native-tls = "0.2" postgres-native-tls = "0.5" spin-core = { path = "../core" } +spin-factor-observe = { path = "../factor-observe" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-pg/src/host.rs b/crates/factor-outbound-pg/src/host.rs index 289934446f..8d8fd2d8b9 100644 --- a/crates/factor-outbound-pg/src/host.rs +++ b/crates/factor-outbound-pg/src/host.rs @@ -157,6 +157,7 @@ impl v2::Host for InstanceState {} impl v2::HostConnection for InstanceState { #[instrument(name = "spin_outbound_pg.open", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", db.address = Empty, server.port = Empty, db.namespace = Empty))] async fn open(&mut self, address: String) -> Result, v2::Error> { + self.observe_context.reparent_tracing_span(); spin_factor_outbound_networking::record_address_fields(&address); if !self @@ -178,6 +179,8 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); + Ok(self .get_client(connection) .await? @@ -192,6 +195,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); Ok(self .get_client(connection) .await? diff --git a/crates/factor-outbound-pg/src/lib.rs b/crates/factor-outbound-pg/src/lib.rs index 4ca3663531..17067d88c4 100644 --- a/crates/factor-outbound-pg/src/lib.rs +++ b/crates/factor-outbound-pg/src/lib.rs @@ -2,6 +2,7 @@ pub mod client; mod host; use client::Client; +use spin_factor_observe::ObserveContext; use spin_factor_outbound_networking::{OutboundAllowedHosts, OutboundNetworkingFactor}; use spin_factors::{ anyhow, ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -41,9 +42,12 @@ impl Factor for OutboundPgFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState { allowed_hosts, connections: Default::default(), + observe_context, }) } } @@ -65,6 +69,7 @@ impl OutboundPgFactor { pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table, + observe_context: ObserveContext, } impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/factor-outbound-redis/Cargo.toml b/crates/factor-outbound-redis/Cargo.toml index 092363069d..cf9077d349 100644 --- a/crates/factor-outbound-redis/Cargo.toml +++ b/crates/factor-outbound-redis/Cargo.toml @@ -8,6 +8,7 @@ edition = { workspace = true } anyhow = { workspace = true } redis = { version = "0.25", features = ["tokio-comp", "tokio-native-tls-comp", "aio"] } spin-core = { path = "../core" } +spin-factor-observe = { path = "../factor-observe" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-redis/src/host.rs b/crates/factor-outbound-redis/src/host.rs index d0beca4b82..2664884e14 100644 --- a/crates/factor-outbound-redis/src/host.rs +++ b/crates/factor-outbound-redis/src/host.rs @@ -1,6 +1,7 @@ use anyhow::Result; use redis::{aio::MultiplexedConnection, AsyncCommands, FromRedisValue, Value}; use spin_core::wasmtime::component::Resource; +use spin_factor_observe::ObserveContext; use spin_factor_outbound_networking::OutboundAllowedHosts; use spin_world::v1::{redis as v1, redis_types}; use spin_world::v2::redis::{ @@ -12,6 +13,7 @@ use tracing::{instrument, Level}; pub struct InstanceState { pub allowed_hosts: OutboundAllowedHosts, pub connections: spin_resource_table::Table, + pub observe_context: ObserveContext, } impl InstanceState { @@ -55,8 +57,7 @@ impl v2::Host for crate::InstanceState { impl v2::HostConnection for crate::InstanceState { #[instrument(name = "spin_outbound_redis.open_connection", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "redis", db.address = Empty, server.port = Empty, db.namespace = Empty))] async fn open(&mut self, address: String) -> Result, Error> { - spin_factor_outbound_networking::record_address_fields(&address); - + self.observe_context.reparent_tracing_span(); if !self .is_address_allowed(&address) .await @@ -75,6 +76,8 @@ impl v2::HostConnection for crate::InstanceState { channel: String, payload: Vec, ) -> Result<(), Error> { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; // The `let () =` syntax is needed to suppress a warning when the result type is inferred. // You can read more about the issue here: @@ -91,6 +94,8 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result>, Error> { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.get(&key).await.map_err(other_error)?; Ok(value) @@ -103,6 +108,8 @@ impl v2::HostConnection for crate::InstanceState { key: String, value: Vec, ) -> Result<(), Error> { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; // The `let () =` syntax is needed to suppress a warning when the result type is inferred. // You can read more about the issue here: @@ -116,6 +123,8 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.incr(&key, 1).await.map_err(other_error)?; Ok(value) @@ -127,6 +136,8 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, keys: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.del(&keys).await.map_err(other_error)?; Ok(value) @@ -139,6 +150,8 @@ impl v2::HostConnection for crate::InstanceState { key: String, values: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.sadd(&key, &values).await.map_err(|e| { if e.kind() == redis::ErrorKind::TypeError { @@ -156,6 +169,8 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result, Error> { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.smembers(&key).await.map_err(other_error)?; Ok(value) @@ -168,6 +183,8 @@ impl v2::HostConnection for crate::InstanceState { key: String, values: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.srem(&key, &values).await.map_err(other_error)?; Ok(value) @@ -180,6 +197,8 @@ impl v2::HostConnection for crate::InstanceState { command: String, arguments: Vec, ) -> Result, Error> { + self.observe_context.reparent_tracing_span(); + let conn = self.get_conn(connection).await?; let mut cmd = redis::cmd(&command); arguments.iter().for_each(|value| match value { diff --git a/crates/factor-outbound-redis/src/lib.rs b/crates/factor-outbound-redis/src/lib.rs index a818713334..9a5b72a99f 100644 --- a/crates/factor-outbound-redis/src/lib.rs +++ b/crates/factor-outbound-redis/src/lib.rs @@ -1,6 +1,7 @@ mod host; use host::InstanceState; +use spin_factor_observe::ObserveContext; use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factors::{ anyhow, ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -46,9 +47,12 @@ impl Factor for OutboundRedisFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState { allowed_hosts, connections: spin_resource_table::Table::new(1024), + observe_context, }) } } diff --git a/crates/factor-sqlite/Cargo.toml b/crates/factor-sqlite/Cargo.toml index ee945ed4fa..cf2c98bbd6 100644 --- a/crates/factor-sqlite/Cargo.toml +++ b/crates/factor-sqlite/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [dependencies] async-trait = { workspace = true } +spin-factor-observe = { path = "../factor-observe" } spin-factors = { path = "../factors" } spin-locked-app = { path = "../locked-app" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-sqlite/src/host.rs b/crates/factor-sqlite/src/host.rs index ee39f9d2a9..6235f0e035 100644 --- a/crates/factor-sqlite/src/host.rs +++ b/crates/factor-sqlite/src/host.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use spin_factor_observe::ObserveContext; use spin_factors::wasmtime::component::Resource; use spin_factors::{anyhow, SelfInstanceBuilder}; use spin_world::v1::sqlite as v1; @@ -16,6 +17,7 @@ pub struct InstanceState { connections: spin_resource_table::Table>, /// A map from database label to connection creators. connection_creators: HashMap>, + observe_context: ObserveContext, } impl InstanceState { @@ -25,11 +27,13 @@ impl InstanceState { pub fn new( allowed_databases: Arc>, connection_creators: HashMap>, + observe_context: ObserveContext, ) -> Self { Self { allowed_databases, connections: spin_resource_table::Table::new(256), connection_creators, + observe_context, } } @@ -61,6 +65,8 @@ impl v2::Host for InstanceState { impl v2::HostConnection for InstanceState { #[instrument(name = "spin_sqlite.open", skip(self), err(level = Level::INFO), fields(otel.kind = "client", db.system = "sqlite", sqlite.backend = Empty))] async fn open(&mut self, database: String) -> Result, v2::Error> { + self.observe_context.reparent_tracing_span(); + if !self.allowed_databases.contains(&database) { return Err(v2::Error::AccessDenied); } @@ -87,6 +93,8 @@ impl v2::HostConnection for InstanceState { query: String, parameters: Vec, ) -> Result { + self.observe_context.reparent_tracing_span(); + let conn = match self.get_connection(connection) { Ok(c) => c, Err(err) => return Err(err), diff --git a/crates/factor-sqlite/src/lib.rs b/crates/factor-sqlite/src/lib.rs index d7a7618c7a..fc5e756dd4 100644 --- a/crates/factor-sqlite/src/lib.rs +++ b/crates/factor-sqlite/src/lib.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use host::InstanceState; use async_trait::async_trait; +use spin_factor_observe::ObserveContext; use spin_factors::{anyhow, Factor}; use spin_locked_app::MetadataKey; use spin_world::v1::sqlite as v1; @@ -75,7 +76,7 @@ impl Factor for SqliteFactor { fn prepare( &self, - ctx: spin_factors::PrepareContext, + mut ctx: spin_factors::PrepareContext, ) -> spin_factors::anyhow::Result { let allowed_databases = ctx .app_state() @@ -83,9 +84,11 @@ impl Factor for SqliteFactor { .get(ctx.app_component().id()) .cloned() .unwrap_or_default(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; Ok(InstanceState::new( allowed_databases, ctx.app_state().connection_creators.clone(), + observe_context, )) } } diff --git a/crates/factor-variables/Cargo.toml b/crates/factor-variables/Cargo.toml index f02168ed4d..3abe8397dd 100644 --- a/crates/factor-variables/Cargo.toml +++ b/crates/factor-variables/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } [dependencies] spin-expressions = { path = "../expressions" } +spin-factor-observe = { path = "../factor-observe" } spin-factors = { path = "../factors" } spin-world = { path = "../world" } tracing = { workspace = true } diff --git a/crates/factor-variables/src/host.rs b/crates/factor-variables/src/host.rs index 4b562f4808..d057b1b3f7 100644 --- a/crates/factor-variables/src/host.rs +++ b/crates/factor-variables/src/host.rs @@ -7,6 +7,7 @@ use crate::InstanceState; impl variables::Host for InstanceState { #[instrument(name = "spin_variables.get", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))] async fn get(&mut self, key: String) -> Result { + self.observe_context.reparent_tracing_span(); let key = spin_expressions::Key::new(&key).map_err(expressions_to_variables_err)?; self.expression_resolver .resolve(&self.component_id, key) diff --git a/crates/factor-variables/src/lib.rs b/crates/factor-variables/src/lib.rs index 0983696094..a92181a3a5 100644 --- a/crates/factor-variables/src/lib.rs +++ b/crates/factor-variables/src/lib.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use runtime_config::RuntimeConfig; use spin_expressions::{ProviderResolver as ExpressionResolver, Template}; +use spin_factor_observe::ObserveContext; use spin_factors::{ anyhow, ConfigureAppContext, Factor, InitContext, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -62,13 +63,15 @@ impl Factor for VariablesFactor { fn prepare( &self, - ctx: PrepareContext, + mut ctx: PrepareContext, ) -> anyhow::Result { let component_id = ctx.app_component().id().to_string(); let expression_resolver = ctx.app_state().expression_resolver.clone(); + let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { component_id, expression_resolver, + observe_context, }) } } @@ -90,6 +93,7 @@ impl AppState { pub struct InstanceState { component_id: String, expression_resolver: Arc, + observe_context: ObserveContext, } impl InstanceState { diff --git a/crates/runtime-config/Cargo.toml b/crates/runtime-config/Cargo.toml index 75839f47a6..97021360c8 100644 --- a/crates/runtime-config/Cargo.toml +++ b/crates/runtime-config/Cargo.toml @@ -13,6 +13,7 @@ anyhow = { workspace = true } spin-common = { path = "../common" } spin-factor-key-value = { path = "../factor-key-value" } spin-factor-llm = { path = "../factor-llm" } +spin-factor-observe = { path = "../factor-observe" } spin-factor-outbound-http = { path = "../factor-outbound-http" } spin-factor-outbound-mqtt = { path = "../factor-outbound-mqtt" } spin-factor-outbound-mysql = { path = "../factor-outbound-mysql" } diff --git a/crates/runtime-config/src/lib.rs b/crates/runtime-config/src/lib.rs index 3e7f22ada7..7872c0df35 100644 --- a/crates/runtime-config/src/lib.rs +++ b/crates/runtime-config/src/lib.rs @@ -5,6 +5,7 @@ use spin_common::ui::quoted_path; use spin_factor_key_value::runtime_config::spin::{self as key_value}; use spin_factor_key_value::KeyValueFactor; use spin_factor_llm::{spin as llm, LlmFactor}; +use spin_factor_observe::ObserveFactor; use spin_factor_outbound_http::OutboundHttpFactor; use spin_factor_outbound_mqtt::OutboundMqttFactor; use spin_factor_outbound_mysql::OutboundMysqlFactor; @@ -373,6 +374,12 @@ impl FactorRuntimeConfigSource for TomlRuntimeConfigSource<'_, '_> } } +impl FactorRuntimeConfigSource for TomlRuntimeConfigSource<'_, '_> { + fn get_runtime_config(&mut self) -> anyhow::Result> { + Ok(None) + } +} + impl RuntimeConfigSourceFinalizer for TomlRuntimeConfigSource<'_, '_> { fn finalize(&mut self) -> anyhow::Result<()> { Ok(self.toml.validate_all_keys_used()?) diff --git a/crates/runtime-factors/Cargo.toml b/crates/runtime-factors/Cargo.toml index fd982d8bee..3597f41ede 100644 --- a/crates/runtime-factors/Cargo.toml +++ b/crates/runtime-factors/Cargo.toml @@ -19,6 +19,7 @@ clap = { version = "3.1.18", features = ["derive", "env"] } spin-common = { path = "../common" } spin-factor-key-value = { path = "../factor-key-value" } spin-factor-llm = { path = "../factor-llm" } +spin-factor-observe = { path = "../factor-observe" } spin-factor-outbound-http = { path = "../factor-outbound-http" } spin-factor-outbound-mqtt = { path = "../factor-outbound-mqtt" } spin-factor-outbound-mysql = { path = "../factor-outbound-mysql" } diff --git a/crates/runtime-factors/src/lib.rs b/crates/runtime-factors/src/lib.rs index c091e58162..06b710fc53 100644 --- a/crates/runtime-factors/src/lib.rs +++ b/crates/runtime-factors/src/lib.rs @@ -8,6 +8,7 @@ use anyhow::Context as _; use spin_common::arg_parser::parse_kv; use spin_factor_key_value::KeyValueFactor; use spin_factor_llm::LlmFactor; +use spin_factor_observe::ObserveFactor; use spin_factor_outbound_http::OutboundHttpFactor; use spin_factor_outbound_mqtt::{NetworkedMqttClient, OutboundMqttFactor}; use spin_factor_outbound_mysql::OutboundMysqlFactor; @@ -22,6 +23,7 @@ use spin_runtime_config::{ResolvedRuntimeConfig, TomlRuntimeConfigSource}; #[derive(RuntimeFactors)] pub struct TriggerFactors { + pub observe: ObserveFactor, pub wasi: WasiFactor, pub variables: VariablesFactor, pub key_value: KeyValueFactor, @@ -42,6 +44,7 @@ impl TriggerFactors { allow_transient_writes: bool, ) -> anyhow::Result { Ok(Self { + observe: ObserveFactor::new(), wasi: wasi_factor(working_dir, allow_transient_writes), variables: VariablesFactor::default(), key_value: KeyValueFactor::new(), diff --git a/crates/trigger-http/Cargo.toml b/crates/trigger-http/Cargo.toml index ad10fa3edf..6559869cc3 100644 --- a/crates/trigger-http/Cargo.toml +++ b/crates/trigger-http/Cargo.toml @@ -30,6 +30,7 @@ spin-http = { path = "../http" } spin-telemetry = { path = "../telemetry" } spin-trigger = { path = "../trigger" } spin-world = { path = "../world" } +spin-factor-observe = { path = "../factor-observe" } terminal = { path = "../terminal" } tokio = { workspace = true, features = ["full"] } tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"] } diff --git a/crates/trigger-http/src/wasi.rs b/crates/trigger-http/src/wasi.rs index 0e431a488b..3bef6d6256 100644 --- a/crates/trigger-http/src/wasi.rs +++ b/crates/trigger-http/src/wasi.rs @@ -97,7 +97,6 @@ impl HttpExecutor for WasiHttpExecutor { HandlerType::Wagi => unreachable!("should have used WagiExecutor instead"), }; - let span = tracing::debug_span!("execute_wasi"); let handle = task::spawn( async move { let result = match handler { @@ -105,20 +104,13 @@ impl HttpExecutor for WasiHttpExecutor { handler .wasi_http_incoming_handler() .call_handle(&mut store, request, response) - .instrument(span) .await } Handler::Handler2023_10_18(handler) => { - handler - .call_handle(&mut store, request, response) - .instrument(span) - .await + handler.call_handle(&mut store, request, response).await } Handler::Handler2023_11_10(handler) => { - handler - .call_handle(&mut store, request, response) - .instrument(span) - .await + handler.call_handle(&mut store, request, response).await } }; diff --git a/crates/world/Cargo.toml b/crates/world/Cargo.toml index fd4763100d..77f26a7a05 100644 --- a/crates/world/Cargo.toml +++ b/crates/world/Cargo.toml @@ -5,5 +5,7 @@ authors = { workspace = true } edition = { workspace = true } [dependencies] +anyhow = { workspace = true } async-trait = { workspace = true } +opentelemetry = { workspace = true } wasmtime = { workspace = true } diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index cd75d78a2d..8d41dae647 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -494,3 +494,179 @@ mod llm { } } } + +mod observe { + use super::*; + use opentelemetry::StringValue; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + use wasi::observe::tracer; + + impl From for opentelemetry::Value { + fn from(value: tracer::Value) -> Self { + match value { + tracer::Value::String(v) => v.into(), + tracer::Value::Bool(v) => v.into(), + tracer::Value::Float64(v) => v.into(), + tracer::Value::S64(v) => v.into(), + tracer::Value::StringArray(v) => opentelemetry::Value::Array( + v.into_iter() + .map(StringValue::from) + .collect::>() + .into(), + ), + tracer::Value::BoolArray(v) => opentelemetry::Value::Array(v.into()), + tracer::Value::Float64Array(v) => opentelemetry::Value::Array(v.into()), + tracer::Value::S64Array(v) => opentelemetry::Value::Array(v.into()), + } + } + } + + impl From for opentelemetry::KeyValue { + fn from(kv: tracer::KeyValue) -> Self { + opentelemetry::KeyValue::new(kv.key, kv.value) + } + } + + impl From for opentelemetry::trace::TraceFlags { + fn from(flags: tracer::TraceFlags) -> Self { + Self::new(flags.as_array()[0] as u8) + } + } + + impl From for tracer::TraceFlags { + fn from(flags: opentelemetry::trace::TraceFlags) -> Self { + if flags.is_sampled() { + tracer::TraceFlags::SAMPLED + } else { + tracer::TraceFlags::empty() + } + } + } + + impl From for opentelemetry::trace::SpanContext { + fn from(sc: tracer::SpanContext) -> Self { + // TODO(Reviewer): Should this be try_from instead an propagate this error out of the WIT? + let trace_id = opentelemetry::trace::TraceId::from_hex(&sc.trace_id) + .unwrap_or(opentelemetry::trace::TraceId::INVALID); + let span_id = opentelemetry::trace::SpanId::from_hex(&sc.span_id) + .unwrap_or(opentelemetry::trace::SpanId::INVALID); + let trace_state = opentelemetry::trace::TraceState::from_key_value(sc.trace_state) + .unwrap_or_else(|_| opentelemetry::trace::TraceState::default()); + Self::new( + trace_id, + span_id, + sc.trace_flags.into(), + sc.is_remote, + trace_state, + ) + } + } + + impl From for tracer::SpanContext { + fn from(sc: opentelemetry::trace::SpanContext) -> Self { + Self { + trace_id: format!("{:x}", sc.trace_id()), + span_id: format!("{:x}", sc.span_id()), + trace_flags: sc.trace_flags().into(), + is_remote: sc.is_remote(), + trace_state: sc + .trace_state() + .header() + .split(',') + // TODO(Reviewer): Should this be try_from instead an propagate this error out of the WIT? + .filter_map(|s| { + if let Some((key, value)) = s.split_once('=') { + Some((key.to_string(), value.to_string())) + } else { + None + } + }) + .collect(), + } + } + } + + impl From for opentelemetry::trace::Status { + fn from(status: tracer::Status) -> Self { + match status { + tracer::Status::Unset => Self::Unset, + tracer::Status::Ok => Self::Ok, + tracer::Status::Error(s) => Self::Error { + description: s.into(), + }, + } + } + } + + impl From for opentelemetry::trace::SpanKind { + fn from(kind: tracer::SpanKind) -> Self { + match kind { + tracer::SpanKind::Client => opentelemetry::trace::SpanKind::Client, + tracer::SpanKind::Server => opentelemetry::trace::SpanKind::Server, + tracer::SpanKind::Producer => opentelemetry::trace::SpanKind::Producer, + tracer::SpanKind::Consumer => opentelemetry::trace::SpanKind::Consumer, + tracer::SpanKind::Internal => opentelemetry::trace::SpanKind::Internal, + } + } + } + + impl From for opentelemetry::trace::Link { + fn from(link: tracer::Link) -> Self { + Self::new( + link.span_context.into(), + link.attributes.into_iter().map(Into::into).collect(), + 0, + ) + } + } + + impl From for SystemTime { + fn from(timestamp: tracer::Datetime) -> Self { + UNIX_EPOCH + + Duration::from_secs(timestamp.seconds) + + Duration::from_nanos(timestamp.nanoseconds as u64) + } + } + + #[allow(clippy::derivable_impls)] + impl Default for tracer::StartOptions { + fn default() -> Self { + Self { + new_root: false, + span_kind: None, + attributes: None, + links: None, + timestamp: None, + } + } + } + + mod test { + #[test] + fn trace_flags() { + let flags = opentelemetry::trace::TraceFlags::SAMPLED; + let flags2 = crate::wasi::observe::tracer::TraceFlags::from(flags); + let flags3 = opentelemetry::trace::TraceFlags::from(flags2); + assert_eq!(flags, flags3); + } + + #[test] + fn span_context() { + let sc = opentelemetry::trace::SpanContext::new( + opentelemetry::trace::TraceId::from_hex("4fb34cb4484029f7881399b149e41e98") + .unwrap(), + opentelemetry::trace::SpanId::from_hex("9ffd58d3cd4dd90b").unwrap(), + opentelemetry::trace::TraceFlags::SAMPLED, + false, + opentelemetry::trace::TraceState::from_key_value(vec![ + ("foo", "bar"), + ("baz", "qux"), + ]) + .unwrap(), + ); + let sc2 = crate::wasi::observe::tracer::SpanContext::from(sc.clone()); + let sc3 = opentelemetry::trace::SpanContext::from(sc2); + assert_eq!(sc, sc3); + } + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 15529bb563..b01ba3a092 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,6 +1,7 @@ -mod testcases; +pub mod testcases; mod integration_tests { + use anyhow::Context; use sha2::Digest; use std::collections::HashMap; use test_environment::{ @@ -9,13 +10,12 @@ mod integration_tests { }; use testing_framework::runtimes::{spin_cli::SpinConfig, SpinAppType}; - use super::testcases::{ + pub use super::testcases::{ assert_spin_request, bootstap_env, http_smoke_test_template, run_test, spin_binary, }; - use anyhow::Context; - /// Helper macro to assert that a condition is true eventually #[cfg(feature = "extern-dependencies-tests")] + /// Helper macro to assert that a condition is true eventually macro_rules! assert_eventually { ($e:expr, $t:expr) => { let mut i = 0; @@ -154,72 +154,6 @@ mod integration_tests { Ok(()) } - #[test] - #[cfg(feature = "extern-dependencies-tests")] - /// Test that basic otel tracing works - fn otel_smoke_test() -> anyhow::Result<()> { - use anyhow::Context; - - use crate::testcases::run_test_inited; - run_test_inited( - "otel-smoke-test", - SpinConfig { - binary_path: spin_binary(), - spin_up_args: Vec::new(), - app_type: SpinAppType::Http, - }, - ServicesConfig::new(vec!["jaeger"])?, - |env| { - let otel_port = env - .services_mut() - .get_port(4318)? - .context("expected a port for Jaeger")?; - env.set_env_var( - "OTEL_EXPORTER_OTLP_ENDPOINT", - format!("http://localhost:{}", otel_port), - ); - Ok(()) - }, - move |env| { - let spin = env.runtime_mut(); - assert_spin_request( - spin, - Request::new(Method::Get, "/hello"), - Response::new_with_body(200, "Hello, Fermyon!\n"), - )?; - - assert_eventually!( - { - let jaeger_port = env - .services_mut() - .get_port(16686)? - .context("no jaeger port was exposed by test services")?; - let url = format!("http://localhost:{jaeger_port}/api/traces?service=spin"); - match reqwest::blocking::get(&url).context("failed to get jaeger traces")? { - resp if resp.status().is_success() => { - let traces: serde_json::Value = - resp.json().context("failed to parse jaeger traces")?; - let traces = - traces.get("data").context("jaeger traces has no data")?; - let traces = - traces.as_array().context("jaeger traces is not an array")?; - !traces.is_empty() - } - _resp => { - eprintln!("failed to get jaeger traces:"); - false - } - } - }, - 20 - ); - Ok(()) - }, - )?; - - Ok(()) - } - #[test] /// Test dynamic environment variables fn dynamic_env_test() -> anyhow::Result<()> { @@ -1464,3 +1398,573 @@ route = "/..." Ok(()) } } + +// TODO(Reviewer): How can I move this to a new file? I wasn't able to get the imports to work out. +mod otel_integration_tests { + use fake_opentelemetry_collector::FakeCollectorServer; + use std::time::Duration; + use test_environment::{ + http::{Method, Request, Response}, + services::ServicesConfig, + }; + use testing_framework::runtimes::{spin_cli::SpinConfig, SpinAppType}; + + use crate::testcases::run_test_inited; + + use super::testcases::{assert_spin_request, spin_binary}; + + #[test] + // Test that basic otel tracing and inbound/outbound context propagation works + fn otel_smoke_test() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "otel-smoke-test", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request(spin, Request::new(Method::Get, "/one"), Response::new(200))?; + + let spans = rt.block_on(collector.exported_spans(5, Duration::from_secs(5))); + + assert_eq!(spans.len(), 5); + + // They're all part of the same trace which implies context propagation is working + assert!(spans + .iter() + .map(|s| s.trace_id.clone()) + .all(|t| t == spans[0].trace_id)); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_nested_spans() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/nested-spans"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(4, Duration::from_secs(5))); + + assert_eq!(spans.len(), 4); + + let handle_request_span = spans + .iter() + .find(|s| s.name == "GET /...") + .expect("'GET /...' span should exist"); + let exec_component_span = spans + .iter() + .find(|s| s.name == "execute_wasm_component wasi-observe-tracing") + .expect("'execute_wasm_component wasi-observe-tracing' span should exist"); + let outer_span = spans + .iter() + .find(|s| s.name == "outer_func") + .expect("'outer_func' span should exist"); + let inner_span = spans + .iter() + .find(|s| s.name == "inner_func") + .expect("'inner_func' span should exist"); + + assert!( + handle_request_span.trace_id == exec_component_span.trace_id + && exec_component_span.trace_id == outer_span.trace_id + && outer_span.trace_id == inner_span.trace_id + ); + assert_eq!( + exec_component_span.parent_span_id, + handle_request_span.span_id + ); + assert_eq!(outer_span.parent_span_id, exec_component_span.span_id); + assert_eq!(inner_span.parent_span_id, outer_span.span_id); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_drop_semantics() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/drop-semantics"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(3, Duration::from_secs(5))); + + assert_eq!(spans.len(), 3); + + let handle_request_span = spans + .iter() + .find(|s| s.name == "GET /...") + .expect("'GET /...' span should exist"); + let exec_component_span = spans + .iter() + .find(|s| s.name == "execute_wasm_component wasi-observe-tracing") + .expect("'execute_wasm_component wasi-observe-tracing' span should exist"); + let drop_span = spans + .iter() + .find(|s| s.name == "drop_semantics") + .expect("'drop_semantics' span should exist"); + + assert!( + handle_request_span.trace_id == exec_component_span.trace_id + && exec_component_span.trace_id == drop_span.trace_id + ); + assert_eq!( + exec_component_span.parent_span_id, + handle_request_span.span_id + ); + assert_eq!(drop_span.parent_span_id, exec_component_span.span_id); + assert!(drop_span.end_time_unix_nano < exec_component_span.end_time_unix_nano); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_setting_attributes() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/setting-attributes"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(3, Duration::from_secs(5))); + + assert_eq!(spans.len(), 3); + + let attr_span = spans + .iter() + .find(|s| s.name == "setting_attributes") + .expect("'setting_attributes' span should exist"); + + // There are some other attributes already set on the span + assert_eq!(attr_span.attributes.len(), 2); + + assert_eq!( + attr_span + .attributes + .get("foo") + .expect("'foo' attribute should exist"), + "Some(AnyValue { value: Some(StringValue(\"baz\")) })" + ); + assert_eq!( + attr_span.attributes.get("qux").expect("'qux' attribute should exist"), + "Some(AnyValue { value: Some(ArrayValue(ArrayValue { values: [AnyValue { value: Some(StringValue(\"qaz\")) }, AnyValue { value: Some(StringValue(\"thud\")) }] })) })" + ); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_host_guest_host() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/host-guest-host"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(4, Duration::from_secs(5))); + + assert_eq!(spans.len(), 4); + + assert!(spans + .iter() + .map(|s| s.trace_id.clone()) + .all(|t| t == spans[0].trace_id)); + + let exec_component_span = spans + .iter() + .find(|s| s.name == "execute_wasm_component wasi-observe-tracing") + .expect("'execute_wasm_component wasi-observe-tracing' span should exist"); + let guest_span = spans + .iter() + .find(|s| s.name == "guest") + .expect("'guest' span should exist"); + let get_span = spans + .iter() + .find(|s| s.name == "GET") + .expect("'GET' span should exist"); + + assert_eq!(guest_span.parent_span_id, exec_component_span.span_id); + assert_eq!(get_span.parent_span_id, guest_span.span_id); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_events() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/events"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(3, Duration::from_secs(5))); + + assert_eq!(spans.len(), 3); + + let event_span = spans + .iter() + .find(|s| s.name == "events") + .expect("'events' span should exist"); + + let events = event_span.events.clone(); + assert_eq!(events.len(), 3); + + let basic_event = events + .iter() + .find(|e| e.name == "basic-event") + .expect("'basic' event should exist"); + let event_with_attributes = events + .iter() + .find(|e| e.name == "event-with-attributes") + .expect("'event_with_attributes' event should exist"); + let event_with_timestamp = events + .iter() + .find(|e| e.name == "event-with-timestamp") + .expect("'event_with_timestamp' event should exist"); + + assert!(basic_event.time_unix_nano < event_with_attributes.time_unix_nano); + assert_eq!(event_with_attributes.attributes.len(), 1); + assert!(event_with_attributes.time_unix_nano < event_with_timestamp.time_unix_nano); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_links() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/links"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(4, Duration::from_secs(5))); + + assert_eq!(spans.len(), 4); + + let first_span = spans + .iter() + .find(|s| s.name == "first") + .expect("'first' span should exist"); + let second_span = spans + .iter() + .find(|s| s.name == "second") + .expect("'second' span should exist"); + + assert_eq!(first_span.links.len(), 0); + assert_eq!(second_span.links.len(), 1); + assert_eq!( + second_span.links.first().unwrap().span_id, + first_span.span_id + ); + assert_eq!(second_span.links.first().unwrap().attributes.len(), 1); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_child_outlives_parent() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/child-outlives-parent"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(5, Duration::from_secs(5))); + + assert_eq!(spans.len(), 5); + + let parent_span = spans + .iter() + .find(|s| s.name == "parent") + .expect("'parent' span should exist"); + let child_span = spans + .iter() + .find(|s| s.name == "child") + .expect("'child' span should exist"); + let get_span = spans + .iter() + .find(|s| s.name == "GET") + .expect("'GET' span should exist"); + assert_eq!(child_span.parent_span_id, parent_span.span_id); + assert_eq!(get_span.parent_span_id, child_span.span_id); + assert!(child_span.end_time_unix_nano > parent_span.end_time_unix_nano); + + Ok(()) + }, + )?; + + Ok(()) + } + + #[test] + fn wasi_observe_root_span() -> anyhow::Result<()> { + let rt = tokio::runtime::Runtime::new()?; + let mut collector = rt + .block_on(FakeCollectorServer::start()) + .expect("fake collector server should start"); + let collector_endpoint = collector.endpoint().clone(); + + run_test_inited( + "wasi-observe-tracing", + SpinConfig { + binary_path: spin_binary(), + spin_up_args: Vec::new(), + app_type: SpinAppType::Http, + }, + ServicesConfig::none(), + |env| { + env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); + env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); + Ok(()) + }, + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::new(Method::Get, "/root-span"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(7, Duration::from_secs(5))); + + assert_eq!(spans.len(), 7); + + let parent_span = spans + .iter() + .find(|s| s.name == "parent") + .expect("'parent' span should exist"); + let request_one = spans + .iter() + .find(|s| s.name == "GET") + .expect("first 'GET' span should exist"); + let root_span = spans + .iter() + .find(|s| s.name == "root") + .expect("'root' span should exist"); + let request_two = spans + .iter() + .filter(|s| s.name == "GET") + .nth(1) + .expect("second 'GET' span should exist"); + let request_three = spans + .iter() + .filter(|s| s.name == "GET") + .nth(2) + .expect("third 'GET' span should exist"); + + assert_eq!(parent_span.trace_id, request_one.trace_id); + assert_ne!(root_span.trace_id, parent_span.trace_id); + assert_eq!(root_span.trace_id, request_two.trace_id); + assert_eq!(parent_span.trace_id, request_three.trace_id); + assert_eq!(root_span.parent_span_id, "".to_string()); + assert_eq!(request_two.parent_span_id, root_span.span_id); + assert_ne!(request_three.parent_span_id, parent_span.span_id); + + Ok(()) + }, + )?; + + Ok(()) + } +} diff --git a/tests/test-components/components/Cargo.lock b/tests/test-components/components/Cargo.lock index 1f62f17862..f69cae0bf7 100644 --- a/tests/test-components/components/Cargo.lock +++ b/tests/test-components/components/Cargo.lock @@ -14,6 +14,18 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "ai" version = "0.1.0" @@ -502,6 +514,15 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "otel-smoke-test" +version = "0.1.0" +dependencies = [ + "anyhow", + "http 0.2.11", + "spin-sdk 2.2.0", +] + [[package]] name = "outbound-http-component" version = "0.1.0" @@ -978,6 +999,15 @@ dependencies = [ "wit-bindgen 0.32.0", ] +name = "wasi-observe-tracing" +version = "0.1.0" +dependencies = [ + "anyhow", + "http 0.2.11", + "spin-sdk 2.2.0", + "wit-bindgen 0.30.0", +] + [[package]] name = "wasm-encoder" version = "0.36.2" diff --git a/tests/test-components/components/otel-smoke-test/Cargo.toml b/tests/test-components/components/otel-smoke-test/Cargo.toml new file mode 100644 index 0000000000..521468b790 --- /dev/null +++ b/tests/test-components/components/otel-smoke-test/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "otel-smoke-test" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +http = "0.2" +spin-sdk = "2.2.0" diff --git a/tests/test-components/components/otel-smoke-test/src/lib.rs b/tests/test-components/components/otel-smoke-test/src/lib.rs new file mode 100644 index 0000000000..85c4326102 --- /dev/null +++ b/tests/test-components/components/otel-smoke-test/src/lib.rs @@ -0,0 +1,22 @@ +use spin_sdk::{ + http::{Method, Params, Request, Response, Router}, + http_component, +}; + +#[http_component] +fn handle(req: http::Request<()>) -> Response { + let mut router = Router::new(); + router.get_async("/one", one); + router.get_async("/two", two); + router.handle(req) +} + +async fn one(_req: Request, _params: Params) -> Response { + let req = Request::builder().method(Method::Get).uri("/two").build(); + let _res: Response = spin_sdk::http::send(req).await.unwrap(); + Response::new(200, "") +} + +async fn two(_req: Request, _params: Params) -> Response { + Response::new(201, "") +} diff --git a/tests/test-components/components/wasi-observe-tracing/Cargo.toml b/tests/test-components/components/wasi-observe-tracing/Cargo.toml new file mode 100644 index 0000000000..3b4eb0c449 --- /dev/null +++ b/tests/test-components/components/wasi-observe-tracing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wasi-observe-tracing" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +http = "0.2" +spin-sdk = "2.2.0" +wit-bindgen = "0.30.0" diff --git a/tests/test-components/components/wasi-observe-tracing/src/lib.rs b/tests/test-components/components/wasi-observe-tracing/src/lib.rs new file mode 100644 index 0000000000..89ee43d2be --- /dev/null +++ b/tests/test-components/components/wasi-observe-tracing/src/lib.rs @@ -0,0 +1,147 @@ +wit_bindgen::generate!({ + path: "../../../../wit", + world: "wasi:observe/imports@0.2.0-draft", + generate_all, +}); + +use spin_sdk::{ + http::{Method, Params, Request, Response, Router}, + http_component, +}; +use wasi::{ + clocks0_2_0::wall_clock::now, + observe::tracer::{self, KeyValue, Link, StartOptions, Value}, +}; + +#[http_component] +fn handle(req: http::Request<()>) -> Response { + let mut router = Router::new(); + router.get("/nested-spans", nested_spans); + router.get("/drop-semantics", drop_semantics); + router.get("/setting-attributes", setting_attributes); + router.get_async("/host-guest-host", host_guest_host); + router.get("/events", events); + router.get_async("/child-outlives-parent", child_outlives_parent); + router.get("/links", links); + router.get_async("/root-span", root_span); + router.handle(req) +} + +fn nested_spans(_req: Request, _params: Params) -> Response { + let span = tracer::start("outer_func", None); + inner_func(); + span.end(None); + Response::new(200, "") +} + +fn inner_func() { + let span = tracer::start("inner_func", None); + span.end(None); +} + +fn drop_semantics(_req: Request, _params: Params) -> Response { + let _span = tracer::start("drop_semantics", None); + Response::new(200, "") + // _span will drop here and should be ended +} + +fn setting_attributes(_req: Request, _params: Params) -> Response { + let span = tracer::start("setting_attributes", None); + span.set_attributes(&[KeyValue { + key: "foo".to_string(), + value: Value::String("bar".to_string()), + }]); + span.set_attributes(&[ + KeyValue { + key: "foo".to_string(), + value: Value::String("baz".to_string()), + }, + KeyValue { + key: "qux".to_string(), + value: Value::StringArray(vec!["qaz".to_string(), "thud".to_string()]), + }, + ]); + span.end(None); + Response::new(200, "") +} + +async fn host_guest_host(_req: Request, _params: Params) -> Response { + let span = tracer::start("guest", None); + + make_request().await; + span.end(None); + + Response::new(200, "") +} + +fn events(_req: Request, _params: Params) -> Response { + let span = tracer::start("events", None); + span.add_event("basic-event", None, None); + span.add_event( + "event-with-attributes", + None, + Some(&[KeyValue { + key: "foo".to_string(), + value: Value::Bool(true), + }]), + ); + let mut now_plus = now(); + now_plus.seconds += 1; + span.add_event("event-with-timestamp", Some(now_plus), None); + span.end(None); + Response::new(200, "") +} + +async fn child_outlives_parent(_req: Request, _params: Params) -> Response { + let span = tracer::start("parent", None); + let span2 = tracer::start("child", None); + span.end(None); + // Make a host call to test span reparenting when we're messing with the active span stack + make_request().await; + + span2.end(None); + Response::new(200, "") +} + +fn links(_req: Request, _params: Params) -> Response { + let first = tracer::start("first", None); + first.end(None); + let second = tracer::start("second", None); + second.add_link(&Link { + span_context: first.span_context(), + attributes: vec![KeyValue { + key: "foo".to_string(), + value: Value::String("bar".to_string()), + }], + }); + second.end(None); + Response::new(200, "") +} + +async fn root_span(_req: Request, _params: Params) -> Response { + let span1 = tracer::start("parent", None); + make_request().await; + let root = tracer::start( + "root", + Some(&StartOptions { + new_root: true, + span_kind: None, + attributes: None, + links: None, + timestamp: None, + }), + ); + make_request().await; + root.end(None); + span1.end(None); + make_request().await; + Response::new(200, "") +} + +async fn make_request() { + let req = Request::builder() + .method(Method::Get) + .uri("https://asdf.com") + .build(); + let _res: Response = spin_sdk::http::send(req).await.unwrap(); +} diff --git a/tests/testcases/otel-smoke-test/spin.toml b/tests/testcases/otel-smoke-test/spin.toml index a4eb09f671..909149421f 100644 --- a/tests/testcases/otel-smoke-test/spin.toml +++ b/tests/testcases/otel-smoke-test/spin.toml @@ -1,12 +1,13 @@ spin_version = "1" authors = ["Fermyon Engineering "] -description = "A simple application that returns hello and goodbye." -name = "head-rust-sdk-http" +description = "A simple application that tests otel." +name = "otel-smoke-test" trigger = { type = "http" } version = "1.0.0" [[component]] -id = "hello" -source = "%{source=hello-world}" +id = "otel" +source = "%{source=otel-smoke-test}" +allowed_outbound_hosts = ["http://self"] [component.trigger] -route = "/hello/..." +route = "/..." diff --git a/tests/testcases/wasi-observe-tracing/spin.toml b/tests/testcases/wasi-observe-tracing/spin.toml new file mode 100644 index 0000000000..cc4698b5f9 --- /dev/null +++ b/tests/testcases/wasi-observe-tracing/spin.toml @@ -0,0 +1,19 @@ +spin_manifest_version = 2 + +[application] +authors = ["Fermyon Engineering "] +description = "An application to exercise wasi-observe tracing functionality." +name = "wasi-observe-tracing" +version = "1.0.0" + +[[trigger.http]] +route = "/..." +component = "wasi-observe-tracing" + +[component.wasi-observe-tracing] +source = "%{source=wasi-observe-tracing}" +key_value_stores = ["default"] +allowed_outbound_hosts = ["http://self", "https://asdf.com"] +[component.wasi-observe-tracing.build] +command = "cargo build --target wasm32-wasi --release" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/wit/deps/observe/tracer.wit b/wit/deps/observe/tracer.wit new file mode 100644 index 0000000000..cac11286c3 --- /dev/null +++ b/wit/deps/observe/tracer.wit @@ -0,0 +1,151 @@ +interface tracer { + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + /// Starts a new `span` with the given name and options. + start: func(name: string, options: option) -> span; + + /// Represents a unit of work or operation. + resource span { + /// Get the `span-context` for this `span`. + span-context: func() -> span-context; + + /// Returns true when the data provided to this `span` is captured in some form. If it returns false then any data provided is discarded. + is-recording: func() -> bool; + + /// Set attributes of this span. + /// + /// If a key already exists for an attribute of the Span it will be overwritten with the corresponding new value. + set-attributes: func(attributes: list); + + /// Adds an event with the provided name at the curent timestamp. + /// + /// Optionally an alternative timestamp may be provided. You may also provide attributes of this event. + add-event: func(name: string, timestamp: option, attributes: option>); + + /// Associates this `span` with another. + add-link: func(link: link); + + /// Override the default `span` status, which is unset. + set-status: func(status: status); + + /// Updates the `span` name. + update-name: func(name: string); + + /// Signals that the operation described by this span has now ended. + /// + /// If a timestamp is not provided then it is treated equivalent to passing the current time. Dropping this resource is the equivalent of calling `end(none)`. + end: func(timestamp: option); + } + + /// Configuration for starting a `span`. + record start-options { + /// Whether this span should act as the root of a new trace. + new-root: bool, + /// `span-kind` for the new `span`. + span-kind: option, + /// Attributes for the new `span`. + attributes: option>, + /// `link`'s for the new `span`. + links: option>, + /// When the `span` should begin. If this is not provided it defaults to the current time. + timestamp: option, + } + + /// Describes a relationship to another `span`. + record link { + /// Denotes which `span` to link to. + span-context: span-context, + /// Attributes describing the link. + attributes: list, + } + + /// Describes the relationship between the Span, its parents, and its children in a trace. + enum span-kind { + /// Indicates that the span describes a request to some remote service. This span is usually the parent of a remote server span and does not end until the response is received. + client, + /// Indicates that the span covers server-side handling of a synchronous RPC or other remote request. This span is often the child of a remote client span that was expected to wait for a response. + server, + /// Indicates that the span describes the initiators of an asynchronous request. This parent span will often end before the corresponding child consumer span, possibly even before the child span starts. In messaging scenarios with batching, tracing individual messages requires a new producer span per message to be created. + producer, + /// Indicates that the span describes a child of an asynchronous consumer request. + consumer, + /// Default value. Indicates that the span represents an internal operation within an application, as opposed to an operations with remote parents or children. + internal + } + + /// The `status` of a `span`. + variant status { + /// The default status. + unset, + /// The operation has been validated by an Application developer or Operator to have completed successfully. + ok, + /// The operation contains an error with a description. + error(string), + } + + /// A key-value pair describing an attribute. + record key-value { + /// The attribute name. + key: key, + /// The attribute value. + value: value, + } + + /// The key part of attribute `key-value` pairs. + type key = string; + + /// The value part of attribute `key-value` pairs. + variant value { + /// A string value. + %string(string), + /// A boolean value. + %bool(bool), + /// A double precision floating point value. + %float64(float64), + /// A signed 64 bit integer value. + %s64(s64), + /// A homogeneous array of string values. + string-array(list), + /// A homogeneous array of boolean values. + bool-array(list), + /// A homogeneous array of double precision floating point values. + float64-array(list), + /// A homogeneous array of 64 bit integer values. + s64-array(list), + } + + /// Identifying trace information about a span that can be serialized and propagated. + record span-context { + /// The `trace-id` for this `span-context`. + trace-id: trace-id, + /// The `span-id` for this `span-context`. + span-id: span-id, + /// The `trace-flags` for this `span-context`. + trace-flags: trace-flags, + /// Whether this `span-context` was propagated from a remote parent. + is-remote: bool, + /// The `trace-state` for this `span-context`. + trace-state: trace-state, + } + + /// The trace that this `span-context` belongs to. + /// + /// 16 bytes encoded as a hexadecimal string. + type trace-id = string; + + /// The id of this `span-context`. + /// + /// 8 bytes encoded as a hexadecimal string. + type span-id = string; + + /// Flags that can be set on a `span-context`. + flags trace-flags { + /// Whether the `span` should be sampled or not. + sampled, + } + + /// Carries system-specific configuration data, represented as a list of key-value pairs. `trace-state` allows multiple tracing systems to participate in the same trace. + /// + /// If any invalid keys or values are provided then the `trace-state` will be treated as an empty list. + type trace-state = list>; +} diff --git a/wit/deps/observe/world.wit b/wit/deps/observe/world.wit new file mode 100644 index 0000000000..063b1ac821 --- /dev/null +++ b/wit/deps/observe/world.wit @@ -0,0 +1,8 @@ +package wasi:observe@0.2.0-draft; + +world imports { + import tracer; +} + +// TODO(Reviewer): Should we make this an experimental wasi package or a Spin package +// TODO(Reviewer): Would adding a metrics interface to this in a future version be a breaking change? diff --git a/wit/world.wit b/wit/world.wit index 07da9cd4bf..8a328d9bda 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -8,6 +8,7 @@ world http-trigger { /// The imports needed for a guest to run on a Spin host world platform { + include wasi:observe/imports@0.2.0-draft; include fermyon:spin/platform@2.0.0; include wasi:keyvalue/imports@0.2.0-draft2; import spin:postgres/postgres@3.0.0; From 3f98464e24864d06741467ae7d0136d410da5706 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Wed, 18 Sep 2024 11:01:47 -0600 Subject: [PATCH 02/16] Fix traces and metrics not working. Fix slow tests. Rename variable Signed-off-by: Caleb Schoepp --- crates/factor-observe/src/lib.rs | 4 ++-- crates/telemetry/src/traces.rs | 2 ++ tests/integration.rs | 18 +++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/factor-observe/src/lib.rs b/crates/factor-observe/src/lib.rs index 9a8a5a4dc8..640c5f6f07 100644 --- a/crates/factor-observe/src/lib.rs +++ b/crates/factor-observe/src/lib.rs @@ -146,7 +146,7 @@ impl ObserveContext { }; // If there are no active guest spans then there is nothing to do - let Some(current_span_id) = state.active_spans.last() else { + let Some(active_span) = state.active_spans.last() else { return; }; @@ -166,7 +166,7 @@ impl ObserveContext { // Now reparent the current span to the last active guest span let span_context = state .guest_spans - .get(*current_span_id) + .get(*active_span) .unwrap() .inner .span_context() diff --git a/crates/telemetry/src/traces.rs b/crates/telemetry/src/traces.rs index 25fa2e07f2..e0b6a916b6 100644 --- a/crates/telemetry/src/traces.rs +++ b/crates/telemetry/src/traces.rs @@ -57,6 +57,8 @@ pub(crate) fn otel_tracing_layer LookupSpan<'span>>( global::set_tracer_provider(tracer_provider.clone()); + global::set_tracer_provider(tracer_provider.clone()); + let env_filter = match EnvFilter::try_from_env("SPIN_OTEL_TRACING_LEVEL") { Ok(filter) => filter, // If it isn't set or it fails to parse default to info diff --git a/tests/integration.rs b/tests/integration.rs index b01ba3a092..0759fe942f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1431,7 +1431,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1474,7 +1474,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1544,7 +1544,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1609,7 +1609,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1670,7 +1670,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1732,7 +1732,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1798,7 +1798,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1856,7 +1856,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) @@ -1913,7 +1913,7 @@ mod otel_integration_tests { }, ServicesConfig::none(), |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_ENDPOINT", collector_endpoint); + env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); Ok(()) From 7bf9a5aa88378b600c3fd1763682b718d49b0a22 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Sat, 15 Feb 2025 16:56:36 -0700 Subject: [PATCH 03/16] WIP: Broken attempt to back wasi-otel. Failing on the propagator. Something to do with tracing::Span::current() Signed-off-by: Caleb Schoepp --- Cargo.lock | 3 + crates/factor-observe/Cargo.toml | 2 + crates/factor-observe/src/host.rs | 293 +++++------------- crates/factor-observe/src/lib.rs | 81 +++-- crates/telemetry/src/env.rs | 18 +- crates/telemetry/src/lib.rs | 2 +- crates/telemetry/src/traces.rs | 2 - crates/world/Cargo.toml | 1 + crates/world/src/conversions.rs | 186 ++++++----- wit/deps/observe/world.wit | 8 - .../{observe/tracer.wit => otel/tracing.wit} | 106 ++++--- wit/deps/otel/world.wit | 5 + wit/world.wit | 2 +- 13 files changed, 334 insertions(+), 375 deletions(-) delete mode 100644 wit/deps/observe/world.wit rename wit/deps/{observe/tracer.wit => otel/tracing.wit} (65%) create mode 100644 wit/deps/otel/world.wit diff --git a/Cargo.lock b/Cargo.lock index cafe919b36..459db7a260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7919,10 +7919,12 @@ dependencies = [ "anyhow", "indexmap 2.7.1", "opentelemetry 0.27.0", + "opentelemetry-otlp 0.27.0", "opentelemetry_sdk 0.27.0", "spin-core", "spin-factors", "spin-resource-table", + "spin-telemetry", "spin-world", "toml 0.5.11", "tracing", @@ -8637,6 +8639,7 @@ dependencies = [ "anyhow", "async-trait", "opentelemetry 0.27.0", + "opentelemetry_sdk 0.27.0", "wasmtime", ] diff --git a/crates/factor-observe/Cargo.toml b/crates/factor-observe/Cargo.toml index dfde761caa..8410134e3b 100644 --- a/crates/factor-observe/Cargo.toml +++ b/crates/factor-observe/Cargo.toml @@ -9,9 +9,11 @@ anyhow = { workspace = true } indexmap = "2.2.6" opentelemetry = { workspace = true } opentelemetry_sdk = { workspace = true } +opentelemetry-otlp = { version = "0.27", features = ["http-proto", "http", "reqwest-client"] } spin-core = { path = "../core" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } +spin-telemetry = { path = "../telemetry" } spin-world = { path = "../world" } tracing = { workspace = true } tracing-opentelemetry = { workspace = true } diff --git a/crates/factor-observe/src/host.rs b/crates/factor-observe/src/host.rs index b4a401ebd0..226afa63fb 100644 --- a/crates/factor-observe/src/host.rs +++ b/crates/factor-observe/src/host.rs @@ -1,244 +1,117 @@ -use std::time::SystemTime; +// use std::time::SystemTime; use anyhow::anyhow; use anyhow::Result; -use opentelemetry::global::ObjectSafeSpan; use opentelemetry::trace::TraceContextExt; -use opentelemetry::trace::Tracer; use opentelemetry::Context; +use opentelemetry_sdk::trace::SpanProcessor; use spin_core::async_trait; -use spin_core::wasmtime::component::Resource; -use spin_world::wasi::observe::tracer; +use spin_world::wasi::otel::tracing as wasi_otel; +use spin_world::wasi::otel::tracing::SpanContext; +use tracing::span; + use tracing_opentelemetry::OpenTelemetrySpanExt; -use crate::{GuestSpan, InstanceState}; +use crate::InstanceState; #[async_trait] -impl tracer::Host for InstanceState { - async fn start( +impl wasi_otel::Host for InstanceState { + async fn on_start( &mut self, - name: String, - options: Option, - ) -> Result> { + span_data: wasi_otel::SpanData, + _parent: wasi_otel::SpanContext, + ) -> Result<()> { let mut state = self.state.write().unwrap(); - let options = options.unwrap_or_default(); // Before we ever create any new spans make sure we track the original host span ID if state.original_host_span_id.is_none() { - state.original_host_span_id = Some( - tracing::Span::current() - .context() - .span() - .span_context() - .span_id(), - ); + // TODO: Note this is also failing to get anything meaningful through tracing::Span::current() + let context = dbg!(tracing::Span::current().context()); + let span = dbg!(context.span()); + let span_context = dbg!(span.span_context()); + state.original_host_span_id = dbg!(Some(span_context.span_id())); } - // Get span's parent based on whether it's a new root and whether there are any active spans - let parent_context = match (options.new_root, state.active_spans.is_empty()) { - // Not a new root && Active spans -> Last active guest span is parent - (false, false) => { - let span_context = state - .guest_spans - .get(*state.active_spans.last().unwrap()) - .unwrap() - .inner - .span_context() - .clone(); - Context::new().with_remote_span_context(span_context) - } - // Not a new root && No active spans -> Current host span is parent - (false, true) => tracing::Span::current().context(), - // New root && n/a -> No parent - (true, _) => Context::new(), - }; + // // Get span's parent based on whether it's a new root and whether there are any active spans + // let parent_context = match (false, state.active_spans.is_empty()) { + // // Not a new root && Active spans -> Last active guest span is parent + // (false, false) => { + // let span_context = state + // .guest_spans + // .get(*state.active_spans.last().unwrap()) + // .unwrap() + // .inner + // .span_context() + // .clone(); + // Context::new().with_remote_span_context(span_context) + // } + // // Not a new root && No active spans -> Current host span is parent + // (false, true) => tracing::Span::current().context(), + // // New root && n/a -> No parent + // (true, _) => Context::new(), + // }; // Create the underlying opentelemetry span - let mut builder = self.tracer.span_builder(name); - if let Some(kind) = options.span_kind { - builder = builder.with_kind(kind.into()); - } - if let Some(attributes) = options.attributes { - builder = builder.with_attributes(attributes.into_iter().map(Into::into)); - } - if let Some(links) = options.links { - builder = builder.with_links(links.into_iter().map(Into::into).collect()); - } - if let Some(timestamp) = options.timestamp { - builder = builder.with_start_time(timestamp); - } - let otel_span = builder.start_with_context(&self.tracer, &parent_context); - - // Wrap it in a GuestSpan for our own bookkeeping purposes - let guest_span = GuestSpan { inner: otel_span }; - - // Put the GuestSpan in our resource table and push it on to our stack of active spans - let resource_id = state.guest_spans.push(guest_span).unwrap(); - state.active_spans.insert(resource_id); - - Ok(Resource::new_own(resource_id)) - } -} - -#[async_trait] -impl tracer::HostSpan for InstanceState { - async fn span_context( - &mut self, - resource: Resource, - ) -> Result { - if let Some(guest_span) = self.state.read().unwrap().guest_spans.get(resource.rep()) { - Ok(guest_span.inner.span_context().clone().into()) - } else { - Err(anyhow!("BUG: cannot find resource in table")) - } - } - - async fn is_recording(&mut self, resource: Resource) -> Result { - if let Some(guest_span) = self.state.read().unwrap().guest_spans.get(resource.rep()) { - Ok(guest_span.inner.is_recording()) - } else { - Err(anyhow!("BUG: cannot find resource in table")) - } - } - - async fn set_attributes( - &mut self, - resource: Resource, - attributes: Vec, - ) -> Result<()> { - if let Some(guest_span) = self - .state - .write() - .unwrap() - .guest_spans - .get_mut(resource.rep()) - { - for attribute in attributes { - guest_span.inner.set_attribute(attribute.into()); - } - Ok(()) - } else { - Err(anyhow!("BUG: cannot find resource in table")) - } - } - - async fn add_event( - &mut self, - resource: Resource, - name: String, - timestamp: Option, - attributes: Option>, - ) -> Result<()> { - if let Some(guest_span) = self - .state - .write() - .unwrap() - .guest_spans - .get_mut(resource.rep()) - { - let timestamp = timestamp.map(Into::into).unwrap_or_else(SystemTime::now); - let attributes = if let Some(attributes) = attributes { - attributes.into_iter().map(Into::into).collect() - } else { - vec![] - }; - - guest_span - .inner - .add_event_with_timestamp(name.into(), timestamp, attributes); + // let builder = self.tracer.span_builder(span_data.name); + // if let Some(kind) = options.span_kind { + // builder = builder.with_kind(kind.into()); + // } + // if let Some(attributes) = options.attributes { + // builder = builder.with_attributes(attributes.into_iter().map(Into::into)); + // } + // if let Some(links) = options.links { + // builder = builder.with_links(links.into_iter().map(Into::into).collect()); + // } + // if let Some(timestamp) = options.timestamp { + // builder = builder.with_start_time(timestamp); + // } + // let otel_span = builder.start_with_context( + // &self.tracer, + // &Context::new().with_remote_span_context(parent.into()), + // ); + // let span_id = otel_span.span_context().span_id(); + + // Put the span in our map and push it on to our stack of active spans + let span_context = + std::convert::Into::::into(span_data.span_context); + let span_id = span_context.span_id(); + state.guest_span_contexts.insert(span_id, span_context); + state.active_spans.insert(span_id); - Ok(()) - } else { - Err(anyhow!("BUG: cannot find resource in table")) - } - } - - async fn add_link( - &mut self, - resource: Resource, - link: tracer::Link, - ) -> Result<()> { - if let Some(guest_span) = self - .state - .write() - .unwrap() - .guest_spans - .get_mut(resource.rep()) - { - guest_span.inner.add_link( - link.span_context.into(), - link.attributes.into_iter().map(Into::into).collect(), - ); - Ok(()) - } else { - Err(anyhow!("BUG: cannot find resource in table")) - } - } - - async fn set_status( - &mut self, - resource: Resource, - status: tracer::Status, - ) -> Result<()> { - if let Some(guest_span) = self - .state - .write() - .unwrap() - .guest_spans - .get_mut(resource.rep()) - { - guest_span.inner.set_status(status.into()); - Ok(()) - } else { - Err(anyhow!("BUG: cannot find resource in table")) - } - } - - async fn update_name(&mut self, resource: Resource, name: String) -> Result<()> { - if let Some(guest_span) = self - .state - .write() - .unwrap() - .guest_spans - .get_mut(resource.rep()) - { - guest_span.inner.update_name(name.into()); - Ok(()) - } else { - Err(anyhow!("BUG: cannot find resource in table")) - } + Ok(()) } - async fn end( - &mut self, - resource: Resource, - timestamp: Option, - ) -> Result<()> { + async fn on_end(&mut self, span_data: wasi_otel::SpanData) -> Result<()> { let mut state = self.state.write().unwrap(); - if let Some(guest_span) = state.guest_spans.get_mut(resource.rep()) { - if let Some(timestamp) = timestamp { - guest_span.inner.end_with_timestamp(timestamp.into()); - } else { - guest_span.inner.end(); - } + + let span_id = std::convert::Into::::into( + span_data.span_context.clone(), + ) + .span_id(); + self.processor.on_end(span_data.into()); + if let Some(_guest_span) = state.guest_span_contexts.get_mut(&span_id) { + // // TODO: Transfer all the data + // guest_span.end_with_timestamp(span_data.end_time.into()); // Remove the span from active_spans - state.active_spans.shift_remove(&resource.rep()); + state.active_spans.shift_remove(&span_id); Ok(()) } else { + // TODO: This seems to be wrong Err(anyhow!("BUG: cannot find resource in table")) } + // Ok(()) } - async fn drop(&mut self, resource: Resource) -> Result<()> { - // Dropping the resource automatically calls drop on the Span which ends itself with the - // current timestamp if the Span is not already ended. - - // Ensure that the span has been removed from active_spans - let mut state = self.state.write().unwrap(); - state.active_spans.shift_remove(&resource.rep()); - - Ok(()) + async fn current_span_context(&mut self) -> Result { + // TODO: The bug is that tracing::Span::current() is not returning anything + let context = tracing::Span::current().context(); + let span = context.span(); + let span_context = span.span_context(); + let out: SpanContext = span_context.clone().into(); + Ok(out) } } + +// TODO: Rename module to otel diff --git a/crates/factor-observe/src/lib.rs b/crates/factor-observe/src/lib.rs index 640c5f6f07..e2b6bbbd53 100644 --- a/crates/factor-observe/src/lib.rs +++ b/crates/factor-observe/src/lib.rs @@ -1,14 +1,24 @@ mod host; -use std::sync::{Arc, RwLock}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, + time::Duration, +}; +use anyhow::bail; use indexmap::IndexSet; use opentelemetry::{ - global::{self, BoxedTracer, ObjectSafeSpan}, - trace::{SpanId, TraceContextExt}, + trace::{SpanContext, SpanId, TraceContextExt}, Context, }; +use opentelemetry_sdk::{ + resource::{EnvResourceDetector, TelemetryResourceDetector}, + trace::{SimpleSpanProcessor, SpanProcessor}, + Resource, +}; use spin_factors::{Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder}; +use spin_telemetry::{detector::SpinResourceDetector, env::OtlpProtocol}; use tracing_opentelemetry::OpenTelemetrySpanExt; #[derive(Default)] @@ -23,7 +33,7 @@ impl Factor for ObserveFactor { &mut self, mut ctx: spin_factors::InitContext, ) -> anyhow::Result<()> { - ctx.link_bindings(spin_world::wasi::observe::tracer::add_to_linker)?; + ctx.link_bindings(spin_world::wasi::otel::tracing::add_to_linker)?; Ok(()) } @@ -36,16 +46,39 @@ impl Factor for ObserveFactor { fn prepare( &self, - ctx: spin_factors::PrepareContext, + _: spin_factors::PrepareContext, ) -> anyhow::Result { - let tracer = global::tracer(ctx.app_component().app.id().to_string()); + // This will configure the exporter based on the OTEL_EXPORTER_* environment variables. + let exporter = match OtlpProtocol::traces_protocol_from_env() { + OtlpProtocol::Grpc => opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .build()?, + OtlpProtocol::HttpProtobuf => opentelemetry_otlp::SpanExporter::builder() + .with_http() + .build()?, + OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"), + }; + let mut processor = opentelemetry_sdk::trace::SimpleSpanProcessor::new(Box::new(exporter)); + // TODO: Allow guest to dynamically set resource of the processor? + processor.set_resource(&Resource::from_detectors( + Duration::from_secs(5), + vec![ + // Set service.name from env OTEL_SERVICE_NAME > env OTEL_RESOURCE_ATTRIBUTES > spin + // Set service.version from Spin metadata + Box::new(SpinResourceDetector::new("27.0.1 todo".to_string())), + // Sets fields from env OTEL_RESOURCE_ATTRIBUTES + Box::new(EnvResourceDetector::new()), + // Sets telemetry.sdk{name, language, version} + Box::new(TelemetryResourceDetector), + ], + )); Ok(InstanceState { state: Arc::new(RwLock::new(State { - guest_spans: Default::default(), + guest_span_contexts: Default::default(), active_spans: Default::default(), original_host_span_id: None, })), - tracer, + processor, }) } } @@ -58,7 +91,7 @@ impl ObserveFactor { pub struct InstanceState { pub(crate) state: Arc>, - pub(crate) tracer: BoxedTracer, + pub(crate) processor: SimpleSpanProcessor, } impl SelfInstanceBuilder for InstanceState {} @@ -68,14 +101,16 @@ impl SelfInstanceBuilder for InstanceState {} /// This data lives here rather than directly on InstanceState so that we can have multiple things /// take Arc references to it. pub(crate) struct State { - /// A resource table that holds the guest spans. - pub(crate) guest_spans: spin_resource_table::Table, + /// A mapping between immutable [SpanId]s and the actual [BoxedSpan] created by our tracer. + // TODO: Rename to not include "guest" + // TODO: Merge with active_spans + pub(crate) guest_span_contexts: HashMap, - /// A stack of resource ids for all the active guest spans. The topmost span is the active span. + /// A stack of [SpanIds] for all the active spans. The topmost span is the active span. /// - /// When a guest span is ended it is removed from this stack (regardless of whether is the + /// When a span is ended it is removed from this stack (regardless of whether is the /// active span) and all other spans are shifted back to retain relative order. - pub(crate) active_spans: IndexSet, + pub(crate) active_spans: IndexSet, /// Id of the last span emitted from within the host before entering the guest. /// @@ -84,11 +119,11 @@ pub(crate) struct State { pub(crate) original_host_span_id: Option, } -/// The WIT resource Span. Effectively wraps an [opentelemetry::global::BoxedSpan]. -pub struct GuestSpan { - /// The [opentelemetry::global::BoxedSpan] we use to do the actual tracing work. - pub inner: opentelemetry::global::BoxedSpan, -} +// /// The WIT resource Span. Effectively wraps an [opentelemetry::global::BoxedSpan]. +// pub struct GuestSpan { +// /// The [opentelemetry::global::BoxedSpan] we use to do the actual tracing work. +// pub inner: BoxedSpan, +// } /// Manages access to the ObserveFactor state for the purpose of maintaining proper span /// parent/child relationships when WASI Observe spans are being created. @@ -164,13 +199,7 @@ impl ObserveContext { } // Now reparent the current span to the last active guest span - let span_context = state - .guest_spans - .get(*active_span) - .unwrap() - .inner - .span_context() - .clone(); + let span_context = state.guest_span_contexts.get(active_span).unwrap().clone(); let parent_context = Context::new().with_remote_span_context(span_context); tracing::Span::current().set_parent(parent_context); } diff --git a/crates/telemetry/src/env.rs b/crates/telemetry/src/env.rs index 101c671f34..4078d6407b 100644 --- a/crates/telemetry/src/env.rs +++ b/crates/telemetry/src/env.rs @@ -19,7 +19,7 @@ const SPIN_DISABLE_LOG_TO_TRACING: &str = "SPIN_DISABLE_LOG_TO_TRACING"; /// - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` /// /// Note that this is overridden if OTEL_SDK_DISABLED is set and not empty. -pub(crate) fn otel_tracing_enabled() -> bool { +pub fn otel_tracing_enabled() -> bool { any_vars_set(&[ OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, @@ -33,7 +33,7 @@ pub(crate) fn otel_tracing_enabled() -> bool { /// - `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` /// /// Note that this is overridden if OTEL_SDK_DISABLED is set and not empty. -pub(crate) fn otel_metrics_enabled() -> bool { +pub fn otel_metrics_enabled() -> bool { any_vars_set(&[ OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, @@ -47,7 +47,7 @@ pub(crate) fn otel_metrics_enabled() -> bool { /// - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` /// /// Note that this is overridden if OTEL_SDK_DISABLED is set and not empty. -pub(crate) fn otel_logs_enabled() -> bool { +pub fn otel_logs_enabled() -> bool { any_vars_set(&[ OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, @@ -59,7 +59,7 @@ pub(crate) fn otel_logs_enabled() -> bool { /// /// It is considered disabled if the environment variable `SPIN_DISABLED_LOG_TO_TRACING` is set and not /// empty. By default the features is enabled. -pub(crate) fn spin_disable_log_to_tracing() -> bool { +pub fn spin_disable_log_to_tracing() -> bool { any_vars_set(&[SPIN_DISABLE_LOG_TO_TRACING]) } @@ -72,12 +72,12 @@ fn any_vars_set(enabling_vars: &[&str]) -> bool { /// Returns a boolean indicating if the OTEL SDK should be disabled for all signals. /// /// It is considered disabled if the environment variable `OTEL_SDK_DISABLED` is set and not empty. -pub(crate) fn otel_sdk_disabled() -> bool { +pub fn otel_sdk_disabled() -> bool { std::env::var_os(OTEL_SDK_DISABLED).is_some_and(|val| !val.is_empty()) } /// The protocol to use for OTLP exporter. -pub(crate) enum OtlpProtocol { +pub enum OtlpProtocol { Grpc, HttpProtobuf, HttpJson, @@ -85,7 +85,7 @@ pub(crate) enum OtlpProtocol { impl OtlpProtocol { /// Returns the protocol to be used for exporting traces as defined by the environment. - pub(crate) fn traces_protocol_from_env() -> Self { + pub fn traces_protocol_from_env() -> Self { Self::protocol_from_env( std::env::var(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL), std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL), @@ -93,7 +93,7 @@ impl OtlpProtocol { } /// Returns the protocol to be used for exporting metrics as defined by the environment. - pub(crate) fn metrics_protocol_from_env() -> Self { + pub fn metrics_protocol_from_env() -> Self { Self::protocol_from_env( std::env::var(OTEL_EXPORTER_OTLP_METRICS_PROTOCOL), std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL), @@ -101,7 +101,7 @@ impl OtlpProtocol { } /// Returns the protocol to be used for exporting logs as defined by the environment. - pub(crate) fn logs_protocol_from_env() -> Self { + pub fn logs_protocol_from_env() -> Self { Self::protocol_from_env( std::env::var(OTEL_EXPORTER_OTLP_LOGS_PROTOCOL), std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL), diff --git a/crates/telemetry/src/lib.rs b/crates/telemetry/src/lib.rs index 69ba52ee76..c5482b3450 100644 --- a/crates/telemetry/src/lib.rs +++ b/crates/telemetry/src/lib.rs @@ -7,7 +7,7 @@ use opentelemetry_sdk::propagation::TraceContextPropagator; use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter, Layer}; pub mod detector; -mod env; +pub mod env; pub mod logs; pub mod metrics; mod propagation; diff --git a/crates/telemetry/src/traces.rs b/crates/telemetry/src/traces.rs index e0b6a916b6..25fa2e07f2 100644 --- a/crates/telemetry/src/traces.rs +++ b/crates/telemetry/src/traces.rs @@ -57,8 +57,6 @@ pub(crate) fn otel_tracing_layer LookupSpan<'span>>( global::set_tracer_provider(tracer_provider.clone()); - global::set_tracer_provider(tracer_provider.clone()); - let env_filter = match EnvFilter::try_from_env("SPIN_OTEL_TRACING_LEVEL") { Ok(filter) => filter, // If it isn't set or it fails to parse default to info diff --git a/crates/world/Cargo.toml b/crates/world/Cargo.toml index 77f26a7a05..4508fb7e6a 100644 --- a/crates/world/Cargo.toml +++ b/crates/world/Cargo.toml @@ -8,4 +8,5 @@ edition = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } opentelemetry = { workspace = true } +opentelemetry_sdk = { workspace = true } wasmtime = { workspace = true } diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 8d41dae647..83a4b37e9e 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -498,53 +498,75 @@ mod llm { mod observe { use super::*; use opentelemetry::StringValue; + use opentelemetry_sdk::trace::{SpanEvents, SpanLinks}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; - use wasi::observe::tracer; + use wasi::clocks0_2_0::wall_clock; + use wasi::otel::tracing as wasi_otel; - impl From for opentelemetry::Value { - fn from(value: tracer::Value) -> Self { + impl From for opentelemetry_sdk::export::trace::SpanData { + fn from(value: wasi_otel::SpanData) -> Self { + Self { + span_context: value.span_context.into(), + parent_span_id: opentelemetry::trace::SpanId::from_hex(&value.parent_span_id) + .expect("TODO THIS IS BAD"), + span_kind: value.span_kind.into(), + name: value.name.into(), + start_time: value.start_time.into(), + end_time: value.end_time.into(), + attributes: value.attributes.into_iter().map(Into::into).collect(), + dropped_attributes_count: 0, + events: SpanEvents::default(), // TODO + links: SpanLinks::default(), // TODO + status: value.status.into(), + instrumentation_scope: value.instrumentation_scope.into(), + } + } + } + + impl From for opentelemetry::Value { + fn from(value: wasi_otel::Value) -> Self { match value { - tracer::Value::String(v) => v.into(), - tracer::Value::Bool(v) => v.into(), - tracer::Value::Float64(v) => v.into(), - tracer::Value::S64(v) => v.into(), - tracer::Value::StringArray(v) => opentelemetry::Value::Array( + wasi_otel::Value::String(v) => v.into(), + wasi_otel::Value::Bool(v) => v.into(), + wasi_otel::Value::Float64(v) => v.into(), + wasi_otel::Value::S64(v) => v.into(), + wasi_otel::Value::StringArray(v) => opentelemetry::Value::Array( v.into_iter() .map(StringValue::from) .collect::>() .into(), ), - tracer::Value::BoolArray(v) => opentelemetry::Value::Array(v.into()), - tracer::Value::Float64Array(v) => opentelemetry::Value::Array(v.into()), - tracer::Value::S64Array(v) => opentelemetry::Value::Array(v.into()), + wasi_otel::Value::BoolArray(v) => opentelemetry::Value::Array(v.into()), + wasi_otel::Value::Float64Array(v) => opentelemetry::Value::Array(v.into()), + wasi_otel::Value::S64Array(v) => opentelemetry::Value::Array(v.into()), } } } - impl From for opentelemetry::KeyValue { - fn from(kv: tracer::KeyValue) -> Self { + impl From for opentelemetry::KeyValue { + fn from(kv: wasi_otel::KeyValue) -> Self { opentelemetry::KeyValue::new(kv.key, kv.value) } } - impl From for opentelemetry::trace::TraceFlags { - fn from(flags: tracer::TraceFlags) -> Self { + impl From for opentelemetry::trace::TraceFlags { + fn from(flags: wasi_otel::TraceFlags) -> Self { Self::new(flags.as_array()[0] as u8) } } - impl From for tracer::TraceFlags { + impl From for wasi_otel::TraceFlags { fn from(flags: opentelemetry::trace::TraceFlags) -> Self { if flags.is_sampled() { - tracer::TraceFlags::SAMPLED + wasi_otel::TraceFlags::SAMPLED } else { - tracer::TraceFlags::empty() + wasi_otel::TraceFlags::empty() } } } - impl From for opentelemetry::trace::SpanContext { - fn from(sc: tracer::SpanContext) -> Self { + impl From for opentelemetry::trace::SpanContext { + fn from(sc: wasi_otel::SpanContext) -> Self { // TODO(Reviewer): Should this be try_from instead an propagate this error out of the WIT? let trace_id = opentelemetry::trace::TraceId::from_hex(&sc.trace_id) .unwrap_or(opentelemetry::trace::TraceId::INVALID); @@ -562,7 +584,7 @@ mod observe { } } - impl From for tracer::SpanContext { + impl From for wasi_otel::SpanContext { fn from(sc: opentelemetry::trace::SpanContext) -> Self { Self { trace_id: format!("{:x}", sc.trace_id()), @@ -586,32 +608,32 @@ mod observe { } } - impl From for opentelemetry::trace::Status { - fn from(status: tracer::Status) -> Self { + impl From for opentelemetry::trace::Status { + fn from(status: wasi_otel::Status) -> Self { match status { - tracer::Status::Unset => Self::Unset, - tracer::Status::Ok => Self::Ok, - tracer::Status::Error(s) => Self::Error { + wasi_otel::Status::Unset => Self::Unset, + wasi_otel::Status::Ok => Self::Ok, + wasi_otel::Status::Error(s) => Self::Error { description: s.into(), }, } } } - impl From for opentelemetry::trace::SpanKind { - fn from(kind: tracer::SpanKind) -> Self { + impl From for opentelemetry::trace::SpanKind { + fn from(kind: wasi_otel::SpanKind) -> Self { match kind { - tracer::SpanKind::Client => opentelemetry::trace::SpanKind::Client, - tracer::SpanKind::Server => opentelemetry::trace::SpanKind::Server, - tracer::SpanKind::Producer => opentelemetry::trace::SpanKind::Producer, - tracer::SpanKind::Consumer => opentelemetry::trace::SpanKind::Consumer, - tracer::SpanKind::Internal => opentelemetry::trace::SpanKind::Internal, + wasi_otel::SpanKind::Client => opentelemetry::trace::SpanKind::Client, + wasi_otel::SpanKind::Server => opentelemetry::trace::SpanKind::Server, + wasi_otel::SpanKind::Producer => opentelemetry::trace::SpanKind::Producer, + wasi_otel::SpanKind::Consumer => opentelemetry::trace::SpanKind::Consumer, + wasi_otel::SpanKind::Internal => opentelemetry::trace::SpanKind::Internal, } } } - impl From for opentelemetry::trace::Link { - fn from(link: tracer::Link) -> Self { + impl From for opentelemetry::trace::Link { + fn from(link: wasi_otel::Link) -> Self { Self::new( link.span_context.into(), link.attributes.into_iter().map(Into::into).collect(), @@ -620,53 +642,69 @@ mod observe { } } - impl From for SystemTime { - fn from(timestamp: tracer::Datetime) -> Self { + impl From for SystemTime { + fn from(timestamp: wall_clock::Datetime) -> Self { UNIX_EPOCH + Duration::from_secs(timestamp.seconds) + Duration::from_nanos(timestamp.nanoseconds as u64) } } - #[allow(clippy::derivable_impls)] - impl Default for tracer::StartOptions { - fn default() -> Self { - Self { - new_root: false, - span_kind: None, - attributes: None, - links: None, - timestamp: None, + impl From for opentelemetry::InstrumentationScope { + fn from(value: wasi_otel::InstrumentationScope) -> Self { + let builder = Self::builder(value.name) + .with_attributes(value.attributes.into_iter().map(Into::into)); + match (value.version, value.schema_url) { + (Some(version), Some(schema_url)) => builder + .with_version(version) + .with_schema_url(schema_url) + .build(), + (Some(version), None) => builder.with_version(version).build(), + (None, Some(schema_url)) => builder.with_schema_url(schema_url).build(), + (None, None) => builder.build(), } } } - mod test { - #[test] - fn trace_flags() { - let flags = opentelemetry::trace::TraceFlags::SAMPLED; - let flags2 = crate::wasi::observe::tracer::TraceFlags::from(flags); - let flags3 = opentelemetry::trace::TraceFlags::from(flags2); - assert_eq!(flags, flags3); - } - - #[test] - fn span_context() { - let sc = opentelemetry::trace::SpanContext::new( - opentelemetry::trace::TraceId::from_hex("4fb34cb4484029f7881399b149e41e98") - .unwrap(), - opentelemetry::trace::SpanId::from_hex("9ffd58d3cd4dd90b").unwrap(), - opentelemetry::trace::TraceFlags::SAMPLED, - false, - opentelemetry::trace::TraceState::from_key_value(vec![ - ("foo", "bar"), - ("baz", "qux"), - ]) - .unwrap(), - ); - let sc2 = crate::wasi::observe::tracer::SpanContext::from(sc.clone()); - let sc3 = opentelemetry::trace::SpanContext::from(sc2); - assert_eq!(sc, sc3); - } - } + // #[allow(clippy::derivable_impls)] + // impl Default for wasi_otel::StartOptions { + // fn default() -> Self { + // Self { + // new_root: false, + // span_kind: None, + // attributes: None, + // links: None, + // timestamp: None, + // } + // } + // } + + // mod test { + // #[test] + // fn trace_flags() { + // let flags = opentelemetry::trace::TraceFlags::SAMPLED; + // let flags2 = crate::wasi::otel::tracing::TraceFlags::from(flags); + // let flags3 = opentelemetry::trace::TraceFlags::from(flags2); + // assert_eq!(flags, flags3); + // } + + // #[test] + // fn span_context() { + // let sc = opentelemetry::trace::SpanContext::new( + // opentelemetry::trace::TraceId::from_hex("4fb34cb4484029f7881399b149e41e98") + // .unwrap(), + // opentelemetry::trace::SpanId::from_hex("9ffd58d3cd4dd90b").unwrap(), + // opentelemetry::trace::TraceFlags::SAMPLED, + // false, + // opentelemetry::trace::TraceState::from_key_value(vec![ + // ("foo", "bar"), + // ("baz", "qux"), + // ]) + // .unwrap(), + // ); + // let sc2 = crate::wasi::otel::tracing::SpanContext::from(sc.clone()); + // let sc3 = opentelemetry::trace::SpanContext::from(sc2); + // assert_eq!(sc, sc3); + // } + // } } diff --git a/wit/deps/observe/world.wit b/wit/deps/observe/world.wit deleted file mode 100644 index 063b1ac821..0000000000 --- a/wit/deps/observe/world.wit +++ /dev/null @@ -1,8 +0,0 @@ -package wasi:observe@0.2.0-draft; - -world imports { - import tracer; -} - -// TODO(Reviewer): Should we make this an experimental wasi package or a Spin package -// TODO(Reviewer): Would adding a metrics interface to this in a future version be a breaking change? diff --git a/wit/deps/observe/tracer.wit b/wit/deps/otel/tracing.wit similarity index 65% rename from wit/deps/observe/tracer.wit rename to wit/deps/otel/tracing.wit index cac11286c3..cb7befc095 100644 --- a/wit/deps/observe/tracer.wit +++ b/wit/deps/otel/tracing.wit @@ -1,54 +1,54 @@ -interface tracer { +interface tracing { use wasi:clocks/wall-clock@0.2.0.{datetime}; - /// Starts a new `span` with the given name and options. - start: func(name: string, options: option) -> span; + /// TODO + on-start: func(span: span-data, parent: span-context); - /// Represents a unit of work or operation. - resource span { - /// Get the `span-context` for this `span`. - span-context: func() -> span-context; + /// TODO + on-end: func(span: span-data); - /// Returns true when the data provided to this `span` is captured in some form. If it returns false then any data provided is discarded. - is-recording: func() -> bool; + /// TODO + current-span-context: func() -> span-context; - /// Set attributes of this span. - /// - /// If a key already exists for an attribute of the Span it will be overwritten with the corresponding new value. - set-attributes: func(attributes: list); - - /// Adds an event with the provided name at the curent timestamp. - /// - /// Optionally an alternative timestamp may be provided. You may also provide attributes of this event. - add-event: func(name: string, timestamp: option, attributes: option>); - - /// Associates this `span` with another. - add-link: func(link: link); - - /// Override the default `span` status, which is unset. - set-status: func(status: status); - - /// Updates the `span` name. - update-name: func(name: string); - - /// Signals that the operation described by this span has now ended. - /// - /// If a timestamp is not provided then it is treated equivalent to passing the current time. Dropping this resource is the equivalent of calling `end(none)`. - end: func(timestamp: option); + /// TODO + record span-data { + /// Span context + span-context: span-context, + /// Span parent id + // TODO: No clue what this is for + parent-span-id: string, + /// Span kind + span-kind: span-kind, + // Span name + name: string, + /// Span start time + start-time: datetime, + /// Span end time + end-time: datetime, + /// Span attributes + attributes: list, + /// Span events + events: list, + /// Span Links + links: list, + /// Span status + status: status, + /// Instrumentation scope that produced this span + instrumentation-scope: instrumentation-scope, + + // TODO: + // - Dropped counts + // - parent span id vs parent span context } - /// Configuration for starting a `span`. - record start-options { - /// Whether this span should act as the root of a new trace. - new-root: bool, - /// `span-kind` for the new `span`. - span-kind: option, - /// Attributes for the new `span`. - attributes: option>, - /// `link`'s for the new `span`. - links: option>, - /// When the `span` should begin. If this is not provided it defaults to the current time. - timestamp: option, + /// An event describing a specific moment in time on a span and associated attributes. + record event { + /// Event name + name: string, + /// Event time + time: datetime, + /// Event attributes + attributes: list, } /// Describes a relationship to another `span`. @@ -148,4 +148,22 @@ interface tracer { /// /// If any invalid keys or values are provided then the `trace-state` will be treated as an empty list. type trace-state = list>; + + /// TODO + record instrumentation-scope { + /// Name of the instrumentation scope. + name: string, + + /// The library version. + version: option, + + /// Schema URL used by this library. + /// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url + schema-url: option, + + /// Specifies the instrumentation scope attributes to associate with emitted telemetry. + attributes: list, + } } + +// TODO: order things diff --git a/wit/deps/otel/world.wit b/wit/deps/otel/world.wit new file mode 100644 index 0000000000..d1d9701abf --- /dev/null +++ b/wit/deps/otel/world.wit @@ -0,0 +1,5 @@ +package wasi:otel@0.2.0-draft; + +world imports { + import tracing; +} diff --git a/wit/world.wit b/wit/world.wit index 8a328d9bda..f8b9db246c 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -8,7 +8,7 @@ world http-trigger { /// The imports needed for a guest to run on a Spin host world platform { - include wasi:observe/imports@0.2.0-draft; + include wasi:otel/imports@0.2.0-draft; include fermyon:spin/platform@2.0.0; include wasi:keyvalue/imports@0.2.0-draft2; import spin:postgres/postgres@3.0.0; From fa09aed4d76a520d17435ba0706e9fca6c80cfdd Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Fri, 21 Feb 2025 12:45:27 -0700 Subject: [PATCH 04/16] debugging with lann Signed-off-by: Caleb Schoepp --- crates/factor-observe/src/host.rs | 2 +- crates/factor-observe/src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/factor-observe/src/host.rs b/crates/factor-observe/src/host.rs index 226afa63fb..7cc70e7329 100644 --- a/crates/factor-observe/src/host.rs +++ b/crates/factor-observe/src/host.rs @@ -106,7 +106,7 @@ impl wasi_otel::Host for InstanceState { async fn current_span_context(&mut self) -> Result { // TODO: The bug is that tracing::Span::current() is not returning anything - let context = tracing::Span::current().context(); + let context = dbg!(dbg!(tracing::Span::current()).context()); let span = context.span(); let span_context = span.span_context(); let out: SpanContext = span_context.clone().into(); diff --git a/crates/factor-observe/src/lib.rs b/crates/factor-observe/src/lib.rs index e2b6bbbd53..60ee668660 100644 --- a/crates/factor-observe/src/lib.rs +++ b/crates/factor-observe/src/lib.rs @@ -48,6 +48,7 @@ impl Factor for ObserveFactor { &self, _: spin_factors::PrepareContext, ) -> anyhow::Result { + // TODO: Configuring the processor should move to init // This will configure the exporter based on the OTEL_EXPORTER_* environment variables. let exporter = match OtlpProtocol::traces_protocol_from_env() { OtlpProtocol::Grpc => opentelemetry_otlp::SpanExporter::builder() From 30f18f3b7d3160f94373ff3e1bb19619ae442b6c Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Sun, 23 Feb 2025 17:14:07 -0700 Subject: [PATCH 05/16] Fix bug when using preview 2 path Signed-off-by: Caleb Schoepp --- crates/factor-observe/src/host.rs | 24 +++++++++++++----------- crates/telemetry/src/lib.rs | 10 ++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/factor-observe/src/host.rs b/crates/factor-observe/src/host.rs index 7cc70e7329..10e39293de 100644 --- a/crates/factor-observe/src/host.rs +++ b/crates/factor-observe/src/host.rs @@ -25,11 +25,13 @@ impl wasi_otel::Host for InstanceState { // Before we ever create any new spans make sure we track the original host span ID if state.original_host_span_id.is_none() { - // TODO: Note this is also failing to get anything meaningful through tracing::Span::current() - let context = dbg!(tracing::Span::current().context()); - let span = dbg!(context.span()); - let span_context = dbg!(span.span_context()); - state.original_host_span_id = dbg!(Some(span_context.span_id())); + state.original_host_span_id = Some( + tracing::Span::current() + .context() + .span() + .span_context() + .span_id(), + ); } // // Get span's parent based on whether it's a new root and whether there are any active spans @@ -105,12 +107,12 @@ impl wasi_otel::Host for InstanceState { } async fn current_span_context(&mut self) -> Result { - // TODO: The bug is that tracing::Span::current() is not returning anything - let context = dbg!(dbg!(tracing::Span::current()).context()); - let span = context.span(); - let span_context = span.span_context(); - let out: SpanContext = span_context.clone().into(); - Ok(out) + Ok(tracing::Span::current() + .context() + .span() + .span_context() + .clone() + .into()) } } diff --git a/crates/telemetry/src/lib.rs b/crates/telemetry/src/lib.rs index c5482b3450..5732ae1c87 100644 --- a/crates/telemetry/src/lib.rs +++ b/crates/telemetry/src/lib.rs @@ -48,6 +48,15 @@ pub use propagation::inject_trace_context; /// spin_telemetry::metrics::monotonic_counter!(spin.metric_name = 1, metric_attribute = "value"); /// ``` pub fn init(spin_version: String) -> anyhow::Result { + // This filter globally filters out spans produced by wasi_http so that they don't conflict with + // the behaviour of the wasi-otel factor. + let wasi_http_trace_filter = tracing_subscriber::filter::filter_fn(|metadata| { + if metadata.is_span() && metadata.name() == "wit-bindgen export" { + return false; + } + true + }); + // This layer will print all tracing library log messages to stderr. let fmt_layer = fmt::layer() .with_writer(std::io::stderr) @@ -78,6 +87,7 @@ pub fn init(spin_version: String) -> anyhow::Result { // Build a registry subscriber with the layers we want to use. registry() + .with(wasi_http_trace_filter) .with(otel_tracing_layer) .with(otel_metrics_layer) .with(fmt_layer) From 9d73e160f8e4d43610688bf9cd7f593aed1f4ba9 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Mon, 24 Feb 2025 17:58:14 -0700 Subject: [PATCH 06/16] s/observe/otel/g Signed-off-by: Caleb Schoepp --- Cargo.lock | 26 +++++------ crates/factor-key-value/Cargo.toml | 2 +- crates/factor-key-value/src/host.rs | 32 +++++++------- crates/factor-key-value/src/lib.rs | 12 ++--- crates/factor-llm/Cargo.toml | 2 +- crates/factor-llm/src/host.rs | 4 +- crates/factor-llm/src/lib.rs | 8 ++-- .../Cargo.toml | 2 +- .../src/host.rs | 0 .../src/lib.rs | 34 +++++++------- crates/factor-outbound-http/Cargo.toml | 2 +- crates/factor-outbound-http/src/lib.rs | 8 ++-- crates/factor-outbound-http/src/spin.rs | 2 +- crates/factor-outbound-http/src/wasi.rs | 2 +- crates/factor-outbound-mqtt/Cargo.toml | 2 +- crates/factor-outbound-mqtt/src/host.rs | 12 ++--- crates/factor-outbound-mqtt/src/lib.rs | 6 +-- crates/factor-outbound-mysql/Cargo.toml | 2 +- crates/factor-outbound-mysql/src/host.rs | 6 +-- crates/factor-outbound-mysql/src/lib.rs | 8 ++-- crates/factor-outbound-pg/Cargo.toml | 2 +- crates/factor-outbound-pg/src/host.rs | 6 +-- crates/factor-outbound-pg/src/lib.rs | 8 ++-- crates/factor-outbound-redis/Cargo.toml | 2 +- crates/factor-outbound-redis/src/host.rs | 24 +++++----- crates/factor-outbound-redis/src/lib.rs | 6 +-- crates/factor-sqlite/Cargo.toml | 2 +- crates/factor-sqlite/src/host.rs | 12 ++--- crates/factor-sqlite/src/lib.rs | 6 +-- crates/factor-variables/Cargo.toml | 2 +- crates/factor-variables/src/host.rs | 2 +- crates/factor-variables/src/lib.rs | 8 ++-- crates/runtime-config/Cargo.toml | 2 +- crates/runtime-config/src/lib.rs | 4 +- crates/runtime-factors/Cargo.toml | 2 +- crates/runtime-factors/src/lib.rs | 6 +-- crates/trigger-http/Cargo.toml | 2 +- crates/world/src/conversions.rs | 2 +- tests/integration.rs | 44 +++++++++---------- .../wasi-observe-tracing/Cargo.toml | 2 +- .../wasi-observe-tracing/src/lib.rs | 4 +- .../testcases/wasi-observe-tracing/spin.toml | 12 ++--- 42 files changed, 166 insertions(+), 166 deletions(-) rename crates/{factor-observe => factor-otel}/Cargo.toml (95%) rename crates/{factor-observe => factor-otel}/src/host.rs (100%) rename crates/{factor-observe => factor-otel}/src/lib.rs (87%) diff --git a/Cargo.lock b/Cargo.lock index 459db7a260..c06ac7631f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7876,7 +7876,7 @@ dependencies = [ "anyhow", "serde", "spin-core", - "spin-factor-observe", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-key-value-redis", @@ -7898,7 +7898,7 @@ dependencies = [ "anyhow", "async-trait", "serde", - "spin-factor-observe", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-llm-local", @@ -7913,7 +7913,7 @@ dependencies = [ ] [[package]] -name = "spin-factor-observe" +name = "spin-factor-otel" version = "3.2.0-pre0" dependencies = [ "anyhow", @@ -7942,7 +7942,7 @@ dependencies = [ "ip_network", "reqwest 0.12.9", "rustls 0.23.18", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -7964,7 +7964,7 @@ dependencies = [ "anyhow", "rumqttc", "spin-core", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -7982,7 +7982,7 @@ dependencies = [ "anyhow", "mysql_async", "spin-core", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8033,7 +8033,7 @@ dependencies = [ "native-tls", "postgres-native-tls", "spin-core", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8052,7 +8052,7 @@ dependencies = [ "anyhow", "redis 0.25.4", "spin-core", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8068,7 +8068,7 @@ name = "spin-factor-sqlite" version = "3.2.0-pre0" dependencies = [ "async-trait", - "spin-factor-observe", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-locked-app", @@ -8083,7 +8083,7 @@ name = "spin-factor-variables" version = "3.2.0-pre0" dependencies = [ "spin-expressions", - "spin-factor-observe", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-world", @@ -8386,7 +8386,7 @@ dependencies = [ "spin-common", "spin-factor-key-value", "spin-factor-llm", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-http", "spin-factor-outbound-mqtt", "spin-factor-outbound-mysql", @@ -8420,7 +8420,7 @@ dependencies = [ "spin-common", "spin-factor-key-value", "spin-factor-llm", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-http", "spin-factor-outbound-mqtt", "spin-factor-outbound-mysql", @@ -8580,7 +8580,7 @@ dependencies = [ "serde_json", "spin-app", "spin-core", - "spin-factor-observe", + "spin-factor-otel", "spin-factor-outbound-http", "spin-factor-outbound-networking", "spin-factor-wasi", diff --git a/crates/factor-key-value/Cargo.toml b/crates/factor-key-value/Cargo.toml index 88d4f347f2..b233075259 100644 --- a/crates/factor-key-value/Cargo.toml +++ b/crates/factor-key-value/Cargo.toml @@ -8,7 +8,7 @@ edition = { workspace = true } anyhow = { workspace = true } serde = { workspace = true } spin-core = { path = "../core" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factors = { path = "../factors" } spin-locked-app = { path = "../locked-app" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-key-value/src/host.rs b/crates/factor-key-value/src/host.rs index aae50cdf05..ec885b6290 100644 --- a/crates/factor-key-value/src/host.rs +++ b/crates/factor-key-value/src/host.rs @@ -1,7 +1,7 @@ use super::{Cas, SwapError}; use anyhow::{Context, Result}; use spin_core::{async_trait, wasmtime::component::Resource}; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_resource_table::Table; use spin_world::v2::key_value; use spin_world::wasi::keyvalue as wasi_keyvalue; @@ -49,7 +49,7 @@ pub struct KeyValueDispatch { manager: Arc, stores: Table>, compare_and_swaps: Table>, - observe_context: Option, + otel_context: Option, } impl KeyValueDispatch { @@ -61,14 +61,14 @@ impl KeyValueDispatch { allowed_stores: HashSet, manager: Arc, capacity: u32, - observe_context: Option, + otel_context: Option, ) -> Self { Self { allowed_stores, manager, stores: Table::new(capacity), compare_and_swaps: Table::new(capacity), - observe_context, + otel_context, } } @@ -112,8 +112,8 @@ impl key_value::Host for KeyValueDispatch {} impl key_value::HostStore for KeyValueDispatch { #[instrument(name = "spin_key_value.open", skip(self), err(level = Level::INFO), fields(otel.kind = "client", kv.backend=self.manager.summary(&name).unwrap_or("unknown".to_string())))] async fn open(&mut self, name: String) -> Result, Error>> { - if let Some(observe_context) = self.observe_context.as_ref() { - observe_context.reparent_tracing_span() + if let Some(otel_context) = self.otel_context.as_ref() { + otel_context.reparent_tracing_span() } Ok(async { if self.allowed_stores.contains(&name) { @@ -137,8 +137,8 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result>, Error>> { - if let Some(observe_context) = self.observe_context.as_ref() { - observe_context.reparent_tracing_span() + if let Some(otel_context) = self.otel_context.as_ref() { + otel_context.reparent_tracing_span() } let store = self.get_store(store)?; Ok(store.get(&key).await) @@ -151,8 +151,8 @@ impl key_value::HostStore for KeyValueDispatch { key: String, value: Vec, ) -> Result> { - if let Some(observe_context) = self.observe_context.as_ref() { - observe_context.reparent_tracing_span() + if let Some(otel_context) = self.otel_context.as_ref() { + otel_context.reparent_tracing_span() } let store = self.get_store(store)?; Ok(store.set(&key, &value).await) @@ -164,8 +164,8 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result> { - if let Some(observe_context) = self.observe_context.as_ref() { - observe_context.reparent_tracing_span() + if let Some(otel_context) = self.otel_context.as_ref() { + otel_context.reparent_tracing_span() } let store = self.get_store(store)?; Ok(store.delete(&key).await) @@ -177,8 +177,8 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result> { - if let Some(observe_context) = self.observe_context.as_ref() { - observe_context.reparent_tracing_span() + if let Some(otel_context) = self.otel_context.as_ref() { + otel_context.reparent_tracing_span() } let store = self.get_store(store)?; Ok(store.exists(&key).await) @@ -189,8 +189,8 @@ impl key_value::HostStore for KeyValueDispatch { &mut self, store: Resource, ) -> Result, Error>> { - if let Some(observe_context) = self.observe_context.as_ref() { - observe_context.reparent_tracing_span() + if let Some(otel_context) = self.otel_context.as_ref() { + otel_context.reparent_tracing_span() } let store = self.get_store(store)?; Ok(store.get_keys().await) diff --git a/crates/factor-key-value/src/lib.rs b/crates/factor-key-value/src/lib.rs index 5fdff3ef22..557245be90 100644 --- a/crates/factor-key-value/src/lib.rs +++ b/crates/factor-key-value/src/lib.rs @@ -8,7 +8,7 @@ use std::{ }; use anyhow::ensure; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factors::{ ConfigureAppContext, Factor, FactorInstanceBuilder, InitContext, PrepareContext, RuntimeFactors, }; @@ -93,11 +93,11 @@ impl Factor for KeyValueFactor { .get(ctx.app_component().id()) .expect("component should be in component_stores") .clone(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceBuilder { store_manager: app_state.store_manager.clone(), allowed_stores, - observe_context, + otel_context, }) } } @@ -177,7 +177,7 @@ pub struct InstanceBuilder { store_manager: Arc, /// The allowed stores for this component instance. allowed_stores: HashSet, - observe_context: ObserveContext, + otel_context: OtelContext, } impl FactorInstanceBuilder for InstanceBuilder { @@ -187,13 +187,13 @@ impl FactorInstanceBuilder for InstanceBuilder { let Self { store_manager, allowed_stores, - observe_context, + otel_context, } = self; Ok(KeyValueDispatch::new_with_capacity( allowed_stores, store_manager, u32::MAX, - Some(observe_context), + Some(otel_context), )) } } diff --git a/crates/factor-llm/Cargo.toml b/crates/factor-llm/Cargo.toml index a1563d565d..db7a7ff45e 100644 --- a/crates/factor-llm/Cargo.toml +++ b/crates/factor-llm/Cargo.toml @@ -17,7 +17,7 @@ llm-cublas = ["llm", "spin-llm-local/cublas"] anyhow = { workspace = true } async-trait = { workspace = true } serde = { workspace = true } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factors = { path = "../factors" } spin-llm-local = { path = "../llm-local", optional = true } spin-llm-remote-http = { path = "../llm-remote-http" } diff --git a/crates/factor-llm/src/host.rs b/crates/factor-llm/src/host.rs index 00e82813f7..61c49b5313 100644 --- a/crates/factor-llm/src/host.rs +++ b/crates/factor-llm/src/host.rs @@ -13,7 +13,7 @@ impl v2::Host for InstanceState { prompt: String, params: Option, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); if !self.allowed_models.contains(&model) { return Err(access_denied_error(&model)); @@ -42,7 +42,7 @@ impl v2::Host for InstanceState { model: v1::EmbeddingModel, data: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); if !self.allowed_models.contains(&model) { return Err(access_denied_error(&model)); diff --git a/crates/factor-llm/src/lib.rs b/crates/factor-llm/src/lib.rs index 34f9faaf19..e894393abe 100644 --- a/crates/factor-llm/src/lib.rs +++ b/crates/factor-llm/src/lib.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use async_trait::async_trait; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factors::{ ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, }; @@ -86,12 +86,12 @@ impl Factor for LlmFactor { .cloned() .unwrap_or_default(); let engine = ctx.app_state().engine.clone(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { engine, allowed_models, - observe_context, + otel_context, }) } } @@ -106,7 +106,7 @@ pub struct AppState { pub struct InstanceState { engine: Arc>, pub allowed_models: Arc>, - observe_context: ObserveContext, + otel_context: OtelContext, } /// The runtime configuration for the LLM factor. diff --git a/crates/factor-observe/Cargo.toml b/crates/factor-otel/Cargo.toml similarity index 95% rename from crates/factor-observe/Cargo.toml rename to crates/factor-otel/Cargo.toml index 8410134e3b..22df37e1a4 100644 --- a/crates/factor-observe/Cargo.toml +++ b/crates/factor-otel/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "spin-factor-observe" +name = "spin-factor-otel" version = { workspace = true } authors = { workspace = true } edition = { workspace = true } diff --git a/crates/factor-observe/src/host.rs b/crates/factor-otel/src/host.rs similarity index 100% rename from crates/factor-observe/src/host.rs rename to crates/factor-otel/src/host.rs diff --git a/crates/factor-observe/src/lib.rs b/crates/factor-otel/src/lib.rs similarity index 87% rename from crates/factor-observe/src/lib.rs rename to crates/factor-otel/src/lib.rs index 60ee668660..d6d4616182 100644 --- a/crates/factor-observe/src/lib.rs +++ b/crates/factor-otel/src/lib.rs @@ -22,9 +22,9 @@ use spin_telemetry::{detector::SpinResourceDetector, env::OtlpProtocol}; use tracing_opentelemetry::OpenTelemetrySpanExt; #[derive(Default)] -pub struct ObserveFactor {} +pub struct OtelFactor {} -impl Factor for ObserveFactor { +impl Factor for OtelFactor { type RuntimeConfig = (); type AppState = (); type InstanceBuilder = InstanceState; @@ -84,7 +84,7 @@ impl Factor for ObserveFactor { } } -impl ObserveFactor { +impl OtelFactor { pub fn new() -> Self { Self::default() } @@ -97,7 +97,7 @@ pub struct InstanceState { impl SelfInstanceBuilder for InstanceState {} -/// Internal state of the ObserveFactor instance state. +/// Internal state of the OtelFactor instance state. /// /// This data lives here rather than directly on InstanceState so that we can have multiple things /// take Arc references to it. @@ -126,21 +126,21 @@ pub(crate) struct State { // pub inner: BoxedSpan, // } -/// Manages access to the ObserveFactor state for the purpose of maintaining proper span -/// parent/child relationships when WASI Observe spans are being created. -pub struct ObserveContext { +/// Manages access to the OtelFactor state for the purpose of maintaining proper span +/// parent/child relationships when WASI Otel spans are being created. +pub struct OtelContext { pub(crate) state: Option>>, } -impl ObserveContext { - /// Creates an [`ObserveContext`] from a [`PrepareContext`]. +impl OtelContext { + /// Creates an [`OtelContext`] from a [`PrepareContext`]. /// - /// If [`RuntimeFactors`] does not contain an [`ObserveFactor`], then calling - /// [`ObserveContext::reparent_tracing_span`] will be a no-op. + /// If [`RuntimeFactors`] does not contain an [`OtelFactor`], then calling + /// [`OtelContext::reparent_tracing_span`] will be a no-op. pub fn from_prepare_context( prepare_context: &mut PrepareContext, ) -> anyhow::Result { - let state = match prepare_context.instance_builder::() { + let state = match prepare_context.instance_builder::() { Ok(instance_state) => Some(instance_state.state.clone()), Err(spin_factors::Error::NoSuchFactor(_)) => None, Err(e) => return Err(e.into()), @@ -150,7 +150,7 @@ impl ObserveContext { /// Reparents the current [tracing] span to be a child of the last active guest span. /// - /// The observe factor enables guests to emit spans that should be part of the same trace as the + /// The otel factor enables guests to emit spans that should be part of the same trace as the /// host is producing for a request. Below is an example trace. A request is made to an app, a /// guest span is created and then the host is re-entered to fetch a key value. /// @@ -163,10 +163,10 @@ impl ObserveContext { /// /// Setting the guest spans parent as the host is trivially done. However, the more difficult /// task is having the host factor spans be children of the guest span. - /// [`ObserveContext::reparent_tracing_span`] handles this by reparenting the current span to be - /// a child of the last active guest span (which is tracked internally in the observe factor). + /// [`OtelContext::reparent_tracing_span`] handles this by reparenting the current span to be + /// a child of the last active guest span (which is tracked internally in the otel factor). /// - /// Note that if the observe factor is not in your [`RuntimeFactors`] than this is effectively a + /// Note that if the otel factor is not in your [`RuntimeFactors`] than this is effectively a /// no-op. /// /// This MUST only be called from a factor host implementation function that is instrumented. @@ -174,7 +174,7 @@ impl ObserveContext { /// This MUST be called at the very start of the function before any awaits. pub fn reparent_tracing_span(&self) { // If state is None then we want to return early b/c the factor doesn't depend on the - // Observe factor and therefore there is nothing to do + // Otel factor and therefore there is nothing to do let state = if let Some(state) = self.state.as_ref() { state.read().unwrap() } else { diff --git a/crates/factor-outbound-http/Cargo.toml b/crates/factor-outbound-http/Cargo.toml index 49ffc3a020..6230694504 100644 --- a/crates/factor-outbound-http/Cargo.toml +++ b/crates/factor-outbound-http/Cargo.toml @@ -12,7 +12,7 @@ hyper = { workspace = true } ip_network = "0.4" reqwest = { version = "0.12", features = ["gzip"] } rustls = { workspace = true } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-telemetry = { path = "../telemetry" } diff --git a/crates/factor-outbound-http/src/lib.rs b/crates/factor-outbound-http/src/lib.rs index 714ec56bc3..ad7c98e0f5 100644 --- a/crates/factor-outbound-http/src/lib.rs +++ b/crates/factor-outbound-http/src/lib.rs @@ -12,7 +12,7 @@ use http::{ HeaderValue, Uri, }; use intercept::OutboundHttpInterceptor; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::{ ComponentTlsConfigs, OutboundAllowedHosts, OutboundNetworkingFactor, }; @@ -76,7 +76,7 @@ impl Factor for OutboundHttpFactor { let outbound_networking = ctx.instance_builder::()?; let allowed_hosts = outbound_networking.allowed_hosts(); let component_tls_configs = outbound_networking.component_tls_configs().clone(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { wasi_http_ctx: WasiHttpCtx::new(), allowed_hosts, @@ -85,7 +85,7 @@ impl Factor for OutboundHttpFactor { self_request_origin: None, request_interceptor: None, spin_http_client: None, - observe_context, + otel_context, }) } } @@ -99,7 +99,7 @@ pub struct InstanceState { request_interceptor: Option>, // Connection-pooling client for 'fermyon:spin/http' interface spin_http_client: Option, - observe_context: ObserveContext, + otel_context: OtelContext, } impl InstanceState { diff --git a/crates/factor-outbound-http/src/spin.rs b/crates/factor-outbound-http/src/spin.rs index 52b5fd8684..c089f89035 100644 --- a/crates/factor-outbound-http/src/spin.rs +++ b/crates/factor-outbound-http/src/spin.rs @@ -12,7 +12,7 @@ impl spin_http::Host for crate::InstanceState { fields(otel.kind = "client", url.full = Empty, http.request.method = Empty, http.response.status_code = Empty, otel.name = Empty, server.address = Empty, server.port = Empty))] async fn send_request(&mut self, req: Request) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let span = Span::current(); record_request_fields(&span, &req); diff --git a/crates/factor-outbound-http/src/wasi.rs b/crates/factor-outbound-http/src/wasi.rs index 00b86bee68..aa7c45bc05 100644 --- a/crates/factor-outbound-http/src/wasi.rs +++ b/crates/factor-outbound-http/src/wasi.rs @@ -87,7 +87,7 @@ impl WasiHttpView for WasiHttpImplInner<'_> { request: Request, config: wasmtime_wasi_http::types::OutgoingRequestConfig, ) -> wasmtime_wasi_http::HttpResult { - self.state.observe_context.reparent_tracing_span(); + self.state.otel_context.reparent_tracing_span(); Ok(HostFutureIncomingResponse::Pending( wasmtime_wasi::runtime::spawn( diff --git a/crates/factor-outbound-mqtt/Cargo.toml b/crates/factor-outbound-mqtt/Cargo.toml index 8d087dc2bb..962b5efb0a 100644 --- a/crates/factor-outbound-mqtt/Cargo.toml +++ b/crates/factor-outbound-mqtt/Cargo.toml @@ -8,7 +8,7 @@ edition = { workspace = true } anyhow = { workspace = true } rumqttc = { version = "0.24", features = ["url"] } spin-core = { path = "../core" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-mqtt/src/host.rs b/crates/factor-outbound-mqtt/src/host.rs index d22ba8541a..10570b874f 100644 --- a/crates/factor-outbound-mqtt/src/host.rs +++ b/crates/factor-outbound-mqtt/src/host.rs @@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration}; use anyhow::Result; use spin_core::{async_trait, wasmtime::component::Resource}; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::OutboundAllowedHosts; use spin_world::v2::mqtt::{self as v2, Connection, Error, Qos}; use tracing::{instrument, Level}; @@ -13,20 +13,20 @@ pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table>, create_client: Arc, - observe_context: ObserveContext, + otel_context: OtelContext, } impl InstanceState { pub fn new( allowed_hosts: OutboundAllowedHosts, create_client: Arc, - observe_context: ObserveContext, + otel_context: OtelContext, ) -> Self { Self { allowed_hosts, create_client, connections: spin_resource_table::Table::new(1024), - observe_context, + otel_context, } } } @@ -79,7 +79,7 @@ impl v2::HostConnection for InstanceState { password: String, keep_alive_interval: u64, ) -> Result, Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); if !self .is_address_allowed(&address) @@ -114,7 +114,7 @@ impl v2::HostConnection for InstanceState { payload: Vec, qos: Qos, ) -> Result<(), Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; diff --git a/crates/factor-outbound-mqtt/src/lib.rs b/crates/factor-outbound-mqtt/src/lib.rs index d5b8fc0867..9636e0ce18 100644 --- a/crates/factor-outbound-mqtt/src/lib.rs +++ b/crates/factor-outbound-mqtt/src/lib.rs @@ -7,7 +7,7 @@ use host::other_error; use host::InstanceState; use rumqttc::{AsyncClient, Event, Incoming, Outgoing, QoS}; use spin_core::async_trait; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factors::{ ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -54,12 +54,12 @@ impl Factor for OutboundMqttFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState::new( allowed_hosts, self.create_client.clone(), - observe_context, + otel_context, )) } } diff --git a/crates/factor-outbound-mysql/Cargo.toml b/crates/factor-outbound-mysql/Cargo.toml index ad73b37cba..67d6d7e46e 100644 --- a/crates/factor-outbound-mysql/Cargo.toml +++ b/crates/factor-outbound-mysql/Cargo.toml @@ -14,7 +14,7 @@ mysql_async = { version = "0.34", default-features = false, features = [ "native-tls-tls", ] } spin-core = { path = "../core" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-mysql/src/host.rs b/crates/factor-outbound-mysql/src/host.rs index 4435c57ae9..ca113206aa 100644 --- a/crates/factor-outbound-mysql/src/host.rs +++ b/crates/factor-outbound-mysql/src/host.rs @@ -38,7 +38,7 @@ impl v2::Host for InstanceState {} impl v2::HostConnection for InstanceState { #[instrument(name = "spin_outbound_mysql.open", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "mysql", db.address = Empty, server.port = Empty, db.namespace = Empty))] async fn open(&mut self, address: String) -> Result, v2::Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); spin_factor_outbound_networking::record_address_fields(&address); if !self @@ -60,7 +60,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result<(), v2::Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); self.get_client(connection) .await? .execute(statement, params) @@ -74,7 +74,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); self.get_client(connection) .await? .query(statement, params) diff --git a/crates/factor-outbound-mysql/src/lib.rs b/crates/factor-outbound-mysql/src/lib.rs index d8c7d33a82..ce33e82e03 100644 --- a/crates/factor-outbound-mysql/src/lib.rs +++ b/crates/factor-outbound-mysql/src/lib.rs @@ -3,7 +3,7 @@ mod host; use client::Client; use mysql_async::Conn as MysqlClient; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::{OutboundAllowedHosts, OutboundNetworkingFactor}; use spin_factors::{Factor, InitContext, RuntimeFactors, SelfInstanceBuilder}; use spin_world::v1::mysql as v1; @@ -38,12 +38,12 @@ impl Factor for OutboundMysqlFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { allowed_hosts, connections: Default::default(), - observe_context, + otel_context, }) } } @@ -65,7 +65,7 @@ impl OutboundMysqlFactor { pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table, - observe_context: ObserveContext, + otel_context: OtelContext, } impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/factor-outbound-pg/Cargo.toml b/crates/factor-outbound-pg/Cargo.toml index b6217b1ca0..e0dfd4914c 100644 --- a/crates/factor-outbound-pg/Cargo.toml +++ b/crates/factor-outbound-pg/Cargo.toml @@ -10,7 +10,7 @@ chrono = "0.4" native-tls = "0.2" postgres-native-tls = "0.5" spin-core = { path = "../core" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-pg/src/host.rs b/crates/factor-outbound-pg/src/host.rs index 8d8fd2d8b9..6e70e0f90e 100644 --- a/crates/factor-outbound-pg/src/host.rs +++ b/crates/factor-outbound-pg/src/host.rs @@ -157,7 +157,7 @@ impl v2::Host for InstanceState {} impl v2::HostConnection for InstanceState { #[instrument(name = "spin_outbound_pg.open", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", db.address = Empty, server.port = Empty, db.namespace = Empty))] async fn open(&mut self, address: String) -> Result, v2::Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); spin_factor_outbound_networking::record_address_fields(&address); if !self @@ -179,7 +179,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); Ok(self .get_client(connection) @@ -195,7 +195,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); Ok(self .get_client(connection) .await? diff --git a/crates/factor-outbound-pg/src/lib.rs b/crates/factor-outbound-pg/src/lib.rs index 17067d88c4..9c91cfcb45 100644 --- a/crates/factor-outbound-pg/src/lib.rs +++ b/crates/factor-outbound-pg/src/lib.rs @@ -2,7 +2,7 @@ pub mod client; mod host; use client::Client; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::{OutboundAllowedHosts, OutboundNetworkingFactor}; use spin_factors::{ anyhow, ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -42,12 +42,12 @@ impl Factor for OutboundPgFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { allowed_hosts, connections: Default::default(), - observe_context, + otel_context, }) } } @@ -69,7 +69,7 @@ impl OutboundPgFactor { pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table, - observe_context: ObserveContext, + otel_context: OtelContext, } impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/factor-outbound-redis/Cargo.toml b/crates/factor-outbound-redis/Cargo.toml index cf9077d349..76ccc73958 100644 --- a/crates/factor-outbound-redis/Cargo.toml +++ b/crates/factor-outbound-redis/Cargo.toml @@ -8,7 +8,7 @@ edition = { workspace = true } anyhow = { workspace = true } redis = { version = "0.25", features = ["tokio-comp", "tokio-native-tls-comp", "aio"] } spin-core = { path = "../core" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-outbound-redis/src/host.rs b/crates/factor-outbound-redis/src/host.rs index 2664884e14..db25441c06 100644 --- a/crates/factor-outbound-redis/src/host.rs +++ b/crates/factor-outbound-redis/src/host.rs @@ -1,7 +1,7 @@ use anyhow::Result; use redis::{aio::MultiplexedConnection, AsyncCommands, FromRedisValue, Value}; use spin_core::wasmtime::component::Resource; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::OutboundAllowedHosts; use spin_world::v1::{redis as v1, redis_types}; use spin_world::v2::redis::{ @@ -13,7 +13,7 @@ use tracing::{instrument, Level}; pub struct InstanceState { pub allowed_hosts: OutboundAllowedHosts, pub connections: spin_resource_table::Table, - pub observe_context: ObserveContext, + pub otel_context: OtelContext, } impl InstanceState { @@ -57,7 +57,7 @@ impl v2::Host for crate::InstanceState { impl v2::HostConnection for crate::InstanceState { #[instrument(name = "spin_outbound_redis.open_connection", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "redis", db.address = Empty, server.port = Empty, db.namespace = Empty))] async fn open(&mut self, address: String) -> Result, Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); if !self .is_address_allowed(&address) .await @@ -76,7 +76,7 @@ impl v2::HostConnection for crate::InstanceState { channel: String, payload: Vec, ) -> Result<(), Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; // The `let () =` syntax is needed to suppress a warning when the result type is inferred. @@ -94,7 +94,7 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result>, Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.get(&key).await.map_err(other_error)?; @@ -108,7 +108,7 @@ impl v2::HostConnection for crate::InstanceState { key: String, value: Vec, ) -> Result<(), Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; // The `let () =` syntax is needed to suppress a warning when the result type is inferred. @@ -123,7 +123,7 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.incr(&key, 1).await.map_err(other_error)?; @@ -136,7 +136,7 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, keys: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.del(&keys).await.map_err(other_error)?; @@ -150,7 +150,7 @@ impl v2::HostConnection for crate::InstanceState { key: String, values: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.sadd(&key, &values).await.map_err(|e| { @@ -169,7 +169,7 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result, Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.smembers(&key).await.map_err(other_error)?; @@ -183,7 +183,7 @@ impl v2::HostConnection for crate::InstanceState { key: String, values: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await.map_err(other_error)?; let value = conn.srem(&key, &values).await.map_err(other_error)?; @@ -197,7 +197,7 @@ impl v2::HostConnection for crate::InstanceState { command: String, arguments: Vec, ) -> Result, Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = self.get_conn(connection).await?; let mut cmd = redis::cmd(&command); diff --git a/crates/factor-outbound-redis/src/lib.rs b/crates/factor-outbound-redis/src/lib.rs index 9a5b72a99f..0e0b112fae 100644 --- a/crates/factor-outbound-redis/src/lib.rs +++ b/crates/factor-outbound-redis/src/lib.rs @@ -1,7 +1,7 @@ mod host; use host::InstanceState; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factors::{ anyhow, ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -47,12 +47,12 @@ impl Factor for OutboundRedisFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { allowed_hosts, connections: spin_resource_table::Table::new(1024), - observe_context, + otel_context, }) } } diff --git a/crates/factor-sqlite/Cargo.toml b/crates/factor-sqlite/Cargo.toml index cf2c98bbd6..98a6325ad7 100644 --- a/crates/factor-sqlite/Cargo.toml +++ b/crates/factor-sqlite/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true [dependencies] async-trait = { workspace = true } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factors = { path = "../factors" } spin-locked-app = { path = "../locked-app" } spin-resource-table = { path = "../table" } diff --git a/crates/factor-sqlite/src/host.rs b/crates/factor-sqlite/src/host.rs index 6235f0e035..6dc6e897ad 100644 --- a/crates/factor-sqlite/src/host.rs +++ b/crates/factor-sqlite/src/host.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factors::wasmtime::component::Resource; use spin_factors::{anyhow, SelfInstanceBuilder}; use spin_world::v1::sqlite as v1; @@ -17,7 +17,7 @@ pub struct InstanceState { connections: spin_resource_table::Table>, /// A map from database label to connection creators. connection_creators: HashMap>, - observe_context: ObserveContext, + otel_context: OtelContext, } impl InstanceState { @@ -27,13 +27,13 @@ impl InstanceState { pub fn new( allowed_databases: Arc>, connection_creators: HashMap>, - observe_context: ObserveContext, + otel_context: OtelContext, ) -> Self { Self { allowed_databases, connections: spin_resource_table::Table::new(256), connection_creators, - observe_context, + otel_context, } } @@ -65,7 +65,7 @@ impl v2::Host for InstanceState { impl v2::HostConnection for InstanceState { #[instrument(name = "spin_sqlite.open", skip(self), err(level = Level::INFO), fields(otel.kind = "client", db.system = "sqlite", sqlite.backend = Empty))] async fn open(&mut self, database: String) -> Result, v2::Error> { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); if !self.allowed_databases.contains(&database) { return Err(v2::Error::AccessDenied); @@ -93,7 +93,7 @@ impl v2::HostConnection for InstanceState { query: String, parameters: Vec, ) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let conn = match self.get_connection(connection) { Ok(c) => c, diff --git a/crates/factor-sqlite/src/lib.rs b/crates/factor-sqlite/src/lib.rs index fc5e756dd4..00e47feb43 100644 --- a/crates/factor-sqlite/src/lib.rs +++ b/crates/factor-sqlite/src/lib.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use host::InstanceState; use async_trait::async_trait; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factors::{anyhow, Factor}; use spin_locked_app::MetadataKey; use spin_world::v1::sqlite as v1; @@ -84,11 +84,11 @@ impl Factor for SqliteFactor { .get(ctx.app_component().id()) .cloned() .unwrap_or_default(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState::new( allowed_databases, ctx.app_state().connection_creators.clone(), - observe_context, + otel_context, )) } } diff --git a/crates/factor-variables/Cargo.toml b/crates/factor-variables/Cargo.toml index 3abe8397dd..86a1c084ea 100644 --- a/crates/factor-variables/Cargo.toml +++ b/crates/factor-variables/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } [dependencies] spin-expressions = { path = "../expressions" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factors = { path = "../factors" } spin-world = { path = "../world" } tracing = { workspace = true } diff --git a/crates/factor-variables/src/host.rs b/crates/factor-variables/src/host.rs index d057b1b3f7..0907e9fd37 100644 --- a/crates/factor-variables/src/host.rs +++ b/crates/factor-variables/src/host.rs @@ -7,7 +7,7 @@ use crate::InstanceState; impl variables::Host for InstanceState { #[instrument(name = "spin_variables.get", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))] async fn get(&mut self, key: String) -> Result { - self.observe_context.reparent_tracing_span(); + self.otel_context.reparent_tracing_span(); let key = spin_expressions::Key::new(&key).map_err(expressions_to_variables_err)?; self.expression_resolver .resolve(&self.component_id, key) diff --git a/crates/factor-variables/src/lib.rs b/crates/factor-variables/src/lib.rs index a92181a3a5..5eb3eef9c8 100644 --- a/crates/factor-variables/src/lib.rs +++ b/crates/factor-variables/src/lib.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use runtime_config::RuntimeConfig; use spin_expressions::{ProviderResolver as ExpressionResolver, Template}; -use spin_factor_observe::ObserveContext; +use spin_factor_otel::OtelContext; use spin_factors::{ anyhow, ConfigureAppContext, Factor, InitContext, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -67,11 +67,11 @@ impl Factor for VariablesFactor { ) -> anyhow::Result { let component_id = ctx.app_component().id().to_string(); let expression_resolver = ctx.app_state().expression_resolver.clone(); - let observe_context = ObserveContext::from_prepare_context(&mut ctx)?; + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { component_id, expression_resolver, - observe_context, + otel_context, }) } } @@ -93,7 +93,7 @@ impl AppState { pub struct InstanceState { component_id: String, expression_resolver: Arc, - observe_context: ObserveContext, + otel_context: OtelContext, } impl InstanceState { diff --git a/crates/runtime-config/Cargo.toml b/crates/runtime-config/Cargo.toml index 97021360c8..77cfabbf32 100644 --- a/crates/runtime-config/Cargo.toml +++ b/crates/runtime-config/Cargo.toml @@ -13,7 +13,7 @@ anyhow = { workspace = true } spin-common = { path = "../common" } spin-factor-key-value = { path = "../factor-key-value" } spin-factor-llm = { path = "../factor-llm" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factor-outbound-http = { path = "../factor-outbound-http" } spin-factor-outbound-mqtt = { path = "../factor-outbound-mqtt" } spin-factor-outbound-mysql = { path = "../factor-outbound-mysql" } diff --git a/crates/runtime-config/src/lib.rs b/crates/runtime-config/src/lib.rs index 7872c0df35..5b0f78db67 100644 --- a/crates/runtime-config/src/lib.rs +++ b/crates/runtime-config/src/lib.rs @@ -5,7 +5,7 @@ use spin_common::ui::quoted_path; use spin_factor_key_value::runtime_config::spin::{self as key_value}; use spin_factor_key_value::KeyValueFactor; use spin_factor_llm::{spin as llm, LlmFactor}; -use spin_factor_observe::ObserveFactor; +use spin_factor_otel::OtelFactor; use spin_factor_outbound_http::OutboundHttpFactor; use spin_factor_outbound_mqtt::OutboundMqttFactor; use spin_factor_outbound_mysql::OutboundMysqlFactor; @@ -374,7 +374,7 @@ impl FactorRuntimeConfigSource for TomlRuntimeConfigSource<'_, '_> } } -impl FactorRuntimeConfigSource for TomlRuntimeConfigSource<'_, '_> { +impl FactorRuntimeConfigSource for TomlRuntimeConfigSource<'_, '_> { fn get_runtime_config(&mut self) -> anyhow::Result> { Ok(None) } diff --git a/crates/runtime-factors/Cargo.toml b/crates/runtime-factors/Cargo.toml index 3597f41ede..fca1c5a106 100644 --- a/crates/runtime-factors/Cargo.toml +++ b/crates/runtime-factors/Cargo.toml @@ -19,7 +19,7 @@ clap = { version = "3.1.18", features = ["derive", "env"] } spin-common = { path = "../common" } spin-factor-key-value = { path = "../factor-key-value" } spin-factor-llm = { path = "../factor-llm" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } spin-factor-outbound-http = { path = "../factor-outbound-http" } spin-factor-outbound-mqtt = { path = "../factor-outbound-mqtt" } spin-factor-outbound-mysql = { path = "../factor-outbound-mysql" } diff --git a/crates/runtime-factors/src/lib.rs b/crates/runtime-factors/src/lib.rs index 06b710fc53..6d0095ed3e 100644 --- a/crates/runtime-factors/src/lib.rs +++ b/crates/runtime-factors/src/lib.rs @@ -8,7 +8,7 @@ use anyhow::Context as _; use spin_common::arg_parser::parse_kv; use spin_factor_key_value::KeyValueFactor; use spin_factor_llm::LlmFactor; -use spin_factor_observe::ObserveFactor; +use spin_factor_otel::OtelFactor; use spin_factor_outbound_http::OutboundHttpFactor; use spin_factor_outbound_mqtt::{NetworkedMqttClient, OutboundMqttFactor}; use spin_factor_outbound_mysql::OutboundMysqlFactor; @@ -23,7 +23,7 @@ use spin_runtime_config::{ResolvedRuntimeConfig, TomlRuntimeConfigSource}; #[derive(RuntimeFactors)] pub struct TriggerFactors { - pub observe: ObserveFactor, + pub otel: OtelFactor, pub wasi: WasiFactor, pub variables: VariablesFactor, pub key_value: KeyValueFactor, @@ -44,7 +44,7 @@ impl TriggerFactors { allow_transient_writes: bool, ) -> anyhow::Result { Ok(Self { - observe: ObserveFactor::new(), + otel: OtelFactor::new(), wasi: wasi_factor(working_dir, allow_transient_writes), variables: VariablesFactor::default(), key_value: KeyValueFactor::new(), diff --git a/crates/trigger-http/Cargo.toml b/crates/trigger-http/Cargo.toml index 6559869cc3..e8653516a3 100644 --- a/crates/trigger-http/Cargo.toml +++ b/crates/trigger-http/Cargo.toml @@ -30,7 +30,7 @@ spin-http = { path = "../http" } spin-telemetry = { path = "../telemetry" } spin-trigger = { path = "../trigger" } spin-world = { path = "../world" } -spin-factor-observe = { path = "../factor-observe" } +spin-factor-otel = { path = "../factor-otel" } terminal = { path = "../terminal" } tokio = { workspace = true, features = ["full"] } tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"] } diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 83a4b37e9e..a4fa3b0cad 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -495,7 +495,7 @@ mod llm { } } -mod observe { +mod otel { use super::*; use opentelemetry::StringValue; use opentelemetry_sdk::trace::{SpanEvents, SpanLinks}; diff --git a/tests/integration.rs b/tests/integration.rs index 0759fe942f..139c5c5ee1 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1458,7 +1458,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_nested_spans() -> anyhow::Result<()> { + fn wasi_otel_nested_spans() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1466,7 +1466,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), @@ -1497,8 +1497,8 @@ mod otel_integration_tests { .expect("'GET /...' span should exist"); let exec_component_span = spans .iter() - .find(|s| s.name == "execute_wasm_component wasi-observe-tracing") - .expect("'execute_wasm_component wasi-observe-tracing' span should exist"); + .find(|s| s.name == "execute_wasm_component wasi-otel-tracing") + .expect("'execute_wasm_component wasi-otel-tracing' span should exist"); let outer_span = spans .iter() .find(|s| s.name == "outer_func") @@ -1528,7 +1528,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_drop_semantics() -> anyhow::Result<()> { + fn wasi_otel_drop_semantics() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1536,7 +1536,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), @@ -1567,8 +1567,8 @@ mod otel_integration_tests { .expect("'GET /...' span should exist"); let exec_component_span = spans .iter() - .find(|s| s.name == "execute_wasm_component wasi-observe-tracing") - .expect("'execute_wasm_component wasi-observe-tracing' span should exist"); + .find(|s| s.name == "execute_wasm_component wasi-otel-tracing") + .expect("'execute_wasm_component wasi-otel-tracing' span should exist"); let drop_span = spans .iter() .find(|s| s.name == "drop_semantics") @@ -1593,7 +1593,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_setting_attributes() -> anyhow::Result<()> { + fn wasi_otel_setting_attributes() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1601,7 +1601,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), @@ -1654,7 +1654,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_host_guest_host() -> anyhow::Result<()> { + fn wasi_otel_host_guest_host() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1662,7 +1662,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), @@ -1694,8 +1694,8 @@ mod otel_integration_tests { let exec_component_span = spans .iter() - .find(|s| s.name == "execute_wasm_component wasi-observe-tracing") - .expect("'execute_wasm_component wasi-observe-tracing' span should exist"); + .find(|s| s.name == "execute_wasm_component wasi-otel-tracing") + .expect("'execute_wasm_component wasi-otel-tracing' span should exist"); let guest_span = spans .iter() .find(|s| s.name == "guest") @@ -1716,7 +1716,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_events() -> anyhow::Result<()> { + fn wasi_otel_events() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1724,7 +1724,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), @@ -1782,7 +1782,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_links() -> anyhow::Result<()> { + fn wasi_otel_links() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1790,7 +1790,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), @@ -1840,7 +1840,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_child_outlives_parent() -> anyhow::Result<()> { + fn wasi_otel_child_outlives_parent() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1848,7 +1848,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), @@ -1897,7 +1897,7 @@ mod otel_integration_tests { } #[test] - fn wasi_observe_root_span() -> anyhow::Result<()> { + fn wasi_otel_root_span() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let mut collector = rt .block_on(FakeCollectorServer::start()) @@ -1905,7 +1905,7 @@ mod otel_integration_tests { let collector_endpoint = collector.endpoint().clone(); run_test_inited( - "wasi-observe-tracing", + "wasi-otel-tracing", SpinConfig { binary_path: spin_binary(), spin_up_args: Vec::new(), diff --git a/tests/test-components/components/wasi-observe-tracing/Cargo.toml b/tests/test-components/components/wasi-observe-tracing/Cargo.toml index 3b4eb0c449..0f40967436 100644 --- a/tests/test-components/components/wasi-observe-tracing/Cargo.toml +++ b/tests/test-components/components/wasi-observe-tracing/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wasi-observe-tracing" +name = "wasi-otel-tracing" version = "0.1.0" edition = "2021" diff --git a/tests/test-components/components/wasi-observe-tracing/src/lib.rs b/tests/test-components/components/wasi-observe-tracing/src/lib.rs index 89ee43d2be..8c5efe265b 100644 --- a/tests/test-components/components/wasi-observe-tracing/src/lib.rs +++ b/tests/test-components/components/wasi-observe-tracing/src/lib.rs @@ -1,6 +1,6 @@ wit_bindgen::generate!({ path: "../../../../wit", - world: "wasi:observe/imports@0.2.0-draft", + world: "wasi:otel/imports@0.2.0-draft", generate_all, }); @@ -10,7 +10,7 @@ use spin_sdk::{ }; use wasi::{ clocks0_2_0::wall_clock::now, - observe::tracer::{self, KeyValue, Link, StartOptions, Value}, + otel::tracer::{self, KeyValue, Link, StartOptions, Value}, }; #[http_component] diff --git a/tests/testcases/wasi-observe-tracing/spin.toml b/tests/testcases/wasi-observe-tracing/spin.toml index cc4698b5f9..a54f57210b 100644 --- a/tests/testcases/wasi-observe-tracing/spin.toml +++ b/tests/testcases/wasi-observe-tracing/spin.toml @@ -2,18 +2,18 @@ spin_manifest_version = 2 [application] authors = ["Fermyon Engineering "] -description = "An application to exercise wasi-observe tracing functionality." -name = "wasi-observe-tracing" +description = "An application to exercise wasi-otel tracing functionality." +name = "wasi-otel-tracing" version = "1.0.0" [[trigger.http]] route = "/..." -component = "wasi-observe-tracing" +component = "wasi-otel-tracing" -[component.wasi-observe-tracing] -source = "%{source=wasi-observe-tracing}" +[component.wasi-otel-tracing] +source = "%{source=wasi-otel-tracing}" key_value_stores = ["default"] allowed_outbound_hosts = ["http://self", "https://asdf.com"] -[component.wasi-observe-tracing.build] +[component.wasi-otel-tracing.build] command = "cargo build --target wasm32-wasi --release" watch = ["src/**/*.rs", "Cargo.toml"] From f53e4669f5e92d167fcecadf9844fcd3de596116 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Tue, 25 Feb 2025 14:03:29 -0700 Subject: [PATCH 07/16] fixup: update wit Signed-off-by: Caleb Schoepp --- .../wasi-observe-tracing/Cargo.toml | 2 +- wit/deps/otel/tracing.wit | 120 +++++++++--------- 2 files changed, 60 insertions(+), 62 deletions(-) diff --git a/tests/test-components/components/wasi-observe-tracing/Cargo.toml b/tests/test-components/components/wasi-observe-tracing/Cargo.toml index 0f40967436..cc71655728 100644 --- a/tests/test-components/components/wasi-observe-tracing/Cargo.toml +++ b/tests/test-components/components/wasi-observe-tracing/Cargo.toml @@ -9,5 +9,5 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1" http = "0.2" -spin-sdk = "2.2.0" +spin-sdk = "3.1.0" wit-bindgen = "0.30.0" diff --git a/wit/deps/otel/tracing.wit b/wit/deps/otel/tracing.wit index cb7befc095..dacc10ae2a 100644 --- a/wit/deps/otel/tracing.wit +++ b/wit/deps/otel/tracing.wit @@ -1,16 +1,16 @@ interface tracing { use wasi:clocks/wall-clock@0.2.0.{datetime}; - /// TODO + /// Called when a span is started. on-start: func(span: span-data, parent: span-context); - /// TODO + /// Called when a span is ended. on-end: func(span: span-data); - /// TODO + /// Returns the current span context of the host. current-span-context: func() -> span-context; - /// TODO + /// The data associated with a span. record span-data { /// Span context span-context: span-context, @@ -41,24 +41,41 @@ interface tracing { // - parent span id vs parent span context } - /// An event describing a specific moment in time on a span and associated attributes. - record event { - /// Event name - name: string, - /// Event time - time: datetime, - /// Event attributes - attributes: list, + /// Identifying trace information about a span that can be serialized and propagated. + record span-context { + /// The `trace-id` for this `span-context`. + trace-id: trace-id, + /// The `span-id` for this `span-context`. + span-id: span-id, + /// The `trace-flags` for this `span-context`. + trace-flags: trace-flags, + /// Whether this `span-context` was propagated from a remote parent. + is-remote: bool, + /// The `trace-state` for this `span-context`. + trace-state: trace-state, } - /// Describes a relationship to another `span`. - record link { - /// Denotes which `span` to link to. - span-context: span-context, - /// Attributes describing the link. - attributes: list, + /// The trace that this `span-context` belongs to. + /// + /// 16 bytes encoded as a hexadecimal string. + type trace-id = string; + + /// The id of this `span-context`. + /// + /// 8 bytes encoded as a hexadecimal string. + type span-id = string; + + /// Flags that can be set on a `span-context`. + flags trace-flags { + /// Whether the `span` should be sampled or not. + sampled, } + /// Carries system-specific configuration data, represented as a list of key-value pairs. `trace-state` allows multiple tracing systems to participate in the same trace. + /// + /// If any invalid keys or values are provided then the `trace-state` will be treated as an empty list. + type trace-state = list>; + /// Describes the relationship between the Span, its parents, and its children in a trace. enum span-kind { /// Indicates that the span describes a request to some remote service. This span is usually the parent of a remote server span and does not end until the response is received. @@ -73,16 +90,6 @@ interface tracing { internal } - /// The `status` of a `span`. - variant status { - /// The default status. - unset, - /// The operation has been validated by an Application developer or Operator to have completed successfully. - ok, - /// The operation contains an error with a description. - error(string), - } - /// A key-value pair describing an attribute. record key-value { /// The attribute name. @@ -114,42 +121,35 @@ interface tracing { s64-array(list), } - /// Identifying trace information about a span that can be serialized and propagated. - record span-context { - /// The `trace-id` for this `span-context`. - trace-id: trace-id, - /// The `span-id` for this `span-context`. - span-id: span-id, - /// The `trace-flags` for this `span-context`. - trace-flags: trace-flags, - /// Whether this `span-context` was propagated from a remote parent. - is-remote: bool, - /// The `trace-state` for this `span-context`. - trace-state: trace-state, + /// An event describing a specific moment in time on a span and associated attributes. + record event { + /// Event name + name: string, + /// Event time + time: datetime, + /// Event attributes + attributes: list, } - /// The trace that this `span-context` belongs to. - /// - /// 16 bytes encoded as a hexadecimal string. - type trace-id = string; - - /// The id of this `span-context`. - /// - /// 8 bytes encoded as a hexadecimal string. - type span-id = string; - - /// Flags that can be set on a `span-context`. - flags trace-flags { - /// Whether the `span` should be sampled or not. - sampled, + /// Describes a relationship to another `span`. + record link { + /// Denotes which `span` to link to. + span-context: span-context, + /// Attributes describing the link. + attributes: list, } - /// Carries system-specific configuration data, represented as a list of key-value pairs. `trace-state` allows multiple tracing systems to participate in the same trace. - /// - /// If any invalid keys or values are provided then the `trace-state` will be treated as an empty list. - type trace-state = list>; + /// The `status` of a `span`. + variant status { + /// The default status. + unset, + /// The operation has been validated by an Application developer or Operator to have completed successfully. + ok, + /// The operation contains an error with a description. + error(string), + } - /// TODO + /// Describes the instrumentation scope that produced a span. record instrumentation-scope { /// Name of the instrumentation scope. name: string, @@ -165,5 +165,3 @@ interface tracing { attributes: list, } } - -// TODO: order things From 79b3591d8d17e5e2b8866b03ce3f3aa6b7e3a38a Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Tue, 25 Feb 2025 16:02:00 -0700 Subject: [PATCH 08/16] Get tests compiling and move processor to init and make processor batch Signed-off-by: Caleb Schoepp --- crates/factor-otel/src/lib.rs | 49 ++- crates/runtime-factors/src/lib.rs | 2 +- tests/integration.rs | 154 +------ tests/test-components/components/Cargo.lock | 397 +++++++++++++++++- .../wasi-observe-tracing/Cargo.toml | 13 - .../wasi-observe-tracing/src/lib.rs | 147 ------- .../components/wasi-otel-tracing/Cargo.toml | 16 + .../components/wasi-otel-tracing/src/lib.rs | 130 ++++++ .../spin.toml | 0 9 files changed, 572 insertions(+), 336 deletions(-) delete mode 100644 tests/test-components/components/wasi-observe-tracing/Cargo.toml delete mode 100644 tests/test-components/components/wasi-observe-tracing/src/lib.rs create mode 100644 tests/test-components/components/wasi-otel-tracing/Cargo.toml create mode 100644 tests/test-components/components/wasi-otel-tracing/src/lib.rs rename tests/testcases/{wasi-observe-tracing => wasi-otel-tracing}/spin.toml (100%) diff --git a/crates/factor-otel/src/lib.rs b/crates/factor-otel/src/lib.rs index d6d4616182..64083f38d0 100644 --- a/crates/factor-otel/src/lib.rs +++ b/crates/factor-otel/src/lib.rs @@ -14,15 +14,17 @@ use opentelemetry::{ }; use opentelemetry_sdk::{ resource::{EnvResourceDetector, TelemetryResourceDetector}, - trace::{SimpleSpanProcessor, SpanProcessor}, + runtime::Tokio, + trace::{BatchSpanProcessor, SpanProcessor}, Resource, }; use spin_factors::{Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder}; use spin_telemetry::{detector::SpinResourceDetector, env::OtlpProtocol}; use tracing_opentelemetry::OpenTelemetrySpanExt; -#[derive(Default)] -pub struct OtelFactor {} +pub struct OtelFactor { + processor: Arc>, +} impl Factor for OtelFactor { type RuntimeConfig = (); @@ -48,6 +50,19 @@ impl Factor for OtelFactor { &self, _: spin_factors::PrepareContext, ) -> anyhow::Result { + Ok(InstanceState { + state: Arc::new(RwLock::new(State { + guest_span_contexts: Default::default(), + active_spans: Default::default(), + original_host_span_id: None, + })), + processor: self.processor.clone(), + }) + } +} + +impl OtelFactor { + pub fn new() -> anyhow::Result { // TODO: Configuring the processor should move to init // This will configure the exporter based on the OTEL_EXPORTER_* environment variables. let exporter = match OtlpProtocol::traces_protocol_from_env() { @@ -59,40 +74,34 @@ impl Factor for OtelFactor { .build()?, OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"), }; - let mut processor = opentelemetry_sdk::trace::SimpleSpanProcessor::new(Box::new(exporter)); - // TODO: Allow guest to dynamically set resource of the processor? + let mut processor = opentelemetry_sdk::trace::BatchSpanProcessor::builder( + exporter, + opentelemetry_sdk::runtime::Tokio, + ) + .build(); + // This is a hack b/c we know the version of this crate will be the same as the version of Spin + let spin_version = env!("CARGO_PKG_VERSION").to_string(); processor.set_resource(&Resource::from_detectors( Duration::from_secs(5), vec![ // Set service.name from env OTEL_SERVICE_NAME > env OTEL_RESOURCE_ATTRIBUTES > spin // Set service.version from Spin metadata - Box::new(SpinResourceDetector::new("27.0.1 todo".to_string())), + Box::new(SpinResourceDetector::new(spin_version)), // Sets fields from env OTEL_RESOURCE_ATTRIBUTES Box::new(EnvResourceDetector::new()), // Sets telemetry.sdk{name, language, version} Box::new(TelemetryResourceDetector), ], )); - Ok(InstanceState { - state: Arc::new(RwLock::new(State { - guest_span_contexts: Default::default(), - active_spans: Default::default(), - original_host_span_id: None, - })), - processor, + Ok(Self { + processor: Arc::new(processor), }) } } -impl OtelFactor { - pub fn new() -> Self { - Self::default() - } -} - pub struct InstanceState { pub(crate) state: Arc>, - pub(crate) processor: SimpleSpanProcessor, + pub(crate) processor: Arc>, } impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/runtime-factors/src/lib.rs b/crates/runtime-factors/src/lib.rs index 6d0095ed3e..a133036686 100644 --- a/crates/runtime-factors/src/lib.rs +++ b/crates/runtime-factors/src/lib.rs @@ -44,7 +44,7 @@ impl TriggerFactors { allow_transient_writes: bool, ) -> anyhow::Result { Ok(Self { - otel: OtelFactor::new(), + otel: OtelFactor::new()?, wasi: wasi_factor(working_dir, allow_transient_writes), variables: VariablesFactor::default(), key_value: KeyValueFactor::new(), diff --git a/tests/integration.rs b/tests/integration.rs index 139c5c5ee1..d7340be7ea 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1527,71 +1527,6 @@ mod otel_integration_tests { Ok(()) } - #[test] - fn wasi_otel_drop_semantics() -> anyhow::Result<()> { - let rt = tokio::runtime::Runtime::new()?; - let mut collector = rt - .block_on(FakeCollectorServer::start()) - .expect("fake collector server should start"); - let collector_endpoint = collector.endpoint().clone(); - - run_test_inited( - "wasi-otel-tracing", - SpinConfig { - binary_path: spin_binary(), - spin_up_args: Vec::new(), - app_type: SpinAppType::Http, - }, - ServicesConfig::none(), - |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); - env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); - env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); - Ok(()) - }, - move |env| { - let spin = env.runtime_mut(); - assert_spin_request( - spin, - Request::new(Method::Get, "/drop-semantics"), - Response::new(200), - )?; - - let spans = rt.block_on(collector.exported_spans(3, Duration::from_secs(5))); - - assert_eq!(spans.len(), 3); - - let handle_request_span = spans - .iter() - .find(|s| s.name == "GET /...") - .expect("'GET /...' span should exist"); - let exec_component_span = spans - .iter() - .find(|s| s.name == "execute_wasm_component wasi-otel-tracing") - .expect("'execute_wasm_component wasi-otel-tracing' span should exist"); - let drop_span = spans - .iter() - .find(|s| s.name == "drop_semantics") - .expect("'drop_semantics' span should exist"); - - assert!( - handle_request_span.trace_id == exec_component_span.trace_id - && exec_component_span.trace_id == drop_span.trace_id - ); - assert_eq!( - exec_component_span.parent_span_id, - handle_request_span.span_id - ); - assert_eq!(drop_span.parent_span_id, exec_component_span.span_id); - assert!(drop_span.end_time_unix_nano < exec_component_span.end_time_unix_nano); - - Ok(()) - }, - )?; - - Ok(()) - } - #[test] fn wasi_otel_setting_attributes() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; @@ -1839,63 +1774,6 @@ mod otel_integration_tests { Ok(()) } - #[test] - fn wasi_otel_child_outlives_parent() -> anyhow::Result<()> { - let rt = tokio::runtime::Runtime::new()?; - let mut collector = rt - .block_on(FakeCollectorServer::start()) - .expect("fake collector server should start"); - let collector_endpoint = collector.endpoint().clone(); - - run_test_inited( - "wasi-otel-tracing", - SpinConfig { - binary_path: spin_binary(), - spin_up_args: Vec::new(), - app_type: SpinAppType::Http, - }, - ServicesConfig::none(), - |env| { - env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", collector_endpoint); - env.set_env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "grpc"); - env.set_env_var("OTEL_BSP_SCHEDULE_DELAY", "5"); - Ok(()) - }, - move |env| { - let spin = env.runtime_mut(); - assert_spin_request( - spin, - Request::new(Method::Get, "/child-outlives-parent"), - Response::new(200), - )?; - - let spans = rt.block_on(collector.exported_spans(5, Duration::from_secs(5))); - - assert_eq!(spans.len(), 5); - - let parent_span = spans - .iter() - .find(|s| s.name == "parent") - .expect("'parent' span should exist"); - let child_span = spans - .iter() - .find(|s| s.name == "child") - .expect("'child' span should exist"); - let get_span = spans - .iter() - .find(|s| s.name == "GET") - .expect("'GET' span should exist"); - assert_eq!(child_span.parent_span_id, parent_span.span_id); - assert_eq!(get_span.parent_span_id, child_span.span_id); - assert!(child_span.end_time_unix_nano > parent_span.end_time_unix_nano); - - Ok(()) - }, - )?; - - Ok(()) - } - #[test] fn wasi_otel_root_span() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; @@ -1928,38 +1806,20 @@ mod otel_integration_tests { let spans = rt.block_on(collector.exported_spans(7, Duration::from_secs(5))); - assert_eq!(spans.len(), 7); + assert_eq!(spans.len(), 4); - let parent_span = spans - .iter() - .find(|s| s.name == "parent") - .expect("'parent' span should exist"); - let request_one = spans - .iter() - .find(|s| s.name == "GET") - .expect("first 'GET' span should exist"); let root_span = spans .iter() .find(|s| s.name == "root") .expect("'root' span should exist"); - let request_two = spans - .iter() - .filter(|s| s.name == "GET") - .nth(1) - .expect("second 'GET' span should exist"); - let request_three = spans + let request_span = spans .iter() - .filter(|s| s.name == "GET") - .nth(2) - .expect("third 'GET' span should exist"); - - assert_eq!(parent_span.trace_id, request_one.trace_id); - assert_ne!(root_span.trace_id, parent_span.trace_id); - assert_eq!(root_span.trace_id, request_two.trace_id); - assert_eq!(parent_span.trace_id, request_three.trace_id); + .find(|s| s.name == "GET") + .expect("'GET' span should exist"); + + assert_eq!(root_span.trace_id, request_span.trace_id); + assert_eq!(root_span.span_id, request_span.parent_span_id); assert_eq!(root_span.parent_span_id, "".to_string()); - assert_eq!(request_two.parent_span_id, root_span.span_id); - assert_ne!(request_three.parent_span_id, parent_span.span_id); Ok(()) }, diff --git a/tests/test-components/components/Cargo.lock b/tests/test-components/components/Cargo.lock index f69cae0bf7..d944257063 100644 --- a/tests/test-components/components/Cargo.lock +++ b/tests/test-components/components/Cargo.lock @@ -34,6 +34,21 @@ dependencies = [ "wit-bindgen 0.32.0", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.89" @@ -72,18 +87,59 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "cc" +version = "1.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -233,6 +289,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "hashbrown" version = "0.14.3" @@ -328,6 +401,29 @@ dependencies = [ "spin-sdk 2.2.0", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "id-arena" version = "2.2.1" @@ -436,7 +532,7 @@ dependencies = [ "anyhow", "futures", "helper", - "spin-sdk 3.0.0", + "spin-sdk 3.1.0", ] [[package]] @@ -445,6 +541,16 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "key-value" version = "0.1.0" @@ -508,12 +614,65 @@ dependencies = [ "ryu", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opentelemetry" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror", + "tracing", +] + +[[package]] +name = "opentelemetry-wasi" +version = "0.27.0" +source = "git+https://github.com/calebschoepp/opentelemetry-wasi?rev=bf4a6bf93b93bec48835360cc4d2f4155858a697#bf4a6bf93b93bec48835360cc4d2f4155858a697" +dependencies = [ + "anyhow", + "opentelemetry", + "opentelemetry_sdk", + "wit-bindgen 0.30.0", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror", + "tracing", +] + [[package]] name = "otel-smoke-test" version = "0.1.0" @@ -582,6 +741,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.29" @@ -610,6 +778,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "routefinder" version = "0.5.3" @@ -620,6 +818,12 @@ dependencies = [ "smartstring", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.16" @@ -685,6 +889,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.9" @@ -729,6 +939,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin-executor" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7298013c6a0dc3361331cec17c744e45d76df7696d59b87801a9a3f5edbe0f54" +dependencies = [ + "futures", + "once_cell", + "wit-bindgen 0.16.0", +] + [[package]] name = "spin-macro" version = "2.2.0" @@ -745,9 +966,9 @@ dependencies = [ [[package]] name = "spin-macro" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a161ae2fefde8582ef555ead81d87cf897cd31a23a1d1e0c22a9c43fd9af421a" +checksum = "f3953755ea3415f4c0ecba8c5cdab51f05a5d2480b9bb8f2f42bf5a0ffaf18e6" dependencies = [ "anyhow", "bytes", @@ -787,13 +1008,14 @@ dependencies = [ [[package]] name = "spin-sdk" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c02cf00c243c03fb330cb5be7bf4eb3c8db7c5476425068c7385ddff1567aa" +checksum = "eac0de7538d3986cc989ce3649d50b1d8f50ec46a0c9e62b4a14b911db6fe0de" dependencies = [ "anyhow", "async-trait", "bytes", + "chrono", "form_urlencoded", "futures", "http 1.1.0", @@ -801,7 +1023,8 @@ dependencies = [ "routefinder", "serde", "serde_json", - "spin-macro 3.0.0", + "spin-executor", + "spin-macro 3.1.0", "thiserror", "wit-bindgen 0.16.0", ] @@ -885,6 +1108,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" + [[package]] name = "typenum" version = "1.17.0" @@ -958,6 +1197,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasi-config" version = "0.1.0" @@ -999,15 +1244,77 @@ dependencies = [ "wit-bindgen 0.32.0", ] -name = "wasi-observe-tracing" +[[package]] +name = "wasi-otel-tracing" version = "0.1.0" dependencies = [ "anyhow", "http 0.2.11", - "spin-sdk 2.2.0", + "opentelemetry", + "opentelemetry-wasi", + "opentelemetry_sdk", + "spin-sdk 3.1.0", "wit-bindgen 0.30.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.74", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.36.2" @@ -1121,6 +1428,79 @@ dependencies = [ "semver", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen" version = "0.13.1" @@ -1395,6 +1775,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] diff --git a/tests/test-components/components/wasi-observe-tracing/Cargo.toml b/tests/test-components/components/wasi-observe-tracing/Cargo.toml deleted file mode 100644 index cc71655728..0000000000 --- a/tests/test-components/components/wasi-observe-tracing/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "wasi-otel-tracing" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -anyhow = "1" -http = "0.2" -spin-sdk = "3.1.0" -wit-bindgen = "0.30.0" diff --git a/tests/test-components/components/wasi-observe-tracing/src/lib.rs b/tests/test-components/components/wasi-observe-tracing/src/lib.rs deleted file mode 100644 index 8c5efe265b..0000000000 --- a/tests/test-components/components/wasi-observe-tracing/src/lib.rs +++ /dev/null @@ -1,147 +0,0 @@ -wit_bindgen::generate!({ - path: "../../../../wit", - world: "wasi:otel/imports@0.2.0-draft", - generate_all, -}); - -use spin_sdk::{ - http::{Method, Params, Request, Response, Router}, - http_component, -}; -use wasi::{ - clocks0_2_0::wall_clock::now, - otel::tracer::{self, KeyValue, Link, StartOptions, Value}, -}; - -#[http_component] -fn handle(req: http::Request<()>) -> Response { - let mut router = Router::new(); - router.get("/nested-spans", nested_spans); - router.get("/drop-semantics", drop_semantics); - router.get("/setting-attributes", setting_attributes); - router.get_async("/host-guest-host", host_guest_host); - router.get("/events", events); - router.get_async("/child-outlives-parent", child_outlives_parent); - router.get("/links", links); - router.get_async("/root-span", root_span); - router.handle(req) -} - -fn nested_spans(_req: Request, _params: Params) -> Response { - let span = tracer::start("outer_func", None); - inner_func(); - span.end(None); - Response::new(200, "") -} - -fn inner_func() { - let span = tracer::start("inner_func", None); - span.end(None); -} - -fn drop_semantics(_req: Request, _params: Params) -> Response { - let _span = tracer::start("drop_semantics", None); - Response::new(200, "") - // _span will drop here and should be ended -} - -fn setting_attributes(_req: Request, _params: Params) -> Response { - let span = tracer::start("setting_attributes", None); - span.set_attributes(&[KeyValue { - key: "foo".to_string(), - value: Value::String("bar".to_string()), - }]); - span.set_attributes(&[ - KeyValue { - key: "foo".to_string(), - value: Value::String("baz".to_string()), - }, - KeyValue { - key: "qux".to_string(), - value: Value::StringArray(vec!["qaz".to_string(), "thud".to_string()]), - }, - ]); - span.end(None); - Response::new(200, "") -} - -async fn host_guest_host(_req: Request, _params: Params) -> Response { - let span = tracer::start("guest", None); - - make_request().await; - span.end(None); - - Response::new(200, "") -} - -fn events(_req: Request, _params: Params) -> Response { - let span = tracer::start("events", None); - span.add_event("basic-event", None, None); - span.add_event( - "event-with-attributes", - None, - Some(&[KeyValue { - key: "foo".to_string(), - value: Value::Bool(true), - }]), - ); - let mut now_plus = now(); - now_plus.seconds += 1; - span.add_event("event-with-timestamp", Some(now_plus), None); - span.end(None); - Response::new(200, "") -} - -async fn child_outlives_parent(_req: Request, _params: Params) -> Response { - let span = tracer::start("parent", None); - let span2 = tracer::start("child", None); - span.end(None); - // Make a host call to test span reparenting when we're messing with the active span stack - make_request().await; - - span2.end(None); - Response::new(200, "") -} - -fn links(_req: Request, _params: Params) -> Response { - let first = tracer::start("first", None); - first.end(None); - let second = tracer::start("second", None); - second.add_link(&Link { - span_context: first.span_context(), - attributes: vec![KeyValue { - key: "foo".to_string(), - value: Value::String("bar".to_string()), - }], - }); - second.end(None); - Response::new(200, "") -} - -async fn root_span(_req: Request, _params: Params) -> Response { - let span1 = tracer::start("parent", None); - make_request().await; - let root = tracer::start( - "root", - Some(&StartOptions { - new_root: true, - span_kind: None, - attributes: None, - links: None, - timestamp: None, - }), - ); - make_request().await; - root.end(None); - span1.end(None); - make_request().await; - Response::new(200, "") -} - -async fn make_request() { - let req = Request::builder() - .method(Method::Get) - .uri("https://asdf.com") - .build(); - let _res: Response = spin_sdk::http::send(req).await.unwrap(); -} diff --git a/tests/test-components/components/wasi-otel-tracing/Cargo.toml b/tests/test-components/components/wasi-otel-tracing/Cargo.toml new file mode 100644 index 0000000000..40bfd42dec --- /dev/null +++ b/tests/test-components/components/wasi-otel-tracing/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wasi-otel-tracing" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +http = "0.2" +opentelemetry = "0.27.0" +opentelemetry_sdk = "0.27.0" +opentelemetry-wasi = { git = "https://github.com/calebschoepp/opentelemetry-wasi", rev = "bf4a6bf93b93bec48835360cc4d2f4155858a697" } +spin-sdk = "3.1.0" +wit-bindgen = "0.30.0" diff --git a/tests/test-components/components/wasi-otel-tracing/src/lib.rs b/tests/test-components/components/wasi-otel-tracing/src/lib.rs new file mode 100644 index 0000000000..a166657344 --- /dev/null +++ b/tests/test-components/components/wasi-otel-tracing/src/lib.rs @@ -0,0 +1,130 @@ +use std::time; + +use opentelemetry::{ + global::{self, BoxedTracer, ObjectSafeSpan}, + trace::{TraceContextExt, Tracer}, + Array, Context, ContextGuard, KeyValue, Value, +}; +use opentelemetry_sdk::trace::TracerProvider; +use opentelemetry_wasi::WasiPropagator; +use spin_sdk::{ + http::{IntoResponse, Method, Params, Request, Response, Router}, + http_component, +}; + +#[http_component] +fn handle(req: Request) -> anyhow::Result { + let mut router = Router::new(); + router.get("/nested-spans", nested_spans); + router.get("/setting-attributes", setting_attributes); + router.get_async("/host-guest-host", host_guest_host); + router.get("/events", events); + router.get("/links", links); + router.get_async("/root-span", root_span); + Ok(router.handle(req)) +} + +fn setup_tracer(propagate_context: bool) -> (BoxedTracer, Option) { + // Set up a tracer using the WASI processor + let wasi_processor = opentelemetry_wasi::WasiProcessor::new(); + let tracer_provider = TracerProvider::builder() + .with_span_processor(wasi_processor) + .build(); + global::set_tracer_provider(tracer_provider); + let tracer = global::tracer("wasi-otel-tracing"); + + if propagate_context { + let wasi_propagator = opentelemetry_wasi::TraceContextPropagator::new(); + ( + tracer, + Some(wasi_propagator.extract(&Context::current()).attach()), + ) + } else { + (tracer, None) + } +} + +fn nested_spans(_req: Request, _params: Params) -> Response { + let (tracer, _ctx) = setup_tracer(true); + tracer.in_span("outer_func", |_| { + tracer.in_span("inner_func", |_| {}); + }); + Response::new(200, "") +} + +// TODO: Test what happens if start is called but not end +// TODO: Test what happens if end is called but not start +// TODO: What happens if child span outlives parent + +fn setting_attributes(_req: Request, _params: Params) -> Response { + let (tracer, _ctx) = setup_tracer(true); + tracer.in_span("setting_attributes", |cx| { + let span = cx.span(); + span.set_attribute(KeyValue::new("foo", "bar")); + span.set_attribute(KeyValue::new("foo", "baz")); + span.set_attribute(KeyValue::new( + "qux", + Value::Array(Array::String(vec!["qaz".into(), "thud".into()])), + )); + }); + + Response::new(200, "") +} + +async fn host_guest_host(_req: Request, _params: Params) -> Response { + let (tracer, _ctx) = setup_tracer(true); + let mut span = tracer.start("guest"); + make_request().await; + span.end(); + + Response::new(200, "") +} + +fn events(_req: Request, _params: Params) -> Response { + let (tracer, _ctx) = setup_tracer(true); + tracer.in_span("events", |cx| { + let span = cx.span(); + span.add_event("basic-event".to_string(), vec![]); + span.add_event( + "event-with-attributes".to_string(), + vec![KeyValue::new("foo", true)], + ); + let time = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap(); + let time = time.as_secs_f64(); + let time = time::Duration::from_secs_f64(time + 1.0); + let time = time::SystemTime::UNIX_EPOCH + time; + span.add_event_with_timestamp("event-with-timestamp", time, vec![]); + }); + Response::new(200, "") +} + +fn links(_req: Request, _params: Params) -> Response { + let (tracer, _ctx) = setup_tracer(true); + let mut first = tracer.start("first"); + first.end(); + let mut second = tracer.start("second"); + second.add_link( + first.span_context().clone(), + vec![KeyValue::new("foo", "bar")], + ); + second.end(); + Response::new(200, "") +} + +async fn root_span(_req: Request, _params: Params) -> Response { + let (tracer, _ctx) = setup_tracer(false); + let mut span = tracer.start("root"); + make_request().await; + span.end(); + Response::new(200, "") +} + +async fn make_request() { + let req = Request::builder() + .method(Method::Get) + .uri("https://asdf.com") + .build(); + let _res: Response = spin_sdk::http::send(req).await.unwrap(); +} diff --git a/tests/testcases/wasi-observe-tracing/spin.toml b/tests/testcases/wasi-otel-tracing/spin.toml similarity index 100% rename from tests/testcases/wasi-observe-tracing/spin.toml rename to tests/testcases/wasi-otel-tracing/spin.toml From 7cafec53f4944526793e423dd4f595487554d590 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Wed, 26 Feb 2025 08:50:55 -0700 Subject: [PATCH 09/16] Cleanup conversion code Signed-off-by: Caleb Schoepp --- crates/world/src/conversions.rs | 198 ++++++++++++++++---------------- 1 file changed, 100 insertions(+), 98 deletions(-) diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index a4fa3b0cad..32f38f7b1d 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -505,6 +505,10 @@ mod otel { impl From for opentelemetry_sdk::export::trace::SpanData { fn from(value: wasi_otel::SpanData) -> Self { + let mut span_events = SpanEvents::default(); + span_events.events = value.events.into_iter().map(Into::into).collect(); + let mut span_links = SpanLinks::default(); + span_links.links = value.links.into_iter().map(Into::into).collect(); Self { span_context: value.span_context.into(), parent_span_id: opentelemetry::trace::SpanId::from_hex(&value.parent_span_id) @@ -515,56 +519,14 @@ mod otel { end_time: value.end_time.into(), attributes: value.attributes.into_iter().map(Into::into).collect(), dropped_attributes_count: 0, - events: SpanEvents::default(), // TODO - links: SpanLinks::default(), // TODO + events: span_events, + links: span_links, status: value.status.into(), instrumentation_scope: value.instrumentation_scope.into(), } } } - impl From for opentelemetry::Value { - fn from(value: wasi_otel::Value) -> Self { - match value { - wasi_otel::Value::String(v) => v.into(), - wasi_otel::Value::Bool(v) => v.into(), - wasi_otel::Value::Float64(v) => v.into(), - wasi_otel::Value::S64(v) => v.into(), - wasi_otel::Value::StringArray(v) => opentelemetry::Value::Array( - v.into_iter() - .map(StringValue::from) - .collect::>() - .into(), - ), - wasi_otel::Value::BoolArray(v) => opentelemetry::Value::Array(v.into()), - wasi_otel::Value::Float64Array(v) => opentelemetry::Value::Array(v.into()), - wasi_otel::Value::S64Array(v) => opentelemetry::Value::Array(v.into()), - } - } - } - - impl From for opentelemetry::KeyValue { - fn from(kv: wasi_otel::KeyValue) -> Self { - opentelemetry::KeyValue::new(kv.key, kv.value) - } - } - - impl From for opentelemetry::trace::TraceFlags { - fn from(flags: wasi_otel::TraceFlags) -> Self { - Self::new(flags.as_array()[0] as u8) - } - } - - impl From for wasi_otel::TraceFlags { - fn from(flags: opentelemetry::trace::TraceFlags) -> Self { - if flags.is_sampled() { - wasi_otel::TraceFlags::SAMPLED - } else { - wasi_otel::TraceFlags::empty() - } - } - } - impl From for opentelemetry::trace::SpanContext { fn from(sc: wasi_otel::SpanContext) -> Self { // TODO(Reviewer): Should this be try_from instead an propagate this error out of the WIT? @@ -608,14 +570,18 @@ mod otel { } } - impl From for opentelemetry::trace::Status { - fn from(status: wasi_otel::Status) -> Self { - match status { - wasi_otel::Status::Unset => Self::Unset, - wasi_otel::Status::Ok => Self::Ok, - wasi_otel::Status::Error(s) => Self::Error { - description: s.into(), - }, + impl From for opentelemetry::trace::TraceFlags { + fn from(flags: wasi_otel::TraceFlags) -> Self { + Self::new(flags.as_array()[0] as u8) + } + } + + impl From for wasi_otel::TraceFlags { + fn from(flags: opentelemetry::trace::TraceFlags) -> Self { + if flags.is_sampled() { + wasi_otel::TraceFlags::SAMPLED + } else { + wasi_otel::TraceFlags::empty() } } } @@ -632,6 +598,43 @@ mod otel { } } + impl From for opentelemetry::KeyValue { + fn from(kv: wasi_otel::KeyValue) -> Self { + opentelemetry::KeyValue::new(kv.key, kv.value) + } + } + + impl From for opentelemetry::Value { + fn from(value: wasi_otel::Value) -> Self { + match value { + wasi_otel::Value::String(v) => v.into(), + wasi_otel::Value::Bool(v) => v.into(), + wasi_otel::Value::Float64(v) => v.into(), + wasi_otel::Value::S64(v) => v.into(), + wasi_otel::Value::StringArray(v) => opentelemetry::Value::Array( + v.into_iter() + .map(StringValue::from) + .collect::>() + .into(), + ), + wasi_otel::Value::BoolArray(v) => opentelemetry::Value::Array(v.into()), + wasi_otel::Value::Float64Array(v) => opentelemetry::Value::Array(v.into()), + wasi_otel::Value::S64Array(v) => opentelemetry::Value::Array(v.into()), + } + } + } + + impl From for opentelemetry::trace::Event { + fn from(event: wasi_otel::Event) -> Self { + Self::new( + event.name, + event.time.into(), + event.attributes.into_iter().map(Into::into).collect(), + 0, + ) + } + } + impl From for opentelemetry::trace::Link { fn from(link: wasi_otel::Link) -> Self { Self::new( @@ -642,11 +645,15 @@ mod otel { } } - impl From for SystemTime { - fn from(timestamp: wall_clock::Datetime) -> Self { - UNIX_EPOCH - + Duration::from_secs(timestamp.seconds) - + Duration::from_nanos(timestamp.nanoseconds as u64) + impl From for opentelemetry::trace::Status { + fn from(status: wasi_otel::Status) -> Self { + match status { + wasi_otel::Status::Unset => Self::Unset, + wasi_otel::Status::Ok => Self::Ok, + wasi_otel::Status::Error(s) => Self::Error { + description: s.into(), + }, + } } } @@ -666,45 +673,40 @@ mod otel { } } - // #[allow(clippy::derivable_impls)] - // impl Default for wasi_otel::StartOptions { - // fn default() -> Self { - // Self { - // new_root: false, - // span_kind: None, - // attributes: None, - // links: None, - // timestamp: None, - // } - // } - // } - - // mod test { - // #[test] - // fn trace_flags() { - // let flags = opentelemetry::trace::TraceFlags::SAMPLED; - // let flags2 = crate::wasi::otel::tracing::TraceFlags::from(flags); - // let flags3 = opentelemetry::trace::TraceFlags::from(flags2); - // assert_eq!(flags, flags3); - // } - - // #[test] - // fn span_context() { - // let sc = opentelemetry::trace::SpanContext::new( - // opentelemetry::trace::TraceId::from_hex("4fb34cb4484029f7881399b149e41e98") - // .unwrap(), - // opentelemetry::trace::SpanId::from_hex("9ffd58d3cd4dd90b").unwrap(), - // opentelemetry::trace::TraceFlags::SAMPLED, - // false, - // opentelemetry::trace::TraceState::from_key_value(vec![ - // ("foo", "bar"), - // ("baz", "qux"), - // ]) - // .unwrap(), - // ); - // let sc2 = crate::wasi::otel::tracing::SpanContext::from(sc.clone()); - // let sc3 = opentelemetry::trace::SpanContext::from(sc2); - // assert_eq!(sc, sc3); - // } - // } + impl From for SystemTime { + fn from(timestamp: wall_clock::Datetime) -> Self { + UNIX_EPOCH + + Duration::from_secs(timestamp.seconds) + + Duration::from_nanos(timestamp.nanoseconds as u64) + } + } + + mod test { + #[test] + fn trace_flags() { + let flags = opentelemetry::trace::TraceFlags::SAMPLED; + let flags2 = crate::wasi::otel::tracing::TraceFlags::from(flags); + let flags3 = opentelemetry::trace::TraceFlags::from(flags2); + assert_eq!(flags, flags3); + } + + #[test] + fn span_context() { + let sc = opentelemetry::trace::SpanContext::new( + opentelemetry::trace::TraceId::from_hex("4fb34cb4484029f7881399b149e41e98") + .unwrap(), + opentelemetry::trace::SpanId::from_hex("9ffd58d3cd4dd90b").unwrap(), + opentelemetry::trace::TraceFlags::SAMPLED, + false, + opentelemetry::trace::TraceState::from_key_value(vec![ + ("foo", "bar"), + ("baz", "qux"), + ]) + .unwrap(), + ); + let sc2 = crate::wasi::otel::tracing::SpanContext::from(sc.clone()); + let sc3 = opentelemetry::trace::SpanContext::from(sc2); + assert_eq!(sc, sc3); + } + } } From 413a5125301fcf937587298f32e028dac7f551bc Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Wed, 26 Feb 2025 15:02:06 -0700 Subject: [PATCH 10/16] Don't panic where we shouldn't Signed-off-by: Caleb Schoepp --- crates/world/src/conversions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 32f38f7b1d..f4fea270d7 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -512,7 +512,7 @@ mod otel { Self { span_context: value.span_context.into(), parent_span_id: opentelemetry::trace::SpanId::from_hex(&value.parent_span_id) - .expect("TODO THIS IS BAD"), + .unwrap_or(opentelemetry::trace::SpanId::INVALID), span_kind: value.span_kind.into(), name: value.name.into(), start_time: value.start_time.into(), From 40be93ffd8dd14ae27ab73f1eef3fee6eb6eb758 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Sat, 1 Mar 2025 23:23:42 -0700 Subject: [PATCH 11/16] Rebase fixup Signed-off-by: Caleb Schoepp --- crates/factor-otel/src/host.rs | 4 - crates/world/src/conversions.rs | 4 +- tests/test-components/components/Cargo.lock | 181 +++++++++++++++--- .../components/wasi-otel-tracing/Cargo.toml | 2 +- wit/deps/otel/tracing.wit | 4 +- 5 files changed, 161 insertions(+), 34 deletions(-) diff --git a/crates/factor-otel/src/host.rs b/crates/factor-otel/src/host.rs index 10e39293de..bdffe6dd89 100644 --- a/crates/factor-otel/src/host.rs +++ b/crates/factor-otel/src/host.rs @@ -5,7 +5,6 @@ use anyhow::Result; use opentelemetry::trace::TraceContextExt; use opentelemetry::Context; use opentelemetry_sdk::trace::SpanProcessor; -use spin_core::async_trait; use spin_world::wasi::otel::tracing as wasi_otel; use spin_world::wasi::otel::tracing::SpanContext; use tracing::span; @@ -14,7 +13,6 @@ use tracing_opentelemetry::OpenTelemetrySpanExt; use crate::InstanceState; -#[async_trait] impl wasi_otel::Host for InstanceState { async fn on_start( &mut self, @@ -115,5 +113,3 @@ impl wasi_otel::Host for InstanceState { .into()) } } - -// TODO: Rename module to otel diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index f4fea270d7..3f4f2550e9 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -609,7 +609,7 @@ mod otel { match value { wasi_otel::Value::String(v) => v.into(), wasi_otel::Value::Bool(v) => v.into(), - wasi_otel::Value::Float64(v) => v.into(), + wasi_otel::Value::F64(v) => v.into(), wasi_otel::Value::S64(v) => v.into(), wasi_otel::Value::StringArray(v) => opentelemetry::Value::Array( v.into_iter() @@ -618,7 +618,7 @@ mod otel { .into(), ), wasi_otel::Value::BoolArray(v) => opentelemetry::Value::Array(v.into()), - wasi_otel::Value::Float64Array(v) => opentelemetry::Value::Array(v.into()), + wasi_otel::Value::F64Array(v) => opentelemetry::Value::Array(v.into()), wasi_otel::Value::S64Array(v) => opentelemetry::Value::Array(v.into()), } } diff --git a/tests/test-components/components/Cargo.lock b/tests/test-components/components/Cargo.lock index d944257063..2c2f86623f 100644 --- a/tests/test-components/components/Cargo.lock +++ b/tests/test-components/components/Cargo.lock @@ -14,18 +14,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "ai" version = "0.1.0" @@ -107,9 +95,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.2.15" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "shlex", ] @@ -122,16 +110,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets", + "windows-link", ] [[package]] @@ -291,9 +279,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -576,9 +564,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "log" @@ -646,7 +634,7 @@ dependencies = [ [[package]] name = "opentelemetry-wasi" version = "0.27.0" -source = "git+https://github.com/calebschoepp/opentelemetry-wasi?rev=bf4a6bf93b93bec48835360cc4d2f4155858a697#bf4a6bf93b93bec48835360cc4d2f4155858a697" +source = "git+https://github.com/calebschoepp/opentelemetry-wasi?rev=78d0cf0d1e2674591ab0dfdca8443a81a1e402bd#78d0cf0d1e2674591ab0dfdca8443a81a1e402bd" dependencies = [ "anyhow", "opentelemetry", @@ -1279,7 +1267,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -1301,7 +1289,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1342,6 +1330,16 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.215.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb56df3e06b8e6b77e37d2969a50ba51281029a9aeb3855e76b7f49b6418847" +dependencies = [ + "leb128", + "wasmparser 0.215.0", +] + [[package]] name = "wasm-encoder" version = "0.217.0" @@ -1368,6 +1366,22 @@ dependencies = [ "wasmparser 0.121.2", ] +[[package]] +name = "wasm-metadata" +version = "0.215.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6bb07c5576b608f7a2a9baa2294c1a3584a249965d695a9814a496cb6d232f" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.215.0", + "wasmparser 0.215.0", +] + [[package]] name = "wasm-metadata" version = "0.217.0" @@ -1415,6 +1429,19 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.215.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fbde0881f24199b81cf49b6ff8f9c145ac8eb1b7fc439adb5c099734f7d90e" +dependencies = [ + "ahash", + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + [[package]] name = "wasmparser" version = "0.217.0" @@ -1437,6 +1464,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-targets" version = "0.52.6" @@ -1521,13 +1554,23 @@ dependencies = [ "wit-bindgen-rust-macro 0.16.0", ] +[[package]] +name = "wit-bindgen" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4bac478334a647374ff24a74b66737a4cb586dc8288bc3080a93252cd1105c" +dependencies = [ + "wit-bindgen-rt 0.30.0", + "wit-bindgen-rust-macro 0.30.0", +] + [[package]] name = "wit-bindgen" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eb9327b2afd6af02ab39f8fbde6bfc7d369d14bc8c8688311d3defcda3952bd" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen-rt 0.32.0", "wit-bindgen-rust-macro 0.32.0", ] @@ -1553,6 +1596,17 @@ dependencies = [ "wit-parser 0.13.0", ] +[[package]] +name = "wit-bindgen-core" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7e3df01cd43cfa1cb52602e4fc05cb2b62217655f6705639b6953eb0a3fed2" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser 0.215.0", +] + [[package]] name = "wit-bindgen-core" version = "0.32.0" @@ -1564,6 +1618,15 @@ dependencies = [ "wit-parser 0.217.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2de7a3b06b9725d129b5cbd1beca968feed919c433305a23da46843185ecdd6" +dependencies = [ + "bitflags", +] + [[package]] name = "wit-bindgen-rt" version = "0.32.0" @@ -1599,6 +1662,22 @@ dependencies = [ "wit-component 0.18.2", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a767d1a8eb4e908bfc53febc48b87ada545703b16fe0148ee7736a29a01417" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.98", + "wasm-metadata 0.215.0", + "wit-bindgen-core 0.30.0", + "wit-component 0.215.0", +] + [[package]] name = "wit-bindgen-rust" version = "0.32.0" @@ -1645,6 +1724,21 @@ dependencies = [ "wit-component 0.18.2", ] +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b185c342d0d27bd83d4080f5a66cf3b4f247fa49d679bceb66e11cc7eb58b99" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.98", + "wit-bindgen-core 0.30.0", + "wit-bindgen-rust 0.30.0", +] + [[package]] name = "wit-bindgen-rust-macro" version = "0.32.0" @@ -1698,6 +1792,25 @@ dependencies = [ "wit-parser 0.13.0", ] +[[package]] +name = "wit-component" +version = "0.215.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f725e3885fc5890648be5c5cbc1353b755dc932aa5f1aa7de968b912a3280743" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.215.0", + "wasm-metadata 0.215.0", + "wasmparser 0.215.0", + "wit-parser 0.215.0", +] + [[package]] name = "wit-component" version = "0.217.0" @@ -1751,6 +1864,24 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "wit-parser" +version = "0.215.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935a97eaffd57c3b413aa510f8f0b550a4a9fe7d59e79cd8b89a83dcb860321f" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.215.0", +] + [[package]] name = "wit-parser" version = "0.217.0" diff --git a/tests/test-components/components/wasi-otel-tracing/Cargo.toml b/tests/test-components/components/wasi-otel-tracing/Cargo.toml index 40bfd42dec..03007dc3ed 100644 --- a/tests/test-components/components/wasi-otel-tracing/Cargo.toml +++ b/tests/test-components/components/wasi-otel-tracing/Cargo.toml @@ -11,6 +11,6 @@ anyhow = "1" http = "0.2" opentelemetry = "0.27.0" opentelemetry_sdk = "0.27.0" -opentelemetry-wasi = { git = "https://github.com/calebschoepp/opentelemetry-wasi", rev = "bf4a6bf93b93bec48835360cc4d2f4155858a697" } +opentelemetry-wasi = { git = "https://github.com/calebschoepp/opentelemetry-wasi", rev = "78d0cf0d1e2674591ab0dfdca8443a81a1e402bd" } spin-sdk = "3.1.0" wit-bindgen = "0.30.0" diff --git a/wit/deps/otel/tracing.wit b/wit/deps/otel/tracing.wit index dacc10ae2a..f247d42025 100644 --- a/wit/deps/otel/tracing.wit +++ b/wit/deps/otel/tracing.wit @@ -108,7 +108,7 @@ interface tracing { /// A boolean value. %bool(bool), /// A double precision floating point value. - %float64(float64), + %f64(f64), /// A signed 64 bit integer value. %s64(s64), /// A homogeneous array of string values. @@ -116,7 +116,7 @@ interface tracing { /// A homogeneous array of boolean values. bool-array(list), /// A homogeneous array of double precision floating point values. - float64-array(list), + f64-array(list), /// A homogeneous array of 64 bit integer values. s64-array(list), } From d4e30c48a6af0c0f297c328c19cb9656d70fef9d Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Sat, 1 Mar 2025 23:37:54 -0700 Subject: [PATCH 12/16] A bit of cleanup Signed-off-by: Caleb Schoepp --- crates/factor-otel/src/host.rs | 63 ++++------------------------------ 1 file changed, 7 insertions(+), 56 deletions(-) diff --git a/crates/factor-otel/src/host.rs b/crates/factor-otel/src/host.rs index bdffe6dd89..3a82e0d631 100644 --- a/crates/factor-otel/src/host.rs +++ b/crates/factor-otel/src/host.rs @@ -1,13 +1,8 @@ -// use std::time::SystemTime; - use anyhow::anyhow; use anyhow::Result; use opentelemetry::trace::TraceContextExt; -use opentelemetry::Context; use opentelemetry_sdk::trace::SpanProcessor; use spin_world::wasi::otel::tracing as wasi_otel; -use spin_world::wasi::otel::tracing::SpanContext; -use tracing::span; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -21,7 +16,7 @@ impl wasi_otel::Host for InstanceState { ) -> Result<()> { let mut state = self.state.write().unwrap(); - // Before we ever create any new spans make sure we track the original host span ID + // Before we do anything make sure we track the original host span ID for reparenting if state.original_host_span_id.is_none() { state.original_host_span_id = Some( tracing::Span::current() @@ -32,48 +27,8 @@ impl wasi_otel::Host for InstanceState { ); } - // // Get span's parent based on whether it's a new root and whether there are any active spans - // let parent_context = match (false, state.active_spans.is_empty()) { - // // Not a new root && Active spans -> Last active guest span is parent - // (false, false) => { - // let span_context = state - // .guest_spans - // .get(*state.active_spans.last().unwrap()) - // .unwrap() - // .inner - // .span_context() - // .clone(); - // Context::new().with_remote_span_context(span_context) - // } - // // Not a new root && No active spans -> Current host span is parent - // (false, true) => tracing::Span::current().context(), - // // New root && n/a -> No parent - // (true, _) => Context::new(), - // }; - - // Create the underlying opentelemetry span - // let builder = self.tracer.span_builder(span_data.name); - // if let Some(kind) = options.span_kind { - // builder = builder.with_kind(kind.into()); - // } - // if let Some(attributes) = options.attributes { - // builder = builder.with_attributes(attributes.into_iter().map(Into::into)); - // } - // if let Some(links) = options.links { - // builder = builder.with_links(links.into_iter().map(Into::into).collect()); - // } - // if let Some(timestamp) = options.timestamp { - // builder = builder.with_start_time(timestamp); - // } - // let otel_span = builder.start_with_context( - // &self.tracer, - // &Context::new().with_remote_span_context(parent.into()), - // ); - // let span_id = otel_span.span_context().span_id(); - - // Put the span in our map and push it on to our stack of active spans - let span_context = - std::convert::Into::::into(span_data.span_context); + // Put the span in our map and push it on to our stack of active spans TODO: Fix comment + let span_context: opentelemetry::trace::SpanContext = span_data.span_context.into(); let span_id = span_context.span_id(); state.guest_span_contexts.insert(span_id, span_context); state.active_spans.insert(span_id); @@ -84,15 +39,12 @@ impl wasi_otel::Host for InstanceState { async fn on_end(&mut self, span_data: wasi_otel::SpanData) -> Result<()> { let mut state = self.state.write().unwrap(); - let span_id = std::convert::Into::::into( - span_data.span_context.clone(), - ) - .span_id(); + let span_context: opentelemetry::trace::SpanContext = span_data.span_context.clone().into(); + let span_id: opentelemetry::trace::SpanId = span_context.span_id(); + self.processor.on_end(span_data.into()); - if let Some(_guest_span) = state.guest_span_contexts.get_mut(&span_id) { - // // TODO: Transfer all the data - // guest_span.end_with_timestamp(span_data.end_time.into()); + if let Some(_guest_span) = state.guest_span_contexts.get_mut(&span_id) { // Remove the span from active_spans state.active_spans.shift_remove(&span_id); @@ -101,7 +53,6 @@ impl wasi_otel::Host for InstanceState { // TODO: This seems to be wrong Err(anyhow!("BUG: cannot find resource in table")) } - // Ok(()) } async fn current_span_context(&mut self) -> Result { From cd6768a0b82db5d2d83eddd4669c4d7db1fa299f Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Mon, 3 Mar 2025 12:46:27 -0700 Subject: [PATCH 13/16] Clean up the host a bunch Signed-off-by: Caleb Schoepp --- crates/factor-otel/src/host.rs | 22 ++++++++----------- crates/factor-otel/src/lib.rs | 39 ++++++++++++---------------------- 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/crates/factor-otel/src/host.rs b/crates/factor-otel/src/host.rs index 3a82e0d631..e9c17eab79 100644 --- a/crates/factor-otel/src/host.rs +++ b/crates/factor-otel/src/host.rs @@ -27,11 +27,11 @@ impl wasi_otel::Host for InstanceState { ); } - // Put the span in our map and push it on to our stack of active spans TODO: Fix comment + // Track the guest spans context in our ordered map let span_context: opentelemetry::trace::SpanContext = span_data.span_context.into(); - let span_id = span_context.span_id(); - state.guest_span_contexts.insert(span_id, span_context); - state.active_spans.insert(span_id); + state + .guest_span_contexts + .insert(span_context.span_id(), span_context); Ok(()) } @@ -42,17 +42,13 @@ impl wasi_otel::Host for InstanceState { let span_context: opentelemetry::trace::SpanContext = span_data.span_context.clone().into(); let span_id: opentelemetry::trace::SpanId = span_context.span_id(); - self.processor.on_end(span_data.into()); + if state.guest_span_contexts.shift_remove(&span_id).is_none() { + Err(anyhow!("Trying to end a span that was not started"))?; + } - if let Some(_guest_span) = state.guest_span_contexts.get_mut(&span_id) { - // Remove the span from active_spans - state.active_spans.shift_remove(&span_id); + self.processor.on_end(span_data.into()); - Ok(()) - } else { - // TODO: This seems to be wrong - Err(anyhow!("BUG: cannot find resource in table")) - } + Ok(()) } async fn current_span_context(&mut self) -> Result { diff --git a/crates/factor-otel/src/lib.rs b/crates/factor-otel/src/lib.rs index 64083f38d0..4f36da6125 100644 --- a/crates/factor-otel/src/lib.rs +++ b/crates/factor-otel/src/lib.rs @@ -1,13 +1,12 @@ mod host; use std::{ - collections::HashMap, sync::{Arc, RwLock}, time::Duration, }; use anyhow::bail; -use indexmap::IndexSet; +use indexmap::IndexMap; use opentelemetry::{ trace::{SpanContext, SpanId, TraceContextExt}, Context, @@ -53,7 +52,6 @@ impl Factor for OtelFactor { Ok(InstanceState { state: Arc::new(RwLock::new(State { guest_span_contexts: Default::default(), - active_spans: Default::default(), original_host_span_id: None, })), processor: self.processor.clone(), @@ -63,7 +61,6 @@ impl Factor for OtelFactor { impl OtelFactor { pub fn new() -> anyhow::Result { - // TODO: Configuring the processor should move to init // This will configure the exporter based on the OTEL_EXPORTER_* environment variables. let exporter = match OtlpProtocol::traces_protocol_from_env() { OtlpProtocol::Grpc => opentelemetry_otlp::SpanExporter::builder() @@ -111,16 +108,13 @@ impl SelfInstanceBuilder for InstanceState {} /// This data lives here rather than directly on InstanceState so that we can have multiple things /// take Arc references to it. pub(crate) struct State { - /// A mapping between immutable [SpanId]s and the actual [BoxedSpan] created by our tracer. - // TODO: Rename to not include "guest" - // TODO: Merge with active_spans - pub(crate) guest_span_contexts: HashMap, - - /// A stack of [SpanIds] for all the active spans. The topmost span is the active span. + /// An order-preserved mapping between immutable [SpanId]s of guest created spans and their + /// corresponding [SpanContext]. /// - /// When a span is ended it is removed from this stack (regardless of whether is the - /// active span) and all other spans are shifted back to retain relative order. - pub(crate) active_spans: IndexSet, + /// The topmost [SpanId] is the last active span. When a span is ended it is removed from this + /// map (regardless of whether it is the active span) and all other spans are shifted back to + /// retain relative order. + pub(crate) guest_span_contexts: IndexMap, /// Id of the last span emitted from within the host before entering the guest. /// @@ -129,12 +123,6 @@ pub(crate) struct State { pub(crate) original_host_span_id: Option, } -// /// The WIT resource Span. Effectively wraps an [opentelemetry::global::BoxedSpan]. -// pub struct GuestSpan { -// /// The [opentelemetry::global::BoxedSpan] we use to do the actual tracing work. -// pub inner: BoxedSpan, -// } - /// Manages access to the OtelFactor state for the purpose of maintaining proper span /// parent/child relationships when WASI Otel spans are being created. pub struct OtelContext { @@ -170,10 +158,10 @@ impl OtelContext { /// | spin_key_value.get | /// ``` /// - /// Setting the guest spans parent as the host is trivially done. However, the more difficult - /// task is having the host factor spans be children of the guest span. - /// [`OtelContext::reparent_tracing_span`] handles this by reparenting the current span to be - /// a child of the last active guest span (which is tracked internally in the otel factor). + /// Setting the guest spans parent as the host is enabled through current_span_context. + /// However, the more difficult task is having the host factor spans be children of the guest + /// span. [`OtelContext::reparent_tracing_span`] handles this by reparenting the current span to + /// be a child of the last active guest span (which is tracked internally in the otel factor). /// /// Note that if the otel factor is not in your [`RuntimeFactors`] than this is effectively a /// no-op. @@ -191,7 +179,7 @@ impl OtelContext { }; // If there are no active guest spans then there is nothing to do - let Some(active_span) = state.active_spans.last() else { + let Some((_, active_span_context)) = state.guest_span_contexts.last() else { return; }; @@ -209,8 +197,7 @@ impl OtelContext { } // Now reparent the current span to the last active guest span - let span_context = state.guest_span_contexts.get(active_span).unwrap().clone(); - let parent_context = Context::new().with_remote_span_context(span_context); + let parent_context = Context::new().with_remote_span_context(active_span_context.clone()); tracing::Span::current().set_parent(parent_context); } } From ee6f41fd7f024c549bf9003afdb61b88acc368cc Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Thu, 6 Mar 2025 14:16:13 -0700 Subject: [PATCH 14/16] Support dropped counts Signed-off-by: Caleb Schoepp --- crates/world/src/conversions.rs | 6 +-- tests/test-components/components/Cargo.lock | 2 +- .../components/wasi-otel-tracing/Cargo.toml | 2 +- wit/deps/otel/tracing.wit | 39 ++++++++++--------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 3f4f2550e9..49dca08009 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -507,8 +507,10 @@ mod otel { fn from(value: wasi_otel::SpanData) -> Self { let mut span_events = SpanEvents::default(); span_events.events = value.events.into_iter().map(Into::into).collect(); + span_events.dropped_count = value.dropped_events; let mut span_links = SpanLinks::default(); span_links.links = value.links.into_iter().map(Into::into).collect(); + span_links.dropped_count = value.dropped_links; Self { span_context: value.span_context.into(), parent_span_id: opentelemetry::trace::SpanId::from_hex(&value.parent_span_id) @@ -518,7 +520,7 @@ mod otel { start_time: value.start_time.into(), end_time: value.end_time.into(), attributes: value.attributes.into_iter().map(Into::into).collect(), - dropped_attributes_count: 0, + dropped_attributes_count: value.dropped_attributes, events: span_events, links: span_links, status: value.status.into(), @@ -529,7 +531,6 @@ mod otel { impl From for opentelemetry::trace::SpanContext { fn from(sc: wasi_otel::SpanContext) -> Self { - // TODO(Reviewer): Should this be try_from instead an propagate this error out of the WIT? let trace_id = opentelemetry::trace::TraceId::from_hex(&sc.trace_id) .unwrap_or(opentelemetry::trace::TraceId::INVALID); let span_id = opentelemetry::trace::SpanId::from_hex(&sc.span_id) @@ -557,7 +558,6 @@ mod otel { .trace_state() .header() .split(',') - // TODO(Reviewer): Should this be try_from instead an propagate this error out of the WIT? .filter_map(|s| { if let Some((key, value)) = s.split_once('=') { Some((key.to_string(), value.to_string())) diff --git a/tests/test-components/components/Cargo.lock b/tests/test-components/components/Cargo.lock index 2c2f86623f..4190d52f1c 100644 --- a/tests/test-components/components/Cargo.lock +++ b/tests/test-components/components/Cargo.lock @@ -634,7 +634,7 @@ dependencies = [ [[package]] name = "opentelemetry-wasi" version = "0.27.0" -source = "git+https://github.com/calebschoepp/opentelemetry-wasi?rev=78d0cf0d1e2674591ab0dfdca8443a81a1e402bd#78d0cf0d1e2674591ab0dfdca8443a81a1e402bd" +source = "git+https://github.com/calebschoepp/opentelemetry-wasi?rev=bd0fad4dd41c07a64e02fb048b2ec56dc08d19ed#bd0fad4dd41c07a64e02fb048b2ec56dc08d19ed" dependencies = [ "anyhow", "opentelemetry", diff --git a/tests/test-components/components/wasi-otel-tracing/Cargo.toml b/tests/test-components/components/wasi-otel-tracing/Cargo.toml index 03007dc3ed..e4356e29e1 100644 --- a/tests/test-components/components/wasi-otel-tracing/Cargo.toml +++ b/tests/test-components/components/wasi-otel-tracing/Cargo.toml @@ -11,6 +11,6 @@ anyhow = "1" http = "0.2" opentelemetry = "0.27.0" opentelemetry_sdk = "0.27.0" -opentelemetry-wasi = { git = "https://github.com/calebschoepp/opentelemetry-wasi", rev = "78d0cf0d1e2674591ab0dfdca8443a81a1e402bd" } +opentelemetry-wasi = { git = "https://github.com/calebschoepp/opentelemetry-wasi", rev = "bd0fad4dd41c07a64e02fb048b2ec56dc08d19ed" } spin-sdk = "3.1.0" wit-bindgen = "0.30.0" diff --git a/wit/deps/otel/tracing.wit b/wit/deps/otel/tracing.wit index f247d42025..171b51ee84 100644 --- a/wit/deps/otel/tracing.wit +++ b/wit/deps/otel/tracing.wit @@ -12,33 +12,34 @@ interface tracing { /// The data associated with a span. record span-data { - /// Span context + /// Span context. span-context: span-context, - /// Span parent id - // TODO: No clue what this is for + /// Span parent id. parent-span-id: string, - /// Span kind + /// Span kind. span-kind: span-kind, - // Span name + // Span name. name: string, - /// Span start time + /// Span start time. start-time: datetime, - /// Span end time + /// Span end time. end-time: datetime, - /// Span attributes + /// Span attributes. attributes: list, - /// Span events + /// Span events. events: list, - /// Span Links + /// Span Links. links: list, - /// Span status + /// Span status. status: status, - /// Instrumentation scope that produced this span + /// Instrumentation scope that produced this span. instrumentation-scope: instrumentation-scope, - - // TODO: - // - Dropped counts - // - parent span id vs parent span context + /// Number of attributes dropped by the span due to limits being reached. + dropped-attributes: u32, + /// Number of events dropped by the span due to limits being reached. + dropped-events: u32, + /// Number of links dropped by the span due to limits being reached. + dropped-links: u32, } /// Identifying trace information about a span that can be serialized and propagated. @@ -123,11 +124,11 @@ interface tracing { /// An event describing a specific moment in time on a span and associated attributes. record event { - /// Event name + /// Event name. name: string, - /// Event time + /// Event time. time: datetime, - /// Event attributes + /// Event attributes. attributes: list, } From db94e84bb8256ed936e485ec1c896651495cc4f9 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Thu, 6 Mar 2025 14:56:16 -0700 Subject: [PATCH 15/16] shift some things around Signed-off-by: Caleb Schoepp --- tests/integration.rs | 1 - .../components/wasi-otel-tracing/src/lib.rs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index d7340be7ea..c8ff4211ad 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1399,7 +1399,6 @@ route = "/..." } } -// TODO(Reviewer): How can I move this to a new file? I wasn't able to get the imports to work out. mod otel_integration_tests { use fake_opentelemetry_collector::FakeCollectorServer; use std::time::Duration; diff --git a/tests/test-components/components/wasi-otel-tracing/src/lib.rs b/tests/test-components/components/wasi-otel-tracing/src/lib.rs index a166657344..8fc55e4e43 100644 --- a/tests/test-components/components/wasi-otel-tracing/src/lib.rs +++ b/tests/test-components/components/wasi-otel-tracing/src/lib.rs @@ -52,10 +52,6 @@ fn nested_spans(_req: Request, _params: Params) -> Response { Response::new(200, "") } -// TODO: Test what happens if start is called but not end -// TODO: Test what happens if end is called but not start -// TODO: What happens if child span outlives parent - fn setting_attributes(_req: Request, _params: Params) -> Response { let (tracer, _ctx) = setup_tracer(true); tracer.in_span("setting_attributes", |cx| { @@ -128,3 +124,7 @@ async fn make_request() { .build(); let _res: Response = spin_sdk::http::send(req).await.unwrap(); } + +// TODO: Test what happens if start is called but not end +// TODO: Test what happens if end is called but not start +// TODO: What happens if child span outlives parent From 77361660ebdce4ba7132660756a2a97adb116f77 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Wed, 19 Mar 2025 17:19:30 -0600 Subject: [PATCH 16/16] Cleanup wit a bit Signed-off-by: Caleb Schoepp --- crates/factor-otel/src/host.rs | 10 +++------- wit/deps/otel/tracing.wit | 6 +++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/factor-otel/src/host.rs b/crates/factor-otel/src/host.rs index e9c17eab79..12513ab9e2 100644 --- a/crates/factor-otel/src/host.rs +++ b/crates/factor-otel/src/host.rs @@ -9,11 +9,7 @@ use tracing_opentelemetry::OpenTelemetrySpanExt; use crate::InstanceState; impl wasi_otel::Host for InstanceState { - async fn on_start( - &mut self, - span_data: wasi_otel::SpanData, - _parent: wasi_otel::SpanContext, - ) -> Result<()> { + async fn on_start(&mut self, context: wasi_otel::SpanContext) -> Result<()> { let mut state = self.state.write().unwrap(); // Before we do anything make sure we track the original host span ID for reparenting @@ -28,7 +24,7 @@ impl wasi_otel::Host for InstanceState { } // Track the guest spans context in our ordered map - let span_context: opentelemetry::trace::SpanContext = span_data.span_context.into(); + let span_context: opentelemetry::trace::SpanContext = context.into(); state .guest_span_contexts .insert(span_context.span_id(), span_context); @@ -51,7 +47,7 @@ impl wasi_otel::Host for InstanceState { Ok(()) } - async fn current_span_context(&mut self) -> Result { + async fn outer_span_context(&mut self) -> Result { Ok(tracing::Span::current() .context() .span() diff --git a/wit/deps/otel/tracing.wit b/wit/deps/otel/tracing.wit index 171b51ee84..8ac7385652 100644 --- a/wit/deps/otel/tracing.wit +++ b/wit/deps/otel/tracing.wit @@ -2,13 +2,13 @@ interface tracing { use wasi:clocks/wall-clock@0.2.0.{datetime}; /// Called when a span is started. - on-start: func(span: span-data, parent: span-context); + on-start: func(context: span-context); /// Called when a span is ended. on-end: func(span: span-data); - /// Returns the current span context of the host. - current-span-context: func() -> span-context; + /// Returns the span context of the host. + outer-span-context: func() -> span-context; /// The data associated with a span. record span-data {