Skip to content

Commit 8586466

Browse files
authored
feat(tracing): Improve structure for tracing errors (#585)
1 parent 9ca275f commit 8586466

File tree

5 files changed

+99
-13
lines changed

5 files changed

+99
-13
lines changed

sentry-backtrace/src/trim.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const WELL_KNOWN_SYS_MODULES: &[&str] = &[
1616
// these are well-known library frames
1717
"anyhow::",
1818
"log::",
19+
"tokio::",
20+
"tracing_core::",
1921
];
2022

2123
const WELL_KNOWN_BORDER_FRAMES: &[&str] = &[
@@ -24,6 +26,7 @@ const WELL_KNOWN_BORDER_FRAMES: &[&str] = &[
2426
// well-known library frames
2527
"anyhow::",
2628
"<sentry_log::Logger as log::Log>::log",
29+
"tracing_core::",
2730
];
2831

2932
/// A helper function to trim a stacktrace.

sentry-tracing/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ Sentry integration for tracing and tracing-subscriber crates.
1212
edition = "2021"
1313
rust-version = "1.66"
1414

15+
[features]
16+
default = []
17+
backtrace = ["dep:sentry-backtrace"]
18+
1519
[dependencies]
1620
sentry-core = { version = "0.31.2", path = "../sentry-core", features = ["client"] }
1721
tracing-core = "0.1"
1822
tracing-subscriber = { version = "0.3.1", default-features = false, features = ["std"] }
23+
sentry-backtrace = { version = "0.31.2", path = "../sentry-backtrace", optional = true }
1924

2025
[dev-dependencies]
2126
log = "0.4"

sentry-tracing/src/converters.rs

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::BTreeMap;
22
use std::error::Error;
33

4-
use sentry_core::protocol::{Event, Exception, Value};
4+
use sentry_core::protocol::{Event, Exception, Mechanism, Thread, Value};
55
use sentry_core::{event_from_error, Breadcrumb, Level};
66
use tracing_core::field::{Field, Visit};
77
use tracing_core::{span, Subscriber};
@@ -18,6 +18,16 @@ fn convert_tracing_level(level: &tracing_core::Level) -> Level {
1818
}
1919
}
2020

21+
fn level_to_exception_type(level: &tracing_core::Level) -> &'static str {
22+
match *level {
23+
tracing_core::Level::TRACE => "tracing::trace!",
24+
tracing_core::Level::DEBUG => "tracing::debug!",
25+
tracing_core::Level::INFO => "tracing::info!",
26+
tracing_core::Level::WARN => "tracing::warn!",
27+
tracing_core::Level::ERROR => "tracing::error!",
28+
}
29+
}
30+
2131
/// Extracts the message and metadata from an event
2232
fn extract_event_data(event: &tracing_core::Event) -> (Option<String>, FieldVisitor) {
2333
// Find message of the event, if any
@@ -106,9 +116,32 @@ pub fn breadcrumb_from_event(event: &tracing_core::Event) -> Breadcrumb {
106116
}
107117
}
108118

119+
fn tags_from_event(fields: &mut BTreeMap<String, Value>) -> BTreeMap<String, String> {
120+
let mut tags = BTreeMap::new();
121+
122+
fields.retain(|key, value| {
123+
let Some(key) = key.strip_prefix("tags.") else { return true };
124+
let string = match value {
125+
Value::Bool(b) => b.to_string(),
126+
Value::Number(n) => n.to_string(),
127+
Value::String(s) => std::mem::take(s),
128+
// remove null entries since empty tags are not allowed
129+
Value::Null => return false,
130+
// keep entries that cannot be represented as simple string
131+
Value::Array(_) | Value::Object(_) => return true,
132+
};
133+
134+
tags.insert(key.to_owned(), string);
135+
136+
false
137+
});
138+
139+
tags
140+
}
141+
109142
fn contexts_from_event(
110143
event: &tracing_core::Event,
111-
event_tags: BTreeMap<String, Value>,
144+
fields: BTreeMap<String, Value>,
112145
) -> BTreeMap<String, sentry_core::protocol::Context> {
113146
let event_meta = event.metadata();
114147
let mut location_map = BTreeMap::new();
@@ -123,10 +156,10 @@ fn contexts_from_event(
123156
}
124157

125158
let mut context = BTreeMap::new();
126-
if !event_tags.is_empty() {
159+
if !fields.is_empty() {
127160
context.insert(
128-
"Rust Tracing Tags".to_string(),
129-
sentry_core::protocol::Context::Other(event_tags),
161+
"Rust Tracing Fields".to_string(),
162+
sentry_core::protocol::Context::Other(fields),
130163
);
131164
}
132165
if !location_map.is_empty() {
@@ -143,12 +176,13 @@ pub fn event_from_event<S>(event: &tracing_core::Event, _ctx: Context<S>) -> Eve
143176
where
144177
S: Subscriber + for<'a> LookupSpan<'a>,
145178
{
146-
let (message, visitor) = extract_event_data(event);
179+
let (message, mut visitor) = extract_event_data(event);
147180

148181
Event {
149182
logger: Some(event.metadata().target().to_owned()),
150183
level: convert_tracing_level(event.metadata().level()),
151184
message,
185+
tags: tags_from_event(&mut visitor.json_values),
152186
contexts: contexts_from_event(event, visitor.json_values),
153187
..Default::default()
154188
}
@@ -159,17 +193,61 @@ pub fn exception_from_event<S>(event: &tracing_core::Event, _ctx: Context<S>) ->
159193
where
160194
S: Subscriber + for<'a> LookupSpan<'a>,
161195
{
162-
// TODO: Exception records in Sentry need a valid type, value and full stack trace to support
196+
// Exception records in Sentry need a valid type, value and full stack trace to support
163197
// proper grouping and issue metadata generation. tracing_core::Record does not contain sufficient
164198
// information for this. However, it may contain a serialized error which we can parse to emit
165199
// an exception record.
166-
let (message, visitor) = extract_event_data(event);
200+
let (mut message, visitor) = extract_event_data(event);
201+
let FieldVisitor {
202+
mut exceptions,
203+
mut json_values,
204+
} = visitor;
205+
206+
// If there are both a message and an exception, then add the message as synthetic wrapper
207+
// around the exception to support proper grouping. If configured, also add the current stack
208+
// trace to this exception directly, since it points to the place where the exception is
209+
// captured.
210+
if !exceptions.is_empty() && message.is_some() {
211+
#[allow(unused_mut)]
212+
let mut thread = Thread::default();
213+
214+
#[cfg(feature = "backtrace")]
215+
if let Some(client) = sentry_core::Hub::current().client() {
216+
if client.options().attach_stacktrace {
217+
thread = sentry_backtrace::current_thread(true);
218+
}
219+
}
220+
221+
let exception = Exception {
222+
ty: level_to_exception_type(event.metadata().level()).to_owned(),
223+
value: message.take(),
224+
module: event.metadata().module_path().map(str::to_owned),
225+
stacktrace: thread.stacktrace,
226+
raw_stacktrace: thread.raw_stacktrace,
227+
thread_id: thread.id,
228+
mechanism: Some(Mechanism {
229+
synthetic: Some(true),
230+
..Mechanism::default()
231+
}),
232+
};
233+
234+
exceptions.push(exception);
235+
}
236+
237+
if let Some(exception) = exceptions.last_mut() {
238+
exception
239+
.mechanism
240+
.get_or_insert_with(Mechanism::default)
241+
.ty = "tracing".to_owned();
242+
}
243+
167244
Event {
168245
logger: Some(event.metadata().target().to_owned()),
169246
level: convert_tracing_level(event.metadata().level()),
170247
message,
171-
exception: visitor.exceptions.into(),
172-
contexts: contexts_from_event(event, visitor.json_values),
248+
exception: exceptions.into(),
249+
tags: tags_from_event(&mut json_values),
250+
contexts: contexts_from_event(event, json_values),
173251
..Default::default()
174252
}
175253
}

sentry/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ rustdoc-args = ["--cfg", "doc_cfg"]
2424
default = ["backtrace", "contexts", "debug-images", "panic", "transport"]
2525

2626
# default integrations
27-
backtrace = ["sentry-backtrace"]
27+
backtrace = ["sentry-backtrace", "sentry-tracing?/backtrace"]
2828
contexts = ["sentry-contexts"]
2929
panic = ["sentry-panic"]
3030
# other integrations

sentry/tests/test_tracing.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ fn test_tracing() {
4545
event.breadcrumbs[0].message,
4646
Some("Hello Tracing World!".into())
4747
);
48-
match event.contexts.get("Rust Tracing Tags").unwrap() {
48+
match event.contexts.get("Rust Tracing Fields").unwrap() {
4949
Context::Other(tags) => {
5050
let value = Value::String("tagvalue".to_string());
5151
assert_eq!(*tags.get("tagname").unwrap(), value);
@@ -84,7 +84,7 @@ fn test_tracing() {
8484
event.exception[0].value,
8585
Some("invalid digit found in string".into())
8686
);
87-
match event.contexts.get("Rust Tracing Tags").unwrap() {
87+
match event.contexts.get("Rust Tracing Fields").unwrap() {
8888
Context::Other(tags) => {
8989
let value = Value::String("tagvalue".to_string());
9090
assert_eq!(*tags.get("tagname").unwrap(), value);

0 commit comments

Comments
 (0)