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