Repository: getsentry/sentry-rust
Pull request #847 (2025-06-18 13:05:47)

feat(tracing): support combined EventFilters and EventMappings
It seems common enough that someone would want to want to map  a `tracing` event to multiple item types, and right now that's not possible, you have to choose between Ignore/Event/Breadcrumb/Log.
For example, in Relay we want to send everything at or above INFO level to logs, while still sending ERROR events to Sentry events at the same time.

This is a proposal for how to implement that.
This particular solution requires 2 breaking changes, even though both could be avoided.
I would argue that they are not actually breaking for most users though, i.e. those that just set up a custom filter, as this is still backwards compatible in terms of syntax, even though the underlying type changes.

github-actions[bot]: 
<!--
  1 failure:  Please consider a...
  0 warning: 
  1 messages
  1 markdown notices
  DangerID: danger-id-Danger;
-->

<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Fails</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:no_entry_sign:</td>
      <td>Please consider adding a changelog entry for the next release.</td>
    </tr>
  </tbody>
</table>



<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Messages</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:book:</td>
      <td>Do not forget to update <a href="https://github.com/getsentry/sentry-docs">Sentry-docs</a> with your feature once the pull request gets approved.</td>
    </tr>
  </tbody>
</table>

### Instructions and example for changelog

Please add an entry to `CHANGELOG.md` to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

```markdown
## Unreleased

- support combined EventFilters and EventMappings ([#847](https://github.com/getsentry/sentry-rust/pull/847))
```

If none of the above apply, you can opt out of this check by adding `#skip-changelog` to the PR description.
<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against 49f727317ecf7ba031c508db87f95df006ecf806
</p>

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getsentry/sentry-rust/pull/847?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
Attention: Patch coverage is `95.00000%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 74.23%. Comparing base [(`333b14e`)](https://app.codecov.io/gh/getsentry/sentry-rust/commit/333b14ee10f09aa569e8bd542d7f40885d146dd8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) to head [(`49f7273`)](https://app.codecov.io/gh/getsentry/sentry-rust/commit/49f727317ecf7ba031c508db87f95df006ecf806?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #847      +/-   ##
==========================================
+ Coverage   74.05%   74.23%   +0.18%     
==========================================
  Files          64       64              
  Lines        7781     7805      +24     
==========================================
+ Hits         5762     5794      +32     
+ Misses       2019     2011       -8     
```

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac98b5c..40fde26 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<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
diff --git a/Cargo.lock b/Cargo.lock
index 3222105..feb8b94 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 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())
+    );
+}