From 186e3e3a9baa961035dcac63ea7d5d1d34b49a40 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Fri, 22 Mar 2024 12:22:48 -0600 Subject: [PATCH 1/6] feat(wasi-observe): A WIP WASI Observe host component Signed-off-by: Caleb Schoepp --- Cargo.lock | 24 ++++ crates/core/src/lib.rs | 2 + crates/observe/Cargo.toml | 25 ++++ crates/observe/src/future.rs | 95 ++++++++++++++ crates/observe/src/host_component.rs | 182 +++++++++++++++++++++++++++ crates/observe/src/lib.rs | 4 + crates/trigger-http/Cargo.toml | 1 + crates/trigger-http/src/handler.rs | 41 ++++-- crates/trigger/Cargo.toml | 1 + crates/trigger/src/lib.rs | 4 + wit/observe.wit | 7 ++ wit/world.wit | 1 + 12 files changed, 380 insertions(+), 7 deletions(-) create mode 100644 crates/observe/Cargo.toml create mode 100644 crates/observe/src/future.rs create mode 100644 crates/observe/src/host_component.rs create mode 100644 crates/observe/src/lib.rs create mode 100644 wit/observe.wit diff --git a/Cargo.lock b/Cargo.lock index b23ccf79a..bb7ecb181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7660,6 +7660,28 @@ dependencies = [ "url", ] +[[package]] +name = "spin-observe" +version = "2.6.0-pre0" +dependencies = [ + "anyhow", + "async-trait", + "dotenvy", + "once_cell", + "pin-project-lite", + "serde", + "spin-app", + "spin-core", + "spin-expressions", + "spin-world", + "table", + "thiserror", + "tokio", + "toml 0.5.11", + "tracing", + "vaultrs", +] + [[package]] name = "spin-oci" version = "2.6.0-pre0" @@ -7886,6 +7908,7 @@ dependencies = [ "spin-llm-remote-http", "spin-loader", "spin-manifest", + "spin-observe", "spin-outbound-networking", "spin-sqlite", "spin-sqlite-inproc", @@ -7928,6 +7951,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..add1cf825 --- /dev/null +++ b/crates/observe/Cargo.toml @@ -0,0 +1,25 @@ +[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" } +table = { path = "../table" } +thiserror = "1" +tokio = { version = "1", features = ["rt-multi-thread"] } +vaultrs = "0.6.2" +serde = "1.0.188" +tracing = "0.1.40" +pin-project-lite = "0.2" + +[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..683c45626 --- /dev/null +++ b/crates/observe/src/host_component.rs @@ -0,0 +1,182 @@ +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use spin_app::{AppComponent, DynamicHostComponent}; +use spin_core::wasmtime::component::Resource; +use spin_core::{async_trait, HostComponent}; +use spin_world::v2::observe; +use spin_world::v2::observe::Span as WitSpan; + +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_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); + + // 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.push(resource_id); + + Ok(Resource::new_own(resource_id)) + } + + async fn close(&mut self, resource: Resource) -> Result<()> { + self.safely_close(resource, false); + Ok(()) + } + + fn drop(&mut self, resource: Resource) -> Result<()> { + self.safely_close(resource, 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: Resource, 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.rep()) + { + state.close_from_back_to(index); + } else { + tracing::debug!("found no active spans to close") + } + + if drop_resource { + state.guest_spans.remove(resource.rep()).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. + pub active_spans: Vec, +} + +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/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/wit/observe.wit b/wit/observe.wit new file mode 100644 index 000000000..0ace6f1f1 --- /dev/null +++ b/wit/observe.wit @@ -0,0 +1,7 @@ +interface observe { + resource span { + close: func(); + + enter: static func(name: string) -> span; + } +} \ No newline at end of file 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 From 637029145766fde9f139eb6d0367b7a1f2357b59 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Fri, 3 May 2024 09:54:52 -0600 Subject: [PATCH 2/6] Support setting attributes Signed-off-by: Caleb Schoepp --- Cargo.lock | 1 + crates/observe/Cargo.toml | 1 + crates/observe/src/host_component.rs | 21 +++++++++++++++++++++ examples/spin-timer/Cargo.lock | 23 +++++++++++++++++++++++ wit/observe.wit | 14 +++++++++++--- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb7ecb181..75872d6ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7679,6 +7679,7 @@ dependencies = [ "tokio", "toml 0.5.11", "tracing", + "tracing-opentelemetry", "vaultrs", ] diff --git a/crates/observe/Cargo.toml b/crates/observe/Cargo.toml index add1cf825..044885f1f 100644 --- a/crates/observe/Cargo.toml +++ b/crates/observe/Cargo.toml @@ -19,6 +19,7 @@ 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" [dev-dependencies] diff --git a/crates/observe/src/host_component.rs b/crates/observe/src/host_component.rs index 683c45626..913e7d346 100644 --- a/crates/observe/src/host_component.rs +++ b/crates/observe/src/host_component.rs @@ -6,6 +6,7 @@ use spin_core::wasmtime::component::Resource; use spin_core::{async_trait, HostComponent}; use spin_world::v2::observe; use spin_world::v2::observe::Span as WitSpan; +use tracing_opentelemetry::OpenTelemetrySpanExt; pub struct ObserveHostComponent {} @@ -70,6 +71,26 @@ impl observe::HostSpan for ObserveData { 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, false); Ok(()) 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 index 0ace6f1f1..5885914a3 100644 --- a/wit/observe.wit +++ b/wit/observe.wit @@ -1,7 +1,15 @@ interface observe { resource span { - close: func(); - + /// 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(); } -} \ No newline at end of file +} + +// TODO: Maybe support events? +// TODO: Make a Rust tracing provider that uses this as a backend From bb9b0e7516c780411b4c0574fbca60bd643dd15f Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Tue, 21 May 2024 08:23:04 -0600 Subject: [PATCH 3/6] Support sink mode in observe.wit Signed-off-by: Caleb Schoepp --- Cargo.lock | 2 ++ crates/observe/Cargo.toml | 2 ++ crates/observe/src/host_component.rs | 30 +++++++++++++++++++- wit/observe.wit | 42 ++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75872d6ee..66e9a9c06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7668,6 +7668,8 @@ dependencies = [ "async-trait", "dotenvy", "once_cell", + "opentelemetry", + "opentelemetry_sdk", "pin-project-lite", "serde", "spin-app", diff --git a/crates/observe/Cargo.toml b/crates/observe/Cargo.toml index 044885f1f..4db5dd4f8 100644 --- a/crates/observe/Cargo.toml +++ b/crates/observe/Cargo.toml @@ -21,6 +21,8 @@ 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"] } [dev-dependencies] toml = "0.5" diff --git a/crates/observe/src/host_component.rs b/crates/observe/src/host_component.rs index 913e7d346..716b28b69 100644 --- a/crates/observe/src/host_component.rs +++ b/crates/observe/src/host_component.rs @@ -1,10 +1,14 @@ use std::sync::{Arc, RwLock}; +use std::time::{Duration, UNIX_EPOCH}; use anyhow::Result; +use opentelemetry::trace::{Span, Tracer, TracerProvider}; +use opentelemetry::Context; use spin_app::{AppComponent, DynamicHostComponent}; use spin_core::wasmtime::component::Resource; use spin_core::{async_trait, HostComponent}; use spin_world::v2::observe; +use spin_world::v2::observe::ReadOnlySpan; use spin_world::v2::observe::Span as WitSpan; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -48,7 +52,31 @@ pub struct ObserveData { } #[async_trait] -impl observe::Host for ObserveData {} +impl observe::Host for ObserveData { + async fn emit_span(&mut self, read_only_span: ReadOnlySpan) -> Result<()> { + let tracer = opentelemetry::global::tracer_provider().tracer("wasi_observe"); + + let mut span = tracer + .span_builder(read_only_span.name) + .with_start_time( + UNIX_EPOCH + + Duration::from_secs(read_only_span.start_time.seconds) + + Duration::from_nanos(read_only_span.start_time.nanoseconds.into()), + ) + .with_kind(opentelemetry::trace::SpanKind::Internal) + .with_attributes(vec![]) + .with_events(vec![]) + .with_links(vec![]) + .start_with_context(&tracer, &Context::new()); + + span.end_with_timestamp( + UNIX_EPOCH + + Duration::from_secs(read_only_span.end_time.seconds) + + Duration::from_nanos(read_only_span.end_time.nanoseconds.into()), + ); + Ok(()) + } +} #[async_trait] impl observe::HostSpan for ObserveData { diff --git a/wit/observe.wit b/wit/observe.wit index 5885914a3..cfba51287 100644 --- a/wit/observe.wit +++ b/wit/observe.wit @@ -1,4 +1,6 @@ interface observe { + use wasi:clocks/wall-clock@0.2.0.{datetime}; + resource span { /// enter returns a new span with the given name. enter: static func(name: string) -> span; @@ -9,7 +11,41 @@ interface observe { /// close closes the span. close: func(); } -} -// TODO: Maybe support events? -// TODO: Make a Rust tracing provider that uses this as a backend + // Emit a given completed read-only-span to the o11y host. + emit-span: func(span: read-only-span); + + enum span-kind { + client, + server, + producer, + consumer, + internal + } + + record read-only-span { + /// Name of the span. + name: string, + + // TODO: Span parent context stuff + + /// Kind of the span + span-kind: span-kind, + + /// Start time of the span. + start-time: datetime, + + /// End time of the span. + end-time: datetime, + + // TODO: Span attributes and dropped count + + // TODO: Span events + + // TODO: Span links + + // TODO: Status + + // TODO: Resource and instrumentation lib + } +} From 4a5b89a02948a6521fc23afa5db871e172b2461d Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Mon, 27 May 2024 14:05:25 -0600 Subject: [PATCH 4/6] More progress along the exporter interface Signed-off-by: Caleb Schoepp --- crates/observe/src/host_component.rs | 28 +++++++++++++++++++++++++--- wit/observe.wit | 15 ++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/observe/src/host_component.rs b/crates/observe/src/host_component.rs index 716b28b69..c905a4f4b 100644 --- a/crates/observe/src/host_component.rs +++ b/crates/observe/src/host_component.rs @@ -2,16 +2,15 @@ use std::sync::{Arc, RwLock}; use std::time::{Duration, UNIX_EPOCH}; use anyhow::Result; -use opentelemetry::trace::{Span, Tracer, TracerProvider}; +use opentelemetry::trace::{Span, TraceContextExt, Tracer, TracerProvider}; use opentelemetry::Context; use spin_app::{AppComponent, DynamicHostComponent}; use spin_core::wasmtime::component::Resource; use spin_core::{async_trait, HostComponent}; -use spin_world::v2::observe; use spin_world::v2::observe::ReadOnlySpan; use spin_world::v2::observe::Span as WitSpan; +use spin_world::v2::observe::{self, SpanContext}; use tracing_opentelemetry::OpenTelemetrySpanExt; - pub struct ObserveHostComponent {} impl ObserveHostComponent { @@ -56,6 +55,14 @@ impl observe::Host for ObserveData { async fn emit_span(&mut self, read_only_span: ReadOnlySpan) -> Result<()> { let tracer = opentelemetry::global::tracer_provider().tracer("wasi_observe"); + let trace_id_array: [u8; 16] = read_only_span + .span_context + .trace_id + .into_iter() + .collect::>() + .try_into() + .unwrap(); + let mut span = tracer .span_builder(read_only_span.name) .with_start_time( @@ -63,6 +70,8 @@ impl observe::Host for ObserveData { + Duration::from_secs(read_only_span.start_time.seconds) + Duration::from_nanos(read_only_span.start_time.nanoseconds.into()), ) + .with_span_id(read_only_span.span_context.span_id.into()) + .with_trace_id(u128::from_be_bytes(trace_id_array).into()) .with_kind(opentelemetry::trace::SpanKind::Internal) .with_attributes(vec![]) .with_events(vec![]) @@ -76,6 +85,19 @@ impl observe::Host for ObserveData { ); Ok(()) } + + async fn get_parent(&mut self) -> Result { + let sc = tracing::Span::current() + .context() + .span() + .span_context() + .clone(); + + Ok(SpanContext { + trace_id: sc.trace_id().to_bytes().to_vec(), + span_id: u64::from_be_bytes(sc.span_id().to_bytes()), + }) + } } #[async_trait] diff --git a/wit/observe.wit b/wit/observe.wit index cfba51287..d9d9b65b8 100644 --- a/wit/observe.wit +++ b/wit/observe.wit @@ -12,9 +12,12 @@ interface observe { close: func(); } - // Emit a given completed read-only-span to the o11y host. + /// Emit a given completed read-only-span to the o11y host. emit-span: func(span: read-only-span); + /// TODO + get-parent: func() -> span-context; + enum span-kind { client, server, @@ -23,11 +26,21 @@ interface observe { internal } + record span-context { + // TODO: This is really jank + trace-id: list, + span-id: u64, + // trace_flags: TraceFlags, + // is_remote: bool, + // trace_state: TraceState, + } + record read-only-span { /// Name of the span. name: string, // TODO: Span parent context stuff + span-context: span-context, /// Kind of the span span-kind: span-kind, From a4ece300c2f6e98a05889406efb50e124e69cdfa Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Wed, 29 May 2024 11:48:01 -0600 Subject: [PATCH 5/6] Directly use exporter for sink rather than proxying through tracer Signed-off-by: Caleb Schoepp --- Cargo.lock | 4 ++ crates/observe/Cargo.toml | 3 + crates/observe/src/host_component.rs | 77 +++++++++++++----------- crates/telemetry/Cargo.toml | 1 + crates/telemetry/src/lib.rs | 2 +- crates/telemetry/src/traces.rs | 37 +++++++++++- wit/observe.wit | 90 +++++++++++++++++----------- 7 files changed, 141 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66e9a9c06..a9251e720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7667,14 +7667,17 @@ dependencies = [ "anyhow", "async-trait", "dotenvy", + "futures-executor", "once_cell", "opentelemetry", + "opentelemetry-otlp", "opentelemetry_sdk", "pin-project-lite", "serde", "spin-app", "spin-core", "spin-expressions", + "spin-telemetry", "spin-world", "table", "thiserror", @@ -7818,6 +7821,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry_sdk", "terminal", + "tokio", "tracing", "tracing-appender", "tracing-opentelemetry", diff --git a/crates/observe/Cargo.toml b/crates/observe/Cargo.toml index 4db5dd4f8..19b4ef868 100644 --- a/crates/observe/Cargo.toml +++ b/crates/observe/Cargo.toml @@ -13,6 +13,7 @@ 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"] } @@ -23,6 +24,8 @@ 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" [dev-dependencies] toml = "0.5" diff --git a/crates/observe/src/host_component.rs b/crates/observe/src/host_component.rs index c905a4f4b..d8965b92b 100644 --- a/crates/observe/src/host_component.rs +++ b/crates/observe/src/host_component.rs @@ -2,8 +2,14 @@ use std::sync::{Arc, RwLock}; use std::time::{Duration, UNIX_EPOCH}; use anyhow::Result; -use opentelemetry::trace::{Span, TraceContextExt, Tracer, TracerProvider}; -use opentelemetry::Context; +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}; @@ -11,6 +17,7 @@ use spin_world::v2::observe::ReadOnlySpan; use spin_world::v2::observe::Span as WitSpan; use spin_world::v2::observe::{self, SpanContext}; use tracing_opentelemetry::OpenTelemetrySpanExt; + pub struct ObserveHostComponent {} impl ObserveHostComponent { @@ -52,41 +59,38 @@ pub struct ObserveData { #[async_trait] impl observe::Host for ObserveData { - async fn emit_span(&mut self, read_only_span: ReadOnlySpan) -> Result<()> { - let tracer = opentelemetry::global::tracer_provider().tracer("wasi_observe"); - - let trace_id_array: [u8; 16] = read_only_span - .span_context - .trace_id - .into_iter() - .collect::>() - .try_into() - .unwrap(); - - let mut span = tracer - .span_builder(read_only_span.name) - .with_start_time( - UNIX_EPOCH - + Duration::from_secs(read_only_span.start_time.seconds) - + Duration::from_nanos(read_only_span.start_time.nanoseconds.into()), - ) - .with_span_id(read_only_span.span_context.span_id.into()) - .with_trace_id(u128::from_be_bytes(trace_id_array).into()) - .with_kind(opentelemetry::trace::SpanKind::Internal) - .with_attributes(vec![]) - .with_events(vec![]) - .with_links(vec![]) - .start_with_context(&tracer, &Context::new()); - - span.end_with_timestamp( - UNIX_EPOCH + 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()), - ); - Ok(()) + 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(&mut self) -> Result { + async fn get_parent_span_context(&mut self) -> Result { let sc = tracing::Span::current() .context() .span() @@ -94,8 +98,11 @@ impl observe::Host for ObserveData { .clone(); Ok(SpanContext { - trace_id: sc.trace_id().to_bytes().to_vec(), - span_id: u64::from_be_bytes(sc.span_id().to_bytes()), + 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 }) } } 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/wit/observe.wit b/wit/observe.wit index d9d9b65b8..a557742ea 100644 --- a/wit/observe.wit +++ b/wit/observe.wit @@ -1,64 +1,86 @@ 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 returns a new span with the given name. enter: static func(name: string) -> span; - /// set-attribute sets an attribute on the span. + // set-attribute sets an attribute on the span. set-attribute: func(key: string, value: string); - /// close closes the span. + // close closes the span. close: func(); } - /// Emit a given completed read-only-span to the o11y host. - emit-span: func(span: read-only-span); + // Emit a given completed read-only-span to the o11y host. + emit-span: func(span: read-only-span) -> result<_, string>; - /// TODO - get-parent: func() -> span-context; - - enum span-kind { - client, - server, - producer, - consumer, - internal - } - - record span-context { - // TODO: This is really jank - trace-id: list, - span-id: u64, - // trace_flags: TraceFlags, - // is_remote: bool, - // trace_state: TraceState, - } + // get-parent-span-context returns the parent span context of the host. + get-parent-span-context: func() -> span-context; + // TODO: Document. record read-only-span { - /// Name of the span. + // Span name. name: string, - // TODO: Span parent context stuff + // Span context. span-context: span-context, - /// Kind of the span + // Span parent id. + parent-span-id: string, + + // Span kind. span-kind: span-kind, - /// Start time of the span. + // Span start time. start-time: datetime, - /// End time of the span. + // Span end time. end-time: datetime, - // TODO: Span attributes and dropped count + // 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, - // TODO: Span events + // Hexidecimal representation of the trace flags + trace-flags: string, + + // Span remoteness + is-remote: bool, + + // Entirity of tracestate + trace-state: string, + } - // TODO: Span links + // TODO: Document this and children. + enum span-kind { + client, + server, + producer, + consumer, + internal + } - // TODO: Status + // An immutable representation of the entity producing telemetry as attributes. + record otel-resource { + // Resource attributes. + attrs: list>, - // TODO: Resource and instrumentation lib + // Resource schema url. + schema-url: option, } } From cdc22e2885e13ed6f74e59b7e82fca80d77599b1 Mon Sep 17 00:00:00 2001 From: Caleb Schoepp Date: Mon, 3 Jun 2024 16:59:51 -0600 Subject: [PATCH 6/6] Support processor Signed-off-by: Caleb Schoepp --- Cargo.lock | 3 +- crates/observe/Cargo.toml | 1 + crates/observe/src/host_component.rs | 90 ++++++++++++++++++++++++---- wit/observe.wit | 6 ++ 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9251e720..0faa3c4ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7668,12 +7668,13 @@ dependencies = [ "async-trait", "dotenvy", "futures-executor", + "indexmap 2.2.6", "once_cell", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "pin-project-lite", - "serde", + "serde 1.0.197", "spin-app", "spin-core", "spin-expressions", diff --git a/crates/observe/Cargo.toml b/crates/observe/Cargo.toml index 19b4ef868..e59419cea 100644 --- a/crates/observe/Cargo.toml +++ b/crates/observe/Cargo.toml @@ -26,6 +26,7 @@ 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/host_component.rs b/crates/observe/src/host_component.rs index d8965b92b..2b88c23e7 100644 --- a/crates/observe/src/host_component.rs +++ b/crates/observe/src/host_component.rs @@ -1,7 +1,9 @@ +use core::panic; use std::sync::{Arc, RwLock}; use std::time::{Duration, UNIX_EPOCH}; -use anyhow::Result; +use anyhow::{bail, Result}; +use indexmap::IndexMap; use opentelemetry::trace::{ SpanContext as OtelSpanContext, SpanId, SpanKind as OtelSpanKind, Status, TraceContextExt, TraceFlags, TraceId, TraceState, @@ -16,6 +18,7 @@ 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 {} @@ -105,6 +108,63 @@ impl observe::Host for ObserveData { 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] @@ -112,6 +172,12 @@ 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 { @@ -123,7 +189,7 @@ impl observe::HostSpan for ObserveData { // 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.push(resource_id); + state.active_spans.insert(span_id, resource_id); Ok(Resource::new_own(resource_id)) } @@ -149,12 +215,12 @@ impl observe::HostSpan for ObserveData { } async fn close(&mut self, resource: Resource) -> Result<()> { - self.safely_close(resource, false); + self.safely_close(resource.rep(), false); Ok(()) } fn drop(&mut self, resource: Resource) -> Result<()> { - self.safely_close(resource, true); + self.safely_close(resource.rep(), true); Ok(()) } } @@ -165,13 +231,13 @@ impl ObserveData { /// in reverse order. /// /// Exiting any spans that were already closed will not cause this to error. - fn safely_close(&mut self, resource: Resource, drop_resource: bool) { + 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.rep()) + .rposition(|(_, id)| *id == resource_id) { state.close_from_back_to(index); } else { @@ -179,7 +245,7 @@ impl ObserveData { } if drop_resource { - state.guest_spans.remove(resource.rep()).unwrap(); + state.guest_spans.remove(resource_id).unwrap(); } } } @@ -188,11 +254,13 @@ impl ObserveData { 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. - pub active_spans: Vec, + /// TODO: Fix comment + pub active_spans: IndexMap, // TODO: Use an indexmap? } impl State { @@ -203,7 +271,7 @@ impl State { .split_off(index) .iter() .rev() - .for_each(|id| { + .for_each(|(_, id)| { if let Some(guest_span) = self.guest_spans.get(*id) { guest_span.exit(); } else { @@ -214,7 +282,7 @@ impl State { /// Enter the inner [tracing] span for all active spans. pub(crate) fn enter_all(&self) { - for guest_span_id in self.active_spans.iter() { + 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 { @@ -225,7 +293,7 @@ impl State { /// 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() { + 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 { diff --git a/wit/observe.wit b/wit/observe.wit index a557742ea..f66d126e1 100644 --- a/wit/observe.wit +++ b/wit/observe.wit @@ -19,6 +19,12 @@ interface observe { // 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.