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 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!(
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 Box::new(SpinResourceDetector::new(spin_version.to_string()))
109 as Box<dyn ResourceDetector>,
110 Box::new(EnvResourceDetector::new()),
112 Box::new(TelemetryResourceDetector),
114 ])
115 .build();
116
117 let span_processor = if otel_tracing_enabled() {
118 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
187pub(crate) struct TracingState {
193 pub(crate) guest_span_contexts: IndexMap<SpanId, SpanContext>,
200
201 pub(crate) original_host_span_id: Option<SpanId>,
206
207 span_processor: Arc<BatchSpanProcessor<Tokio>>,
209}
210
211#[derive(Default)]
214pub struct OtelFactorState {
215 pub(crate) tracing_state: Option<Arc<RwLock<TracingState>>>,
216}
217
218impl OtelFactorState {
219 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 pub fn reparent_tracing_span(&self) {
259 let tracing_state = if let Some(state) = self.tracing_state.as_ref() {
261 state.read().unwrap()
262 } else {
263 return;
264 };
265
266 let Some((_, active_span_context)) = tracing_state.guest_span_contexts.last() else {
268 return;
269 };
270
271 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 let parent_context = Context::new().with_remote_span_context(active_span_context.clone());
286 tracing::Span::current().set_parent(parent_context);
287 }
288}