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::SENTRY_NAME_FIELD;
16use crate::SENTRY_OP_FIELD;
17use crate::SENTRY_TRACE_FIELD;
18use crate::TAGS_PREFIX;
19
20bitflags! {
21    /// The action that Sentry should perform for a given [`Event`]
22    #[derive(Debug, Clone, Copy)]
23    pub struct EventFilter: u32 {
24        /// Ignore the [`Event`]
25        const Ignore = 0b000;
26        /// Create a [`Breadcrumb`] from this [`Event`]
27        const Breadcrumb = 0b001;
28        /// Create a [`sentry_core::protocol::Event`] from this [`Event`]
29        const Event = 0b010;
30        /// Create a [`sentry_core::protocol::Log`] from this [`Event`]
31        const Log = 0b100;
32    }
33}
34
35/// The type of data Sentry should ingest for an [`Event`].
36#[derive(Debug)]
37#[allow(clippy::large_enum_variant)]
38pub enum EventMapping {
39    /// Ignore the [`Event`]
40    Ignore,
41    /// Adds the [`Breadcrumb`] to the Sentry scope.
42    Breadcrumb(Breadcrumb),
43    /// Captures the [`sentry_core::protocol::Event`] to Sentry.
44    Event(sentry_core::protocol::Event<'static>),
45    /// Captures the [`sentry_core::protocol::Log`] to Sentry.
46    #[cfg(feature = "logs")]
47    Log(sentry_core::protocol::Log),
48    /// Captures multiple items to Sentry.
49    /// Nesting multiple `EventMapping::Combined` inside each other will cause the inner mappings to be ignored.
50    Combined(CombinedEventMapping),
51}
52
53/// A list of event mappings.
54#[derive(Debug)]
55pub struct CombinedEventMapping(Vec<EventMapping>);
56
57impl From<EventMapping> for CombinedEventMapping {
58    fn from(value: EventMapping) -> Self {
59        match value {
60            EventMapping::Combined(combined) => combined,
61            _ => CombinedEventMapping(vec![value]),
62        }
63    }
64}
65
66impl From<Vec<EventMapping>> for CombinedEventMapping {
67    fn from(value: Vec<EventMapping>) -> Self {
68        Self(value)
69    }
70}
71
72/// The default event filter.
73///
74/// By default, an exception event is captured for `error`, a breadcrumb for
75/// `warning` and `info`, and `debug` and `trace` logs are ignored.
76pub fn default_event_filter(metadata: &Metadata) -> EventFilter {
77    match metadata.level() {
78        #[cfg(feature = "logs")]
79        &Level::ERROR => EventFilter::Event | EventFilter::Log,
80        #[cfg(not(feature = "logs"))]
81        &Level::ERROR => EventFilter::Event,
82        #[cfg(feature = "logs")]
83        &Level::WARN | &Level::INFO => EventFilter::Breadcrumb | EventFilter::Log,
84        #[cfg(not(feature = "logs"))]
85        &Level::WARN | &Level::INFO => EventFilter::Breadcrumb,
86        &Level::DEBUG | &Level::TRACE => EventFilter::Ignore,
87    }
88}
89
90/// The default span filter.
91///
92/// By default, spans at the `error`, `warning`, and `info`
93/// levels are captured
94pub fn default_span_filter(metadata: &Metadata) -> bool {
95    matches!(
96        metadata.level(),
97        &Level::ERROR | &Level::WARN | &Level::INFO
98    )
99}
100
101type EventMapper<S> = Box<dyn Fn(&Event, Context<'_, S>) -> EventMapping + Send + Sync>;
102
103/// Provides a tracing layer that dispatches events to sentry
104pub struct SentryLayer<S> {
105    event_filter: Box<dyn Fn(&Metadata) -> EventFilter + Send + Sync>,
106    event_mapper: Option<EventMapper<S>>,
107
108    span_filter: Box<dyn Fn(&Metadata) -> bool + Send + Sync>,
109
110    with_span_attributes: bool,
111}
112
113impl<S> SentryLayer<S> {
114    /// Sets a custom event filter function.
115    ///
116    /// The filter classifies how sentry should handle [`Event`]s based
117    /// on their [`Metadata`].
118    #[must_use]
119    pub fn event_filter<F>(mut self, filter: F) -> Self
120    where
121        F: Fn(&Metadata) -> EventFilter + Send + Sync + 'static,
122    {
123        self.event_filter = Box::new(filter);
124        self
125    }
126
127    /// Sets a custom event mapper function.
128    ///
129    /// The mapper is responsible for creating either breadcrumbs or events from
130    /// [`Event`]s.
131    #[must_use]
132    pub fn event_mapper<F>(mut self, mapper: F) -> Self
133    where
134        F: Fn(&Event, Context<'_, S>) -> EventMapping + Send + Sync + 'static,
135    {
136        self.event_mapper = Some(Box::new(mapper));
137        self
138    }
139
140    /// Sets a custom span filter function.
141    ///
142    /// The filter classifies whether sentry should handle [`tracing::Span`]s based
143    /// on their [`Metadata`].
144    ///
145    /// [`tracing::Span`]: https://docs.rs/tracing/latest/tracing/struct.Span.html
146    #[must_use]
147    pub fn span_filter<F>(mut self, filter: F) -> Self
148    where
149        F: Fn(&Metadata) -> bool + Send + Sync + 'static,
150    {
151        self.span_filter = Box::new(filter);
152        self
153    }
154
155    /// Enable every parent span's attributes to be sent along with own event's attributes.
156    ///
157    /// Note that the root span is considered a [transaction][sentry_core::protocol::Transaction]
158    /// so its context will only be grabbed only if you set the transaction to be sampled.
159    /// The most straightforward way to do this is to set
160    /// the [traces_sample_rate][sentry_core::ClientOptions::traces_sample_rate] to `1.0`
161    /// while configuring your sentry client.
162    #[must_use]
163    pub fn enable_span_attributes(mut self) -> Self {
164        self.with_span_attributes = true;
165        self
166    }
167}
168
169impl<S> Default for SentryLayer<S>
170where
171    S: Subscriber + for<'a> LookupSpan<'a>,
172{
173    fn default() -> Self {
174        Self {
175            event_filter: Box::new(default_event_filter),
176            event_mapper: None,
177
178            span_filter: Box::new(default_span_filter),
179
180            with_span_attributes: false,
181        }
182    }
183}
184
185#[inline(always)]
186fn record_fields<'a, K: AsRef<str> + Into<Cow<'a, str>>>(
187    span: &TransactionOrSpan,
188    data: BTreeMap<K, Value>,
189) {
190    match span {
191        TransactionOrSpan::Span(span) => {
192            let mut span = span.data();
193            for (key, value) in data {
194                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
195                    match value {
196                        Value::Bool(value) => {
197                            span.set_tag(stripped_key.to_owned(), value.to_string())
198                        }
199                        Value::Number(value) => {
200                            span.set_tag(stripped_key.to_owned(), value.to_string())
201                        }
202                        Value::String(value) => span.set_tag(stripped_key.to_owned(), value),
203                        _ => span.set_data(key.into().into_owned(), value),
204                    }
205                } else {
206                    span.set_data(key.into().into_owned(), value);
207                }
208            }
209        }
210        TransactionOrSpan::Transaction(transaction) => {
211            let mut transaction = transaction.data();
212            for (key, value) in data {
213                if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) {
214                    match value {
215                        Value::Bool(value) => {
216                            transaction.set_tag(stripped_key.into(), value.to_string())
217                        }
218                        Value::Number(value) => {
219                            transaction.set_tag(stripped_key.into(), value.to_string())
220                        }
221                        Value::String(value) => transaction.set_tag(stripped_key.into(), value),
222                        _ => transaction.set_data(key.into(), value),
223                    }
224                } else {
225                    transaction.set_data(key.into(), value);
226                }
227            }
228        }
229    }
230}
231
232/// Data that is attached to the tracing Spans `extensions`, in order to
233/// `finish` the corresponding sentry span `on_close`, and re-set its parent as
234/// the *current* span.
235pub(super) struct SentrySpanData {
236    pub(super) sentry_span: TransactionOrSpan,
237    parent_sentry_span: Option<TransactionOrSpan>,
238    hub: Arc<sentry_core::Hub>,
239    hub_switch_guard: Option<sentry_core::HubSwitchGuard>,
240}
241
242impl<S> Layer<S> for SentryLayer<S>
243where
244    S: Subscriber + for<'a> LookupSpan<'a>,
245{
246    fn on_event(&self, event: &Event, ctx: Context<'_, S>) {
247        let items = match &self.event_mapper {
248            Some(mapper) => mapper(event, ctx),
249            None => {
250                let span_ctx = self.with_span_attributes.then_some(ctx);
251                let filter = (self.event_filter)(event.metadata());
252                let mut items = vec![];
253                if filter.contains(EventFilter::Breadcrumb) {
254                    items.push(EventMapping::Breadcrumb(breadcrumb_from_event(
255                        event,
256                        span_ctx.as_ref(),
257                    )));
258                }
259                if filter.contains(EventFilter::Event) {
260                    items.push(EventMapping::Event(event_from_event(
261                        event,
262                        span_ctx.as_ref(),
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 {
338            sentry_span,
339            parent_sentry_span,
340            hub,
341            hub_switch_guard: None,
342        });
343    }
344
345    /// Sets entered span as *current* sentry span. A tracing span can be
346    /// entered and existed multiple times, for example, when using a `tracing::Instrumented` future.
347    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
348        let span = match ctx.span(id) {
349            Some(span) => span,
350            None => return,
351        };
352
353        let mut extensions = span.extensions_mut();
354        if let Some(data) = extensions.get_mut::<SentrySpanData>() {
355            data.hub_switch_guard = Some(sentry_core::HubSwitchGuard::new(data.hub.clone()));
356            data.hub.configure_scope(|scope| {
357                scope.set_span(Some(data.sentry_span.clone()));
358            })
359        }
360    }
361
362    /// Set exited span's parent as *current* sentry span.
363    fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
364        let span = match ctx.span(id) {
365            Some(span) => span,
366            None => return,
367        };
368
369        let mut extensions = span.extensions_mut();
370        if let Some(data) = extensions.get_mut::<SentrySpanData>() {
371            data.hub.configure_scope(|scope| {
372                scope.set_span(data.parent_sentry_span.clone());
373            });
374            data.hub_switch_guard.take();
375        }
376    }
377
378    /// When a span gets closed, finish the underlying sentry span, and set back
379    /// its parent as the *current* sentry span.
380    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
381        let span = match ctx.span(&id) {
382            Some(span) => span,
383            None => return,
384        };
385
386        let mut extensions = span.extensions_mut();
387        let SentrySpanData { sentry_span, .. } = match extensions.remove::<SentrySpanData>() {
388            Some(data) => data,
389            None => return,
390        };
391
392        sentry_span.finish();
393    }
394
395    /// Implement the writing of extra data to span
396    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
397        let span = match ctx.span(span) {
398            Some(s) => s,
399            _ => return,
400        };
401
402        let mut extensions = span.extensions_mut();
403        let span = match extensions.get_mut::<SentrySpanData>() {
404            Some(t) => &t.sentry_span,
405            _ => return,
406        };
407
408        let mut data = FieldVisitor::default();
409        values.record(&mut data);
410
411        let sentry_name = data
412            .json_values
413            .remove(SENTRY_NAME_FIELD)
414            .and_then(|v| match v {
415                Value::String(s) => Some(s),
416                _ => None,
417            });
418
419        let sentry_op = data
420            .json_values
421            .remove(SENTRY_OP_FIELD)
422            .and_then(|v| match v {
423                Value::String(s) => Some(s),
424                _ => None,
425            });
426
427        // `sentry.trace` cannot be applied retroactively
428        data.json_values.remove(SENTRY_TRACE_FIELD);
429
430        if let Some(name) = sentry_name {
431            span.set_name(&name);
432        }
433        if let Some(op) = sentry_op {
434            span.set_op(&op);
435        }
436
437        record_fields(span, data.json_values);
438    }
439}
440
441fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) {
442    span.set_data("sentry.tracing.target", metadata.target().into());
443
444    if let Some(module) = metadata.module_path() {
445        span.set_data("code.module.name", module.into());
446    }
447
448    if let Some(file) = metadata.file() {
449        span.set_data("code.file.path", file.into());
450    }
451
452    if let Some(line) = metadata.line() {
453        span.set_data("code.line.number", line.into());
454    }
455}
456
457/// Creates a default Sentry layer
458pub fn layer<S>() -> SentryLayer<S>
459where
460    S: Subscriber + for<'a> LookupSpan<'a>,
461{
462    Default::default()
463}
464
465/// Extracts the attributes from a span,
466/// returning the values of SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD separately
467fn extract_span_data(
468    attrs: &span::Attributes,
469) -> (
470    BTreeMap<&'static str, Value>,
471    Option<String>,
472    Option<String>,
473    Option<String>,
474) {
475    let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| {
476        let mut visitor = SpanFieldVisitor {
477            debug_buffer,
478            json_values: Default::default(),
479        };
480        attrs.record(&mut visitor);
481        visitor.json_values
482    });
483
484    let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v {
485        Value::String(s) => Some(s),
486        _ => None,
487    });
488
489    let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v {
490        Value::String(s) => Some(s),
491        _ => None,
492    });
493
494    let sentry_trace = json_values
495        .remove(SENTRY_TRACE_FIELD)
496        .and_then(|v| match v {
497            Value::String(s) => Some(s),
498            _ => None,
499        });
500
501    (json_values, name, op, sentry_trace)
502}
503
504thread_local! {
505    static VISITOR_BUFFER: RefCell<String> = const { RefCell::new(String::new()) };
506}
507
508/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer.
509struct SpanFieldVisitor<'s> {
510    debug_buffer: &'s mut String,
511    json_values: BTreeMap<&'static str, Value>,
512}
513
514impl SpanFieldVisitor<'_> {
515    fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
516        self.json_values.insert(field.name(), value.into());
517    }
518}
519
520impl Visit for SpanFieldVisitor<'_> {
521    fn record_i64(&mut self, field: &Field, value: i64) {
522        self.record(field, value);
523    }
524
525    fn record_u64(&mut self, field: &Field, value: u64) {
526        self.record(field, value);
527    }
528
529    fn record_bool(&mut self, field: &Field, value: bool) {
530        self.record(field, value);
531    }
532
533    fn record_str(&mut self, field: &Field, value: &str) {
534        self.record(field, value);
535    }
536
537    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
538        use std::fmt::Write;
539        self.debug_buffer.reserve(128);
540        write!(self.debug_buffer, "{value:?}").unwrap();
541        self.json_values
542            .insert(field.name(), self.debug_buffer.as_str().into());
543        self.debug_buffer.clear();
544    }
545}