Skip to content

Commit 0071773

Browse files
authored
feat: Add span / transaction collection to sentry-tracing (#350)
1 parent 1bdb7ef commit 0071773

File tree

7 files changed

+578
-75
lines changed

7 files changed

+578
-75
lines changed

Diff for: sentry-core/src/client.rs

+11
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,17 @@ impl Client {
337337
random::<f32>() <= rate
338338
}
339339
}
340+
341+
/// Returns a random boolean with a probability defined
342+
/// by the [`ClientOptions`]'s `traces_sample_rate`
343+
pub fn sample_traces_should_send(&self) -> bool {
344+
let rate = self.options.traces_sample_rate;
345+
if rate >= 1.0 {
346+
true
347+
} else {
348+
random::<f32>() <= rate
349+
}
350+
}
340351
}
341352

342353
// Make this unwind safe. It's not out of the box because of the

Diff for: sentry-core/src/clientoptions.rs

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ pub struct ClientOptions {
7272
pub environment: Option<Cow<'static, str>>,
7373
/// The sample rate for event submission. (0.0 - 1.0, defaults to 1.0)
7474
pub sample_rate: f32,
75+
/// The sample rate for tracing transactions. (0.0 - 1.0, defaults to 0.0)
76+
pub traces_sample_rate: f32,
7577
/// Maximum number of breadcrumbs. (defaults to 100)
7678
pub max_breadcrumbs: usize,
7779
/// Attaches stacktraces to messages.
@@ -179,6 +181,7 @@ impl fmt::Debug for ClientOptions {
179181
.field("release", &self.release)
180182
.field("environment", &self.environment)
181183
.field("sample_rate", &self.sample_rate)
184+
.field("traces_sample_rate", &self.traces_sample_rate)
182185
.field("max_breadcrumbs", &self.max_breadcrumbs)
183186
.field("attach_stacktrace", &self.attach_stacktrace)
184187
.field("send_default_pii", &self.send_default_pii)
@@ -210,6 +213,7 @@ impl Default for ClientOptions {
210213
release: None,
211214
environment: None,
212215
sample_rate: 1.0,
216+
traces_sample_rate: 0.0,
213217
max_breadcrumbs: 100,
214218
attach_stacktrace: false,
215219
send_default_pii: false,

Diff for: sentry-tracing/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ edition = "2018"
1414
[dependencies]
1515
sentry-core = { version = "0.23.0", path = "../sentry-core" }
1616
tracing-core = "0.1"
17-
tracing-subscriber = "0.2"
17+
tracing-subscriber = "0.2.19"
1818

1919
[dev-dependencies]
2020
log = "0.4"
2121
sentry = { version = "0.23.0", path = "../sentry", default-features = false, features = ["test"] }
2222
tracing = "0.1"
23+
tokio = { version = "1.8", features = ["rt-multi-thread", "macros", "time"] }

Diff for: sentry-tracing/README.md

+55-21
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66

77
# Sentry Rust SDK: sentry-tracing
88

9-
Adds support for automatic Breadcrumb and Event capturing from tracing events,
10-
similar to the `sentry-log` crate.
9+
Adds support for automatic Breadcrumb, Event and Transaction capturing from
10+
tracing events, similar to the `sentry-log` crate.
1111

12-
The `tracing` crate is supported in two ways. First, events can be captured as
13-
breadcrumbs for later. Secondly, error events can be captured as events to
14-
Sentry. By default, anything above `Info` is recorded as breadcrumb and
15-
anything above `Error` is captured as error event.
12+
The `tracing` crate is supported in three ways. First, events can be captured
13+
as breadcrumbs for later. Secondly, error events can be captured as events
14+
to Sentry. Finally, spans can be recorded as structured transaction events.
15+
By default, events above `Info` are recorded as breadcrumbs, events above
16+
`Error` are captured as error events, and spans above `Info` are recorded
17+
as transactions.
1618

1719
By using this crate in combination with `tracing-subscriber` and its `log`
1820
integration, `sentry-log` does not need to be used, as logs will be ingested
@@ -22,33 +24,66 @@ effectively replaces `sentry-log` when tracing is used.
2224
## Examples
2325

2426
```rust
27+
use std::time::Duration;
28+
29+
use tokio::time::sleep;
2530
use tracing_subscriber::prelude::*;
2631

27-
tracing_subscriber::registry()
28-
.with(tracing_subscriber::fmt::layer())
29-
.with(sentry_tracing::layer())
30-
.try_init()
31-
.unwrap();
32-
33-
let _sentry = sentry::init(());
34-
35-
tracing::info!("Generates a breadcrumb");
36-
tracing::error!("Generates an event");
37-
// Also works, since log events are ingested by the tracing system
38-
log::info!("Generates a breadcrumb");
39-
log::error!("Generates an event");
32+
#[tokio::main]
33+
async fn main() {
34+
let _guard = sentry::init(sentry::ClientOptions {
35+
// Set this a to lower value in production
36+
traces_sample_rate: 1.0,
37+
..sentry::ClientOptions::default()
38+
});
39+
40+
tracing_subscriber::registry()
41+
.with(tracing_subscriber::fmt::layer())
42+
.with(sentry_tracing::layer())
43+
.init();
44+
45+
outer().await;
46+
}
47+
48+
// Functions instrumented by tracing automatically report
49+
// their span as transactions
50+
#[tracing::instrument]
51+
async fn outer() {
52+
tracing::info!("Generates a breadcrumb");
53+
54+
for _ in 0..10 {
55+
inner().await;
56+
}
57+
58+
tracing::error!("Generates an event");
59+
}
60+
61+
#[tracing::instrument]
62+
async fn inner() {
63+
// Also works, since log events are ingested by the tracing system
64+
log::info!("Generates a breadcrumb");
65+
66+
sleep(Duration::from_millis(100)).await;
67+
68+
log::error!("Generates an event");
69+
}
4070
```
4171

4272
Or one might also set an explicit filter, to customize how to treat log
4373
records:
4474

4575
```rust
4676
use sentry_tracing::EventFilter;
77+
use tracing_subscriber::prelude::*;
4778

48-
let layer = sentry_tracing::layer().filter(|md| match md.level() {
79+
let layer = sentry_tracing::layer().event_filter(|md| match md.level() {
4980
&tracing::Level::ERROR => EventFilter::Event,
5081
_ => EventFilter::Ignore,
5182
});
83+
84+
tracing_subscriber::registry()
85+
.with(layer)
86+
.init();
5287
```
5388

5489
## Resources
@@ -57,4 +92,3 @@ License: Apache-2.0
5792

5893
- [Discord](https://discord.gg/ez5KZN7) server for project discussions.
5994
- Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates
60-

Diff for: sentry-tracing/src/converters.rs

+67-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
use std::collections::BTreeMap;
22

3-
use sentry_core::protocol::{Event, Value};
3+
use sentry_core::protocol::{self, Event, TraceContext, Value};
44
use sentry_core::{Breadcrumb, Level};
5-
use tracing_core::field::{Field, Visit};
5+
use tracing_core::{
6+
field::{Field, Visit},
7+
span, Subscriber,
8+
};
9+
use tracing_subscriber::layer::Context;
10+
use tracing_subscriber::registry::LookupSpan;
11+
12+
use crate::Trace;
613

714
/// Converts a [`tracing_core::Level`] to a Sentry [`Level`]
815
pub fn convert_tracing_level(level: &tracing_core::Level) -> Level {
@@ -15,7 +22,9 @@ pub fn convert_tracing_level(level: &tracing_core::Level) -> Level {
1522
}
1623

1724
/// Extracts the message and metadata from an event
18-
pub fn extract_data(event: &tracing_core::Event) -> (Option<String>, BTreeMap<String, Value>) {
25+
pub fn extract_event_data(
26+
event: &tracing_core::Event,
27+
) -> (Option<String>, BTreeMap<String, Value>) {
1928
// Find message of the event, if any
2029
let mut data = BTreeMapRecorder::default();
2130
event.record(&mut data);
@@ -28,6 +37,21 @@ pub fn extract_data(event: &tracing_core::Event) -> (Option<String>, BTreeMap<St
2837
(message, data.0)
2938
}
3039

40+
/// Extracts the message and metadata from a span
41+
pub fn extract_span_data(attrs: &span::Attributes) -> (Option<String>, BTreeMap<String, Value>) {
42+
let mut data = BTreeMapRecorder::default();
43+
attrs.record(&mut data);
44+
45+
// Find message of the span, if any
46+
let message = data
47+
.0
48+
.remove("message")
49+
.map(|v| v.as_str().map(|s| s.to_owned()))
50+
.flatten();
51+
52+
(message, data.0)
53+
}
54+
3155
#[derive(Default)]
3256
/// Records all fields of [`tracing_core::Event`] for easy access
3357
struct BTreeMapRecorder(pub BTreeMap<String, Value>);
@@ -58,7 +82,7 @@ impl Visit for BTreeMapRecorder {
5882

5983
/// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`]
6084
pub fn breadcrumb_from_event(event: &tracing_core::Event) -> Breadcrumb {
61-
let (message, data) = extract_data(event);
85+
let (message, data) = extract_event_data(event);
6286
Breadcrumb {
6387
category: Some(event.metadata().target().to_owned()),
6488
ty: "log".into(),
@@ -70,22 +94,56 @@ pub fn breadcrumb_from_event(event: &tracing_core::Event) -> Breadcrumb {
7094
}
7195

7296
/// Creates an [`Event`] from a given [`tracing_core::Event`]
73-
pub fn event_from_event(event: &tracing_core::Event) -> Event<'static> {
74-
let (message, extra) = extract_data(event);
75-
Event {
97+
pub fn event_from_event<S>(event: &tracing_core::Event, ctx: Context<S>) -> Event<'static>
98+
where
99+
S: Subscriber + for<'a> LookupSpan<'a>,
100+
{
101+
let (message, extra) = extract_event_data(event);
102+
103+
let mut result = Event {
76104
logger: Some(event.metadata().target().to_owned()),
77105
level: convert_tracing_level(event.metadata().level()),
78106
message,
79107
extra,
80108
..Default::default()
109+
};
110+
111+
let parent = event
112+
.parent()
113+
.and_then(|id| ctx.span(id))
114+
.or_else(|| ctx.lookup_current());
115+
116+
if let Some(parent) = parent {
117+
let extensions = parent.extensions();
118+
if let Some(trace) = extensions.get::<Trace>() {
119+
let context = protocol::Context::from(TraceContext {
120+
span_id: trace.span.span_id,
121+
trace_id: trace.span.trace_id,
122+
..TraceContext::default()
123+
});
124+
125+
result.contexts.insert(String::from("trace"), context);
126+
127+
result.transaction = parent
128+
.parent()
129+
.into_iter()
130+
.flat_map(|span| span.scope())
131+
.last()
132+
.map(|root| root.name().into());
133+
}
81134
}
135+
136+
result
82137
}
83138

84139
/// Creates an exception [`Event`] from a given [`tracing_core::Event`]
85-
pub fn exception_from_event(event: &tracing_core::Event) -> Event<'static> {
140+
pub fn exception_from_event<S>(event: &tracing_core::Event, ctx: Context<S>) -> Event<'static>
141+
where
142+
S: Subscriber + for<'a> LookupSpan<'a>,
143+
{
86144
// TODO: Exception records in Sentry need a valid type, value and full stack trace to support
87145
// proper grouping and issue metadata generation. tracing_core::Record does not contain sufficient
88146
// information for this. However, it may contain a serialized error which we can parse to emit
89147
// an exception record.
90-
event_from_event(event)
148+
event_from_event(event, ctx)
91149
}

0 commit comments

Comments
 (0)