Skip to main content

spin_factor_otel/
host.rs

1use crate::InstanceState;
2use anyhow::anyhow;
3use anyhow::Result;
4use opentelemetry::trace::TraceContextExt;
5use opentelemetry_sdk::error::OTelSdkError;
6use opentelemetry_sdk::logs::LogProcessor;
7use opentelemetry_sdk::metrics::exporter::PushMetricExporter;
8use opentelemetry_sdk::trace::SpanProcessor;
9use spin_world::wasi;
10use tracing_opentelemetry::OpenTelemetrySpanExt;
11
12impl wasi::otel::tracing::Host for InstanceState {
13    async fn on_start(&mut self, context: wasi::otel::tracing::SpanContext) -> Result<()> {
14        // If the host does not have tracing enabled we just no-op
15        let Some(tracing_state) = self.tracing_state.as_ref() else {
16            return Ok(());
17        };
18        let mut tracing_state = tracing_state.write().unwrap();
19
20        // Before we do anything make sure we track the original host span ID for reparenting
21        if tracing_state.original_host_span_id.is_none() {
22            tracing_state.original_host_span_id = Some(
23                tracing::Span::current()
24                    .context()
25                    .span()
26                    .span_context()
27                    .span_id(),
28            );
29        }
30
31        // Track the guest spans context in our ordered map
32        let span_context: opentelemetry::trace::SpanContext = context.into();
33        tracing_state
34            .guest_span_contexts
35            .insert(span_context.span_id(), span_context);
36
37        Ok(())
38    }
39
40    async fn on_end(&mut self, span_data: wasi::otel::tracing::SpanData) -> Result<()> {
41        // If the host does not have tracing enabled we just no-op
42        let Some(tracing_state) = self.tracing_state.as_ref() else {
43            return Ok(());
44        };
45        let mut tracing_state = tracing_state.write().unwrap();
46
47        let span_context: opentelemetry::trace::SpanContext = span_data.span_context.clone().into();
48        let span_id: opentelemetry::trace::SpanId = span_context.span_id();
49
50        if tracing_state
51            .guest_span_contexts
52            .shift_remove(&span_id)
53            .is_none()
54        {
55            Err(anyhow!("Trying to end a span that was not started"))?;
56        }
57
58        tracing_state.span_processor.on_end(span_data.into());
59
60        Ok(())
61    }
62
63    async fn outer_span_context(&mut self) -> Result<wasi::otel::tracing::SpanContext> {
64        Ok(tracing::Span::current()
65            .context()
66            .span()
67            .span_context()
68            .clone()
69            .into())
70    }
71}
72
73impl wasi::otel::metrics::Host for InstanceState {
74    async fn export(
75        &mut self,
76        metrics: wasi::otel::metrics::ResourceMetrics,
77    ) -> anyhow::Result<Result<(), wasi::otel::metrics::Error>> {
78        // If the host does not have metrics enabled we just no-op
79        let Some(metric_exporter) = self.metric_exporter.as_ref() else {
80            return Ok(Ok(()));
81        };
82
83        match metric_exporter.export(&mut metrics.into()).await {
84            Ok(_) => Ok(Ok(())),
85            Err(e) => match e {
86                OTelSdkError::AlreadyShutdown => {
87                    let msg = "Shutdown has already been invoked";
88                    tracing::error!(msg);
89                    Ok(Err(msg.to_string()))
90                }
91                OTelSdkError::InternalFailure(e) => {
92                    let detailed_msg = format!("Internal failure: {}", e);
93                    tracing::error!(detailed_msg);
94                    Ok(Err("Internal failure.".to_string()))
95                }
96                OTelSdkError::Timeout(d) => {
97                    let detailed_msg = format!("Operation timed out after {} seconds", d.as_secs());
98                    tracing::error!(detailed_msg);
99                    Ok(Err("Operation timed out.".to_string()))
100                }
101            },
102        }
103    }
104}
105
106impl wasi::otel::logs::Host for InstanceState {
107    async fn on_emit(&mut self, data: wasi::otel::logs::LogRecord) -> anyhow::Result<()> {
108        // If the host does not have logs enabled we just no-op
109        let Some(log_processor) = self.log_processor.as_ref() else {
110            return Ok(());
111        };
112
113        let (mut record, scope) = spin_world::wasi_otel::parse_wasi_log_record(data);
114        log_processor.emit(&mut record, &scope);
115        Ok(())
116    }
117}