diff --git a/Cargo.lock b/Cargo.lock index b23ccf79a..0faa3c4ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7660,6 +7660,35 @@ dependencies = [ "url", ] +[[package]] +name = "spin-observe" +version = "2.6.0-pre0" +dependencies = [ + "anyhow", + "async-trait", + "dotenvy", + "futures-executor", + "indexmap 2.2.6", + "once_cell", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "pin-project-lite", + "serde 1.0.197", + "spin-app", + "spin-core", + "spin-expressions", + "spin-telemetry", + "spin-world", + "table", + "thiserror", + "tokio", + "toml 0.5.11", + "tracing", + "tracing-opentelemetry", + "vaultrs", +] + [[package]] name = "spin-oci" version = "2.6.0-pre0" @@ -7793,6 +7822,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry_sdk", "terminal", + "tokio", "tracing", "tracing-appender", "tracing-opentelemetry", @@ -7886,6 +7916,7 @@ dependencies = [ "spin-llm-remote-http", "spin-loader", "spin-manifest", + "spin-observe", "spin-outbound-networking", "spin-sqlite", "spin-sqlite-inproc", @@ -7928,6 +7959,7 @@ dependencies = [ "spin-app", "spin-core", "spin-http", + "spin-observe", "spin-outbound-networking", "spin-telemetry", "spin-testing", diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index b4b0a0c82..9ba48f292 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -483,7 +483,9 @@ impl Engine { let inner = self.module_linker.instantiate_pre(module)?; Ok(ModuleInstancePre { inner }) } +} +impl Engine { /// Find the [`HostComponentDataHandle`] for a [`HostComponent`] if configured for this engine. /// Note: [`DynamicHostComponent`]s are implicitly wrapped in `Arc`s and need to be explicitly /// typed as such here, e.g. `find_host_component_handle::>()`. diff --git a/crates/observe/Cargo.toml b/crates/observe/Cargo.toml new file mode 100644 index 000000000..e59419cea --- /dev/null +++ b/crates/observe/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "spin-observe" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = "1.0" +async-trait = "0.1" +dotenvy = "0.15" +once_cell = "1" +spin-app = { path = "../app" } +spin-core = { path = "../core" } +spin-expressions = { path = "../expressions" } +spin-world = { path = "../world" } +spin-telemetry = { path = "../telemetry" } +table = { path = "../table" } +thiserror = "1" +tokio = { version = "1", features = ["rt-multi-thread"] } +vaultrs = "0.6.2" +serde = "1.0.188" +tracing = "0.1.40" +tracing-opentelemetry = "0.23.0" +pin-project-lite = "0.2" +opentelemetry = { version = "0.22.0", features = [ "metrics", "trace"] } +opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] } +opentelemetry-otlp = { version = "0.15.0", default-features=false, features = ["http-proto", "trace", "http", "reqwest-client", "metrics", "grpc-tonic"] } +futures-executor = "0.3" +indexmap = "2.2.6" + +[dev-dependencies] +toml = "0.5" diff --git a/crates/observe/src/future.rs b/crates/observe/src/future.rs new file mode 100644 index 000000000..32b6432f9 --- /dev/null +++ b/crates/observe/src/future.rs @@ -0,0 +1,95 @@ +use anyhow::{Context, Result}; +use pin_project_lite::pin_project; +use std::{ + future::Future, + sync::{Arc, RwLock}, +}; + +use spin_core::{Engine, Store}; + +use crate::{host_component::State, ObserveHostComponent}; + +pin_project! { + struct Instrumented { + #[pin] + inner: F, + observe_context: ObserveContext, + } + + impl PinnedDrop for Instrumented { + fn drop(this: Pin<&mut Self>) { + this.project().observe_context.drop_all(); + } + } +} + +pub trait FutureExt: Future + Sized { + /// Manage WASI Observe guest spans. + fn manage_guest_spans( + self, + observe_context: ObserveContext, + ) -> Result>; +} + +impl FutureExt for F { + fn manage_guest_spans( + self, + observe_context: ObserveContext, + ) -> Result> { + Ok(Instrumented { + inner: self, + observe_context, + }) + } +} + +impl Future for Instrumented { + type Output = F::Output; + + /// Maintains the invariant that all active spans are entered before polling the inner future + /// and exited otherwise. If we don't do this then the timing (among many other things) of the + /// spans becomes wildly incorrect. + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let this = self.project(); + + // Enter the active spans before entering the inner poll + { + this.observe_context.state.write().unwrap().enter_all(); + } + + let ret = this.inner.poll(cx); + + // Exit the active spans after exiting the inner poll + { + this.observe_context.state.write().unwrap().exit_all(); + } + + ret + } +} + +/// The context necessary for the observe host component to function. +pub struct ObserveContext { + state: Arc>, +} + +impl ObserveContext { + pub fn new(store: &mut Store, engine: &Engine) -> Result { + let handle = engine + .find_host_component_handle::>() + .context("host component handle not found")?; + let state = store + .host_components_data() + .get_or_insert(handle) + .state + .clone(); + Ok(Self { state }) + } + + fn drop_all(&self) { + self.state.write().unwrap().close_from_back_to(0); + } +} diff --git a/crates/observe/src/host_component.rs b/crates/observe/src/host_component.rs new file mode 100644 index 000000000..2b88c23e7 --- /dev/null +++ b/crates/observe/src/host_component.rs @@ -0,0 +1,328 @@ +use core::panic; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, UNIX_EPOCH}; + +use anyhow::{bail, Result}; +use indexmap::IndexMap; +use opentelemetry::trace::{ + SpanContext as OtelSpanContext, SpanId, SpanKind as OtelSpanKind, Status, TraceContextExt, + TraceFlags, TraceId, TraceState, +}; +use opentelemetry::InstrumentationLibrary; +use opentelemetry_sdk::export::trace::SpanData; +use opentelemetry_sdk::trace::{SpanEvents, SpanLinks}; +use opentelemetry_sdk::Resource as OtelResource; +use spin_app::{AppComponent, DynamicHostComponent}; +use spin_core::wasmtime::component::Resource; +use spin_core::{async_trait, HostComponent}; +use spin_world::v2::observe::ReadOnlySpan; +use spin_world::v2::observe::Span as WitSpan; +use spin_world::v2::observe::{self, SpanContext}; +use tracing::field; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +pub struct ObserveHostComponent {} + +impl ObserveHostComponent { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self {} + } +} + +impl HostComponent for ObserveHostComponent { + type Data = ObserveData; + + fn add_to_linker( + linker: &mut spin_core::Linker, + get: impl Fn(&mut spin_core::Data) -> &mut Self::Data + Send + Sync + Copy + 'static, + ) -> anyhow::Result<()> { + observe::add_to_linker(linker, get) + } + + fn build_data(&self) -> Self::Data { + ObserveData { + state: Arc::new(RwLock::new(State { + guest_spans: table::Table::new(1024), + active_spans: Default::default(), + })), + } + } +} + +impl DynamicHostComponent for ObserveHostComponent { + fn update_data(&self, _data: &mut Self::Data, _component: &AppComponent) -> anyhow::Result<()> { + Ok(()) + } +} + +pub struct ObserveData { + pub(crate) state: Arc>, +} + +#[async_trait] +impl observe::Host for ObserveData { + async fn emit_span(&mut self, read_only_span: ReadOnlySpan) -> Result> { + let span_data = SpanData { + span_context: OtelSpanContext::new( + TraceId::from_hex(&read_only_span.span_context.trace_id)?, + SpanId::from_hex(&read_only_span.span_context.span_id)?, + TraceFlags::SAMPLED, + false, + TraceState::default(), + ), + parent_span_id: SpanId::from_hex(&read_only_span.parent_span_id)?, + span_kind: OtelSpanKind::Internal, + name: read_only_span.name.into(), + start_time: UNIX_EPOCH + + Duration::from_secs(read_only_span.start_time.seconds) + + Duration::from_nanos(read_only_span.start_time.nanoseconds.into()), + end_time: UNIX_EPOCH + + Duration::from_secs(read_only_span.end_time.seconds) + + Duration::from_nanos(read_only_span.end_time.nanoseconds.into()), + attributes: vec![], + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::default(), + resource: std::borrow::Cow::Owned(OtelResource::default().clone()), + instrumentation_lib: InstrumentationLibrary::default(), + }; + + spin_telemetry::traces::send_message(span_data).await?; + Ok(Ok(())) + } + + async fn get_parent_span_context(&mut self) -> Result { + let sc = tracing::Span::current() + .context() + .span() + .span_context() + .clone(); + + Ok(SpanContext { + trace_id: sc.trace_id().to_string(), + span_id: sc.span_id().to_string(), + trace_flags: format!("{:x}", sc.trace_flags()), + is_remote: sc.is_remote(), + trace_state: "".to_string(), // TODO + }) + } + + async fn on_span_start(&mut self, span_context: SpanContext) -> Result<()> { + // Create the underlying tracing span + let tracing_span = tracing::info_span!("WASI Observe guest", "otel.name" = field::Empty); + + // Wrap it in a GuestSpan for our own bookkeeping purposes and enter it + let guest_span = GuestSpan { + name: "unknown".to_string(), + inner: tracing_span, + }; + guest_span.enter(); + + // Put the GuestSpan in our resource table and push it to our stack of active spans + let mut state = self.state.write().unwrap(); + let resource_id = state.guest_spans.push(guest_span).unwrap(); + state.active_spans.insert(span_context.span_id, resource_id); + + println!("on_span_start"); + println!("state.active_spans: {:?}", state.active_spans); + + Ok(()) + } + + async fn on_span_end(&mut self, read_only_span: ReadOnlySpan) -> Result<()> { + // TODO: Spans seem to be inverted somehow.... + let res_id: u32; + { + let state: std::sync::RwLockWriteGuard = self.state.write().unwrap(); + println!("on_span_end"); + println!("state.active_spans: {:?}", state.active_spans); + match state.active_spans.get(&read_only_span.span_context.span_id) { + Some(r) => res_id = *r, + None => { + println!("COULD NOT FIND SPAN ID IN ACTIVE SPANS"); + println!("state.active_spans: {:?}", state.active_spans); + println!( + "read_only_span.span_context.span_id: {:?}", + read_only_span.span_context.span_id + ); + return Ok(()); + } + }; + } + + self.state + .write() + .unwrap() + .guest_spans + .get(res_id) + .unwrap() + .inner + .record("otel.name", read_only_span.name); + + self.safely_close(res_id, true); + + Ok(()) + } +} + +#[async_trait] +impl observe::HostSpan for ObserveData { + async fn enter(&mut self, name: String) -> Result> { + // Create the underlying tracing span + let tracing_span = tracing::info_span!("WASI Observe guest", "otel.name" = name); + let span_id = tracing_span + .context() + .span() + .span_context() + .span_id() + .to_string(); + + // Wrap it in a GuestSpan for our own bookkeeping purposes and enter it + let guest_span = GuestSpan { + name: name.clone(), + inner: tracing_span, + }; + guest_span.enter(); + + // Put the GuestSpan in our resource table and push it to our stack of active spans + let mut state = self.state.write().unwrap(); + let resource_id = state.guest_spans.push(guest_span).unwrap(); + state.active_spans.insert(span_id, resource_id); + + Ok(Resource::new_own(resource_id)) + } + + async fn set_attribute( + &mut self, + resource: Resource, + key: String, + value: String, + ) -> Result<()> { + if let Some(guest_span) = self + .state + .write() + .unwrap() + .guest_spans + .get_mut(resource.rep()) + { + guest_span.inner.set_attribute(key, value); + } else { + tracing::debug!("can't find guest span to set attribute on") + } + Ok(()) + } + + async fn close(&mut self, resource: Resource) -> Result<()> { + self.safely_close(resource.rep(), false); + Ok(()) + } + + fn drop(&mut self, resource: Resource) -> Result<()> { + self.safely_close(resource.rep(), true); + Ok(()) + } +} + +impl ObserveData { + /// Close the span associated with the given resource and optionally drop the resource + /// from the table. Additionally close any other active spans that are more recent on the stack + /// in reverse order. + /// + /// Exiting any spans that were already closed will not cause this to error. + fn safely_close(&mut self, resource_id: u32, drop_resource: bool) { + let mut state: std::sync::RwLockWriteGuard = self.state.write().unwrap(); + + if let Some(index) = state + .active_spans + .iter() + .rposition(|(_, id)| *id == resource_id) + { + state.close_from_back_to(index); + } else { + tracing::debug!("found no active spans to close") + } + + if drop_resource { + state.guest_spans.remove(resource_id).unwrap(); + } + } +} + +/// Internal state of the observe host component. +pub(crate) struct State { + /// A resource table that holds the guest spans. + pub guest_spans: table::Table, + + /// A LIFO stack of guest spans that are currently active. + /// + /// Only a reference ID to the guest span is held here. The actual guest span must be looked up + /// in the `guest_spans` table using the reference ID. + /// TODO: Fix comment + pub active_spans: IndexMap, // TODO: Use an indexmap? +} + +impl State { + /// Close all active spans from the top of the stack to the given index. Closing entails exiting + /// the inner [tracing] span and removing it from the active spans stack. + pub(crate) fn close_from_back_to(&mut self, index: usize) { + self.active_spans + .split_off(index) + .iter() + .rev() + .for_each(|(_, id)| { + if let Some(guest_span) = self.guest_spans.get(*id) { + guest_span.exit(); + } else { + tracing::debug!("active_span {id:?} already removed from resource table"); + } + }); + } + + /// Enter the inner [tracing] span for all active spans. + pub(crate) fn enter_all(&self) { + for (_, guest_span_id) in self.active_spans.iter() { + if let Some(span_resource) = self.guest_spans.get(*guest_span_id) { + span_resource.enter(); + } else { + tracing::debug!("guest span already dropped") + } + } + } + + /// Exit the inner [tracing] span for all active spans. + pub(crate) fn exit_all(&self) { + for (_, guest_span_id) in self.active_spans.iter().rev() { + if let Some(span_resource) = self.guest_spans.get(*guest_span_id) { + span_resource.exit(); + } else { + tracing::debug!("guest span already dropped") + } + } + } +} + +/// The WIT resource Span. Effectively wraps a [tracing] span. +pub struct GuestSpan { + /// The [tracing] span we use to do the actual tracing work. + pub inner: tracing::Span, + pub name: String, +} + +// Note: We use tracing enter instead of Entered because Entered is not Send +impl GuestSpan { + /// Enter the inner [tracing] span. + pub fn enter(&self) { + self.inner.with_subscriber(|(id, dispatch)| { + dispatch.enter(id); + }); + } + + /// Exits the inner [tracing] span. + pub fn exit(&self) { + self.inner.with_subscriber(|(id, dispatch)| { + dispatch.exit(id); + }); + } +} diff --git a/crates/observe/src/lib.rs b/crates/observe/src/lib.rs new file mode 100644 index 000000000..8e3d7a27e --- /dev/null +++ b/crates/observe/src/lib.rs @@ -0,0 +1,4 @@ +pub mod future; +mod host_component; + +pub use crate::host_component::ObserveHostComponent; diff --git a/crates/telemetry/Cargo.toml b/crates/telemetry/Cargo.toml index b91bd4b9a..00336dfe0 100644 --- a/crates/telemetry/Cargo.toml +++ b/crates/telemetry/Cargo.toml @@ -18,6 +18,7 @@ tracing-opentelemetry = { version = "0.23.0", default-features = false, features tracing-subscriber = { version = "0.3.17", default-features = false, features = ["smallvec", "fmt", "ansi", "std", "env-filter", "json", "registry"] } url = "2.2.2" terminal = { path = "../terminal" } +tokio = { version = "1.23", features = ["full"] } [features] tracing-log-compat = ["tracing-subscriber/tracing-log", "tracing-opentelemetry/tracing-log"] diff --git a/crates/telemetry/src/lib.rs b/crates/telemetry/src/lib.rs index 7e5013867..333658926 100644 --- a/crates/telemetry/src/lib.rs +++ b/crates/telemetry/src/lib.rs @@ -10,7 +10,7 @@ mod env; pub mod log; pub mod metrics; mod propagation; -mod traces; +pub mod traces; pub use propagation::extract_trace_context; pub use propagation::inject_trace_context; diff --git a/crates/telemetry/src/traces.rs b/crates/telemetry/src/traces.rs index 43fe600f3..cef85447d 100644 --- a/crates/telemetry/src/traces.rs +++ b/crates/telemetry/src/traces.rs @@ -1,17 +1,22 @@ use std::time::Duration; use anyhow::bail; -use opentelemetry_otlp::SpanExporterBuilder; +use opentelemetry_otlp::{SpanExporter, SpanExporterBuilder}; +use opentelemetry_sdk::export::trace::SpanExporter as _; use opentelemetry_sdk::{ + export::trace::SpanData, resource::{EnvResourceDetector, TelemetryResourceDetector}, Resource, }; +use tokio::sync::Mutex; use tracing::Subscriber; use tracing_subscriber::{registry::LookupSpan, EnvFilter, Layer}; use crate::detector::SpinResourceDetector; use crate::env::OtlpProtocol; +static WASI_OBSERVE_EXPORTER: Mutex> = Mutex::const_new(None); + /// Constructs a layer for the tracing subscriber that sends spans to an OTEL collector. /// /// It pulls OTEL configuration from the environment based on the variables defined @@ -37,7 +42,7 @@ pub(crate) fn otel_tracing_layer LookupSpan<'span>>( // currently default to using the HTTP exporter but in the future we could select off of the // combination of OTEL_EXPORTER_OTLP_PROTOCOL and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL to // determine whether we should use http/protobuf or grpc. - let exporter: SpanExporterBuilder = match OtlpProtocol::traces_protocol_from_env() { + let exporter_builder: SpanExporterBuilder = match OtlpProtocol::traces_protocol_from_env() { OtlpProtocol::Grpc => opentelemetry_otlp::new_exporter().tonic().into(), OtlpProtocol::HttpProtobuf => opentelemetry_otlp::new_exporter().http().into(), OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"), @@ -45,7 +50,7 @@ pub(crate) fn otel_tracing_layer LookupSpan<'span>>( let tracer = opentelemetry_otlp::new_pipeline() .tracing() - .with_exporter(exporter) + .with_exporter(exporter_builder) .with_trace_config(opentelemetry_sdk::trace::config().with_resource(resource)) .install_batch(opentelemetry_sdk::runtime::Tokio)?; @@ -60,3 +65,29 @@ pub(crate) fn otel_tracing_layer LookupSpan<'span>>( .with_threads(false) .with_filter(env_filter)) } + +pub async fn send_message(span_data: SpanData) -> anyhow::Result<()> { + let mut exporter_lock = WASI_OBSERVE_EXPORTER.lock().await; + + // Lazily initialize exporter + if exporter_lock.is_none() { + // This will configure the exporter based on the OTEL_EXPORTER_* environment variables. We + // currently default to using the HTTP exporter but in the future we could select off of the + // combination of OTEL_EXPORTER_OTLP_PROTOCOL and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL to + // determine whether we should use http/protobuf or grpc. + let exporter_builder: SpanExporterBuilder = match OtlpProtocol::traces_protocol_from_env() { + OtlpProtocol::Grpc => opentelemetry_otlp::new_exporter().tonic().into(), + OtlpProtocol::HttpProtobuf => opentelemetry_otlp::new_exporter().http().into(), + OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"), + }; + + *exporter_lock = Some(exporter_builder.build_span_exporter()?); + } + + exporter_lock + .as_mut() + .unwrap() + .export(vec![span_data]) + .await?; + Ok(()) +} diff --git a/crates/trigger-http/Cargo.toml b/crates/trigger-http/Cargo.toml index 3104f5378..9904c1d5c 100644 --- a/crates/trigger-http/Cargo.toml +++ b/crates/trigger-http/Cargo.toml @@ -30,6 +30,7 @@ spin-outbound-networking = { path = "../outbound-networking" } spin-telemetry = { path = "../telemetry" } spin-trigger = { path = "../trigger" } spin-world = { path = "../world" } +spin-observe = { path = "../observe" } terminal = { path = "../terminal" } tls-listener = { version = "0.10.0", features = ["rustls"] } tokio = { version = "1.23", features = ["full"] } diff --git a/crates/trigger-http/src/handler.rs b/crates/trigger-http/src/handler.rs index 828d758d3..7d283a98f 100644 --- a/crates/trigger-http/src/handler.rs +++ b/crates/trigger-http/src/handler.rs @@ -13,6 +13,7 @@ use spin_core::wasi_2023_11_10::exports::wasi::http::incoming_handler::Guest as use spin_core::{Component, Engine, Instance}; use spin_http::body; use spin_http::routes::RouteMatch; +use spin_observe::future::{FutureExt, ObserveContext}; use spin_trigger::TriggerAppEngine; use spin_world::v1::http_types; use std::sync::Arc; @@ -48,14 +49,32 @@ impl HttpExecutor for HttpHandlerExecutor { set_http_origin_from_request(&mut store, engine.clone(), self, &req); + let observe_context = ObserveContext::new(&mut store, &engine.engine)?; + let resp = match ty { - HandlerType::Spin => { - Self::execute_spin(store, instance, base, route_match, req, client_addr) - .await - .map_err(contextualise_err)? - } + HandlerType::Spin => Self::execute_spin( + store, + observe_context, + instance, + base, + route_match, + req, + client_addr, + ) + .await + .map_err(contextualise_err)?, _ => { - Self::execute_wasi(store, instance, ty, base, route_match, req, client_addr).await? + Self::execute_wasi( + store, + observe_context, + instance, + ty, + base, + route_match, + req, + client_addr, + ) + .await? } }; @@ -70,6 +89,7 @@ impl HttpExecutor for HttpHandlerExecutor { impl HttpHandlerExecutor { pub async fn execute_spin( mut store: Store, + observe_context: ObserveContext, instance: Instance, base: &str, route_match: &RouteMatch, @@ -113,7 +133,10 @@ impl HttpHandlerExecutor { body: Some(bytes), }; - let (resp,) = func.call_async(&mut store, (req,)).await?; + let (resp,) = func + .call_async(&mut store, (req,)) + .manage_guest_spans(observe_context)? + .await?; if resp.status < 100 || resp.status > 600 { tracing::error!("malformed HTTP status code"); @@ -150,6 +173,7 @@ impl HttpHandlerExecutor { async fn execute_wasi( mut store: Store, + observe_context: ObserveContext, instance: Instance, ty: HandlerType, base: &str, @@ -219,18 +243,21 @@ impl HttpHandlerExecutor { proxy .wasi_http_incoming_handler() .call_handle(&mut store, request, response) + .manage_guest_spans(observe_context)? .instrument(span) .await } Handler::Handler2023_10_18(proxy) => { proxy .call_handle(&mut store, request, response) + .manage_guest_spans(observe_context)? .instrument(span) .await } Handler::Handler2023_11_10(proxy) => { proxy .call_handle(&mut store, request, response) + .manage_guest_spans(observe_context)? .instrument(span) .await } diff --git a/crates/trigger/Cargo.toml b/crates/trigger/Cargo.toml index b9d392024..d8f63210a 100644 --- a/crates/trigger/Cargo.toml +++ b/crates/trigger/Cargo.toml @@ -50,6 +50,7 @@ spin-core = { path = "../core" } spin-loader = { path = "../loader" } spin-manifest = { path = "../manifest" } spin-variables = { path = "../variables" } +spin-observe = { path = "../observe" } terminal = { path = "../terminal" } tokio = { version = "1.23", features = ["fs"] } toml = "0.5.9" diff --git a/crates/trigger/src/lib.rs b/crates/trigger/src/lib.rs index d0cb6536d..4277a1935 100644 --- a/crates/trigger/src/lib.rs +++ b/crates/trigger/src/lib.rs @@ -206,6 +206,10 @@ impl TriggerExecutorBuilder { runtime_config.variables_providers(), ), )?; + self.loader.add_dynamic_host_component( + &mut builder, + spin_observe::ObserveHostComponent::new(), + )?; } Executor::configure_engine(&mut builder)?; diff --git a/examples/spin-timer/Cargo.lock b/examples/spin-timer/Cargo.lock index 566a98837..9d6db04e1 100644 --- a/examples/spin-timer/Cargo.lock +++ b/examples/spin-timer/Cargo.lock @@ -5903,6 +5903,28 @@ dependencies = [ "url", ] +[[package]] +name = "spin-observe" +version = "2.5.0-pre0" +dependencies = [ + "anyhow", + "async-trait", + "dotenvy", + "once_cell", + "pin-project-lite", + "serde", + "spin-app", + "spin-core", + "spin-expressions", + "spin-world", + "table", + "thiserror", + "tokio", + "tracing", + "tracing-opentelemetry", + "vaultrs", +] + [[package]] name = "spin-outbound-networking" version = "2.6.0-pre0" @@ -6021,6 +6043,7 @@ dependencies = [ "spin-llm-remote-http", "spin-loader", "spin-manifest", + "spin-observe", "spin-outbound-networking", "spin-sqlite", "spin-sqlite-inproc", diff --git a/wit/observe.wit b/wit/observe.wit new file mode 100644 index 000000000..f66d126e1 --- /dev/null +++ b/wit/observe.wit @@ -0,0 +1,92 @@ +interface observe { + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + // TODO: Document. + resource span { + // enter returns a new span with the given name. + enter: static func(name: string) -> span; + + // set-attribute sets an attribute on the span. + set-attribute: func(key: string, value: string); + + // close closes the span. + close: func(); + } + + // Emit a given completed read-only-span to the o11y host. + emit-span: func(span: read-only-span) -> result<_, string>; + + // get-parent-span-context returns the parent span context of the host. + get-parent-span-context: func() -> span-context; + + // TODO + on-span-start: func(span-context: span-context); + + // TODO + on-span-end: func(span: read-only-span); + + // TODO: Document. + record read-only-span { + // Span name. + name: string, + + // Span context. + span-context: span-context, + + // Span parent id. + parent-span-id: string, + + // Span kind. + span-kind: span-kind, + + // Span start time. + start-time: datetime, + + // Span end time. + end-time: datetime, + + // Span attributes. TODO: Support multiple types + attributes: list>, + + // Span resource. + otel-resource: otel-resource, + + // TODO: Support dropped_attributes_count, events, links, status, and instrumentation lib + } + + // Identifying trace information about a span. + record span-context { + // Hexidecimal representation of the trace id. + trace-id: string, + + // Hexidecimal representation of the span id. + span-id: string, + + // Hexidecimal representation of the trace flags + trace-flags: string, + + // Span remoteness + is-remote: bool, + + // Entirity of tracestate + trace-state: string, + } + + // TODO: Document this and children. + enum span-kind { + client, + server, + producer, + consumer, + internal + } + + // An immutable representation of the entity producing telemetry as attributes. + record otel-resource { + // Resource attributes. + attrs: list>, + + // Resource schema url. + schema-url: option, + } +} diff --git a/wit/world.wit b/wit/world.wit index b16e42905..496a7e140 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -24,6 +24,7 @@ world platform { import sqlite; import key-value; import variables; + import observe; } /// Like `platform`, but using WASI 0.2.0-rc-2023-10-18