Repository: getsentry/sentry-rust
Pull request #632 (2023-12-27 08:20:05)

chore(deps): bump `anyhow` and disable its `backtrace` feature
Since anyhow 1.0.77, backtrace feature is not needed for Rust 1.65+ (https://github.com/dtolnay/anyhow/releases/tag/1.0.77). Because sentry has a higher MSRV than that, it's no longer necessary to enable this feature.

Swatinem: It might not be necessary, but I doubt it hurts to keep it?

I would have to take a closer look at this in any case.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac98b5c..c5675e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,22 @@
 
 ## 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<Option<&'context Context<'context, S>>>` to avoid cloning the `Context` when mapping to multiple items.
+
 ### Fixes
 
 - fix(logs): stringify u64 attributes greater than `i64::MAX` (#846) by @lcian
 
+### Dependencies
+
+- chore(deps): bump `anyhow` and disable its `backtrace` feature (#632) by @LunaBorowska
+
 ## 0.40.0
 
 ### Breaking changes
diff --git a/Cargo.lock b/Cargo.lock
index 3222105..01a443b 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",
@@ -265,9 +265,6 @@ name = "anyhow"
 version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
-dependencies = [
- "backtrace",
-]
 
 [[package]]
 name = "as-slice"
@@ -448,7 +445,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 +467,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 +2149,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 +2306,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 +2404,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 +2849,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 +3025,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 +3038,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 +3131,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 +3331,7 @@ dependencies = [
 name = "sentry-tracing"
 version = "0.40.0"
 dependencies = [
+ "bitflags 2.9.1",
  "log",
  "sentry",
  "sentry-backtrace",
@@ -4514,7 +4512,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-anyhow/Cargo.toml b/sentry-anyhow/Cargo.toml
index a790a06..9bce796 100644
--- a/sentry-anyhow/Cargo.toml
+++ b/sentry-anyhow/Cargo.toml
@@ -14,12 +14,12 @@ rust-version = "1.81"
 
 [features]
 default = ["backtrace"]
-backtrace = ["anyhow/backtrace"]
+backtrace = []
 
 [dependencies]
 sentry-backtrace = { version = "0.40.0", path = "../sentry-backtrace" }
 sentry-core = { version = "0.40.0", path = "../sentry-core" }
-anyhow = "1.0.39"
+anyhow = "1.0.77"
 
 [dev-dependencies]
 sentry = { path = "../sentry", default-features = false, features = ["test"] }
diff --git a/sentry-tracing/Cargo.toml b/sentry-tracing/Cargo.toml
index b4ac562..73f94cb 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.0.0"
 
 [dev-dependencies]
 log = "0.4"
diff --git a/sentry-tracing/src/converters.rs b/sentry-tracing/src/converters.rs
index 65735f5..d09f532 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<S>(
     event: &tracing_core::Event,
-    ctx: Option<Context<S>>,
+    ctx: Option<&Context<S>>,
     store_errors_in_values: bool,
 ) -> (Option<String>, 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<Option<Context<'context, S>>>,
+    ctx: impl Into<Option<&'context Context<'context, S>>>,
 ) -> 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<Option<Context<'context, S>>>,
+    ctx: impl Into<Option<&'context Context<'context, S>>>,
 ) -> 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<Option<Context<'context, S>>>,
+    ctx: impl Into<Option<&'context Context<'context, S>>>,
 ) -> Log
 where
     S: Subscriber + for<'a> LookupSpan<'a>,
diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs
index f9cbf3a..582427e 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,28 @@ pub enum EventMapping {
     /// Captures the [`sentry_core::protocol::Log`] to Sentry.
     #[cfg(feature = "logs")]
     Log(sentry_core::protocol::Log),
+    /// Captures multiple items to Sentry.
+    /// Nesting multiple `EventMapping::Combined` inside each other will cause the inner mappings to be ignored.
+    Combined(CombinedEventMapping),
+}
+
+/// A list of event mappings.
+#[derive(Debug)]
+pub struct CombinedEventMapping(Vec<EventMapping>);
+
+impl From<EventMapping> for CombinedEventMapping {
+    fn from(value: EventMapping) -> Self {
+        match value {
+            EventMapping::Combined(combined) => combined,
+            _ => CombinedEventMapping(vec![value]),
+        }
+    }
+}
+
+impl From<Vec<EventMapping>> for CombinedEventMapping {
+    fn from(value: Vec<EventMapping>) -> Self {
+        Self(value)
+    }
 }
 
 /// The default event filter.
@@ -211,30 +235,48 @@ 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::Combined(_) => {
+                    sentry_core::sentry_debug!(
+                        "[SentryLayer] found nested CombinedEventMapping, ignoring"
+                    )
+                }
             }
-            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-tracing/src/lib.rs b/sentry-tracing/src/lib.rs
index 97115ba..01483b8 100644
--- a/sentry-tracing/src/lib.rs
+++ b/sentry-tracing/src/lib.rs
@@ -86,7 +86,7 @@
 //!
 //! Tracing events can be captured as traditional structured logs in Sentry.
 //! This is gated by the `logs` feature flag and requires setting up a custom Event filter/mapper
-//! to capture logs.
+//! to capture logs. You also need to pass `enable_logs: true` in your `sentry::init` call.
 //!
 //! ```
 //! // assuming `tracing::Level::INFO => EventFilter::Log` in your `event_filter`
@@ -141,6 +141,23 @@
 //! tracing::error!(error = &custom_error as &dyn Error, "my operation failed");
 //! ```
 //!
+//! # Sending multiple items to Sentry
+//!
+//! To map a `tracing` event to multiple items in Sentry, you can combine multiple event filters
+//! using the bitwise or operator:
+//!
+//! ```
+//! let sentry_layer = sentry::integrations::tracing::layer()
+//!     .event_filter(|md| match *md.level() {
+//!         tracing::Level::ERROR => EventFilter::Event | EventFilter::Log,
+//!         tracing::Level::TRACE => EventFilter::Ignore,
+//!         _ => EventFilter::Log,
+//!     })
+//!     .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN));
+//! ```
+//!
+//! If you're using a custom event mapper instead of an event filter, use `EventMapping::Combined`.
+//!
 //! # Tracing Spans
 //!
 //! The integration automatically tracks `tracing` spans as spans in Sentry. A convenient way to do


Test patch:
diff --git a/sentry/tests/test_tracing.rs b/sentry/tests/test_tracing.rs
index 6ec5131..f71e5b2 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())
+    );
+}