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