Skip to main content

sentry_tracing/
converters.rs

1use std::collections::BTreeMap;
2use std::error::Error;
3
4use sentry_core::protocol::{Event, Exception, Mechanism, Value};
5#[cfg(feature = "logs")]
6use sentry_core::protocol::{Log, LogAttribute, LogLevel};
7use sentry_core::{event_from_error, Breadcrumb, Level, TransactionOrSpan};
8#[cfg(feature = "logs")]
9use std::time::SystemTime;
10use tracing_core::field::{Field, Visit};
11use tracing_core::Subscriber;
12use tracing_subscriber::layer::Context;
13use tracing_subscriber::registry::LookupSpan;
14
15use super::layer::SentrySpanData;
16use crate::TAGS_PREFIX;
17
18/// Converts a [`tracing_core::Level`] to a Sentry [`Level`], used for events and breadcrumbs.
19fn level_to_sentry_level(level: &tracing_core::Level) -> Level {
20    match *level {
21        tracing_core::Level::TRACE | tracing_core::Level::DEBUG => Level::Debug,
22        tracing_core::Level::INFO => Level::Info,
23        tracing_core::Level::WARN => Level::Warning,
24        tracing_core::Level::ERROR => Level::Error,
25    }
26}
27
28/// Converts a [`tracing_core::Level`] to a Sentry [`LogLevel`], used for logs.
29#[cfg(feature = "logs")]
30fn level_to_log_level(level: &tracing_core::Level) -> LogLevel {
31    match *level {
32        tracing_core::Level::TRACE => LogLevel::Trace,
33        tracing_core::Level::DEBUG => LogLevel::Debug,
34        tracing_core::Level::INFO => LogLevel::Info,
35        tracing_core::Level::WARN => LogLevel::Warn,
36        tracing_core::Level::ERROR => LogLevel::Error,
37    }
38}
39
40/// Converts a [`tracing_core::Level`] to the corresponding Sentry [`Exception::ty`] entry.
41#[cfg(feature = "backtrace")]
42fn level_to_exception_type(level: &tracing_core::Level) -> &'static str {
43    match *level {
44        tracing_core::Level::TRACE => "tracing::trace!",
45        tracing_core::Level::DEBUG => "tracing::debug!",
46        tracing_core::Level::INFO => "tracing::info!",
47        tracing_core::Level::WARN => "tracing::warn!",
48        tracing_core::Level::ERROR => "tracing::error!",
49    }
50}
51
52/// Extracts the message and metadata from an event.
53fn extract_event_data(
54    event: &tracing_core::Event,
55    store_errors_in_values: bool,
56) -> (Option<String>, FieldVisitor) {
57    // Find message of the event, if any
58    let mut visitor = FieldVisitor {
59        store_errors_in_values,
60        ..Default::default()
61    };
62    event.record(&mut visitor);
63    let message = visitor
64        .json_values
65        .remove("message")
66        // When #[instrument(err)] is used the event does not have a message attached to it.
67        // the error message is attached to the field "error".
68        .or_else(|| visitor.json_values.remove("error"))
69        .and_then(|v| match v {
70            Value::String(s) => Some(s),
71            _ => None,
72        });
73
74    (message, visitor)
75}
76
77/// Extracts the message and metadata from an event, including the data in the current span.
78fn extract_event_data_with_context<S>(
79    event: &tracing_core::Event,
80    ctx: Option<&Context<S>>,
81    store_errors_in_values: bool,
82) -> (Option<String>, FieldVisitor)
83where
84    S: Subscriber + for<'a> LookupSpan<'a>,
85{
86    let (message, mut visitor) = extract_event_data(event, store_errors_in_values);
87
88    // Add the context fields of every parent span.
89    let current_span = ctx.as_ref().and_then(|ctx| {
90        event
91            .parent()
92            .and_then(|id| ctx.span(id))
93            .or_else(|| ctx.lookup_current())
94    });
95    if let Some(span) = current_span {
96        for span in span.scope() {
97            let name = span.name();
98            let ext = span.extensions();
99
100            if let Some(span_data) = ext.get::<SentrySpanData>() {
101                match &span_data.sentry_span {
102                    TransactionOrSpan::Span(span) => {
103                        for (key, value) in span.data().iter() {
104                            if is_sentry_span_attribute(key) {
105                                continue;
106                            }
107                            if key != "message" {
108                                let key = format!("{name}:{key}");
109                                visitor.json_values.insert(key, value.clone());
110                            }
111                        }
112                    }
113                    TransactionOrSpan::Transaction(transaction) => {
114                        for (key, value) in transaction.data().iter() {
115                            if is_sentry_span_attribute(key) {
116                                continue;
117                            }
118                            if key != "message" {
119                                let key = format!("{name}:{key}");
120                                visitor.json_values.insert(key, value.clone());
121                            }
122                        }
123                    }
124                }
125            }
126        }
127    }
128
129    (message, visitor)
130}
131
132/// Checks whether the given attribute name is one of those set on a span by the Sentry layer.
133/// In that case, we want to skip materializing it when propagating attributes, as it would mostly create noise.
134fn is_sentry_span_attribute(name: &str) -> bool {
135    matches!(
136        name,
137        "sentry.tracing.target" | "code.module.name" | "code.file.path" | "code.line.number"
138    )
139}
140
141/// Records the fields of a [`tracing_core::Event`].
142#[derive(Default)]
143pub(crate) struct FieldVisitor {
144    pub(crate) json_values: BTreeMap<String, Value>,
145    pub(crate) exceptions: Vec<Exception>,
146    /// If `true`, stringify and store errors in `self.json_values` under the original field name
147    /// else (default), convert to `Exception`s and store in `self.exceptions`.
148    store_errors_in_values: bool,
149}
150
151impl FieldVisitor {
152    fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
153        self.json_values
154            .insert(field.name().to_owned(), value.into());
155    }
156}
157
158impl Visit for FieldVisitor {
159    fn record_i64(&mut self, field: &Field, value: i64) {
160        self.record(field, value);
161    }
162
163    fn record_u64(&mut self, field: &Field, value: u64) {
164        self.record(field, value);
165    }
166
167    fn record_bool(&mut self, field: &Field, value: bool) {
168        self.record(field, value);
169    }
170
171    fn record_f64(&mut self, field: &Field, value: f64) {
172        self.record(field, value);
173    }
174
175    fn record_str(&mut self, field: &Field, value: &str) {
176        self.record(field, value);
177    }
178
179    fn record_error(&mut self, field: &Field, value: &(dyn Error + 'static)) {
180        let event = event_from_error(value);
181        if self.store_errors_in_values {
182            let error_chain = event
183                .exception
184                .iter()
185                .rev()
186                .filter_map(|x| x.value.as_ref().map(|v| format!("{}: {}", x.ty, *v)))
187                .collect::<Vec<String>>();
188            self.record(field, error_chain);
189        } else {
190            for exception in event.exception {
191                self.exceptions.push(exception);
192            }
193        }
194    }
195
196    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
197        self.record(field, format!("{value:?}"));
198    }
199}
200
201pub(crate) fn breadcrumb_from_event_impl<'context, S, C>(
202    event: &tracing_core::Event,
203    ctx: C,
204) -> Breadcrumb
205where
206    S: Subscriber + for<'a> LookupSpan<'a>,
207    C: Into<Option<&'context Context<'context, S>>>,
208{
209    let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true);
210
211    Breadcrumb {
212        category: Some(event.metadata().target().to_owned()),
213        ty: "log".into(),
214        level: level_to_sentry_level(event.metadata().level()),
215        message,
216        data: visitor.json_values,
217        ..Default::default()
218    }
219}
220
221// TODO: When removing this function, rename `breadcrumb_from_event_impl` to `breadcrumb_from_event`.
222/// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`].
223#[deprecated(
224    note = "Only useful for custom `SentryLayer::event_mapper` callbacks; that API is deprecated"
225)]
226#[expect(
227    clippy::impl_trait_in_params,
228    reason = "preserving the public helper signature"
229)]
230pub fn breadcrumb_from_event<'context, S>(
231    event: &tracing_core::Event,
232    ctx: impl Into<Option<&'context Context<'context, S>>>,
233) -> Breadcrumb
234where
235    S: Subscriber + for<'a> LookupSpan<'a>,
236{
237    breadcrumb_from_event_impl(event, ctx)
238}
239
240/// Convert `tracing` fields to the corresponding Sentry tags, removing them from `fields`.
241fn extract_and_remove_tags(fields: &mut BTreeMap<String, Value>) -> BTreeMap<String, String> {
242    let mut tags = BTreeMap::new();
243
244    fields.retain(|key, value| {
245        let Some(key) = key.strip_prefix(TAGS_PREFIX) else {
246            return true;
247        };
248        let string = match value {
249            Value::Bool(b) => b.to_string(),
250            Value::Number(n) => n.to_string(),
251            Value::String(s) => std::mem::take(s),
252            // remove null entries since empty tags are not allowed
253            Value::Null => return false,
254            // keep entries that cannot be represented as simple string
255            Value::Array(_) | Value::Object(_) => return true,
256        };
257
258        tags.insert(key.to_owned(), string);
259
260        false
261    });
262
263    tags
264}
265
266/// Create Sentry Contexts out of the `tracing` event and fields.
267fn contexts_from_event(
268    event: &tracing_core::Event,
269    fields: BTreeMap<String, Value>,
270) -> BTreeMap<String, sentry_core::protocol::Context> {
271    let event_meta = event.metadata();
272    let mut location_map = BTreeMap::new();
273    if let Some(module_path) = event_meta.module_path() {
274        location_map.insert("module_path".to_string(), module_path.into());
275    }
276    if let Some(file) = event_meta.file() {
277        location_map.insert("file".to_string(), file.into());
278    }
279    if let Some(line) = event_meta.line() {
280        location_map.insert("line".to_string(), line.into());
281    }
282
283    let mut context = BTreeMap::new();
284    if !fields.is_empty() {
285        context.insert(
286            "Rust Tracing Fields".to_string(),
287            sentry_core::protocol::Context::Other(fields),
288        );
289    }
290    if !location_map.is_empty() {
291        context.insert(
292            "Rust Tracing Location".to_string(),
293            sentry_core::protocol::Context::Other(location_map),
294        );
295    }
296    context
297}
298
299pub(crate) fn event_from_event_impl<'context, S, C>(
300    event: &tracing_core::Event,
301    ctx: C,
302) -> Event<'static>
303where
304    S: Subscriber + for<'a> LookupSpan<'a>,
305    C: Into<Option<&'context Context<'context, S>>>,
306{
307    // Exception records in Sentry need a valid type, value and full stack trace to support
308    // proper grouping and issue metadata generation. tracing_core::Record does not contain sufficient
309    // information for this. However, it may contain a serialized error which we can parse to emit
310    // an exception record.
311    let (message, visitor) = extract_event_data_with_context(event, ctx.into(), false);
312    let FieldVisitor {
313        mut exceptions,
314        mut json_values,
315        store_errors_in_values: _,
316    } = visitor;
317
318    // If there are a message, an exception, and we are capturing stack traces, then add the message
319    // as synthetic wrapper around the exception to support proper grouping. The stack trace to
320    // attach is the current one, since it points to the place where the exception is captured.
321    // We should only do this if we're capturing stack traces, otherwise the issue title will be `<unknown>`
322    // as Sentry will attempt to use missing stack trace to determine the title.
323    #[cfg(feature = "backtrace")]
324    let message = {
325        let mut message = message;
326        if !exceptions.is_empty() && message.is_some() {
327            if let Some(client) = sentry_core::Hub::current().client() {
328                if client.options().attach_stacktrace {
329                    let thread = sentry_backtrace::current_thread(true);
330                    let exception = Exception {
331                        ty: level_to_exception_type(event.metadata().level()).to_owned(),
332                        value: message.take(),
333                        module: event.metadata().module_path().map(str::to_owned),
334                        stacktrace: thread.stacktrace,
335                        raw_stacktrace: thread.raw_stacktrace,
336                        thread_id: thread.id,
337                        mechanism: Some(Mechanism {
338                            synthetic: Some(true),
339                            ..Mechanism::default()
340                        }),
341                    };
342                    exceptions.push(exception)
343                }
344            }
345        }
346        message
347    };
348
349    if let Some(exception) = exceptions.last_mut() {
350        "tracing".clone_into(
351            &mut exception
352                .mechanism
353                .get_or_insert_with(Mechanism::default)
354                .ty,
355        );
356    }
357
358    Event {
359        logger: Some(event.metadata().target().to_owned()),
360        level: level_to_sentry_level(event.metadata().level()),
361        message,
362        exception: exceptions.into(),
363        tags: extract_and_remove_tags(&mut json_values),
364        contexts: contexts_from_event(event, json_values),
365        ..Default::default()
366    }
367}
368
369// TODO: When removing this function, rename `event_from_event_impl` to `event_from_event`.
370/// Creates an [`Event`] (possibly carrying exceptions) from a given [`tracing_core::Event`].
371#[deprecated(
372    note = "Only useful for custom `SentryLayer::event_mapper` callbacks; that API is deprecated"
373)]
374#[expect(
375    clippy::impl_trait_in_params,
376    reason = "preserving the public helper signature"
377)]
378pub fn event_from_event<'context, S>(
379    event: &tracing_core::Event,
380    ctx: impl Into<Option<&'context Context<'context, S>>>,
381) -> Event<'static>
382where
383    S: Subscriber + for<'a> LookupSpan<'a>,
384{
385    event_from_event_impl(event, ctx)
386}
387
388// TODO: When removing this function, rename `log_from_event_impl` to `log_from_event`.
389/// Creates a [`Log`] from a given [`tracing_core::Event`].
390///
391/// If a context is provided, include available parent-span data in the returned log's attributes.
392/// See [`crate::LogLayer::enable_span_attributes`] for span-capture and filtering requirements.
393#[cfg(feature = "logs")]
394#[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))]
395#[deprecated(
396    note = "Only useful for custom `SentryLayer::event_mapper` callbacks; that API is deprecated"
397)]
398#[expect(
399    clippy::impl_trait_in_params,
400    reason = "preserving the public helper signature"
401)]
402pub fn log_from_event<'context, S>(
403    event: &tracing_core::Event,
404    ctx: impl Into<Option<&'context Context<'context, S>>>,
405) -> Log
406where
407    S: Subscriber + for<'a> LookupSpan<'a>,
408{
409    log_from_event_impl(event, ctx)
410}
411
412#[cfg(feature = "logs")]
413pub(crate) fn log_from_event_impl<'context, S, C>(event: &tracing_core::Event, ctx: C) -> Log
414where
415    S: Subscriber + for<'a> LookupSpan<'a>,
416    C: Into<Option<&'context Context<'context, S>>>,
417{
418    let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true);
419
420    let mut attributes: BTreeMap<String, LogAttribute> = visitor
421        .json_values
422        .into_iter()
423        .map(|(key, val)| (key, val.into()))
424        .collect();
425
426    let event_meta = event.metadata();
427    if let Some(module_path) = event_meta.module_path() {
428        attributes.insert("code.module.name".to_owned(), module_path.into());
429    }
430    if let Some(file) = event_meta.file() {
431        attributes.insert("code.file.path".to_owned(), file.into());
432    }
433    if let Some(line) = event_meta.line() {
434        attributes.insert("code.line.number".to_owned(), line.into());
435    }
436
437    attributes.insert("sentry.origin".to_owned(), "auto.tracing".into());
438
439    Log {
440        level: level_to_log_level(event.metadata().level()),
441        body: message.unwrap_or_default(),
442        trace_id: None,
443        timestamp: SystemTime::now(),
444        severity_number: None,
445        attributes,
446    }
447}