sentry_tracing/
layer.rs

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