diff --git a/Cargo.lock b/Cargo.lock index 4b4c8cb3d9..dfc16801e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1499,7 +1499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" dependencies = [ "serde", - "toml", + "toml 0.8.19", ] [[package]] @@ -1747,7 +1747,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml", + "toml 0.8.19", "yaml-rust2", ] @@ -2821,6 +2821,25 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "fake-opentelemetry-collector" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce713bed1dfc379216421ebf0c4ee2268b5cb4d7894bed8a96d267834590916" +dependencies = [ + "futures", + "hex", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-proto", + "opentelemetry_sdk", + "serde", + "tokio", + "tokio-stream", + "tonic", + "tracing", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -5998,6 +6017,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" dependencies = [ + "async-std", "async-trait", "futures-channel", "futures-executor", @@ -8228,7 +8248,7 @@ dependencies = [ "spin-factors-test", "spin-locked-app", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -8243,7 +8263,7 @@ dependencies = [ "subprocess", "terminal", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -8262,6 +8282,7 @@ dependencies = [ "conformance-tests", "ctrlc", "dialoguer", + "fake-opentelemetry-collector", "futures", "hex", "http 1.1.0", @@ -8312,7 +8333,7 @@ dependencies = [ "test-environment", "testing-framework", "tokio", - "toml", + "toml 0.8.19", "tracing", "url", "uuid", @@ -8347,7 +8368,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", - "toml", + "toml 0.8.19", "tracing", "wasm-encoder 0.236.1", "wasm-metadata 0.236.1", @@ -8409,7 +8430,7 @@ dependencies = [ "tempfile", "terminal", "tokio", - "toml", + "toml 0.8.19", "toml_edit", "tracing", "ui-testing", @@ -8438,7 +8459,7 @@ dependencies = [ "spin-manifest", "spin-serde", "tokio", - "toml", + "toml 0.8.19", "tracing", "wac-graph", "wac-types", @@ -8459,7 +8480,7 @@ dependencies = [ "spin-locked-app", "thiserror 2.0.12", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -8469,6 +8490,7 @@ dependencies = [ "anyhow", "serde", "spin-core", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-key-value-redis", @@ -8480,7 +8502,7 @@ dependencies = [ "tempfile", "thiserror 2.0.12", "tokio", - "toml", + "toml 0.8.19", "tracing", ] @@ -8491,6 +8513,7 @@ dependencies = [ "anyhow", "async-trait", "serde", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-llm-local", @@ -8499,11 +8522,30 @@ dependencies = [ "spin-telemetry", "spin-world", "tokio", - "toml", + "toml 0.8.19", "tracing", "url", ] +[[package]] +name = "spin-factor-otel" +version = "3.5.0-pre0" +dependencies = [ + "anyhow", + "indexmap 2.7.1", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "spin-core", + "spin-factors", + "spin-resource-table", + "spin-telemetry", + "spin-world", + "toml 0.5.11", + "tracing", + "tracing-opentelemetry", +] + [[package]] name = "spin-factor-outbound-http" version = "3.5.0-pre0" @@ -8517,6 +8559,7 @@ dependencies = [ "reqwest 0.12.9", "rustls 0.23.18", "serde", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8539,6 +8582,7 @@ dependencies = [ "anyhow", "rumqttc", "spin-core", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8556,6 +8600,7 @@ dependencies = [ "anyhow", "mysql_async", "spin-core", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8588,7 +8633,7 @@ dependencies = [ "spin-serde", "tempfile", "tokio", - "toml", + "toml 0.8.19", "tracing", "url", "wasmtime-wasi", @@ -8610,6 +8655,7 @@ dependencies = [ "rust_decimal", "serde_json", "spin-core", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8629,6 +8675,7 @@ dependencies = [ "anyhow", "redis 0.25.4", "spin-core", + "spin-factor-otel", "spin-factor-outbound-networking", "spin-factor-variables", "spin-factors", @@ -8644,6 +8691,7 @@ name = "spin-factor-sqlite" version = "3.5.0-pre0" dependencies = [ "async-trait", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-locked-app", @@ -8658,6 +8706,7 @@ name = "spin-factor-variables" version = "3.5.0-pre0" dependencies = [ "spin-expressions", + "spin-factor-otel", "spin-factors", "spin-factors-test", "spin-telemetry", @@ -8689,7 +8738,7 @@ dependencies = [ "spin-app", "spin-factors-derive", "thiserror 2.0.12", - "toml", + "toml 0.8.19", "wasmtime", ] @@ -8725,7 +8774,7 @@ dependencies = [ "spin-loader", "spin-telemetry", "tempfile", - "toml", + "toml 0.8.19", ] [[package]] @@ -8743,7 +8792,7 @@ dependencies = [ "spin-app", "spin-factor-outbound-http", "spin-http-routes", - "toml", + "toml 0.8.19", "tracing", "wasmtime", "wasmtime-wasi", @@ -8873,7 +8922,7 @@ dependencies = [ "tempfile", "terminal", "tokio", - "toml", + "toml 0.8.19", "tracing", "ui-testing", "wasm-pkg-client", @@ -8906,7 +8955,7 @@ dependencies = [ "spin-serde", "terminal", "thiserror 2.0.12", - "toml", + "toml 0.8.19", "ui-testing", "url", "wasm-pkg-common", @@ -8999,6 +9048,7 @@ dependencies = [ "spin-expressions", "spin-factor-key-value", "spin-factor-llm", + "spin-factor-otel", "spin-factor-outbound-http", "spin-factor-outbound-mqtt", "spin-factor-outbound-mysql", @@ -9023,7 +9073,7 @@ dependencies = [ "spin-world", "tempfile", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -9035,6 +9085,7 @@ dependencies = [ "spin-common", "spin-factor-key-value", "spin-factor-llm", + "spin-factor-otel", "spin-factor-outbound-http", "spin-factor-outbound-mqtt", "spin-factor-outbound-mysql", @@ -9074,7 +9125,7 @@ dependencies = [ "spin-factors", "spin-sqlite-inproc", "spin-sqlite-libsql", - "toml", + "toml 0.8.19", ] [[package]] @@ -9146,7 +9197,7 @@ dependencies = [ "tempfile", "terminal", "tokio", - "toml", + "toml 0.8.19", "toml_edit", "url", "walkdir", @@ -9196,6 +9247,7 @@ dependencies = [ "serde_json", "spin-app", "spin-core", + "spin-factor-otel", "spin-factor-outbound-http", "spin-factor-outbound-networking", "spin-factor-wasi", @@ -9266,7 +9318,7 @@ dependencies = [ "spin-expressions", "spin-factors", "tempfile", - "toml", + "toml 0.8.19", ] [[package]] @@ -9284,7 +9336,10 @@ dependencies = [ name = "spin-world" version = "3.5.0-pre0" dependencies = [ + "anyhow", "async-trait", + "opentelemetry", + "opentelemetry_sdk", "wasmtime", ] @@ -9992,6 +10047,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" @@ -10942,7 +11006,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-util", - "toml", + "toml 0.8.19", "tracing", "tracing-subscriber", "url", @@ -10971,7 +11035,7 @@ dependencies = [ "sha2", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", ] [[package]] @@ -11163,7 +11227,7 @@ dependencies = [ "serde", "serde_derive", "sha2", - "toml", + "toml 0.8.19", "windows-sys 0.60.2", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index 01fddbe860..b030d88b01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ openssl = { version = "0.10" } anyhow = { workspace = true, features = ["backtrace"] } conformance = { path = "tests/conformance-tests" } conformance-tests = { workspace = true } +fake-opentelemetry-collector = "0.26" hex = "0.4" http-body-util = { workspace = true } hyper = { workspace = true } @@ -142,6 +143,9 @@ hyper-util = { version = "0.1", features = ["tokio"] } indexmap = "2" itertools = "0.14" lazy_static = "1.5" +opentelemetry = "0.28" +opentelemetry-otlp = "0.28" +opentelemetry_sdk = "0.28" path-absolutize = "3" quote = "1" rand = "0.9" @@ -168,6 +172,7 @@ toml = "0.8" toml_edit = "0.22" tower-service = "0.3.3" tracing = { version = "0.1.41", features = ["log"] } +tracing-opentelemetry = "0.29" url = "2" walkdir = "2" wasm-encoder = "0.236.1" diff --git a/crates/factor-key-value/Cargo.toml b/crates/factor-key-value/Cargo.toml index 8327737bd1..f0bdd6a2b9 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-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 ada4ece9b2..a7aef7005c 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_otel::OtelContext; use spin_resource_table::Table; use spin_telemetry::traces::{self, Blame}; use spin_world::v2::key_value; @@ -49,23 +50,26 @@ pub struct KeyValueDispatch { manager: Arc, stores: Table>, compare_and_swaps: Table>, + otel_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, + otel_context: Option, ) -> Self { Self { allowed_stores, manager, stores: Table::new(capacity), compare_and_swaps: Table::new(capacity), + otel_context, } } @@ -113,6 +117,9 @@ impl key_value::Host for KeyValueDispatch {} impl key_value::HostStore for KeyValueDispatch { #[instrument(name = "spin_key_value.open", skip(self), err, 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(otel_context) = self.otel_context.as_ref() { + otel_context.reparent_tracing_span() + } Ok(async { if self.allowed_stores.contains(&name) { let store = self.manager.get(&name).await?; @@ -135,6 +142,9 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result>, Error>> { + 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.map_err(track_error_on_span)) } @@ -146,6 +156,9 @@ impl key_value::HostStore for KeyValueDispatch { key: String, value: Vec, ) -> Result> { + 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.map_err(track_error_on_span)) } @@ -156,6 +169,9 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result> { + 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.map_err(track_error_on_span)) } @@ -166,6 +182,9 @@ impl key_value::HostStore for KeyValueDispatch { store: Resource, key: String, ) -> Result> { + 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.map_err(track_error_on_span)) } @@ -175,6 +194,9 @@ impl key_value::HostStore for KeyValueDispatch { &mut self, store: Resource, ) -> Result, Error>> { + 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.map_err(track_error_on_span)) } diff --git a/crates/factor-key-value/src/lib.rs b/crates/factor-key-value/src/lib.rs index 792ea18b92..62929a75ff 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_otel::OtelContext; use spin_factors::{ ConfigureAppContext, Factor, FactorData, FactorInstanceBuilder, InitContext, PrepareContext, RuntimeFactors, @@ -87,7 +88,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 @@ -95,9 +96,11 @@ impl Factor for KeyValueFactor { .get(ctx.app_component().id()) .expect("component should be in component_stores") .clone(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceBuilder { store_manager: app_state.store_manager.clone(), allowed_stores, + otel_context, }) } } @@ -177,6 +180,7 @@ pub struct InstanceBuilder { store_manager: Arc, /// The allowed stores for this component instance. allowed_stores: HashSet, + otel_context: OtelContext, } impl FactorInstanceBuilder for InstanceBuilder { @@ -186,11 +190,13 @@ impl FactorInstanceBuilder for InstanceBuilder { let Self { store_manager, allowed_stores, + otel_context, } = self; Ok(KeyValueDispatch::new_with_capacity( allowed_stores, store_manager, u32::MAX, + Some(otel_context), )) } } diff --git a/crates/factor-llm/Cargo.toml b/crates/factor-llm/Cargo.toml index c43a7bbd6c..61121c6a49 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-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 63a5525146..61c49b5313 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.otel_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.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 4f43feac68..b3e5c6a700 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_otel::OtelContext; use spin_factors::{ ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors, SelfInstanceBuilder, }; @@ -73,7 +74,7 @@ impl Factor for LlmFactor { fn prepare( &self, - ctx: PrepareContext, + mut ctx: PrepareContext, ) -> anyhow::Result { let allowed_models = ctx .app_state() @@ -82,10 +83,12 @@ impl Factor for LlmFactor { .cloned() .unwrap_or_default(); let engine = ctx.app_state().engine.clone(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { engine, allowed_models, + otel_context, }) } } @@ -100,6 +103,7 @@ pub struct AppState { pub struct InstanceState { engine: Arc>, pub allowed_models: Arc>, + otel_context: OtelContext, } /// The runtime configuration for the LLM factor. diff --git a/crates/factor-otel/Cargo.toml b/crates/factor-otel/Cargo.toml new file mode 100644 index 0000000000..41f9498802 --- /dev/null +++ b/crates/factor-otel/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "spin-factor-otel" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +indexmap = "2.2.6" +opentelemetry = { workspace = true } +opentelemetry_sdk = { workspace = true } +opentelemetry-otlp = { workspace = true, 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 } + +[dev-dependencies] +toml = "0.5" + +[lints] +workspace = true diff --git a/crates/factor-otel/src/host.rs b/crates/factor-otel/src/host.rs new file mode 100644 index 0000000000..12513ab9e2 --- /dev/null +++ b/crates/factor-otel/src/host.rs @@ -0,0 +1,58 @@ +use anyhow::anyhow; +use anyhow::Result; +use opentelemetry::trace::TraceContextExt; +use opentelemetry_sdk::trace::SpanProcessor; +use spin_world::wasi::otel::tracing as wasi_otel; + +use tracing_opentelemetry::OpenTelemetrySpanExt; + +use crate::InstanceState; + +impl wasi_otel::Host for InstanceState { + 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 + if state.original_host_span_id.is_none() { + state.original_host_span_id = Some( + tracing::Span::current() + .context() + .span() + .span_context() + .span_id(), + ); + } + + // Track the guest spans context in our ordered map + let span_context: opentelemetry::trace::SpanContext = context.into(); + state + .guest_span_contexts + .insert(span_context.span_id(), span_context); + + Ok(()) + } + + async fn on_end(&mut self, span_data: wasi_otel::SpanData) -> Result<()> { + let mut state = self.state.write().unwrap(); + + let span_context: opentelemetry::trace::SpanContext = span_data.span_context.clone().into(); + let span_id: opentelemetry::trace::SpanId = span_context.span_id(); + + if state.guest_span_contexts.shift_remove(&span_id).is_none() { + Err(anyhow!("Trying to end a span that was not started"))?; + } + + self.processor.on_end(span_data.into()); + + Ok(()) + } + + async fn outer_span_context(&mut self) -> Result { + Ok(tracing::Span::current() + .context() + .span() + .span_context() + .clone() + .into()) + } +} diff --git a/crates/factor-otel/src/lib.rs b/crates/factor-otel/src/lib.rs new file mode 100644 index 0000000000..46bae13f25 --- /dev/null +++ b/crates/factor-otel/src/lib.rs @@ -0,0 +1,197 @@ +mod host; + +use std::{ + sync::{Arc, RwLock}, +}; + +use anyhow::bail; +use indexmap::IndexMap; +use opentelemetry::{ + trace::{SpanContext, SpanId, TraceContextExt}, + Context, +}; +use opentelemetry_sdk::{ + resource::{EnvResourceDetector, TelemetryResourceDetector}, + trace::{BatchSpanProcessor, SpanProcessor}, + Resource, +}; +use spin_factors::{Factor, FactorData, PrepareContext, RuntimeFactors, SelfInstanceBuilder}; +use spin_telemetry::{detector::SpinResourceDetector, env::OtlpProtocol}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +pub struct OtelFactor { + processor: Arc, +} + +impl Factor for OtelFactor { + type RuntimeConfig = (); + type AppState = (); + type InstanceBuilder = InstanceState; + + fn init(&mut self, ctx: &mut impl spin_factors::InitContext) -> anyhow::Result<()> { + ctx.link_bindings(spin_world::wasi::otel::tracing::add_to_linker::<_, FactorData>)?; + Ok(()) + } + + fn configure_app( + &self, + _ctx: spin_factors::ConfigureAppContext, + ) -> anyhow::Result { + Ok(()) + } + + fn prepare( + &self, + _: spin_factors::PrepareContext, + ) -> anyhow::Result { + Ok(InstanceState { + state: Arc::new(RwLock::new(State { + guest_span_contexts: Default::default(), + original_host_span_id: None, + })), + processor: self.processor.clone(), + }) + } +} + +impl OtelFactor { + pub fn new() -> anyhow::Result { + // 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::BatchSpanProcessor::builder(exporter).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(); + + let detectors: &[Box; 3] = &[ + // Set service.name from env OTEL_SERVICE_NAME > env OTEL_RESOURCE_ATTRIBUTES > spin + // Set service.version from Spin metadata + 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), + ]; + + processor.set_resource(&Resource::builder().with_detectors(detectors).build()); + + Ok(Self { + processor: Arc::new(processor), + }) + } +} + +pub struct InstanceState { + pub(crate) state: Arc>, + pub(crate) processor: Arc, +} + +impl SelfInstanceBuilder for InstanceState {} + +/// 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. +pub(crate) struct State { + /// An order-preserved mapping between immutable [SpanId]s of guest created spans and their + /// corresponding [SpanContext]. + /// + /// 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. + /// + /// 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, +} + +/// 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 OtelContext { + /// Creates an [`OtelContext`] from a [`PrepareContext`]. + /// + /// 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::() { + 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 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. + /// + /// ```text + /// | GET /... _________________________________| + /// | execute_wasm_component foo ___________| + /// | my_guest_span ___________________| + /// | spin_key_value.get | + /// ``` + /// + /// 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. + /// + /// 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 + // Otel 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((_, active_span_context)) = state.guest_span_contexts.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 parent_context = Context::new().with_remote_span_context(active_span_context.clone()); + tracing::Span::current().set_parent(parent_context); + } +} diff --git a/crates/factor-outbound-http/Cargo.toml b/crates/factor-outbound-http/Cargo.toml index 2dba3e9f13..23da53694c 100644 --- a/crates/factor-outbound-http/Cargo.toml +++ b/crates/factor-outbound-http/Cargo.toml @@ -14,6 +14,7 @@ hyper-util = { workspace = true } reqwest = { workspace = true, features = ["gzip"] } rustls = { workspace = true } serde = { workspace = true } +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 4fc368de55..600fd38ff0 100644 --- a/crates/factor-outbound-http/src/lib.rs +++ b/crates/factor-outbound-http/src/lib.rs @@ -14,6 +14,7 @@ use http::{ }; use intercept::OutboundHttpInterceptor; use runtime_config::RuntimeConfig; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::{ config::{allowed_hosts::OutboundAllowedHosts, blocked_networks::BlockedNetworks}, ComponentTlsClientConfigs, OutboundNetworkingFactor, @@ -68,6 +69,7 @@ impl Factor for OutboundHttpFactor { let allowed_hosts = outbound_networking.allowed_hosts(); let blocked_networks = outbound_networking.blocked_networks(); let component_tls_configs = outbound_networking.component_tls_configs(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { wasi_http_ctx: WasiHttpCtx::new(), allowed_hosts, @@ -78,6 +80,7 @@ impl Factor for OutboundHttpFactor { spin_http_client: None, wasi_http_clients: ctx.app_state().wasi_http_clients.clone(), connection_pooling: ctx.app_state().connection_pooling, + otel_context, }) } } @@ -101,6 +104,7 @@ pub struct InstanceState { // among all instances of the app. wasi_http_clients: wasi::HttpClients, connection_pooling: bool, + otel_context: OtelContext, } impl InstanceState { diff --git a/crates/factor-outbound-http/src/spin.rs b/crates/factor-outbound-http/src/spin.rs index 2f8fc428fa..d7ec8f3275 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.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 c141be76b5..5413e803ea 100644 --- a/crates/factor-outbound-http/src/wasi.rs +++ b/crates/factor-outbound-http/src/wasi.rs @@ -149,6 +149,8 @@ impl WasiHttpView for WasiHttpImplInner<'_> { request: Request, config: wasmtime_wasi_http::types::OutgoingRequestConfig, ) -> wasmtime_wasi_http::HttpResult { + self.state.otel_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..962b5efb0a 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-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 28ca42885d..53cb6a9830 100644 --- a/crates/factor-outbound-mqtt/src/host.rs +++ b/crates/factor-outbound-mqtt/src/host.rs @@ -4,6 +4,7 @@ use anyhow::Result; use spin_core::{async_trait, wasmtime::component::Resource}; use spin_factor_outbound_networking::config::allowed_hosts::OutboundAllowedHosts; use spin_world::v2::mqtt::{self as v2, Connection, Error, Qos}; +use spin_factor_otel::OtelContext; use tracing::{instrument, Level}; use crate::ClientCreator; @@ -12,14 +13,20 @@ pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table>, create_client: Arc, + otel_context: OtelContext, } impl InstanceState { - pub fn new(allowed_hosts: OutboundAllowedHosts, create_client: Arc) -> Self { + pub fn new( + allowed_hosts: OutboundAllowedHosts, + create_client: Arc, + otel_context: OtelContext, + ) -> Self { Self { allowed_hosts, create_client, connections: spin_resource_table::Table::new(1024), + otel_context, } } } @@ -72,6 +79,8 @@ impl v2::HostConnection for InstanceState { password: String, keep_alive_interval: u64, ) -> Result, Error> { + self.otel_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.otel_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 a5de2696b9..250aa09d5c 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_otel::OtelContext; use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factors::{ ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors, SelfInstanceBuilder, @@ -50,9 +51,12 @@ impl Factor for OutboundMqttFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState::new( allowed_hosts, self.create_client.clone(), + otel_context, )) } } diff --git a/crates/factor-outbound-mysql/Cargo.toml b/crates/factor-outbound-mysql/Cargo.toml index 4ae73f9fbc..ed6a0b01a0 100644 --- a/crates/factor-outbound-mysql/Cargo.toml +++ b/crates/factor-outbound-mysql/Cargo.toml @@ -14,6 +14,7 @@ mysql_async = { version = "0.35", default-features = false, features = [ "native-tls-tls", ] } spin-core = { path = "../core" } +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 e562d4d741..ca113206aa 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.otel_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.otel_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.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 fbe213bf9e..e1bf9aabdb 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_otel::OtelContext; use spin_factor_outbound_networking::{ config::allowed_hosts::OutboundAllowedHosts, OutboundNetworkingFactor, }; @@ -39,9 +40,12 @@ impl Factor for OutboundMysqlFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState { allowed_hosts, connections: Default::default(), + otel_context, }) } } @@ -63,6 +67,7 @@ impl OutboundMysqlFactor { pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, connections: spin_resource_table::Table, + otel_context: OtelContext, } impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/factor-outbound-pg/Cargo.toml b/crates/factor-outbound-pg/Cargo.toml index 6b8f32bb4c..6436d55268 100644 --- a/crates/factor-outbound-pg/Cargo.toml +++ b/crates/factor-outbound-pg/Cargo.toml @@ -16,6 +16,7 @@ postgres_range = "0.11" rust_decimal = { version = "1.37", features = ["db-tokio-postgres"] } serde_json = { workspace = true } spin-core = { path = "../core" } +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 111df6f998..81dda9bbe6 100644 --- a/crates/factor-outbound-pg/src/host.rs +++ b/crates/factor-outbound-pg/src/host.rs @@ -219,6 +219,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.otel_context.reparent_tracing_span(); spin_factor_outbound_networking::record_address_fields(&address); if !self @@ -240,6 +241,8 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { + self.otel_context.reparent_tracing_span(); + Ok(self .get_client(connection) .await? @@ -254,6 +257,7 @@ impl v2::HostConnection for InstanceState { statement: String, params: Vec, ) -> Result { + 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 b3a433946e..a793271fef 100644 --- a/crates/factor-outbound-pg/src/lib.rs +++ b/crates/factor-outbound-pg/src/lib.rs @@ -4,7 +4,9 @@ mod types; use std::sync::Arc; +use client::Client; use client::ClientFactory; +use spin_factor_otel::OtelContext; use spin_factor_outbound_networking::{ config::allowed_hosts::OutboundAllowedHosts, OutboundNetworkingFactor, }; @@ -48,10 +50,13 @@ impl Factor for OutboundPgFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState { allowed_hosts, client_factory: ctx.app_state().clone(), connections: Default::default(), + otel_context, }) } } @@ -74,6 +79,7 @@ pub struct InstanceState { allowed_hosts: OutboundAllowedHosts, client_factory: Arc, connections: spin_resource_table::Table, + otel_context: OtelContext, } impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/factor-outbound-redis/Cargo.toml b/crates/factor-outbound-redis/Cargo.toml index 092363069d..76ccc73958 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-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 d938cf9c31..61a00eb5ae 100644 --- a/crates/factor-outbound-redis/src/host.rs +++ b/crates/factor-outbound-redis/src/host.rs @@ -2,6 +2,7 @@ use anyhow::Result; use redis::{aio::MultiplexedConnection, AsyncCommands, FromRedisValue, Value}; use spin_core::wasmtime::component::Resource; use spin_factor_outbound_networking::config::allowed_hosts::OutboundAllowedHosts; +use spin_factor_otel::OtelContext; use spin_world::v1::{redis as v1, redis_types}; use spin_world::v2::redis::{ self as v2, Connection as RedisConnection, Error, RedisParameter, RedisResult, @@ -12,6 +13,7 @@ use tracing::{instrument, Level}; pub struct InstanceState { pub allowed_hosts: OutboundAllowedHosts, pub connections: spin_resource_table::Table, + pub otel_context: OtelContext, } 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.otel_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.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. // 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.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)?; Ok(value) @@ -103,6 +108,8 @@ impl v2::HostConnection for crate::InstanceState { key: String, value: Vec, ) -> Result<(), Error> { + 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. // You can read more about the issue here: @@ -116,6 +123,8 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result { + 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)?; Ok(value) @@ -127,6 +136,8 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, keys: Vec, ) -> Result { + 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)?; Ok(value) @@ -139,6 +150,8 @@ impl v2::HostConnection for crate::InstanceState { key: String, values: Vec, ) -> Result { + 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| { if e.kind() == redis::ErrorKind::TypeError { @@ -156,6 +169,8 @@ impl v2::HostConnection for crate::InstanceState { connection: Resource, key: String, ) -> Result, Error> { + 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)?; Ok(value) @@ -168,6 +183,8 @@ impl v2::HostConnection for crate::InstanceState { key: String, values: Vec, ) -> Result { + 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)?; Ok(value) @@ -180,6 +197,8 @@ impl v2::HostConnection for crate::InstanceState { command: String, arguments: Vec, ) -> Result, Error> { + self.otel_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 7986d0621a..08e7c3dd5c 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_otel::OtelContext; use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factors::{ anyhow, ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors, @@ -44,9 +45,12 @@ impl Factor for OutboundRedisFactor { let allowed_hosts = ctx .instance_builder::()? .allowed_hosts(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; + Ok(InstanceState { allowed_hosts, connections: spin_resource_table::Table::new(1024), + otel_context, }) } } diff --git a/crates/factor-sqlite/Cargo.toml b/crates/factor-sqlite/Cargo.toml index ee945ed4fa..98a6325ad7 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-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 953ee70cc4..e6ed819a54 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_otel::OtelContext; use spin_factors::wasmtime::component::Resource; use spin_factors::{anyhow, SelfInstanceBuilder}; use spin_world::spin::sqlite::sqlite as v3; @@ -17,6 +18,7 @@ pub struct InstanceState { connections: spin_resource_table::Table>, /// A map from database label to connection creators. connection_creators: HashMap>, + otel_context: OtelContext, } impl InstanceState { @@ -26,11 +28,13 @@ impl InstanceState { pub fn new( allowed_databases: Arc>, connection_creators: HashMap>, + otel_context: OtelContext, ) -> Self { Self { allowed_databases, connections: spin_resource_table::Table::new(256), connection_creators, + otel_context, } } @@ -154,6 +158,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.otel_context.reparent_tracing_span(); self.open_impl(database).await.map_err(to_v2_error) } @@ -164,6 +169,7 @@ impl v2::HostConnection for InstanceState { query: String, parameters: Vec, ) -> Result { + self.otel_context.reparent_tracing_span(); self.execute_impl( connection, query, diff --git a/crates/factor-sqlite/src/lib.rs b/crates/factor-sqlite/src/lib.rs index 9fafdb796a..ae3b4686e0 100644 --- a/crates/factor-sqlite/src/lib.rs +++ b/crates/factor-sqlite/src/lib.rs @@ -8,6 +8,7 @@ use host::InstanceState; use async_trait::async_trait; use spin_factors::{anyhow, Factor, FactorData}; +use spin_factor_otel::OtelContext; use spin_locked_app::MetadataKey; use spin_world::spin::sqlite::sqlite as v3; use spin_world::v1::sqlite as v1; @@ -74,7 +75,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() @@ -82,9 +83,11 @@ impl Factor for SqliteFactor { .get(ctx.app_component().id()) .cloned() .unwrap_or_default(); + let otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState::new( allowed_databases, ctx.app_state().connection_creators.clone(), + otel_context, )) } } diff --git a/crates/factor-variables/Cargo.toml b/crates/factor-variables/Cargo.toml index 4a1a76f83e..c24c6ebb7c 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-otel = { path = "../factor-otel" } spin-factors = { path = "../factors" } spin-telemetry = { path = "../telemetry" } spin-world = { path = "../world" } diff --git a/crates/factor-variables/src/host.rs b/crates/factor-variables/src/host.rs index 31706af0ca..784cebd280 100644 --- a/crates/factor-variables/src/host.rs +++ b/crates/factor-variables/src/host.rs @@ -8,6 +8,7 @@ use crate::InstanceState; impl variables::Host for InstanceState { #[instrument(name = "spin_variables.get", skip(self), fields(otel.kind = "client"))] async fn get(&mut self, key: String) -> Result { + 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 fc60dfed24..e8cbeb7410 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_otel::OtelContext; use spin_factors::{ anyhow, ConfigureAppContext, Factor, FactorData, 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 otel_context = OtelContext::from_prepare_context(&mut ctx)?; Ok(InstanceState { component_id, expression_resolver, + otel_context, }) } } @@ -90,6 +93,7 @@ impl AppState { pub struct InstanceState { component_id: String, expression_resolver: Arc, + otel_context: OtelContext, } impl InstanceState { diff --git a/crates/runtime-config/Cargo.toml b/crates/runtime-config/Cargo.toml index 5a8aa05ad0..1b08cda8f7 100644 --- a/crates/runtime-config/Cargo.toml +++ b/crates/runtime-config/Cargo.toml @@ -15,6 +15,7 @@ spin-common = { path = "../common" } spin-expressions = { path = "../expressions" } spin-factor-key-value = { path = "../factor-key-value" } spin-factor-llm = { path = "../factor-llm" } +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 d8bbc49a19..23109b87d8 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_otel::OtelFactor; use spin_factor_outbound_http::OutboundHttpFactor; use spin_factor_outbound_mqtt::OutboundMqttFactor; use spin_factor_outbound_mysql::OutboundMysqlFactor; @@ -398,6 +399,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 257c3f36fa..4e36fccca2 100644 --- a/crates/runtime-factors/Cargo.toml +++ b/crates/runtime-factors/Cargo.toml @@ -19,6 +19,7 @@ clap = { workspace = true, features = ["derive", "env"] } spin-common = { path = "../common" } spin-factor-key-value = { path = "../factor-key-value" } spin-factor-llm = { path = "../factor-llm" } +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 c0b8baf4d4..dc7ccdc3f2 100644 --- a/crates/runtime-factors/src/lib.rs +++ b/crates/runtime-factors/src/lib.rs @@ -10,6 +10,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_otel::OtelFactor; use spin_factor_outbound_http::OutboundHttpFactor; use spin_factor_outbound_mqtt::{NetworkedMqttClient, OutboundMqttFactor}; use spin_factor_outbound_mysql::OutboundMysqlFactor; @@ -25,6 +26,7 @@ use spin_variables_static::VariableSource; #[derive(RuntimeFactors)] pub struct TriggerFactors { + pub otel: OtelFactor, pub wasi: WasiFactor, pub variables: VariablesFactor, pub key_value: KeyValueFactor, @@ -45,6 +47,7 @@ impl TriggerFactors { allow_transient_writes: bool, ) -> anyhow::Result { Ok(Self { + otel: OtelFactor::new()?, wasi: wasi_factor(working_dir, allow_transient_writes), variables: VariablesFactor::default(), key_value: KeyValueFactor::new(), diff --git a/crates/telemetry/Cargo.toml b/crates/telemetry/Cargo.toml index bf5079ac37..0ef7d9150f 100644 --- a/crates/telemetry/Cargo.toml +++ b/crates/telemetry/Cargo.toml @@ -10,11 +10,11 @@ http0 = { version = "0.2.9", package = "http" } http1 = { version = "1.0.0", package = "http" } opentelemetry = { version = "0.28", features = ["metrics", "trace", "logs"] } opentelemetry-appender-tracing = "0.28" -opentelemetry-otlp = { version = "0.28", features = ["grpc-tonic"] } -opentelemetry_sdk = { version = "0.28", features = ["rt-tokio", "spec_unstable_logs_enabled", "metrics"] } +opentelemetry-otlp = { workspace = true, features = ["grpc-tonic"] } +opentelemetry_sdk = { workspace = true, features = ["rt-tokio", "spec_unstable_logs_enabled", "metrics"] } terminal = { path = "../terminal" } tracing = { workspace = true } -tracing-opentelemetry = { version = "0.29", default-features = false, features = ["metrics"] } +tracing-opentelemetry = { workspace = true, default-features = false, features = ["metrics"] } tracing-subscriber = { version = "0.3", default-features = false, features = ["smallvec", "fmt", "ansi", "std", "env-filter", "json", "registry"] } [features] diff --git a/crates/telemetry/src/env.rs b/crates/telemetry/src/env.rs index 5786e236df..487aad6227 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,13 +72,13 @@ 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. #[derive(Debug)] -pub(crate) enum OtlpProtocol { +pub enum OtlpProtocol { Grpc, HttpProtobuf, HttpJson, @@ -86,7 +86,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), @@ -94,7 +94,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), @@ -102,7 +102,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 4c5c99074e..dd1343e13d 100644 --- a/crates/telemetry/src/lib.rs +++ b/crates/telemetry/src/lib.rs @@ -9,7 +9,7 @@ use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter, Layer}; mod alert_in_dev; pub mod detector; -mod env; +pub mod env; pub mod logs; pub mod metrics; mod propagation; @@ -53,6 +53,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) @@ -91,6 +100,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) diff --git a/crates/trigger-http/Cargo.toml b/crates/trigger-http/Cargo.toml index 6374597910..97b0a23d77 100644 --- a/crates/trigger-http/Cargo.toml +++ b/crates/trigger-http/Cargo.toml @@ -29,6 +29,7 @@ spin-http = { path = "../http" } spin-telemetry = { path = "../telemetry" } spin-trigger = { path = "../trigger" } spin-world = { path = "../world" } +spin-factor-otel = { path = "../factor-otel" } terminal = { path = "../terminal" } tokio = { workspace = true, features = ["full"] } tokio-rustls = { workspace = true } diff --git a/crates/trigger-http/src/wasi.rs b/crates/trigger-http/src/wasi.rs index 0afbbf3fb9..cb08557c5d 100644 --- a/crates/trigger-http/src/wasi.rs +++ b/crates/trigger-http/src/wasi.rs @@ -93,6 +93,7 @@ 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 { diff --git a/crates/world/Cargo.toml b/crates/world/Cargo.toml index fd4763100d..4508fb7e6a 100644 --- a/crates/world/Cargo.toml +++ b/crates/world/Cargo.toml @@ -5,5 +5,8 @@ authors = { workspace = true } edition = { workspace = true } [dependencies] +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 8177b623b2..27b2d3705b 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -553,3 +553,219 @@ mod llm { } } } + +mod otel { + use super::*; + use opentelemetry::StringValue; + use opentelemetry_sdk::trace::{SpanEvents, SpanLinks}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + use wasi::clocks0_2_0::wall_clock; + use wasi::otel::tracing as wasi_otel; + + impl From for opentelemetry_sdk::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(); + 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) + .unwrap_or(opentelemetry::trace::SpanId::INVALID), + 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: value.dropped_attributes, + events: span_events, + links: span_links, + status: value.status.into(), + instrumentation_scope: value.instrumentation_scope.into(), + } + } + } + + impl From for opentelemetry::trace::SpanContext { + fn from(sc: wasi_otel::SpanContext) -> Self { + 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 wasi_otel::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(',') + .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::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::SpanKind { + fn from(kind: wasi_otel::SpanKind) -> Self { + match kind { + 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::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::F64(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::F64Array(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( + link.span_context.into(), + link.attributes.into_iter().map(Into::into).collect(), + 0, + ) + } + } + + 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::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(), + } + } + } + + 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); + } + } +} diff --git a/tests/integration.rs b/tests/integration.rs index a2e358195c..b5af15f952 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; @@ -1582,3 +1582,432 @@ route = "/..." Ok(()) } } + +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_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, "/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_otel_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-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, "/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-otel-tracing") + .expect("'execute_wasm_component wasi-otel-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_otel_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-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, "/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_otel_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-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, "/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-otel-tracing") + .expect("'execute_wasm_component wasi-otel-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_otel_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-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, "/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_otel_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-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, "/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_otel_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-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, "/root-span"), + Response::new(200), + )?; + + let spans = rt.block_on(collector.exported_spans(7, Duration::from_secs(5))); + + assert_eq!(spans.len(), 4); + + let root_span = spans + .iter() + .find(|s| s.name == "root") + .expect("'root' span should exist"); + let request_span = spans + .iter() + .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()); + + Ok(()) + }, + )?; + + Ok(()) + } +} diff --git a/tests/test-components/components/Cargo.lock b/tests/test-components/components/Cargo.lock index db79baac4d..407be6e74a 100644 --- a/tests/test-components/components/Cargo.lock +++ b/tests/test-components/components/Cargo.lock @@ -22,6 +22,21 @@ dependencies = [ "wit-bindgen 0.34.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" @@ -30,9 +45,9 @@ checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", @@ -61,10 +76,31 @@ dependencies = [ ] [[package]] -name = "bytes" +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 = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -72,6 +108,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[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" @@ -221,6 +277,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +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" @@ -316,6 +389,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" @@ -424,7 +520,7 @@ dependencies = [ "anyhow", "futures", "helper", - "spin-sdk 3.0.0", + "spin-sdk 3.1.0", ] [[package]] @@ -433,6 +529,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" @@ -458,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" @@ -496,12 +602,107 @@ 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 1.0.56", + "tracing", +] + +[[package]] +name = "opentelemetry" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.12", + "tracing", +] + +[[package]] +name = "opentelemetry-wasi" +version = "0.27.0" +source = "git+https://github.com/calebschoepp/opentelemetry-wasi?rev=bd0fad4dd41c07a64e02fb048b2ec56dc08d19ed#bd0fad4dd41c07a64e02fb048b2ec56dc08d19ed" +dependencies = [ + "anyhow", + "opentelemetry 0.27.1", + "opentelemetry_sdk 0.27.1", + "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 0.27.1", + "percent-encoding", + "rand", + "serde_json", + "thiserror 1.0.56", + "tracing", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry 0.28.0", + "percent-encoding", + "rand", + "serde_json", + "thiserror 2.0.12", + "tracing", +] + +[[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" @@ -561,6 +762,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" @@ -589,6 +799,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" @@ -599,6 +839,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" @@ -650,7 +896,7 @@ checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.56", ] [[package]] @@ -664,6 +910,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" @@ -708,6 +960,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" @@ -724,9 +987,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", @@ -760,19 +1023,20 @@ dependencies = [ "serde", "serde_json", "spin-macro 2.2.0", - "thiserror", + "thiserror 1.0.56", "wit-bindgen 0.13.1", ] [[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", @@ -780,8 +1044,9 @@ dependencies = [ "routefinder", "serde", "serde_json", - "spin-macro 3.0.0", - "thiserror", + "spin-executor", + "spin-macro 3.1.0", + "thiserror 1.0.56", "wit-bindgen 0.16.0", ] @@ -835,7 +1100,16 @@ version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.56", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -849,6 +1123,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -864,6 +1149,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" @@ -937,6 +1238,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" @@ -978,6 +1285,77 @@ dependencies = [ "wit-bindgen 0.34.0", ] +[[package]] +name = "wasi-otel-tracing" +version = "0.1.0" +dependencies = [ + "anyhow", + "http 0.2.11", + "opentelemetry 0.28.0", + "opentelemetry-wasi", + "opentelemetry_sdk 0.28.0", + "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.98", + "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.98", + "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" @@ -1005,6 +1383,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.219.2" @@ -1031,6 +1419,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.219.2" @@ -1078,6 +1482,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.219.2" @@ -1091,6 +1508,85 @@ 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-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" +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" @@ -1111,13 +1607,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.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e11ad55616555605a60a8b2d1d89e006c2076f46c465c892cc2c153b20d4b30" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen-rt 0.34.0", "wit-bindgen-rust-macro 0.34.0", ] @@ -1143,6 +1649,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.34.0" @@ -1154,6 +1671,15 @@ dependencies = [ "wit-parser 0.219.2", ] +[[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.34.0" @@ -1189,6 +1715,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.34.0" @@ -1235,6 +1777,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.34.0" @@ -1288,6 +1845,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.219.2" @@ -1341,6 +1917,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.219.2" @@ -1365,6 +1959,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/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-otel-tracing/Cargo.toml b/tests/test-components/components/wasi-otel-tracing/Cargo.toml new file mode 100644 index 0000000000..83610c6ea2 --- /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.28.0" +opentelemetry_sdk = "0.28.0" +opentelemetry-wasi = { git = "https://github.com/calebschoepp/opentelemetry-wasi", rev = "bd0fad4dd41c07a64e02fb048b2ec56dc08d19ed" } +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..c7d427b8bc --- /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::SdkTracerProvider; +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 = SdkTracerProvider::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, "") +} + +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(); +} + +// 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 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-otel-tracing/spin.toml b/tests/testcases/wasi-otel-tracing/spin.toml new file mode 100644 index 0000000000..a54f57210b --- /dev/null +++ b/tests/testcases/wasi-otel-tracing/spin.toml @@ -0,0 +1,19 @@ +spin_manifest_version = 2 + +[application] +authors = ["Fermyon Engineering "] +description = "An application to exercise wasi-otel tracing functionality." +name = "wasi-otel-tracing" +version = "1.0.0" + +[[trigger.http]] +route = "/..." +component = "wasi-otel-tracing" + +[component.wasi-otel-tracing] +source = "%{source=wasi-otel-tracing}" +key_value_stores = ["default"] +allowed_outbound_hosts = ["http://self", "https://asdf.com"] +[component.wasi-otel-tracing.build] +command = "cargo build --target wasm32-wasi --release" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/wit/deps/otel/tracing.wit b/wit/deps/otel/tracing.wit new file mode 100644 index 0000000000..8ac7385652 --- /dev/null +++ b/wit/deps/otel/tracing.wit @@ -0,0 +1,168 @@ +interface tracing { + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + /// Called when a span is started. + on-start: func(context: span-context); + + /// Called when a span is ended. + on-end: func(span: span-data); + + /// Returns the span context of the host. + outer-span-context: func() -> span-context; + + /// The data associated with a span. + record span-data { + /// Span context. + span-context: span-context, + /// Span parent id. + 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, + /// 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. + 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>; + + /// 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 + } + + /// 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. + %f64(f64), + /// 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. + f64-array(list), + /// A homogeneous array of 64 bit integer values. + s64-array(list), + } + + /// 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`. + record link { + /// Denotes which `span` to link to. + span-context: span-context, + /// Attributes describing the link. + attributes: 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), + } + + /// Describes the instrumentation scope that produced a span. + 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, + } +} 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 b5d66b3b2f..5ec241aeeb 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: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;