sentry_opentelemetry/
processor.rs

1//! An OpenTelemetry [SpanProcessor](https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-processor) for Sentry.
2//!
3//! [`SentrySpanProcessor`] allows the Sentry Rust SDK to integrate with OpenTelemetry.
4//! It transforms OpenTelemetry spans into Sentry transactions/spans and sends them to Sentry.
5//!
6//! # Configuration
7//!
8//! Unless you have no need for distributed tracing, this should be used together with [`crate::propagator::SentryPropagator`]. An example of
9//! setting up both is provided in the [crate-level documentation](../).
10
11use std::collections::HashMap;
12use std::sync::{Arc, LazyLock, Mutex};
13use std::time::SystemTime;
14
15use opentelemetry::global::ObjectSafeSpan;
16use opentelemetry::trace::{get_active_span, SpanId};
17use opentelemetry::Context;
18use opentelemetry_sdk::error::OTelSdkResult;
19use opentelemetry_sdk::trace::{Span, SpanData, SpanProcessor};
20
21use opentelemetry_sdk::Resource;
22use sentry_core::SentryTrace;
23use sentry_core::{TransactionContext, TransactionOrSpan};
24
25use crate::converters::{
26    convert_span_id, convert_span_kind, convert_span_status, convert_trace_id, convert_value,
27};
28
29/// A mapping from Sentry span IDs to Sentry spans/transactions.
30/// Sentry spans are created with the same SpanId as the corresponding OTEL span, so this is used
31/// to track OTEL spans across start/end calls.
32type SpanMap = Arc<Mutex<HashMap<sentry_core::protocol::SpanId, TransactionOrSpan>>>;
33
34static SPAN_MAP: LazyLock<SpanMap> = LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
35
36/// An OpenTelemetry SpanProcessor that converts OTEL spans to Sentry spans/transactions and sends
37/// them to Sentry.
38#[derive(Debug, Clone)]
39pub struct SentrySpanProcessor {}
40
41impl SentrySpanProcessor {
42    /// Creates a new `SentrySpanProcessor`.
43    pub fn new() -> Self {
44        sentry_core::configure_scope(|scope| {
45            // Associate Sentry events with the correct span and trace.
46            // This works as long as all Sentry spans/transactions are managed exclusively through OTEL APIs.
47            scope.add_event_processor(|mut event| {
48                get_active_span(|otel_span| {
49                    let span_map = SPAN_MAP.lock().unwrap();
50
51                    let Some(sentry_span) =
52                        span_map.get(&convert_span_id(&otel_span.span_context().span_id()))
53                    else {
54                        return;
55                    };
56
57                    let (span_id, trace_id) = match sentry_span {
58                        TransactionOrSpan::Transaction(transaction) => (
59                            transaction.get_trace_context().span_id,
60                            transaction.get_trace_context().trace_id,
61                        ),
62                        TransactionOrSpan::Span(span) => {
63                            (span.get_span_id(), span.get_trace_context().trace_id)
64                        }
65                    };
66
67                    if let Some(sentry_core::protocol::Context::Trace(trace_context)) =
68                        event.contexts.get_mut("trace")
69                    {
70                        trace_context.trace_id = trace_id;
71                        trace_context.span_id = span_id;
72                    } else {
73                        event.contexts.insert(
74                            "trace".into(),
75                            sentry_core::protocol::TraceContext {
76                                span_id,
77                                trace_id,
78                                ..Default::default()
79                            }
80                            .into(),
81                        );
82                    }
83                });
84                Some(event)
85            });
86        });
87        Self {}
88    }
89}
90
91impl Default for SentrySpanProcessor {
92    /// Creates a default `SentrySpanProcessor`.
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl SpanProcessor for SentrySpanProcessor {
99    fn on_start(&self, span: &mut Span, ctx: &Context) {
100        let span_id = span.span_context().span_id();
101        let trace_id = span.span_context().trace_id();
102
103        let mut span_map = SPAN_MAP.lock().unwrap();
104
105        let mut span_description = String::new();
106        let mut span_op = String::new();
107        let mut span_start_timestamp = SystemTime::now();
108        let mut parent_sentry_span = None;
109        if let Some(data) = span.exported_data() {
110            span_description = data.name.to_string();
111            span_op = span_description.clone(); // TODO: infer this from OTEL span attributes
112            span_start_timestamp = data.start_time;
113            if data.parent_span_id != SpanId::INVALID {
114                parent_sentry_span = span_map.get(&convert_span_id(&data.parent_span_id));
115            };
116        }
117        let span_description = span_description.as_str();
118        let span_op = span_op.as_str();
119
120        let sentry_span = {
121            if let Some(parent_sentry_span) = parent_sentry_span {
122                // continue local trace
123                TransactionOrSpan::Span(parent_sentry_span.start_child_with_details(
124                    span_op,
125                    span_description,
126                    convert_span_id(&span_id),
127                    span_start_timestamp,
128                ))
129            } else {
130                let sentry_ctx = {
131                    if let Some(sentry_trace) = ctx.get::<SentryTrace>() {
132                        // continue remote trace
133                        TransactionContext::continue_from_sentry_trace(
134                            span_description,
135                            span_op,
136                            sentry_trace,
137                            Some(convert_span_id(&span_id)),
138                        )
139                    } else {
140                        // start a new trace
141                        TransactionContext::new_with_details(
142                            span_description,
143                            span_op,
144                            convert_trace_id(&trace_id),
145                            Some(convert_span_id(&span_id)),
146                            None,
147                        )
148                    }
149                };
150                let tx =
151                    sentry_core::start_transaction_with_timestamp(sentry_ctx, span_start_timestamp);
152                TransactionOrSpan::Transaction(tx)
153            }
154        };
155        span_map.insert(convert_span_id(&span_id), sentry_span);
156    }
157
158    fn on_end(&self, data: SpanData) {
159        let span_id = data.span_context.span_id();
160
161        let mut span_map = SPAN_MAP.lock().unwrap();
162
163        let Some(sentry_span) = span_map.remove(&convert_span_id(&span_id)) else {
164            return;
165        };
166
167        // TODO: read OTEL span events and convert them to Sentry breadcrumbs/events
168
169        sentry_span.set_data("otel.kind", convert_span_kind(data.span_kind));
170        for attribute in data.attributes {
171            sentry_span.set_data(attribute.key.as_str(), convert_value(attribute.value));
172        }
173
174        // TODO: read OTEL semantic convention span attributes and map them to the appropriate
175        // Sentry span attributes/context values
176
177        if let TransactionOrSpan::Transaction(transaction) = &sentry_span {
178            transaction.set_origin("auto.otel");
179        }
180        sentry_span.set_status(convert_span_status(&data.status));
181        sentry_span.finish_with_timestamp(data.end_time);
182    }
183
184    fn force_flush(&self) -> OTelSdkResult {
185        Ok(())
186    }
187
188    fn shutdown(&self) -> OTelSdkResult {
189        Ok(())
190    }
191
192    fn set_resource(&mut self, resource: &Resource) {
193        sentry_core::configure_scope(|scope| {
194            let otel_context = sentry_core::protocol::OtelContext {
195                resource: resource
196                    .iter()
197                    .map(|(key, value)| (key.as_str().into(), convert_value(value.clone())))
198                    .collect(),
199                ..Default::default()
200            };
201            scope.set_context("otel", sentry_core::protocol::Context::from(otel_context));
202        });
203    }
204}