Skip to main content

sentry_tracing/layer/
span.rs

1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use sentry_core::protocol::Value;
7use sentry_core::{Hub, HubSwitchGuard, TransactionOrSpan};
8use tracing_core::field::Visit;
9use tracing_core::{span, Field, Metadata, Subscriber};
10use tracing_subscriber::layer::{Context, Layer};
11use tracing_subscriber::registry::LookupSpan;
12
13use super::span_guard_stack::SpanGuardStack;
14use crate::converters::FieldVisitor;
15use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX};
16
17/// Captures tracing spans as Sentry transactions and spans.
18///
19/// Install this layer to capture spans; event layers do not manage the span lifecycle.
20/// Without a filter, this layer captures spans at every level. Configure a filter to limit span
21/// volume; see [`SpanLayer::new`] for the recommended levels.
22///
23/// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported.
24/// **Combining the two layers can panic when a span is created.** Use either the legacy layer
25/// or the individual layers, not both.
26#[derive(Default)]
27#[non_exhaustive]
28#[must_use]
29pub struct SpanLayer {}
30
31impl SpanLayer {
32    /// Creates a layer that captures tracing spans as Sentry transactions and spans.
33    ///
34    /// # Filtering
35    ///
36    /// Without a filter, this layer captures spans at every level. Configure a level filter to limit
37    /// span volume. We recommend capturing spans at the `INFO` level, and more severe.
38    ///
39    /// ```rust
40    /// # use tracing_subscriber::prelude::*;
41    /// use sentry::integrations::tracing::SpanLayer;
42    /// use tracing_subscriber::filter::LevelFilter;
43    ///
44    /// tracing_subscriber::registry()
45    ///     .with(SpanLayer::new().with_filter(LevelFilter::INFO))
46    ///     .init();
47    ///
48    /// // This will be captured ...
49    /// let _info = tracing::info_span!("INFO span").entered();
50    ///
51    /// // ... but this will not be, due to LevelFilter::INFO being set.
52    /// let _debug = tracing::debug_span!("DEBUG span").entered();
53    /// ```
54    pub fn new() -> Self {
55        Self::default()
56    }
57}
58
59#[inline(always)]
60fn record_fields<'a, K: AsRef<str> + Into<Cow<'a, str>>>(
61    span: &TransactionOrSpan,
62    data: BTreeMap<K, Value>,
63) {
64    match span {
65        TransactionOrSpan::Span(span) => {
66            let mut span = span.data();
67            for (key, value) in data {
68                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
69                    match value {
70                        Value::Bool(value) => {
71                            span.set_tag(stripped_key.to_owned(), value.to_string())
72                        }
73                        Value::Number(value) => {
74                            span.set_tag(stripped_key.to_owned(), value.to_string())
75                        }
76                        Value::String(value) => span.set_tag(stripped_key.to_owned(), value),
77                        _ => span.set_data(key.into().into_owned(), value),
78                    }
79                } else {
80                    span.set_data(key.into().into_owned(), value);
81                }
82            }
83        }
84        TransactionOrSpan::Transaction(transaction) => {
85            let mut transaction = transaction.data();
86            for (key, value) in data {
87                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
88                    match value {
89                        Value::Bool(value) => {
90                            transaction.set_tag(stripped_key.into(), value.to_string())
91                        }
92                        Value::Number(value) => {
93                            transaction.set_tag(stripped_key.into(), value.to_string())
94                        }
95                        Value::String(value) => transaction.set_tag(stripped_key.into(), value),
96                        _ => transaction.set_data(key.into(), value),
97                    }
98                } else {
99                    transaction.set_data(key.into(), value);
100                }
101            }
102        }
103    }
104}
105
106/// Data attached to tracing span extensions, read by the event converters and
107/// finished when the corresponding tracing span closes.
108pub(crate) struct SentrySpanData {
109    pub(crate) sentry_span: TransactionOrSpan,
110    hub: Arc<Hub>,
111}
112
113impl<S> Layer<S> for SpanLayer
114where
115    S: Subscriber + for<'a> LookupSpan<'a>,
116{
117    /// When a new Span gets created, start a new sentry span, setting it as the *current* sentry
118    /// span.
119    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
120        let span = match ctx.span(id) {
121            Some(span) => span,
122            None => return,
123        };
124
125        let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs);
126        let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name());
127        let sentry_op =
128            sentry_op.unwrap_or_else(|| format!("{}::{}", span.metadata().target(), span.name()));
129
130        let hub = Hub::current();
131        let parent_sentry_span = hub.configure_scope(|scope| scope.get_span());
132
133        let mut sentry_span: TransactionOrSpan = match &parent_sentry_span {
134            Some(parent) => parent.start_child(&sentry_op, sentry_name).into(),
135            None => {
136                let ctx = if let Some(trace_header) = sentry_trace {
137                    sentry_core::TransactionContext::continue_from_headers(
138                        sentry_name,
139                        &sentry_op,
140                        [("sentry-trace", trace_header.as_str())],
141                    )
142                } else {
143                    sentry_core::TransactionContext::new(sentry_name, &sentry_op)
144                };
145
146                let tx = sentry_core::start_transaction(ctx);
147                tx.set_origin("auto.tracing");
148                tx.into()
149            }
150        };
151        record_fields(&sentry_span, data);
152        set_default_attributes(&mut sentry_span, span.metadata());
153
154        let mut extensions = span.extensions_mut();
155        extensions.insert(SentrySpanData { sentry_span, hub });
156    }
157
158    /// Sets the entered span as *current* sentry span.
159    ///
160    /// A tracing span can be entered and exited multiple times, for example,
161    /// when using a `tracing::Instrumented` future.
162    ///
163    /// Spans must be exited on the same thread that they are entered. The
164    /// `sentry-tracing` integration's behavior is undefined if spans are
165    /// exited on threads other than the one they are entered from;
166    /// specifically, doing so will likely cause data to bleed between
167    /// [`Hub`]s in unexpected ways.
168    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
169        let span = match ctx.span(id) {
170            Some(span) => span,
171            None => return,
172        };
173
174        let extensions = span.extensions();
175        if let Some(data) = extensions.get::<SentrySpanData>() {
176            let hub = Arc::new(Hub::new_from_top(&data.hub));
177            hub.configure_scope(|scope| {
178                scope.set_span(Some(data.sentry_span.clone()));
179            });
180
181            let guard = HubSwitchGuard::new(hub);
182            SPAN_GUARDS.with(|guards| {
183                guards.borrow_mut().push(id.clone(), guard);
184            });
185        }
186    }
187
188    /// Drop the current span's [`HubSwitchGuard`] to restore the parent [`Hub`].
189    fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
190        let popped = SPAN_GUARDS.with(|guards| guards.borrow_mut().pop(id.clone()));
191
192        sentry_core::debug_assert_or_log!(
193            popped.is_some()
194                || ctx
195                    .span(id)
196                    .is_none_or(|span| span.extensions().get::<SentrySpanData>().is_none()),
197            "[SentrySpanLayer] missing HubSwitchGuard on exit for span {id:?}. \
198            This span has been exited more times on this thread than it has been entered, \
199            likely due to dropping an `Entered` guard in a different thread than where it was \
200            entered. This mismatch will likely cause the sentry-tracing layer to leak memory."
201        );
202    }
203
204    /// Finish the underlying sentry span when the tracing span closes.
205    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
206        let span = match ctx.span(&id) {
207            Some(span) => span,
208            None => return,
209        };
210
211        let mut extensions = span.extensions_mut();
212        let SentrySpanData { sentry_span, .. } = match extensions.remove::<SentrySpanData>() {
213            Some(data) => data,
214            None => return,
215        };
216        sentry_span.finish();
217    }
218
219    /// Implement the writing of extra data to span.
220    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
221        let span = match ctx.span(span) {
222            Some(s) => s,
223            _ => return,
224        };
225
226        let mut extensions = span.extensions_mut();
227        let span = match extensions.get_mut::<SentrySpanData>() {
228            Some(t) => &t.sentry_span,
229            _ => return,
230        };
231
232        let mut data = FieldVisitor::default();
233        values.record(&mut data);
234
235        let sentry_name = data
236            .json_values
237            .remove(SENTRY_NAME_FIELD)
238            .and_then(|v| match v {
239                Value::String(s) => Some(s),
240                _ => None,
241            });
242
243        let sentry_op = data
244            .json_values
245            .remove(SENTRY_OP_FIELD)
246            .and_then(|v| match v {
247                Value::String(s) => Some(s),
248                _ => None,
249            });
250
251        // `sentry.trace` cannot be applied retroactively
252        data.json_values.remove(SENTRY_TRACE_FIELD);
253
254        if let Some(name) = sentry_name {
255            span.set_name(&name);
256        }
257        if let Some(op) = sentry_op {
258            span.set_op(&op);
259        }
260
261        record_fields(span, data.json_values);
262    }
263}
264
265fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) {
266    span.set_data("sentry.tracing.target", metadata.target().into());
267
268    if let Some(module) = metadata.module_path() {
269        span.set_data("code.module.name", module.into());
270    }
271    if let Some(file) = metadata.file() {
272        span.set_data("code.file.path", file.into());
273    }
274    if let Some(line) = metadata.line() {
275        span.set_data("code.line.number", line.into());
276    }
277}
278
279/// Extracts the attributes from a span, returning Sentry's special fields separately.
280fn extract_span_data(
281    attrs: &span::Attributes,
282) -> (
283    BTreeMap<&'static str, Value>,
284    Option<String>,
285    Option<String>,
286    Option<String>,
287) {
288    let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| {
289        let mut visitor = SpanFieldVisitor {
290            debug_buffer,
291            json_values: Default::default(),
292        };
293        attrs.record(&mut visitor);
294        visitor.json_values
295    });
296
297    let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v {
298        Value::String(s) => Some(s),
299        _ => None,
300    });
301    let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v {
302        Value::String(s) => Some(s),
303        _ => None,
304    });
305    let sentry_trace = json_values
306        .remove(SENTRY_TRACE_FIELD)
307        .and_then(|v| match v {
308            Value::String(s) => Some(s),
309            _ => None,
310        });
311
312    (json_values, name, op, sentry_trace)
313}
314
315thread_local! {
316    static VISITOR_BUFFER: RefCell<String> = const { RefCell::new(String::new()) };
317    /// Hub switch guards keyed by span ID.
318    ///
319    /// Guard bookkeeping is thread-local by design. Correctness expects
320    /// balanced enter/exit callbacks on the same thread.
321    static SPAN_GUARDS: RefCell<SpanGuardStack> = RefCell::new(SpanGuardStack::new());
322}
323
324/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer.
325struct SpanFieldVisitor<'s> {
326    debug_buffer: &'s mut String,
327    json_values: BTreeMap<&'static str, Value>,
328}
329
330impl SpanFieldVisitor<'_> {
331    fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
332        self.json_values.insert(field.name(), value.into());
333    }
334}
335
336impl Visit for SpanFieldVisitor<'_> {
337    fn record_i64(&mut self, field: &Field, value: i64) {
338        self.record(field, value);
339    }
340    fn record_u64(&mut self, field: &Field, value: u64) {
341        self.record(field, value);
342    }
343    fn record_bool(&mut self, field: &Field, value: bool) {
344        self.record(field, value);
345    }
346    fn record_f64(&mut self, field: &Field, value: f64) {
347        self.record(field, value);
348    }
349    fn record_str(&mut self, field: &Field, value: &str) {
350        self.record(field, value);
351    }
352    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
353        use std::fmt::Write;
354        self.debug_buffer.reserve(128);
355        write!(self.debug_buffer, "{value:?}").unwrap();
356        self.json_values
357            .insert(field.name(), self.debug_buffer.as_str().into());
358        self.debug_buffer.clear();
359    }
360}