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 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 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 Box::new(SpinResourceDetector::new(spin_version.to_string()))
107 as Box<dyn ResourceDetector>,
108 Box::new(EnvResourceDetector::new()),
110 Box::new(TelemetryResourceDetector),
112 ])
113 .build();
114
115 let span_processor = if otel_tracing_enabled() {
116 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
185pub(crate) struct TracingState {
191 pub(crate) guest_span_contexts: IndexMap<SpanId, SpanContext>,
198
199 pub(crate) original_host_span_id: Option<SpanId>,
204
205 span_processor: Arc<BatchSpanProcessor<Tokio>>,
207}
208
209#[derive(Default)]
212pub struct OtelFactorState {
213 pub(crate) tracing_state: Option<Arc<RwLock<TracingState>>>,
214}
215
216impl OtelFactorState {
217 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 pub fn reparent_tracing_span(&self) {
257 let tracing_state = if let Some(state) = self.tracing_state.as_ref() {
259 state.read().unwrap()
260 } else {
261 return;
262 };
263
264 let Some((_, active_span_context)) = tracing_state.guest_span_contexts.last() else {
266 return;
267 };
268
269 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 let parent_context = Context::new().with_remote_span_context(active_span_context.clone());
284 tracing::Span::current().set_parent(parent_context);
285 }
286}