sentry_tracing/
converters.rs

1use std::collections::BTreeMap;
2use std::error::Error;
3
4use sentry_core::protocol::{Event, Exception, Mechanism, Value};
5use sentry_core::{event_from_error, Breadcrumb, Level, TransactionOrSpan};
6use tracing_core::field::{Field, Visit};
7use tracing_core::Subscriber;
8use tracing_subscriber::layer::Context;
9use tracing_subscriber::registry::LookupSpan;
10
11use super::layer::SentrySpanData;
12use crate::TAGS_PREFIX;
13
14/// Converts a [`tracing_core::Level`] to a Sentry [`Level`]
15fn convert_tracing_level(level: &tracing_core::Level) -> Level {
16    match level {
17        &tracing_core::Level::TRACE | &tracing_core::Level::DEBUG => Level::Debug,
18        &tracing_core::Level::INFO => Level::Info,
19        &tracing_core::Level::WARN => Level::Warning,
20        &tracing_core::Level::ERROR => Level::Error,
21    }
22}
23
24#[allow(unused)]
25fn level_to_exception_type(level: &tracing_core::Level) -> &'static str {
26    match *level {
27        tracing_core::Level::TRACE => "tracing::trace!",
28        tracing_core::Level::DEBUG => "tracing::debug!",
29        tracing_core::Level::INFO => "tracing::info!",
30        tracing_core::Level::WARN => "tracing::warn!",
31        tracing_core::Level::ERROR => "tracing::error!",
32    }
33}
34
35/// Extracts the message and metadata from an event
36/// and also optionally from its spans chain.
37fn extract_event_data(event: &tracing_core::Event) -> (Option<String>, FieldVisitor) {
38    // Find message of the event, if any
39    let mut visitor = FieldVisitor::default();
40    event.record(&mut visitor);
41    let message = visitor
42        .json_values
43        .remove("message")
44        // When #[instrument(err)] is used the event does not have a message attached to it.
45        // the error message is attached to the field "error".
46        .or_else(|| visitor.json_values.remove("error"))
47        .and_then(|v| match v {
48            Value::String(s) => Some(s),
49            _ => None,
50        });
51
52    (message, visitor)
53}
54
55fn extract_event_data_with_context<S>(
56    event: &tracing_core::Event,
57    ctx: Option<Context<S>>,
58) -> (Option<String>, FieldVisitor)
59where
60    S: Subscriber + for<'a> LookupSpan<'a>,
61{
62    let (message, mut visitor) = extract_event_data(event);
63
64    // Add the context fields of every parent span.
65    let current_span = ctx.as_ref().and_then(|ctx| {
66        event
67            .parent()
68            .and_then(|id| ctx.span(id))
69            .or_else(|| ctx.lookup_current())
70    });
71    if let Some(span) = current_span {
72        for span in span.scope() {
73            let name = span.name();
74            let ext = span.extensions();
75            if let Some(span_data) = ext.get::<SentrySpanData>() {
76                match &span_data.sentry_span {
77                    TransactionOrSpan::Span(span) => {
78                        for (key, value) in span.data().iter() {
79                            if key != "message" {
80                                let key = format!("{}:{}", name, key);
81                                visitor.json_values.insert(key, value.clone());
82                            }
83                        }
84                    }
85                    TransactionOrSpan::Transaction(transaction) => {
86                        for (key, value) in transaction.data().iter() {
87                            if key != "message" {
88                                let key = format!("{}:{}", name, key);
89                                visitor.json_values.insert(key, value.clone());
90                            }
91                        }
92                    }
93                }
94            }
95        }
96    }
97
98    (message, visitor)
99}
100
101/// Records all fields of [`tracing_core::Event`] for easy access
102#[derive(Default)]
103pub(crate) struct FieldVisitor {
104    pub json_values: BTreeMap<String, Value>,
105    pub exceptions: Vec<Exception>,
106}
107
108impl FieldVisitor {
109    fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
110        self.json_values
111            .insert(field.name().to_owned(), value.into());
112    }
113}
114
115impl Visit for FieldVisitor {
116    fn record_i64(&mut self, field: &Field, value: i64) {
117        self.record(field, value);
118    }
119
120    fn record_u64(&mut self, field: &Field, value: u64) {
121        self.record(field, value);
122    }
123
124    fn record_bool(&mut self, field: &Field, value: bool) {
125        self.record(field, value);
126    }
127
128    fn record_str(&mut self, field: &Field, value: &str) {
129        self.record(field, value);
130    }
131
132    fn record_error(&mut self, _field: &Field, value: &(dyn Error + 'static)) {
133        let event = event_from_error(value);
134        for exception in event.exception {
135            self.exceptions.push(exception);
136        }
137    }
138
139    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
140        self.record(field, format!("{value:?}"));
141    }
142}
143
144/// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`]
145pub fn breadcrumb_from_event<'context, S>(
146    event: &tracing_core::Event,
147    ctx: impl Into<Option<Context<'context, S>>>,
148) -> Breadcrumb
149where
150    S: Subscriber + for<'a> LookupSpan<'a>,
151{
152    let (message, visitor) = extract_event_data_with_context(event, ctx.into());
153    Breadcrumb {
154        category: Some(event.metadata().target().to_owned()),
155        ty: "log".into(),
156        level: convert_tracing_level(event.metadata().level()),
157        message,
158        data: visitor.json_values,
159        ..Default::default()
160    }
161}
162
163fn tags_from_event(fields: &mut BTreeMap<String, Value>) -> BTreeMap<String, String> {
164    let mut tags = BTreeMap::new();
165
166    fields.retain(|key, value| {
167        let Some(key) = key.strip_prefix(TAGS_PREFIX) else {
168            return true;
169        };
170        let string = match value {
171            Value::Bool(b) => b.to_string(),
172            Value::Number(n) => n.to_string(),
173            Value::String(s) => std::mem::take(s),
174            // remove null entries since empty tags are not allowed
175            Value::Null => return false,
176            // keep entries that cannot be represented as simple string
177            Value::Array(_) | Value::Object(_) => return true,
178        };
179
180        tags.insert(key.to_owned(), string);
181
182        false
183    });
184
185    tags
186}
187
188fn contexts_from_event(
189    event: &tracing_core::Event,
190    fields: BTreeMap<String, Value>,
191) -> BTreeMap<String, sentry_core::protocol::Context> {
192    let event_meta = event.metadata();
193    let mut location_map = BTreeMap::new();
194    if let Some(module_path) = event_meta.module_path() {
195        location_map.insert("module_path".to_string(), module_path.into());
196    }
197    if let Some(file) = event_meta.file() {
198        location_map.insert("file".to_string(), file.into());
199    }
200    if let Some(line) = event_meta.line() {
201        location_map.insert("line".to_string(), line.into());
202    }
203
204    let mut context = BTreeMap::new();
205    if !fields.is_empty() {
206        context.insert(
207            "Rust Tracing Fields".to_string(),
208            sentry_core::protocol::Context::Other(fields),
209        );
210    }
211    if !location_map.is_empty() {
212        context.insert(
213            "Rust Tracing Location".to_string(),
214            sentry_core::protocol::Context::Other(location_map),
215        );
216    }
217    context
218}
219
220/// Creates an [`Event`] from a given [`tracing_core::Event`]
221pub fn event_from_event<'context, S>(
222    event: &tracing_core::Event,
223    ctx: impl Into<Option<Context<'context, S>>>,
224) -> Event<'static>
225where
226    S: Subscriber + for<'a> LookupSpan<'a>,
227{
228    let (message, mut visitor) = extract_event_data_with_context(event, ctx.into());
229
230    Event {
231        logger: Some(event.metadata().target().to_owned()),
232        level: convert_tracing_level(event.metadata().level()),
233        message,
234        tags: tags_from_event(&mut visitor.json_values),
235        contexts: contexts_from_event(event, visitor.json_values),
236        ..Default::default()
237    }
238}
239
240/// Creates an exception [`Event`] from a given [`tracing_core::Event`]
241pub fn exception_from_event<'context, S>(
242    event: &tracing_core::Event,
243    ctx: impl Into<Option<Context<'context, S>>>,
244) -> Event<'static>
245where
246    S: Subscriber + for<'a> LookupSpan<'a>,
247{
248    // Exception records in Sentry need a valid type, value and full stack trace to support
249    // proper grouping and issue metadata generation. tracing_core::Record does not contain sufficient
250    // information for this. However, it may contain a serialized error which we can parse to emit
251    // an exception record.
252    #[allow(unused_mut)]
253    let (mut message, visitor) = extract_event_data_with_context(event, ctx.into());
254    let FieldVisitor {
255        mut exceptions,
256        mut json_values,
257    } = visitor;
258
259    // If there are a message, an exception, and we are capturing stack traces, then add the message
260    // as synthetic wrapper around the exception to support proper grouping. The stack trace to
261    // attach is the current one, since it points to the place where the exception is captured.
262    // We should only do this if we're capturing stack traces, otherwise the issue title will be `<unknown>`
263    // as Sentry will attempt to use missing stack trace to determine the title.
264    #[cfg(feature = "backtrace")]
265    if !exceptions.is_empty() && message.is_some() {
266        if let Some(client) = sentry_core::Hub::current().client() {
267            if client.options().attach_stacktrace {
268                let thread = sentry_backtrace::current_thread(true);
269                let exception = Exception {
270                    ty: level_to_exception_type(event.metadata().level()).to_owned(),
271                    value: message.take(),
272                    module: event.metadata().module_path().map(str::to_owned),
273                    stacktrace: thread.stacktrace,
274                    raw_stacktrace: thread.raw_stacktrace,
275                    thread_id: thread.id,
276                    mechanism: Some(Mechanism {
277                        synthetic: Some(true),
278                        ..Mechanism::default()
279                    }),
280                };
281                exceptions.push(exception)
282            }
283        }
284    }
285
286    if let Some(exception) = exceptions.last_mut() {
287        "tracing".clone_into(
288            &mut exception
289                .mechanism
290                .get_or_insert_with(Mechanism::default)
291                .ty,
292        );
293    }
294
295    Event {
296        logger: Some(event.metadata().target().to_owned()),
297        level: convert_tracing_level(event.metadata().level()),
298        message,
299        exception: exceptions.into(),
300        tags: tags_from_event(&mut json_values),
301        contexts: contexts_from_event(event, json_values),
302        ..Default::default()
303    }
304}