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