Skip to main content

spin_factor_otel/
lib.rs

1mod host;
2
3use anyhow::bail;
4use indexmap::IndexMap;
5use opentelemetry::{
6    trace::{SpanContext, SpanId, TraceContextExt},
7    Context,
8};
9use opentelemetry_otlp::MetricExporter;
10use opentelemetry_sdk::{
11    logs::{log_processor_with_async_runtime::BatchLogProcessor, LogProcessor},
12    resource::{EnvResourceDetector, ResourceDetector, TelemetryResourceDetector},
13    runtime::Tokio,
14    trace::{span_processor_with_async_runtime::BatchSpanProcessor, SpanProcessor},
15    Resource,
16};
17use spin_factors::{Factor, FactorData, PrepareContext, RuntimeFactors, SelfInstanceBuilder};
18use spin_telemetry::{
19    detector::SpinResourceDetector,
20    env::{otel_logs_enabled, otel_metrics_enabled, otel_tracing_enabled, OtlpProtocol},
21};
22use std::sync::{Arc, RwLock};
23use tracing_opentelemetry::OpenTelemetrySpanExt;
24
25pub struct OtelFactor {
26    span_processor: Option<Arc<BatchSpanProcessor<Tokio>>>,
27    metric_exporter: Option<Arc<MetricExporter>>,
28    log_processor: Option<Arc<BatchLogProcessor<Tokio>>>,
29    enable_interface: bool,
30}
31
32impl Factor for OtelFactor {
33    type RuntimeConfig = ();
34    type AppState = ();
35    type InstanceBuilder = InstanceState;
36
37    fn init(&mut self, ctx: &mut impl spin_factors::InitContext<Self>) -> anyhow::Result<()> {
38        // Only link the WASI OTel bindings if experimental support is enabled. This means that if
39        // the user tries to run a guest component that consumes the WASI OTel WIT without enabling
40        // experimental support they'll see an error like "component imports instance
41        // `wasi:otel/tracing@0.2.0-draft`"
42        if self.enable_interface {
43            ctx.link_bindings(
44                spin_world::wasi::otel::tracing::add_to_linker::<_, FactorData<Self>>,
45            )?;
46            ctx.link_bindings(
47                spin_world::wasi::otel::metrics::add_to_linker::<_, FactorData<Self>>,
48            )?;
49            ctx.link_bindings(spin_world::wasi::otel::logs::add_to_linker::<_, FactorData<Self>>)?;
50        }
51        Ok(())
52    }
53
54    fn configure_app<T: spin_factors::RuntimeFactors>(
55        &self,
56        _ctx: spin_factors::ConfigureAppContext<T, Self>,
57    ) -> anyhow::Result<Self::AppState> {
58        Ok(())
59    }
60
61    fn prepare<T: spin_factors::RuntimeFactors>(
62        &self,
63        _: spin_factors::PrepareContext<T, Self>,
64    ) -> anyhow::Result<Self::InstanceBuilder> {
65        if !self.enable_interface {
66            return Ok(InstanceState::default());
67        }
68
69        // Warn the user if they enabled experimental support but didn't supply any environment variables
70        if self.span_processor.is_none()
71            && self.metric_exporter.is_none()
72            && self.log_processor.is_none()
73        {
74            tracing::warn!("WASI OTel experimental support is enabled but no OTEL_EXPORTER_* environment variables were found. No telemetry will be exported.");
75        }
76
77        Ok(InstanceState {
78            tracing_state: self.span_processor.as_ref().map(|span_processor| {
79                Arc::new(RwLock::new(TracingState {
80                    guest_span_contexts: Default::default(),
81                    original_host_span_id: None,
82                    span_processor: span_processor.clone(),
83                }))
84            }),
85            metric_exporter: self.metric_exporter.clone(),
86            log_processor: self.log_processor.clone(),
87        })
88    }
89}
90
91impl OtelFactor {
92    pub fn new(spin_version: &str, enable_interface: bool) -> anyhow::Result<Self> {
93        if !enable_interface {
94            return Ok(Self {
95                span_processor: None,
96                metric_exporter: None,
97                log_processor: None,
98                enable_interface,
99            });
100        }
101
102        let resource = Resource::builder()
103            .with_detectors(&[
104                // Set service.name from env OTEL_SERVICE_NAME > env OTEL_RESOURCE_ATTRIBUTES > spin
105                // Set service.version from Spin metadata
106                Box::new(SpinResourceDetector::new(spin_version.to_string()))
107                    as Box<dyn ResourceDetector>,
108                // Sets fields from env OTEL_RESOURCE_ATTRIBUTES
109                Box::new(EnvResourceDetector::new()),
110                // Sets telemetry.sdk{name, language, version}
111                Box::new(TelemetryResourceDetector),
112            ])
113            .build();
114
115        let span_processor = if otel_tracing_enabled() {
116            // This will configure the exporter based on the OTEL_EXPORTER_* environment variables.
117            let span_exporter = match OtlpProtocol::traces_protocol_from_env() {
118                OtlpProtocol::Grpc => opentelemetry_otlp::SpanExporter::builder()
119                    .with_tonic()
120                    .build()?,
121                OtlpProtocol::HttpProtobuf => opentelemetry_otlp::SpanExporter::builder()
122                    .with_http()
123                    .build()?,
124                OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"),
125            };
126
127            let mut span_processor = BatchSpanProcessor::builder(span_exporter, Tokio).build();
128            span_processor.set_resource(&resource);
129            Some(Arc::new(span_processor))
130        } else {
131            None
132        };
133
134        let metric_exporter = if otel_metrics_enabled() {
135            let metric_exporter = match OtlpProtocol::metrics_protocol_from_env() {
136                OtlpProtocol::Grpc => opentelemetry_otlp::MetricExporter::builder()
137                    .with_tonic()
138                    .build()?,
139                OtlpProtocol::HttpProtobuf => opentelemetry_otlp::MetricExporter::builder()
140                    .with_http()
141                    .build()?,
142                OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"),
143            };
144            Some(Arc::new(metric_exporter))
145        } else {
146            None
147        };
148
149        let log_processor = if otel_logs_enabled() {
150            let log_exporter = match OtlpProtocol::logs_protocol_from_env() {
151                OtlpProtocol::Grpc => opentelemetry_otlp::LogExporter::builder()
152                    .with_tonic()
153                    .build()?,
154                OtlpProtocol::HttpProtobuf => opentelemetry_otlp::LogExporter::builder()
155                    .with_http()
156                    .build()?,
157                OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"),
158            };
159
160            let log_processor = BatchLogProcessor::builder(log_exporter, Tokio).build();
161            log_processor.set_resource(&resource);
162            Some(Arc::new(log_processor))
163        } else {
164            None
165        };
166
167        Ok(Self {
168            span_processor,
169            metric_exporter,
170            log_processor,
171            enable_interface,
172        })
173    }
174}
175
176#[derive(Default)]
177pub struct InstanceState {
178    tracing_state: Option<Arc<RwLock<TracingState>>>,
179    metric_exporter: Option<Arc<MetricExporter>>,
180    log_processor: Option<Arc<BatchLogProcessor<Tokio>>>,
181}
182
183impl SelfInstanceBuilder for InstanceState {}
184
185/// Internal tracing state of the OtelFactor InstanceState.
186///
187/// This data lives here rather than directly on InstanceState so that we can have multiple things
188/// take Arc references to it and so that if tracing is disabled we don't keep doing needless
189/// bookkeeping of host spans.
190pub(crate) struct TracingState {
191    /// An order-preserved mapping between immutable [SpanId]s of guest created spans and their
192    /// corresponding [SpanContext].
193    ///
194    /// The topmost [SpanId] is the last active span. When a span is ended it is removed from this
195    /// map (regardless of whether it is the active span) and all other spans are shifted back to
196    /// retain relative order.
197    pub(crate) guest_span_contexts: IndexMap<SpanId, SpanContext>,
198
199    /// Id of the last span emitted from within the host before entering the guest.
200    ///
201    /// We use this to avoid accidentally reparenting the original host span as a child of a guest
202    /// span.
203    pub(crate) original_host_span_id: Option<SpanId>,
204
205    /// The span processor used to export spans.
206    span_processor: Arc<BatchSpanProcessor<Tokio>>,
207}
208
209/// Manages access to the OtelFactor tracing state for the purpose of maintaining proper span
210/// parent/child relationships when WASI Otel spans are being created.
211#[derive(Default)]
212pub struct OtelFactorState {
213    pub(crate) tracing_state: Option<Arc<RwLock<TracingState>>>,
214}
215
216impl OtelFactorState {
217    /// Creates an [`OtelFactorState`] from a [`PrepareContext`].
218    ///
219    /// If [`RuntimeFactors`] does not contain an [`OtelFactor`], then calling
220    /// [`OtelFactorState::reparent_tracing_span`] will be a no-op.
221    pub fn from_prepare_context<T: RuntimeFactors, F: Factor>(
222        prepare_context: &mut PrepareContext<T, F>,
223    ) -> anyhow::Result<Self> {
224        let tracing_state = match prepare_context.instance_builder::<OtelFactor>() {
225            Ok(instance_state) => instance_state.tracing_state.clone(),
226            Err(spin_factors::Error::NoSuchFactor(_)) => None,
227            Err(e) => return Err(e.into()),
228        };
229        Ok(Self { tracing_state })
230    }
231
232    /// Reparents the current [tracing] span to be a child of the last active guest span.
233    ///
234    /// The otel factor enables guests to emit spans that should be part of the same trace as the
235    /// host is producing for a request. Below is an example trace. A request is made to an app, a
236    /// guest span is created and then the host is re-entered to fetch a key value.
237    ///
238    /// ```text
239    /// | GET /... _________________________________|
240    ///    | execute_wasm_component foo ___________|
241    ///       | my_guest_span ___________________|
242    ///          | spin_key_value.get |
243    /// ```
244    ///
245    /// Setting the guest spans parent as the host is enabled through current_span_context.
246    /// However, the more difficult task is having the host factor spans be children of the guest
247    /// span. [`OtelFactorState::reparent_tracing_span`] handles this by reparenting the current span to
248    /// be a child of the last active guest span (which is tracked internally in the otel factor).
249    ///
250    /// Note that if the otel factor is not in your [`RuntimeFactors`] than this is effectively a
251    /// no-op.
252    ///
253    /// This MUST only be called from a factor host implementation function that is instrumented.
254    ///
255    /// This MUST be called at the very start of the function before any awaits.
256    pub fn reparent_tracing_span(&self) {
257        // If tracing_state is None then tracing is not enabled so we should return early
258        let tracing_state = if let Some(state) = self.tracing_state.as_ref() {
259            state.read().unwrap()
260        } else {
261            return;
262        };
263
264        // If there are no active guest spans then there is nothing to do
265        let Some((_, active_span_context)) = tracing_state.guest_span_contexts.last() else {
266            return;
267        };
268
269        // Ensure that we are not reparenting the original host span
270        if let Some(original_host_span_id) = tracing_state.original_host_span_id {
271            debug_assert_ne!(
272                &original_host_span_id,
273                &tracing::Span::current()
274                    .context()
275                    .span()
276                    .span_context()
277                    .span_id(),
278                    "Incorrectly attempting to reparent the original host span. Likely `reparent_tracing_span` was called in an incorrect location."
279            );
280        }
281
282        // Now reparent the current span to the last active guest span
283        let parent_context = Context::new().with_remote_span_context(active_span_context.clone());
284        tracing::Span::current().set_parent(parent_context);
285    }
286}