Skip to main content

spin_factor_otel/
lib.rs

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