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#[allow(clippy::large_enum_variant)]
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(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(event_from_event(
262                        event,
263                        span_ctx.as_ref(),
264                    )));
265                }
266                #[cfg(feature = "logs")]
267                if filter.contains(EventFilter::Log) {
268                    items.push(EventMapping::Log(log_from_event(event, span_ctx.as_ref())));
269                }
270                EventMapping::Combined(CombinedEventMapping(items))
271            }
272        };
273        let items = CombinedEventMapping::from(items);
274
275        for item in items.0 {
276            match item {
277                EventMapping::Ignore => (),
278                EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
279                EventMapping::Event(event) => {
280                    sentry_core::capture_event(event);
281                }
282                #[cfg(feature = "logs")]
283                EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
284                EventMapping::Combined(_) => {
285                    sentry_core::sentry_debug!(
286                        "[SentryLayer] found nested CombinedEventMapping, ignoring"
287                    )
288                }
289            }
290        }
291    }
292
293    /// When a new Span gets created, run the filter and start a new sentry span
294    /// if it passes, setting it as the *current* sentry span.
295    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
296        let span = match ctx.span(id) {
297            Some(span) => span,
298            None => return,
299        };
300
301        if !(self.span_filter)(span.metadata()) {
302            return;
303        }
304
305        let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs);
306        let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name());
307        let sentry_op =
308            sentry_op.unwrap_or_else(|| format!("{}::{}", span.metadata().target(), span.name()));
309
310        let hub = sentry_core::Hub::current();
311        let parent_sentry_span = hub.configure_scope(|scope| scope.get_span());
312
313        let mut sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span {
314            Some(parent) => parent.start_child(&sentry_op, sentry_name).into(),
315            None => {
316                let ctx = if let Some(trace_header) = sentry_trace {
317                    sentry_core::TransactionContext::continue_from_headers(
318                        sentry_name,
319                        &sentry_op,
320                        [("sentry-trace", trace_header.as_str())],
321                    )
322                } else {
323                    sentry_core::TransactionContext::new(sentry_name, &sentry_op)
324                };
325
326                let tx = sentry_core::start_transaction(ctx);
327                tx.set_origin("auto.tracing");
328                tx.into()
329            }
330        };
331        // Add the data from the original span to the sentry span.
332        // This comes from typically the `fields` in `tracing::instrument`.
333        record_fields(&sentry_span, data);
334
335        set_default_attributes(&mut sentry_span, span.metadata());
336
337        let mut extensions = span.extensions_mut();
338        extensions.insert(SentrySpanData { sentry_span, hub });
339    }
340
341    /// Sets the entered span as *current* sentry span.
342    ///
343    /// A tracing span can be entered and exited multiple times, for example,
344    /// when using a `tracing::Instrumented` future.
345    ///
346    /// Spans must be exited on the same thread that they are entered. The
347    /// `sentry-tracing` integration's behavior is undefined if spans are
348    /// exited on threads other than the one they are entered from;
349    /// specifically, doing so will likely cause data to bleed between
350    /// [`Hub`]s in unexpected ways.
351    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
352        let span = match ctx.span(id) {
353            Some(span) => span,
354            None => return,
355        };
356
357        let extensions = span.extensions();
358        if let Some(data) = extensions.get::<SentrySpanData>() {
359            // We fork the hub (based on the hub associated with the span)
360            // upon entering the span. This prevents data leakage if the span
361            // is entered and exited multiple times.
362            //
363            // Further, Hubs are meant to manage thread-local state, even
364            // though they can be shared across threads. As the span may being
365            // entered on a different thread than where it was created, we need
366            // to use a new hub to avoid altering state on the original thread.
367            let hub = Arc::new(Hub::new_from_top(&data.hub));
368
369            hub.configure_scope(|scope| {
370                scope.set_span(Some(data.sentry_span.clone()));
371            });
372
373            let guard = HubSwitchGuard::new(hub);
374
375            SPAN_GUARDS.with(|guards| {
376                guards.borrow_mut().push(id.clone(), guard);
377            });
378        }
379    }
380
381    /// Drop the current span's [`HubSwitchGuard`] to restore the parent [`Hub`].
382    fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
383        let popped = SPAN_GUARDS.with(|guards| guards.borrow_mut().pop(id.clone()));
384
385        // We should have popped a guard if the tracing span has `SentrySpanData` extensions.
386        sentry_core::debug_assert_or_log!(
387            popped.is_some()
388                || ctx
389                    .span(id)
390                    .is_none_or(|span| span.extensions().get::<SentrySpanData>().is_none()),
391            "[SentryLayer] missing HubSwitchGuard on exit for span {id:?}. \
392            This span has been exited more times on this thread than it has been entered, \
393            likely due to dropping an `Entered` guard in a different thread than where it was \
394            entered. This mismatch will likely cause the sentry-tracing layer to leak memory."
395        );
396    }
397
398    /// When a span gets closed, finish the underlying sentry span, and set back
399    /// its parent as the *current* sentry span.
400    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
401        let span = match ctx.span(&id) {
402            Some(span) => span,
403            None => return,
404        };
405
406        let mut extensions = span.extensions_mut();
407        let SentrySpanData { sentry_span, .. } = match extensions.remove::<SentrySpanData>() {
408            Some(data) => data,
409            None => return,
410        };
411
412        sentry_span.finish();
413    }
414
415    /// Implement the writing of extra data to span
416    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
417        let span = match ctx.span(span) {
418            Some(s) => s,
419            _ => return,
420        };
421
422        let mut extensions = span.extensions_mut();
423        let span = match extensions.get_mut::<SentrySpanData>() {
424            Some(t) => &t.sentry_span,
425            _ => return,
426        };
427
428        let mut data = FieldVisitor::default();
429        values.record(&mut data);
430
431        let sentry_name = data
432            .json_values
433            .remove(SENTRY_NAME_FIELD)
434            .and_then(|v| match v {
435                Value::String(s) => Some(s),
436                _ => None,
437            });
438
439        let sentry_op = data
440            .json_values
441            .remove(SENTRY_OP_FIELD)
442            .and_then(|v| match v {
443                Value::String(s) => Some(s),
444                _ => None,
445            });
446
447        // `sentry.trace` cannot be applied retroactively
448        data.json_values.remove(SENTRY_TRACE_FIELD);
449
450        if let Some(name) = sentry_name {
451            span.set_name(&name);
452        }
453        if let Some(op) = sentry_op {
454            span.set_op(&op);
455        }
456
457        record_fields(span, data.json_values);
458    }
459}
460
461fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) {
462    span.set_data("sentry.tracing.target", metadata.target().into());
463
464    if let Some(module) = metadata.module_path() {
465        span.set_data("code.module.name", module.into());
466    }
467
468    if let Some(file) = metadata.file() {
469        span.set_data("code.file.path", file.into());
470    }
471
472    if let Some(line) = metadata.line() {
473        span.set_data("code.line.number", line.into());
474    }
475}
476
477/// Creates a default Sentry layer
478pub fn layer<S>() -> SentryLayer<S>
479where
480    S: Subscriber + for<'a> LookupSpan<'a>,
481{
482    Default::default()
483}
484
485/// Extracts the attributes from a span,
486/// returning the values of SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD separately
487fn extract_span_data(
488    attrs: &span::Attributes,
489) -> (
490    BTreeMap<&'static str, Value>,
491    Option<String>,
492    Option<String>,
493    Option<String>,
494) {
495    let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| {
496        let mut visitor = SpanFieldVisitor {
497            debug_buffer,
498            json_values: Default::default(),
499        };
500        attrs.record(&mut visitor);
501        visitor.json_values
502    });
503
504    let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v {
505        Value::String(s) => Some(s),
506        _ => None,
507    });
508
509    let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v {
510        Value::String(s) => Some(s),
511        _ => None,
512    });
513
514    let sentry_trace = json_values
515        .remove(SENTRY_TRACE_FIELD)
516        .and_then(|v| match v {
517            Value::String(s) => Some(s),
518            _ => None,
519        });
520
521    (json_values, name, op, sentry_trace)
522}
523
524thread_local! {
525    static VISITOR_BUFFER: RefCell<String> = const { RefCell::new(String::new()) };
526    /// Hub switch guards keyed by span ID.
527    ///
528    /// Guard bookkeeping is thread-local by design. Correctness expects
529    /// balanced enter/exit callbacks on the same thread.
530    static SPAN_GUARDS: RefCell<SpanGuardStack> = RefCell::new(SpanGuardStack::new());
531}
532
533/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer.
534struct SpanFieldVisitor<'s> {
535    debug_buffer: &'s mut String,
536    json_values: BTreeMap<&'static str, Value>,
537}
538
539impl SpanFieldVisitor<'_> {
540    fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
541        self.json_values.insert(field.name(), value.into());
542    }
543}
544
545impl Visit for SpanFieldVisitor<'_> {
546    fn record_i64(&mut self, field: &Field, value: i64) {
547        self.record(field, value);
548    }
549
550    fn record_u64(&mut self, field: &Field, value: u64) {
551        self.record(field, value);
552    }
553
554    fn record_bool(&mut self, field: &Field, value: bool) {
555        self.record(field, value);
556    }
557
558    fn record_str(&mut self, field: &Field, value: &str) {
559        self.record(field, value);
560    }
561
562    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
563        use std::fmt::Write;
564        self.debug_buffer.reserve(128);
565        write!(self.debug_buffer, "{value:?}").unwrap();
566        self.json_values
567            .insert(field.name(), self.debug_buffer.as_str().into());
568        self.debug_buffer.clear();
569    }
570}