Skip to main content

sentry_tracing/layer/
mod.rs

1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use bitflags::bitflags;
7use sentry_core::protocol::Value;
8use sentry_core::{Breadcrumb, Hub, HubSwitchGuard, TransactionOrSpan};
9use tracing_core::field::Visit;
10use tracing_core::{span, Event, Field, Level, Metadata, Subscriber};
11use tracing_subscriber::layer::{Context, Layer};
12use tracing_subscriber::registry::LookupSpan;
13
14use crate::converters::*;
15use crate::SENTRY_NAME_FIELD;
16use crate::SENTRY_OP_FIELD;
17use crate::SENTRY_TRACE_FIELD;
18use crate::TAGS_PREFIX;
19use span_guard_stack::SpanGuardStack;
20
21mod span_guard_stack;
22
23bitflags! {
24    /// The action that Sentry should perform for a given [`Event`]
25    #[derive(Debug, Clone, Copy)]
26    pub struct EventFilter: u32 {
27        /// Ignore the [`Event`]
28        const Ignore = 0b000;
29        /// Create a [`Breadcrumb`] from this [`Event`]
30        const Breadcrumb = 0b001;
31        /// Create a [`sentry_core::protocol::Event`] from this [`Event`]
32        const Event = 0b010;
33        /// Create a [`sentry_core::protocol::Log`] from this [`Event`]
34        const Log = 0b100;
35    }
36}
37
38/// The type of data Sentry should ingest for an [`Event`].
39#[derive(Debug)]
40#[non_exhaustive]
41pub enum EventMapping {
42    /// Ignore the [`Event`]
43    Ignore,
44    /// Adds the [`Breadcrumb`] to the Sentry scope.
45    Breadcrumb(Breadcrumb),
46    /// Captures the [`sentry_core::protocol::Event`] to Sentry.
47    Event(Box<sentry_core::protocol::Event<'static>>),
48    /// Captures the [`sentry_core::protocol::Log`] to Sentry.
49    #[cfg(feature = "logs")]
50    Log(sentry_core::protocol::Log),
51    /// Captures multiple items to Sentry.
52    /// Nesting multiple `EventMapping::Combined` inside each other will cause the inner mappings to be ignored.
53    Combined(CombinedEventMapping),
54}
55
56/// A list of event mappings.
57#[derive(Debug)]
58pub struct CombinedEventMapping(Vec<EventMapping>);
59
60impl From<EventMapping> for CombinedEventMapping {
61    fn from(value: EventMapping) -> Self {
62        match value {
63            EventMapping::Combined(combined) => combined,
64            _ => CombinedEventMapping(vec![value]),
65        }
66    }
67}
68
69impl From<Vec<EventMapping>> for CombinedEventMapping {
70    fn from(value: Vec<EventMapping>) -> Self {
71        Self(value)
72    }
73}
74
75/// The default event filter.
76///
77/// By default, an exception event is captured for `error`, a breadcrumb for
78/// `warning` and `info`, and `debug` and `trace` logs are ignored.
79pub fn default_event_filter(metadata: &Metadata) -> EventFilter {
80    match metadata.level() {
81        #[cfg(feature = "logs")]
82        &Level::ERROR => EventFilter::Event | EventFilter::Log,
83        #[cfg(not(feature = "logs"))]
84        &Level::ERROR => EventFilter::Event,
85        #[cfg(feature = "logs")]
86        &Level::WARN | &Level::INFO => EventFilter::Breadcrumb | EventFilter::Log,
87        #[cfg(not(feature = "logs"))]
88        &Level::WARN | &Level::INFO => EventFilter::Breadcrumb,
89        &Level::DEBUG | &Level::TRACE => EventFilter::Ignore,
90    }
91}
92
93/// The default span filter.
94///
95/// By default, spans at the `error`, `warning`, and `info`
96/// levels are captured
97pub fn default_span_filter(metadata: &Metadata) -> bool {
98    matches!(
99        metadata.level(),
100        &Level::ERROR | &Level::WARN | &Level::INFO
101    )
102}
103
104type EventMapper<S> = Box<dyn Fn(&Event, Context<'_, S>) -> EventMapping + Send + Sync>;
105
106/// Provides a tracing layer that dispatches events to sentry
107pub struct SentryLayer<S> {
108    event_filter: Box<dyn Fn(&Metadata) -> EventFilter + Send + Sync>,
109    event_mapper: Option<EventMapper<S>>,
110
111    span_filter: Box<dyn Fn(&Metadata) -> bool + Send + Sync>,
112
113    with_span_attributes: bool,
114}
115
116impl<S> SentryLayer<S> {
117    /// Sets a custom event filter function.
118    ///
119    /// The filter classifies how sentry should handle [`Event`]s based
120    /// on their [`Metadata`].
121    #[must_use]
122    pub fn event_filter<F>(mut self, filter: F) -> Self
123    where
124        F: Fn(&Metadata) -> EventFilter + Send + Sync + 'static,
125    {
126        self.event_filter = Box::new(filter);
127        self
128    }
129
130    /// Sets a custom event mapper function.
131    ///
132    /// The mapper is responsible for creating either breadcrumbs or events from
133    /// [`Event`]s.
134    #[must_use]
135    pub fn event_mapper<F>(mut self, mapper: F) -> Self
136    where
137        F: Fn(&Event, Context<'_, S>) -> EventMapping + Send + Sync + 'static,
138    {
139        self.event_mapper = Some(Box::new(mapper));
140        self
141    }
142
143    /// Sets a custom span filter function.
144    ///
145    /// The filter classifies whether sentry should handle [`tracing::Span`]s based
146    /// on their [`Metadata`].
147    ///
148    /// [`tracing::Span`]: https://docs.rs/tracing/latest/tracing/struct.Span.html
149    #[must_use]
150    pub fn span_filter<F>(mut self, filter: F) -> Self
151    where
152        F: Fn(&Metadata) -> bool + Send + Sync + 'static,
153    {
154        self.span_filter = Box::new(filter);
155        self
156    }
157
158    /// Enable every parent span's attributes to be sent along with own event's attributes.
159    ///
160    /// Note that the root span is considered a [transaction][sentry_core::protocol::Transaction]
161    /// so its context will only be grabbed only if you set the transaction to be sampled.
162    /// The most straightforward way to do this is to set
163    /// the [traces_sample_rate][sentry_core::ClientOptions::traces_sample_rate] to `1.0`
164    /// while configuring your sentry client.
165    #[must_use]
166    pub fn enable_span_attributes(mut self) -> Self {
167        self.with_span_attributes = true;
168        self
169    }
170}
171
172impl<S> Default for SentryLayer<S>
173where
174    S: Subscriber + for<'a> LookupSpan<'a>,
175{
176    fn default() -> Self {
177        Self {
178            event_filter: Box::new(default_event_filter),
179            event_mapper: None,
180
181            span_filter: Box::new(default_span_filter),
182
183            with_span_attributes: false,
184        }
185    }
186}
187
188#[inline(always)]
189fn record_fields<'a, K: AsRef<str> + Into<Cow<'a, str>>>(
190    span: &TransactionOrSpan,
191    data: BTreeMap<K, Value>,
192) {
193    match span {
194        TransactionOrSpan::Span(span) => {
195            let mut span = span.data();
196            for (key, value) in data {
197                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
198                    match value {
199                        Value::Bool(value) => {
200                            span.set_tag(stripped_key.to_owned(), value.to_string())
201                        }
202                        Value::Number(value) => {
203                            span.set_tag(stripped_key.to_owned(), value.to_string())
204                        }
205                        Value::String(value) => span.set_tag(stripped_key.to_owned(), value),
206                        _ => span.set_data(key.into().into_owned(), value),
207                    }
208                } else {
209                    span.set_data(key.into().into_owned(), value);
210                }
211            }
212        }
213        TransactionOrSpan::Transaction(transaction) => {
214            let mut transaction = transaction.data();
215            for (key, value) in data {
216                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
217                    match value {
218                        Value::Bool(value) => {
219                            transaction.set_tag(stripped_key.into(), value.to_string())
220                        }
221                        Value::Number(value) => {
222                            transaction.set_tag(stripped_key.into(), value.to_string())
223                        }
224                        Value::String(value) => transaction.set_tag(stripped_key.into(), value),
225                        _ => transaction.set_data(key.into(), value),
226                    }
227                } else {
228                    transaction.set_data(key.into(), value);
229                }
230            }
231        }
232    }
233}
234
235/// Data that is attached to the tracing Spans `extensions`, in order to
236/// `finish` the corresponding sentry span `on_close`, and re-set its parent as
237/// the *current* span.
238pub(super) struct SentrySpanData {
239    pub(super) sentry_span: TransactionOrSpan,
240    hub: Arc<sentry_core::Hub>,
241}
242
243impl<S> Layer<S> for SentryLayer<S>
244where
245    S: Subscriber + for<'a> LookupSpan<'a>,
246{
247    fn on_event(&self, event: &Event, ctx: Context<'_, S>) {
248        let items = match &self.event_mapper {
249            Some(mapper) => mapper(event, ctx),
250            None => {
251                let span_ctx = self.with_span_attributes.then_some(ctx);
252                let filter = (self.event_filter)(event.metadata());
253                let mut items = vec![];
254                if filter.contains(EventFilter::Breadcrumb) {
255                    items.push(EventMapping::Breadcrumb(breadcrumb_from_event(
256                        event,
257                        span_ctx.as_ref(),
258                    )));
259                }
260                if filter.contains(EventFilter::Event) {
261                    items.push(EventMapping::Event(
262                        event_from_event(event, span_ctx.as_ref()).into(),
263                    ));
264                }
265                #[cfg(feature = "logs")]
266                if filter.contains(EventFilter::Log) {
267                    items.push(EventMapping::Log(log_from_event(event, span_ctx.as_ref())));
268                }
269                EventMapping::Combined(CombinedEventMapping(items))
270            }
271        };
272        let items = CombinedEventMapping::from(items);
273
274        for item in items.0 {
275            match item {
276                EventMapping::Ignore => (),
277                EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
278                EventMapping::Event(event) => {
279                    sentry_core::capture_event(*event);
280                }
281                #[cfg(feature = "logs")]
282                EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| {
283                    let enabled = hub.client().is_none_or(|client| {
284                        let options = client.options();
285
286                        #[expect(deprecated, reason = "checking a deprecated field")]
287                        options.enable_logs
288                    });
289                    if enabled {
290                        hub.capture_log(log);
291                    }
292                }),
293                EventMapping::Combined(_) => {
294                    sentry_core::sentry_debug!(
295                        "[SentryLayer] found nested CombinedEventMapping, ignoring"
296                    )
297                }
298            }
299        }
300    }
301
302    /// When a new Span gets created, run the filter and start a new sentry span
303    /// if it passes, setting it as the *current* sentry span.
304    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
305        let span = match ctx.span(id) {
306            Some(span) => span,
307            None => return,
308        };
309
310        if !(self.span_filter)(span.metadata()) {
311            return;
312        }
313
314        let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs);
315        let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name());
316        let sentry_op =
317            sentry_op.unwrap_or_else(|| format!("{}::{}", span.metadata().target(), span.name()));
318
319        let hub = sentry_core::Hub::current();
320        let parent_sentry_span = hub.configure_scope(|scope| scope.get_span());
321
322        let mut sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span {
323            Some(parent) => parent.start_child(&sentry_op, sentry_name).into(),
324            None => {
325                let ctx = if let Some(trace_header) = sentry_trace {
326                    sentry_core::TransactionContext::continue_from_headers(
327                        sentry_name,
328                        &sentry_op,
329                        [("sentry-trace", trace_header.as_str())],
330                    )
331                } else {
332                    sentry_core::TransactionContext::new(sentry_name, &sentry_op)
333                };
334
335                let tx = sentry_core::start_transaction(ctx);
336                tx.set_origin("auto.tracing");
337                tx.into()
338            }
339        };
340        // Add the data from the original span to the sentry span.
341        // This comes from typically the `fields` in `tracing::instrument`.
342        record_fields(&sentry_span, data);
343
344        set_default_attributes(&mut sentry_span, span.metadata());
345
346        let mut extensions = span.extensions_mut();
347        extensions.insert(SentrySpanData { sentry_span, hub });
348    }
349
350    /// Sets the entered span as *current* sentry span.
351    ///
352    /// A tracing span can be entered and exited multiple times, for example,
353    /// when using a `tracing::Instrumented` future.
354    ///
355    /// Spans must be exited on the same thread that they are entered. The
356    /// `sentry-tracing` integration's behavior is undefined if spans are
357    /// exited on threads other than the one they are entered from;
358    /// specifically, doing so will likely cause data to bleed between
359    /// [`Hub`]s in unexpected ways.
360    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
361        let span = match ctx.span(id) {
362            Some(span) => span,
363            None => return,
364        };
365
366        let extensions = span.extensions();
367        if let Some(data) = extensions.get::<SentrySpanData>() {
368            // We fork the hub (based on the hub associated with the span)
369            // upon entering the span. This prevents data leakage if the span
370            // is entered and exited multiple times.
371            //
372            // Further, Hubs are meant to manage thread-local state, even
373            // though they can be shared across threads. As the span may being
374            // entered on a different thread than where it was created, we need
375            // to use a new hub to avoid altering state on the original thread.
376            let hub = Arc::new(Hub::new_from_top(&data.hub));
377
378            hub.configure_scope(|scope| {
379                scope.set_span(Some(data.sentry_span.clone()));
380            });
381
382            let guard = HubSwitchGuard::new(hub);
383
384            SPAN_GUARDS.with(|guards| {
385                guards.borrow_mut().push(id.clone(), guard);
386            });
387        }
388    }
389
390    /// Drop the current span's [`HubSwitchGuard`] to restore the parent [`Hub`].
391    fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
392        let popped = SPAN_GUARDS.with(|guards| guards.borrow_mut().pop(id.clone()));
393
394        // We should have popped a guard if the tracing span has `SentrySpanData` extensions.
395        sentry_core::debug_assert_or_log!(
396            popped.is_some()
397                || ctx
398                    .span(id)
399                    .is_none_or(|span| span.extensions().get::<SentrySpanData>().is_none()),
400            "[SentryLayer] missing HubSwitchGuard on exit for span {id:?}. \
401            This span has been exited more times on this thread than it has been entered, \
402            likely due to dropping an `Entered` guard in a different thread than where it was \
403            entered. This mismatch will likely cause the sentry-tracing layer to leak memory."
404        );
405    }
406
407    /// When a span gets closed, finish the underlying sentry span, and set back
408    /// its parent as the *current* sentry span.
409    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
410        let span = match ctx.span(&id) {
411            Some(span) => span,
412            None => return,
413        };
414
415        let mut extensions = span.extensions_mut();
416        let SentrySpanData { sentry_span, .. } = match extensions.remove::<SentrySpanData>() {
417            Some(data) => data,
418            None => return,
419        };
420
421        sentry_span.finish();
422    }
423
424    /// Implement the writing of extra data to span
425    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
426        let span = match ctx.span(span) {
427            Some(s) => s,
428            _ => return,
429        };
430
431        let mut extensions = span.extensions_mut();
432        let span = match extensions.get_mut::<SentrySpanData>() {
433            Some(t) => &t.sentry_span,
434            _ => return,
435        };
436
437        let mut data = FieldVisitor::default();
438        values.record(&mut data);
439
440        let sentry_name = data
441            .json_values
442            .remove(SENTRY_NAME_FIELD)
443            .and_then(|v| match v {
444                Value::String(s) => Some(s),
445                _ => None,
446            });
447
448        let sentry_op = data
449            .json_values
450            .remove(SENTRY_OP_FIELD)
451            .and_then(|v| match v {
452                Value::String(s) => Some(s),
453                _ => None,
454            });
455
456        // `sentry.trace` cannot be applied retroactively
457        data.json_values.remove(SENTRY_TRACE_FIELD);
458
459        if let Some(name) = sentry_name {
460            span.set_name(&name);
461        }
462        if let Some(op) = sentry_op {
463            span.set_op(&op);
464        }
465
466        record_fields(span, data.json_values);
467    }
468}
469
470fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) {
471    span.set_data("sentry.tracing.target", metadata.target().into());
472
473    if let Some(module) = metadata.module_path() {
474        span.set_data("code.module.name", module.into());
475    }
476
477    if let Some(file) = metadata.file() {
478        span.set_data("code.file.path", file.into());
479    }
480
481    if let Some(line) = metadata.line() {
482        span.set_data("code.line.number", line.into());
483    }
484}
485
486/// Creates a default Sentry layer
487pub fn layer<S>() -> SentryLayer<S>
488where
489    S: Subscriber + for<'a> LookupSpan<'a>,
490{
491    Default::default()
492}
493
494/// Extracts the attributes from a span,
495/// returning the values of SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD separately
496fn extract_span_data(
497    attrs: &span::Attributes,
498) -> (
499    BTreeMap<&'static str, Value>,
500    Option<String>,
501    Option<String>,
502    Option<String>,
503) {
504    let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| {
505        let mut visitor = SpanFieldVisitor {
506            debug_buffer,
507            json_values: Default::default(),
508        };
509        attrs.record(&mut visitor);
510        visitor.json_values
511    });
512
513    let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v {
514        Value::String(s) => Some(s),
515        _ => None,
516    });
517
518    let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v {
519        Value::String(s) => Some(s),
520        _ => None,
521    });
522
523    let sentry_trace = json_values
524        .remove(SENTRY_TRACE_FIELD)
525        .and_then(|v| match v {
526            Value::String(s) => Some(s),
527            _ => None,
528        });
529
530    (json_values, name, op, sentry_trace)
531}
532
533thread_local! {
534    static VISITOR_BUFFER: RefCell<String> = const { RefCell::new(String::new()) };
535    /// Hub switch guards keyed by span ID.
536    ///
537    /// Guard bookkeeping is thread-local by design. Correctness expects
538    /// balanced enter/exit callbacks on the same thread.
539    static SPAN_GUARDS: RefCell<SpanGuardStack> = RefCell::new(SpanGuardStack::new());
540}
541
542/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer.
543struct SpanFieldVisitor<'s> {
544    debug_buffer: &'s mut String,
545    json_values: BTreeMap<&'static str, Value>,
546}
547
548impl SpanFieldVisitor<'_> {
549    fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
550        self.json_values.insert(field.name(), value.into());
551    }
552}
553
554impl Visit for SpanFieldVisitor<'_> {
555    fn record_i64(&mut self, field: &Field, value: i64) {
556        self.record(field, value);
557    }
558
559    fn record_u64(&mut self, field: &Field, value: u64) {
560        self.record(field, value);
561    }
562
563    fn record_bool(&mut self, field: &Field, value: bool) {
564        self.record(field, value);
565    }
566
567    fn record_f64(&mut self, field: &Field, value: f64) {
568        self.record(field, value);
569    }
570
571    fn record_str(&mut self, field: &Field, value: &str) {
572        self.record(field, value);
573    }
574
575    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
576        use std::fmt::Write;
577        self.debug_buffer.reserve(128);
578        write!(self.debug_buffer, "{value:?}").unwrap();
579        self.json_values
580            .insert(field.name(), self.debug_buffer.as_str().into());
581        self.debug_buffer.clear();
582    }
583}