Skip to content

Add span / transaction collection to sentry-tracing #350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions sentry-core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,17 @@ impl Client {
random::<f32>() <= rate
}
}

/// Returns a random boolean with a probability defined
/// by the [`ClientOptions`]'s `traces_sample_rate`
pub fn sample_traces_should_send(&self) -> bool {
let rate = self.options.traces_sample_rate;
if rate >= 1.0 {
true
} else {
random::<f32>() <= rate
}
}
}

// Make this unwind safe. It's not out of the box because of the
Expand Down
4 changes: 4 additions & 0 deletions sentry-core/src/clientoptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub struct ClientOptions {
pub environment: Option<Cow<'static, str>>,
/// The sample rate for event submission. (0.0 - 1.0, defaults to 1.0)
pub sample_rate: f32,
/// The sample rate for tracing transactions. (0.0 - 1.0, defaults to 0.0)
pub traces_sample_rate: f32,
/// Maximum number of breadcrumbs. (defaults to 100)
pub max_breadcrumbs: usize,
/// Attaches stacktraces to messages.
Expand Down Expand Up @@ -179,6 +181,7 @@ impl fmt::Debug for ClientOptions {
.field("release", &self.release)
.field("environment", &self.environment)
.field("sample_rate", &self.sample_rate)
.field("traces_sample_rate", &self.traces_sample_rate)
.field("max_breadcrumbs", &self.max_breadcrumbs)
.field("attach_stacktrace", &self.attach_stacktrace)
.field("send_default_pii", &self.send_default_pii)
Expand Down Expand Up @@ -210,6 +213,7 @@ impl Default for ClientOptions {
release: None,
environment: None,
sample_rate: 1.0,
traces_sample_rate: 0.0,
max_breadcrumbs: 100,
attach_stacktrace: false,
send_default_pii: false,
Expand Down
3 changes: 2 additions & 1 deletion sentry-tracing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ edition = "2018"
[dependencies]
sentry-core = { version = "0.23.0", path = "../sentry-core" }
tracing-core = "0.1"
tracing-subscriber = "0.2"
tracing-subscriber = "0.2.19"

[dev-dependencies]
log = "0.4"
sentry = { version = "0.23.0", path = "../sentry", default-features = false, features = ["test"] }
tracing = "0.1"
tokio = { version = "1.8", features = ["rt-multi-thread", "macros", "time"] }
76 changes: 55 additions & 21 deletions sentry-tracing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

# Sentry Rust SDK: sentry-tracing

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

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

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

```rust
use std::time::Duration;

use tokio::time::sleep;
use tracing_subscriber::prelude::*;

tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(sentry_tracing::layer())
.try_init()
.unwrap();

let _sentry = sentry::init(());

tracing::info!("Generates a breadcrumb");
tracing::error!("Generates an event");
// Also works, since log events are ingested by the tracing system
log::info!("Generates a breadcrumb");
log::error!("Generates an event");
#[tokio::main]
async fn main() {
let _guard = sentry::init(sentry::ClientOptions {
// Set this a to lower value in production
traces_sample_rate: 1.0,
..sentry::ClientOptions::default()
});

tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(sentry_tracing::layer())
.init();

outer().await;
}

// Functions instrumented by tracing automatically report
// their span as transactions
#[tracing::instrument]
async fn outer() {
tracing::info!("Generates a breadcrumb");

for _ in 0..10 {
inner().await;
}

tracing::error!("Generates an event");
}

#[tracing::instrument]
async fn inner() {
// Also works, since log events are ingested by the tracing system
log::info!("Generates a breadcrumb");

sleep(Duration::from_millis(100)).await;

log::error!("Generates an event");
}
```

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

```rust
use sentry_tracing::EventFilter;
use tracing_subscriber::prelude::*;

let layer = sentry_tracing::layer().filter(|md| match md.level() {
let layer = sentry_tracing::layer().event_filter(|md| match md.level() {
&tracing::Level::ERROR => EventFilter::Event,
_ => EventFilter::Ignore,
});

tracing_subscriber::registry()
.with(layer)
.init();
```

## Resources
Expand All @@ -57,4 +92,3 @@ License: Apache-2.0

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

76 changes: 67 additions & 9 deletions sentry-tracing/src/converters.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use std::collections::BTreeMap;

use sentry_core::protocol::{Event, Value};
use sentry_core::protocol::{self, Event, TraceContext, Value};
use sentry_core::{Breadcrumb, Level};
use tracing_core::field::{Field, Visit};
use tracing_core::{
field::{Field, Visit},
span, Subscriber,
};
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;

use crate::Trace;

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

/// Extracts the message and metadata from an event
pub fn extract_data(event: &tracing_core::Event) -> (Option<String>, BTreeMap<String, Value>) {
pub fn extract_event_data(
event: &tracing_core::Event,
) -> (Option<String>, BTreeMap<String, Value>) {
// Find message of the event, if any
let mut data = BTreeMapRecorder::default();
event.record(&mut data);
Expand All @@ -28,6 +37,21 @@ pub fn extract_data(event: &tracing_core::Event) -> (Option<String>, BTreeMap<St
(message, data.0)
}

/// Extracts the message and metadata from a span
pub fn extract_span_data(attrs: &span::Attributes) -> (Option<String>, BTreeMap<String, Value>) {
let mut data = BTreeMapRecorder::default();
attrs.record(&mut data);

// Find message of the span, if any
let message = data
.0
.remove("message")
.map(|v| v.as_str().map(|s| s.to_owned()))
.flatten();

(message, data.0)
}

#[derive(Default)]
/// Records all fields of [`tracing_core::Event`] for easy access
struct BTreeMapRecorder(pub BTreeMap<String, Value>);
Expand Down Expand Up @@ -58,7 +82,7 @@ impl Visit for BTreeMapRecorder {

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

/// Creates an [`Event`] from a given [`tracing_core::Event`]
pub fn event_from_event(event: &tracing_core::Event) -> Event<'static> {
let (message, extra) = extract_data(event);
Event {
pub fn event_from_event<S>(event: &tracing_core::Event, ctx: Context<S>) -> Event<'static>
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let (message, extra) = extract_event_data(event);

let mut result = Event {
logger: Some(event.metadata().target().to_owned()),
level: convert_tracing_level(event.metadata().level()),
message,
extra,
..Default::default()
};

let parent = event
.parent()
.and_then(|id| ctx.span(id))
.or_else(|| ctx.lookup_current());

if let Some(parent) = parent {
let extensions = parent.extensions();
if let Some(trace) = extensions.get::<Trace>() {
let context = protocol::Context::from(TraceContext {
span_id: trace.span.span_id,
trace_id: trace.span.trace_id,
..TraceContext::default()
});

result.contexts.insert(String::from("trace"), context);

result.transaction = parent
.parent()
.into_iter()
.flat_map(|span| span.scope())
.last()
.map(|root| root.name().into());
}
}

result
}

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