sentry_tracing/
converters.rs

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