sentry_tracing/
layer.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, 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::TAGS_PREFIX;
16
17bitflags! {
18    /// The action that Sentry should perform for a given [`Event`]
19    #[derive(Debug, Clone, Copy)]
20    pub struct EventFilter: u32 {
21        /// Ignore the [`Event`]
22        const Ignore = 0b000;
23        /// Create a [`Breadcrumb`] from this [`Event`]
24        const Breadcrumb = 0b001;
25        /// Create a [`sentry_core::protocol::Event`] from this [`Event`]
26        const Event = 0b010;
27        /// Create a [`sentry_core::protocol::Log`] from this [`Event`]
28        const Log = 0b100;
29    }
30}
31
32/// The type of data Sentry should ingest for an [`Event`].
33#[derive(Debug)]
34#[allow(clippy::large_enum_variant)]
35pub enum EventMapping {
36    /// Ignore the [`Event`]
37    Ignore,
38    /// Adds the [`Breadcrumb`] to the Sentry scope.
39    Breadcrumb(Breadcrumb),
40    /// Captures the [`sentry_core::protocol::Event`] to Sentry.
41    Event(sentry_core::protocol::Event<'static>),
42    /// Captures the [`sentry_core::protocol::Log`] to Sentry.
43    #[cfg(feature = "logs")]
44    Log(sentry_core::protocol::Log),
45    /// Captures multiple items to Sentry.
46    /// Nesting multiple `EventMapping::Combined` inside each other will cause the inner mappings to be ignored.
47    Combined(CombinedEventMapping),
48}
49
50/// A list of event mappings.
51#[derive(Debug)]
52pub struct CombinedEventMapping(Vec<EventMapping>);
53
54impl From<EventMapping> for CombinedEventMapping {
55    fn from(value: EventMapping) -> Self {
56        match value {
57            EventMapping::Combined(combined) => combined,
58            _ => CombinedEventMapping(vec![value]),
59        }
60    }
61}
62
63impl From<Vec<EventMapping>> for CombinedEventMapping {
64    fn from(value: Vec<EventMapping>) -> Self {
65        Self(value)
66    }
67}
68
69/// The default event filter.
70///
71/// By default, an exception event is captured for `error`, a breadcrumb for
72/// `warning` and `info`, and `debug` and `trace` logs are ignored.
73pub fn default_event_filter(metadata: &Metadata) -> EventFilter {
74    match metadata.level() {
75        &Level::ERROR => EventFilter::Event,
76        &Level::WARN | &Level::INFO => EventFilter::Breadcrumb,
77        &Level::DEBUG | &Level::TRACE => EventFilter::Ignore,
78    }
79}
80
81/// The default span filter.
82///
83/// By default, spans at the `error`, `warning`, and `info`
84/// levels are captured
85pub fn default_span_filter(metadata: &Metadata) -> bool {
86    matches!(
87        metadata.level(),
88        &Level::ERROR | &Level::WARN | &Level::INFO
89    )
90}
91
92type EventMapper<S> = Box<dyn Fn(&Event, Context<'_, S>) -> EventMapping + Send + Sync>;
93
94/// Provides a tracing layer that dispatches events to sentry
95pub struct SentryLayer<S> {
96    event_filter: Box<dyn Fn(&Metadata) -> EventFilter + Send + Sync>,
97    event_mapper: Option<EventMapper<S>>,
98
99    span_filter: Box<dyn Fn(&Metadata) -> bool + Send + Sync>,
100
101    with_span_attributes: bool,
102}
103
104impl<S> SentryLayer<S> {
105    /// Sets a custom event filter function.
106    ///
107    /// The filter classifies how sentry should handle [`Event`]s based
108    /// on their [`Metadata`].
109    #[must_use]
110    pub fn event_filter<F>(mut self, filter: F) -> Self
111    where
112        F: Fn(&Metadata) -> EventFilter + Send + Sync + 'static,
113    {
114        self.event_filter = Box::new(filter);
115        self
116    }
117
118    /// Sets a custom event mapper function.
119    ///
120    /// The mapper is responsible for creating either breadcrumbs or events from
121    /// [`Event`]s.
122    #[must_use]
123    pub fn event_mapper<F>(mut self, mapper: F) -> Self
124    where
125        F: Fn(&Event, Context<'_, S>) -> EventMapping + Send + Sync + 'static,
126    {
127        self.event_mapper = Some(Box::new(mapper));
128        self
129    }
130
131    /// Sets a custom span filter function.
132    ///
133    /// The filter classifies whether sentry should handle [`tracing::Span`]s based
134    /// on their [`Metadata`].
135    ///
136    /// [`tracing::Span`]: https://docs.rs/tracing/latest/tracing/struct.Span.html
137    #[must_use]
138    pub fn span_filter<F>(mut self, filter: F) -> Self
139    where
140        F: Fn(&Metadata) -> bool + Send + Sync + 'static,
141    {
142        self.span_filter = Box::new(filter);
143        self
144    }
145
146    /// Enable every parent span's attributes to be sent along with own event's attributes.
147    ///
148    /// Note that the root span is considered a [transaction][sentry_core::protocol::Transaction]
149    /// so its context will only be grabbed only if you set the transaction to be sampled.
150    /// The most straightforward way to do this is to set
151    /// the [traces_sample_rate][sentry_core::ClientOptions::traces_sample_rate] to `1.0`
152    /// while configuring your sentry client.
153    #[must_use]
154    pub fn enable_span_attributes(mut self) -> Self {
155        self.with_span_attributes = true;
156        self
157    }
158}
159
160impl<S> Default for SentryLayer<S>
161where
162    S: Subscriber + for<'a> LookupSpan<'a>,
163{
164    fn default() -> Self {
165        Self {
166            event_filter: Box::new(default_event_filter),
167            event_mapper: None,
168
169            span_filter: Box::new(default_span_filter),
170
171            with_span_attributes: false,
172        }
173    }
174}
175
176#[inline(always)]
177fn record_fields<'a, K: AsRef<str> + Into<Cow<'a, str>>>(
178    span: &TransactionOrSpan,
179    data: BTreeMap<K, Value>,
180) {
181    match span {
182        TransactionOrSpan::Span(span) => {
183            let mut span = span.data();
184            for (key, value) in data {
185                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
186                    match value {
187                        Value::Bool(value) => {
188                            span.set_tag(stripped_key.to_owned(), value.to_string())
189                        }
190                        Value::Number(value) => {
191                            span.set_tag(stripped_key.to_owned(), value.to_string())
192                        }
193                        Value::String(value) => span.set_tag(stripped_key.to_owned(), value),
194                        _ => span.set_data(key.into().into_owned(), value),
195                    }
196                } else {
197                    span.set_data(key.into().into_owned(), value);
198                }
199            }
200        }
201        TransactionOrSpan::Transaction(transaction) => {
202            let mut transaction = transaction.data();
203            for (key, value) in data {
204                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
205                    match value {
206                        Value::Bool(value) => {
207                            transaction.set_tag(stripped_key.into(), value.to_string())
208                        }
209                        Value::Number(value) => {
210                            transaction.set_tag(stripped_key.into(), value.to_string())
211                        }
212                        Value::String(value) => transaction.set_tag(stripped_key.into(), value),
213                        _ => transaction.set_data(key.into(), value),
214                    }
215                } else {
216                    transaction.set_data(key.into(), value);
217                }
218            }
219        }
220    }
221}
222
223/// Data that is attached to the tracing Spans `extensions`, in order to
224/// `finish` the corresponding sentry span `on_close`, and re-set its parent as
225/// the *current* span.
226pub(super) struct SentrySpanData {
227    pub(super) sentry_span: TransactionOrSpan,
228    parent_sentry_span: Option<TransactionOrSpan>,
229    hub: Arc<sentry_core::Hub>,
230    hub_switch_guard: Option<sentry_core::HubSwitchGuard>,
231}
232
233impl<S> Layer<S> for SentryLayer<S>
234where
235    S: Subscriber + for<'a> LookupSpan<'a>,
236{
237    fn on_event(&self, event: &Event, ctx: Context<'_, S>) {
238        let items = match &self.event_mapper {
239            Some(mapper) => mapper(event, ctx),
240            None => {
241                let span_ctx = self.with_span_attributes.then_some(ctx);
242                let filter = (self.event_filter)(event.metadata());
243                let mut items = vec![];
244                if filter.contains(EventFilter::Breadcrumb) {
245                    items.push(EventMapping::Breadcrumb(breadcrumb_from_event(
246                        event,
247                        span_ctx.as_ref(),
248                    )));
249                }
250                if filter.contains(EventFilter::Event) {
251                    items.push(EventMapping::Event(event_from_event(
252                        event,
253                        span_ctx.as_ref(),
254                    )));
255                }
256                #[cfg(feature = "logs")]
257                if filter.contains(EventFilter::Log) {
258                    items.push(EventMapping::Log(log_from_event(event, span_ctx.as_ref())));
259                }
260                EventMapping::Combined(CombinedEventMapping(items))
261            }
262        };
263        let items = CombinedEventMapping::from(items);
264
265        for item in items.0 {
266            match item {
267                EventMapping::Ignore => (),
268                EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
269                EventMapping::Event(event) => {
270                    sentry_core::capture_event(event);
271                }
272                #[cfg(feature = "logs")]
273                EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
274                EventMapping::Combined(_) => {
275                    sentry_core::sentry_debug!(
276                        "[SentryLayer] found nested CombinedEventMapping, ignoring"
277                    )
278                }
279            }
280        }
281    }
282
283    /// When a new Span gets created, run the filter and start a new sentry span
284    /// if it passes, setting it as the *current* sentry span.
285    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
286        let span = match ctx.span(id) {
287            Some(span) => span,
288            None => return,
289        };
290
291        if !(self.span_filter)(span.metadata()) {
292            return;
293        }
294
295        let (description, data) = extract_span_data(attrs);
296        let op = span.name();
297
298        // Spans don't always have a description, this ensures our data is not empty,
299        // therefore the Sentry UI will be a lot more valuable for navigating spans.
300        let description = description.unwrap_or_else(|| {
301            let target = span.metadata().target();
302            if target.is_empty() {
303                op.to_string()
304            } else {
305                format!("{target}::{op}")
306            }
307        });
308
309        let hub = sentry_core::Hub::current();
310        let parent_sentry_span = hub.configure_scope(|scope| scope.get_span());
311
312        let sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span {
313            Some(parent) => parent.start_child(op, &description).into(),
314            None => {
315                let ctx = sentry_core::TransactionContext::new(&description, op);
316                sentry_core::start_transaction(ctx).into()
317            }
318        };
319        // Add the data from the original span to the sentry span.
320        // This comes from typically the `fields` in `tracing::instrument`.
321        record_fields(&sentry_span, data);
322
323        let mut extensions = span.extensions_mut();
324        extensions.insert(SentrySpanData {
325            sentry_span,
326            parent_sentry_span,
327            hub,
328            hub_switch_guard: None,
329        });
330    }
331
332    /// Sets entered span as *current* sentry span. A tracing span can be
333    /// entered and existed multiple times, for example, when using a `tracing::Instrumented` future.
334    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
335        let span = match ctx.span(id) {
336            Some(span) => span,
337            None => return,
338        };
339
340        let mut extensions = span.extensions_mut();
341        if let Some(data) = extensions.get_mut::<SentrySpanData>() {
342            data.hub_switch_guard = Some(sentry_core::HubSwitchGuard::new(data.hub.clone()));
343            data.hub.configure_scope(|scope| {
344                scope.set_span(Some(data.sentry_span.clone()));
345            })
346        }
347    }
348
349    /// Set exited span's parent as *current* sentry span.
350    fn on_exit(&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 mut extensions = span.extensions_mut();
357        if let Some(data) = extensions.get_mut::<SentrySpanData>() {
358            data.hub.configure_scope(|scope| {
359                scope.set_span(data.parent_sentry_span.clone());
360            });
361            data.hub_switch_guard.take();
362        }
363    }
364
365    /// When a span gets closed, finish the underlying sentry span, and set back
366    /// its parent as the *current* sentry span.
367    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
368        let span = match ctx.span(&id) {
369            Some(span) => span,
370            None => return,
371        };
372
373        let mut extensions = span.extensions_mut();
374        let SentrySpanData { sentry_span, .. } = match extensions.remove::<SentrySpanData>() {
375            Some(data) => data,
376            None => return,
377        };
378
379        sentry_span.finish();
380    }
381
382    /// Implement the writing of extra data to span
383    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
384        let span = match ctx.span(span) {
385            Some(s) => s,
386            _ => return,
387        };
388
389        let mut extensions = span.extensions_mut();
390        let span = match extensions.get_mut::<SentrySpanData>() {
391            Some(t) => &t.sentry_span,
392            _ => return,
393        };
394
395        let mut data = FieldVisitor::default();
396        values.record(&mut data);
397
398        record_fields(span, data.json_values);
399    }
400}
401
402/// Creates a default Sentry layer
403pub fn layer<S>() -> SentryLayer<S>
404where
405    S: Subscriber + for<'a> LookupSpan<'a>,
406{
407    Default::default()
408}
409
410/// Extracts the message and attributes from a span
411fn extract_span_data(attrs: &span::Attributes) -> (Option<String>, BTreeMap<&'static str, Value>) {
412    let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| {
413        let mut visitor = SpanFieldVisitor {
414            debug_buffer,
415            json_values: Default::default(),
416        };
417        attrs.record(&mut visitor);
418        visitor.json_values
419    });
420
421    // Find message of the span, if any
422    let message = json_values.remove("message").and_then(|v| match v {
423        Value::String(s) => Some(s),
424        _ => None,
425    });
426
427    (message, json_values)
428}
429
430thread_local! {
431    static VISITOR_BUFFER: RefCell<String> = const { RefCell::new(String::new()) };
432}
433
434/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer.
435struct SpanFieldVisitor<'s> {
436    debug_buffer: &'s mut String,
437    json_values: BTreeMap<&'static str, Value>,
438}
439
440impl SpanFieldVisitor<'_> {
441    fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
442        self.json_values.insert(field.name(), value.into());
443    }
444}
445
446impl Visit for SpanFieldVisitor<'_> {
447    fn record_i64(&mut self, field: &Field, value: i64) {
448        self.record(field, value);
449    }
450
451    fn record_u64(&mut self, field: &Field, value: u64) {
452        self.record(field, value);
453    }
454
455    fn record_bool(&mut self, field: &Field, value: bool) {
456        self.record(field, value);
457    }
458
459    fn record_str(&mut self, field: &Field, value: &str) {
460        self.record(field, value);
461    }
462
463    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
464        use std::fmt::Write;
465        self.debug_buffer.reserve(128);
466        write!(self.debug_buffer, "{value:?}").unwrap();
467        self.json_values
468            .insert(field.name(), self.debug_buffer.as_str().into());
469        self.debug_buffer.clear();
470    }
471}