Skip to content

Commit

Permalink
feat(tracing): Add tracing-subscriber integration (getsentry#329)
Browse files Browse the repository at this point in the history
This allows to record tracing events either as breadcrumbs or events,
the same way sentry-log does.
  • Loading branch information
Tuetuopay authored May 7, 2021
1 parent 0414b84 commit 598816f
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ members = [
"sentry-log",
"sentry-panic",
"sentry-slog",
"sentry-tracing",
"sentry-types",
]
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ This workspace contains various crates that provide support for logging events a

An integration for the `slog` crate.

- [sentry-tracing](./sentry-tracing)
[![crates.io](https://img.shields.io/crates/v/sentry-tracing.svg)](https://crates.io/crates/sentry-tracing)
[![docs.rs](https://docs.rs/sentry-tracing/badge.svg)](https://docs.rs/sentry-tracing)

An integration for the `tracing` crate.

- [sentry-types](./sentry-types)
[![crates.io](https://img.shields.io/crates/v/sentry-types.svg)](https://crates.io/crates/sentry-types)
[![docs.rs](https://docs.rs/sentry-types/badge.svg)](https://docs.rs/sentry-types)
Expand Down
22 changes: 22 additions & 0 deletions sentry-tracing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "sentry-tracing"
version = "0.22.0"
authors = ["Sentry <[email protected]>"]
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/getsentry/sentry-rust"
homepage = "https://sentry.io/welcome/"
description = """
Sentry integration for tracing and tracing-subscriber crates.
"""
edition = "2018"

[dependencies]
sentry-core = { version = "0.22.0", path = "../sentry-core" }
tracing-core = "0.1"
tracing-subscriber = "0.2"

[dev-dependencies]
log = "0.4"
sentry = { version = "0.22.0", path = "../sentry", default-features = false, features = ["test"] }
tracing = "0.1"
60 changes: 60 additions & 0 deletions sentry-tracing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<p align="center">
<a href="https://sentry.io" target="_blank" align="center">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
</a>
</p>

# Sentry Rust SDK: sentry-tracing

Adds support for automatic Breadcrumb and Event 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.

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
in the tracing system and generate events, thus be relayed to this crate. It
effectively replaces `sentry-log` when tracing is used.

## Examples

```rust
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");
```

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

```rust
use sentry_tracing::EventFilter;

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

## Resources

License: Apache-2.0

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

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

use sentry_core::protocol::{Event, Value};
use sentry_core::{Breadcrumb, Level};
use tracing_core::field::{Field, Visit};

/// Converts a [`tracing_core::Level`] to a Sentry [`Level`]
pub fn convert_tracing_level(level: &tracing_core::Level) -> Level {
match level {
&tracing_core::Level::TRACE | &tracing_core::Level::DEBUG => Level::Debug,
&tracing_core::Level::INFO => Level::Info,
&tracing_core::Level::WARN => Level::Warning,
&tracing_core::Level::ERROR => Level::Error,
}
}

/// Extracts the message and metadata from an event
pub fn extract_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);
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>);

impl BTreeMapRecorder {
fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
self.0.insert(field.name().to_owned(), value.into());
}
}

impl Visit for BTreeMapRecorder {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
self.record(field, format!("{:?}", value));
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.record(field, value);
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.record(field, value);
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.record(field, value);
}
fn record_str(&mut self, field: &Field, value: &str) {
self.record(field, value);
}
}

/// 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);
Breadcrumb {
category: Some(event.metadata().target().to_owned()),
ty: "log".into(),
level: convert_tracing_level(event.metadata().level()),
message,
data,
..Default::default()
}
}

/// 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 {
logger: Some(event.metadata().target().to_owned()),
level: convert_tracing_level(event.metadata().level()),
message,
extra,
..Default::default()
}
}

/// Creates an exception [`Event`] from a given [`tracing_core::Event`]
pub fn exception_from_event(event: &tracing_core::Event) -> Event<'static> {
// 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)
}
110 changes: 110 additions & 0 deletions sentry-tracing/src/layer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use sentry_core::protocol::Breadcrumb;
use tracing_core::{Event, Level, Metadata, Subscriber};
use tracing_subscriber::layer::{Context, Layer};

use crate::converters::*;

/// The action that Sentry should perform for a [`Metadata`]
#[derive(Debug, Clone, Copy)]
pub enum EventFilter {
/// Ignore the [`Event`]
Ignore,
/// Create a [`Breadcrumb`] from this [`Event`]
Breadcrumb,
/// Create a message [`sentry_core::protocol::Event`] from this [`Event`]
Event,
/// Create an exception [`sentry_core::protocol::Event`] from this [`Event`]
Exception,
}

/// The type of data Sentry should ingest for a [`Event`]
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum EventMapping {
/// Ignore the [`Event`]
Ignore,
/// Adds the [`Breadcrumb`] to the Sentry scope.
Breadcrumb(Breadcrumb),
/// Captures the [`sentry_core::protocol::Event`] to Sentry.
Event(sentry_core::protocol::Event<'static>),
}

/// The default event filter.
///
/// By default, an exception event is captured for `error`, a breadcrumb for
/// `warning` and `info`, and `debug` and `trace` logs are ignored.
pub fn default_filter(metadata: &Metadata) -> EventFilter {
match metadata.level() {
&Level::ERROR => EventFilter::Exception,
&Level::WARN | &Level::INFO => EventFilter::Breadcrumb,
&Level::DEBUG | &Level::TRACE => EventFilter::Ignore,
}
}

/// Provides a tracing layer that dispatches events to sentry
pub struct SentryLayer {
filter: Box<dyn Fn(&Metadata) -> EventFilter + Send + Sync>,
mapper: Option<Box<dyn Fn(&Event) -> EventMapping + Send + Sync>>,
}

impl SentryLayer {
/// Sets a custom filter function.
///
/// The filter classifies how sentry should handle [`Event`]s based
/// on their [`Metadata`].
pub fn filter<F>(mut self, filter: F) -> Self
where
F: Fn(&Metadata) -> EventFilter + Send + Sync + 'static,
{
self.filter = Box::new(filter);
self
}

/// Sets a custom mapper function.
///
/// The mapper is responsible for creating either breadcrumbs or events from
/// [`Event`]s.
pub fn mapper<F>(mut self, mapper: F) -> Self
where
F: Fn(&Event) -> EventMapping + Send + Sync + 'static,
{
self.mapper = Some(Box::new(mapper));
self
}
}

impl Default for SentryLayer {
fn default() -> Self {
Self {
filter: Box::new(default_filter),
mapper: None,
}
}
}

impl<S: Subscriber> Layer<S> for SentryLayer {
fn on_event(&self, event: &Event, _ctx: Context<'_, S>) {
let item = match &self.mapper {
Some(mapper) => mapper(event),
None => match (self.filter)(event.metadata()) {
EventFilter::Ignore => EventMapping::Ignore,
EventFilter::Breadcrumb => EventMapping::Breadcrumb(breadcrumb_from_event(event)),
EventFilter::Event => EventMapping::Event(event_from_event(event)),
EventFilter::Exception => EventMapping::Event(exception_from_event(event)),
},
};

match item {
EventMapping::Event(event) => {
sentry_core::capture_event(event);
}
EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
_ => (),
}
}
}

/// Creates a default Sentry layer
pub fn layer() -> SentryLayer {
Default::default()
}
54 changes: 54 additions & 0 deletions sentry-tracing/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Adds support for automatic Breadcrumb and Event 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.
//!
//! 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
//! in the tracing system and generate events, thus be relayed to this crate. It
//! effectively replaces `sentry-log` when tracing is used.
//!
//! ## Examples
//!
//! ```rust
//! 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");
//! ```
//!
//! Or one might also set an explicit filter, to customize how to treat log
//! records:
//!
//! ```rust
//! use sentry_tracing::EventFilter;
//!
//! let layer = sentry_tracing::layer().filter(|md| match md.level() {
//! &tracing::Level::ERROR => EventFilter::Event,
//! _ => EventFilter::Ignore,
//! });
//! ```
#![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")]
#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
#![warn(missing_docs)]

mod converters;
mod layer;

pub use converters::*;
pub use layer::*;
5 changes: 5 additions & 0 deletions sentry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ anyhow = ["sentry-anyhow"]
debug-images = ["sentry-debug-images"]
log = ["sentry-log"]
slog = ["sentry-slog"]
tracing = ["sentry-tracing"]
# other features
test = ["sentry-core/test"]
debug-logs = ["log_", "sentry-core/debug-logs"]
Expand All @@ -47,6 +48,7 @@ sentry-debug-images = { version = "0.22.0", path = "../sentry-debug-images", opt
sentry-log = { version = "0.22.0", path = "../sentry-log", optional = true }
sentry-panic = { version = "0.22.0", path = "../sentry-panic", optional = true }
sentry-slog = { version = "0.22.0", path = "../sentry-slog", optional = true }
sentry-tracing = { version = "0.22.0", path = "../sentry-tracing", optional = true }
log_ = { package = "log", version = "0.4.8", optional = true, features = ["std"] }
reqwest_ = { package = "reqwest", version = "0.11", optional = true, features = ["blocking", "json"], default-features = false }
curl_ = { package = "curl", version = "0.4.25", optional = true }
Expand All @@ -59,8 +61,11 @@ tokio = { version = "1.0", features = ["rt"] }
sentry-anyhow = { version = "0.22.0", path = "../sentry-anyhow" }
sentry-log = { version = "0.22.0", path = "../sentry-log" }
sentry-slog = { version = "0.22.0", path = "../sentry-slog" }
sentry-tracing = { version = "0.22.0", path = "../sentry-tracing" }
log_ = { package = "log", version = "0.4.8", features = ["std"] }
slog_ = { package = "slog", version = "2.5.2" }
tracing_ = { package = "tracing", version = "0.1" }
tracing-subscriber = { version = "0.2", features = ["fmt", "tracing-log"] }
actix-web = { version = "3", default-features = false }
tokio = { version = "1.0", features = ["macros"] }
pretty_env_logger = "0.4.0"
Expand Down
Loading

0 comments on commit 598816f

Please sign in to comment.