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