diff --git a/CHANGELOG.md b/CHANGELOG.md index ac98b5cc..40fde267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Breaking changes + +- feat(tracing): support combined EventFilters and EventMappings (#847) by @lcian + - `EventFilter` has been changed to a `bitflags` struct. + - It's now possible to map a `tracing` event to multiple items in Sentry by combining multiple event filters in the `event_filter`, e.g. `tracing::Level::ERROR => EventFilter::Event | EventFilter::Log`. + - It's also possible to use `EventMapping::Combined` to map a `tracing` event to multiple items in Sentry. + - `ctx` in the signatures of `event_from_event`, `breadcrumb_from_event` and `log_from_event` has been changed to take `impl Into>>` to avoid cloning the `Context` when mapping to multiple items. + ### Fixes - fix(logs): stringify u64 attributes greater than `i64::MAX` (#846) by @lcian diff --git a/Cargo.lock b/Cargo.lock index 32221058..feb8b944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "futures-core", "futures-sink", @@ -30,7 +30,7 @@ dependencies = [ "actix-service", "actix-utils", "base64", - "bitflags 2.9.0", + "bitflags 2.9.1", "brotli", "bytes", "bytestring", @@ -448,7 +448,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -470,9 +470,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -2152,7 +2152,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -2309,7 +2309,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", @@ -2407,7 +2407,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -2852,7 +2852,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -3028,7 +3028,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3041,7 +3041,7 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", @@ -3134,7 +3134,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -3334,6 +3334,7 @@ dependencies = [ name = "sentry-tracing" version = "0.40.0" dependencies = [ + "bitflags 2.9.1", "log", "sentry", "sentry-backtrace", @@ -4514,7 +4515,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] diff --git a/sentry-tracing/Cargo.toml b/sentry-tracing/Cargo.toml index b4ac5621..00cc7845 100644 --- a/sentry-tracing/Cargo.toml +++ b/sentry-tracing/Cargo.toml @@ -29,6 +29,7 @@ tracing-subscriber = { version = "0.3.1", default-features = false, features = [ "std", ] } sentry-backtrace = { version = "0.40.0", path = "../sentry-backtrace", optional = true } +bitflags = "2" [dev-dependencies] log = "0.4" diff --git a/sentry-tracing/src/converters.rs b/sentry-tracing/src/converters.rs index 65735f54..d09f5325 100644 --- a/sentry-tracing/src/converters.rs +++ b/sentry-tracing/src/converters.rs @@ -77,7 +77,7 @@ fn extract_event_data( /// Extracts the message and metadata from an event, including the data in the current span. fn extract_event_data_with_context( event: &tracing_core::Event, - ctx: Option>, + ctx: Option<&Context>, store_errors_in_values: bool, ) -> (Option, FieldVisitor) where @@ -182,7 +182,7 @@ impl Visit for FieldVisitor { /// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`]. pub fn breadcrumb_from_event<'context, S>( event: &tracing_core::Event, - ctx: impl Into>>, + ctx: impl Into>>, ) -> Breadcrumb where S: Subscriber + for<'a> LookupSpan<'a>, @@ -261,7 +261,7 @@ fn contexts_from_event( /// Creates an [`Event`] (possibly carrying exceptions) from a given [`tracing_core::Event`]. pub fn event_from_event<'context, S>( event: &tracing_core::Event, - ctx: impl Into>>, + ctx: impl Into>>, ) -> Event<'static> where S: Subscriber + for<'a> LookupSpan<'a>, @@ -329,7 +329,7 @@ where #[cfg(feature = "logs")] pub fn log_from_event<'context, S>( event: &tracing_core::Event, - ctx: impl Into>>, + ctx: impl Into>>, ) -> Log where S: Subscriber + for<'a> LookupSpan<'a>, diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs index f9cbf3aa..2787de5c 100644 --- a/sentry-tracing/src/layer.rs +++ b/sentry-tracing/src/layer.rs @@ -3,6 +3,7 @@ use std::cell::RefCell; use std::collections::BTreeMap; use std::sync::Arc; +use bitflags::bitflags; use sentry_core::protocol::Value; use sentry_core::{Breadcrumb, TransactionOrSpan}; use tracing_core::field::Visit; @@ -13,21 +14,22 @@ use tracing_subscriber::registry::LookupSpan; use crate::converters::*; use crate::TAGS_PREFIX; -/// The action that Sentry should perform for a given [`Event`] -#[derive(Debug, Clone, Copy)] -pub enum EventFilter { - /// Ignore the [`Event`] - Ignore, - /// Create a [`Breadcrumb`] from this [`Event`] - Breadcrumb, - /// Create a [`sentry_core::protocol::Event`] from this [`Event`] - Event, - /// Create a [`sentry_core::protocol::Log`] from this [`Event`] - #[cfg(feature = "logs")] - Log, +bitflags! { + /// The action that Sentry should perform for a given [`Event`] + #[derive(Debug, Clone, Copy)] + pub struct EventFilter: u32 { + /// Ignore the [`Event`] + const Ignore = 0b000; + /// Create a [`Breadcrumb`] from this [`Event`] + const Breadcrumb = 0b001; + /// Create a [`sentry_core::protocol::Event`] from this [`Event`] + const Event = 0b010; + /// Create a [`sentry_core::protocol::Log`] from this [`Event`] + const Log = 0b100; + } } -/// The type of data Sentry should ingest for a [`Event`] +/// The type of data Sentry should ingest for an [`Event`]. #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum EventMapping { @@ -40,6 +42,27 @@ pub enum EventMapping { /// Captures the [`sentry_core::protocol::Log`] to Sentry. #[cfg(feature = "logs")] Log(sentry_core::protocol::Log), + /// Captures multiple items to Sentry. + Combined(CombinedEventMapping), +} + +/// A list of event mappings. +#[derive(Debug)] +pub struct CombinedEventMapping(Vec); + +impl From for CombinedEventMapping { + fn from(value: EventMapping) -> Self { + match value { + EventMapping::Combined(combined) => combined, + _ => CombinedEventMapping(vec![value]), + } + } +} + +impl From> for CombinedEventMapping { + fn from(value: Vec) -> Self { + Self(value) + } } /// The default event filter. @@ -211,30 +234,44 @@ where S: Subscriber + for<'a> LookupSpan<'a>, { fn on_event(&self, event: &Event, ctx: Context<'_, S>) { - let item = match &self.event_mapper { + let items = match &self.event_mapper { Some(mapper) => mapper(event, ctx), None => { let span_ctx = self.with_span_attributes.then_some(ctx); - match (self.event_filter)(event.metadata()) { - EventFilter::Ignore => EventMapping::Ignore, - EventFilter::Breadcrumb => { - EventMapping::Breadcrumb(breadcrumb_from_event(event, span_ctx)) - } - EventFilter::Event => EventMapping::Event(event_from_event(event, span_ctx)), - #[cfg(feature = "logs")] - EventFilter::Log => EventMapping::Log(log_from_event(event, span_ctx)), + let filter = (self.event_filter)(event.metadata()); + let mut items = vec![]; + if filter.contains(EventFilter::Breadcrumb) { + items.push(EventMapping::Breadcrumb(breadcrumb_from_event( + event, + span_ctx.as_ref(), + ))); } + if filter.contains(EventFilter::Event) { + items.push(EventMapping::Event(event_from_event( + event, + span_ctx.as_ref(), + ))); + } + #[cfg(feature = "logs")] + if filter.contains(EventFilter::Log) { + items.push(EventMapping::Log(log_from_event(event, span_ctx.as_ref()))); + } + EventMapping::Combined(CombinedEventMapping(items)) } }; - - match item { - EventMapping::Event(event) => { - sentry_core::capture_event(event); + let items = CombinedEventMapping::from(items); + + for item in items.0 { + match item { + EventMapping::Ignore => (), + EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), + EventMapping::Event(event) => { + sentry_core::capture_event(event); + } + #[cfg(feature = "logs")] + EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)), + _ => (), } - EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), - #[cfg(feature = "logs")] - EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)), - _ => (), } } diff --git a/sentry/tests/test_tracing.rs b/sentry/tests/test_tracing.rs index 6ec51317..f71e5b23 100644 --- a/sentry/tests/test_tracing.rs +++ b/sentry/tests/test_tracing.rs @@ -277,3 +277,84 @@ fn test_tracing_logs() { _ => panic!("expected item container"), } } + +#[test] +fn test_combined_event_filters() { + let sentry_layer = sentry_tracing::layer().event_filter(|md| match *md.level() { + tracing::Level::ERROR => { + sentry_tracing::EventFilter::Breadcrumb | sentry_tracing::EventFilter::Event + } + tracing::Level::WARN => sentry_tracing::EventFilter::Event, + _ => sentry_tracing::EventFilter::Ignore, + }); + + let _dispatcher = tracing_subscriber::registry() + .with(sentry_layer) + .set_default(); + + let events = sentry::test::with_captured_events(|| { + tracing::error!("Both a breadcrumb and an event"); + tracing::warn!("An event"); + }); + + assert_eq!(events.len(), 2); + + assert_eq!( + events[0].message, + Some("Both a breadcrumb and an event".to_owned()) + ); + + assert_eq!(events[1].message, Some("An event".to_owned())); + assert_eq!(events[1].breadcrumbs.len(), 1); + assert_eq!( + events[1].breadcrumbs[0].message, + Some("Both a breadcrumb and an event".into()) + ); +} + +#[test] +fn test_combined_event_mapper() { + let sentry_layer = + sentry_tracing::layer().event_mapper(|event, ctx| match *event.metadata().level() { + tracing::Level::ERROR => { + let breadcrumb = sentry_tracing::breadcrumb_from_event(event, Some(&ctx)); + let sentry_event = sentry_tracing::event_from_event(event, Some(&ctx)); + + sentry_tracing::EventMapping::Combined( + vec![ + sentry_tracing::EventMapping::Breadcrumb(breadcrumb), + sentry_tracing::EventMapping::Event(sentry_event), + ] + .into(), + ) + } + tracing::Level::WARN => { + let sentry_event = sentry_tracing::event_from_event(event, Some(&ctx)); + sentry_tracing::EventMapping::Event(sentry_event) + } + _ => sentry_tracing::EventMapping::Ignore, + }); + + let _dispatcher = tracing_subscriber::registry() + .with(sentry_layer) + .set_default(); + + let events = sentry::test::with_captured_events(|| { + tracing::error!("Both a breadcrumb and an event"); + tracing::warn!("An event"); + }); + + assert_eq!(events.len(), 2); + + assert_eq!( + events[0].message, + Some("Both a breadcrumb and an event".to_owned()) + ); + + assert_eq!(events[1].message, Some("An event".to_owned())); + assert_eq!(events[1].breadcrumbs.len(), 1); + assert_eq!( + events[1].breadcrumbs[0].message, + Some("Both a breadcrumb and an event".into()) + ); +}