From dfb3a39abe68b156aa12a278b7eb29fb30d10e4d Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:45:02 +0200 Subject: [PATCH 01/11] feat(log): support combined `LogFilter`s and `RecordMapping`s --- CHANGELOG.md | 9 ++++ Cargo.lock | 1 + sentry-log/Cargo.toml | 1 + sentry-log/src/lib.rs | 17 +++++++ sentry-log/src/logger.rs | 107 +++++++++++++++++++++++++++------------ sentry/tests/test_log.rs | 80 +++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6cd5e90..21d1db4d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +### Breaking changes + +- feat(log): support combined `LogFilter`s and `RecordMapping`s by @lcian + - `sentry::integrations::log::LogFilter` has been changed to a `bitflags` struct. + - It's now possible to map a `log` record to multiple items in Sentry by combining multiple log filters in the filter, e.g. `log::Level::ERROR => LogFilter::Event | LogFilter::Log`. + - It's also possible to use `sentry::integrations::log::RecordMapping::Combined` to map a `log` record to multiple items in Sentry. + ## 0.43.0 ### Breaking changes diff --git a/Cargo.lock b/Cargo.lock index 6a7469908..347312525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3264,6 +3264,7 @@ dependencies = [ name = "sentry-log" version = "0.43.0" dependencies = [ + "bitflags 2.9.1", "log", "pretty_env_logger", "sentry", diff --git a/sentry-log/Cargo.toml b/sentry-log/Cargo.toml index 4559551d0..3c1aaf92b 100644 --- a/sentry-log/Cargo.toml +++ b/sentry-log/Cargo.toml @@ -19,6 +19,7 @@ logs = ["sentry-core/logs"] [dependencies] sentry-core = { version = "0.43.0", path = "../sentry-core" } log = { version = "0.4.8", features = ["std", "kv"] } +bitflags = "2.0.0" [dev-dependencies] sentry = { path = "../sentry", default-features = false, features = ["test"] } diff --git a/sentry-log/src/lib.rs b/sentry-log/src/lib.rs index 56d986539..836fcfc6b 100644 --- a/sentry-log/src/lib.rs +++ b/sentry-log/src/lib.rs @@ -46,6 +46,23 @@ //! _ => LogFilter::Ignore, //! }); //! ``` +//! +//! # Sending multiple items to Sentry +//! +//! To map a log record to multiple items in Sentry, you can combine multiple log filters +//! using the bitwise or operator: +//! +//! ``` +//! use sentry_log::LogFilter; +//! +//! let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { +//! log::Level::Error => LogFilter::Event | LogFilter::Log, +//! log::Level::Warn => LogFilter::Breadcrumb | LogFilter::Log, +//! _ => LogFilter::Ignore, +//! }); +//! ``` +//! +//! If you're using a custom record mapper instead of a filter, use `RecordMapping::Combined`. #![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")] diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index 7308afa95..804632e09 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -1,24 +1,27 @@ use log::Record; use sentry_core::protocol::{Breadcrumb, Event}; +use bitflags::bitflags; + #[cfg(feature = "logs")] use crate::converters::log_from_record; use crate::converters::{breadcrumb_from_record, event_from_record, exception_from_record}; -/// The action that Sentry should perform for a [`log::Metadata`]. -#[derive(Debug)] -pub enum LogFilter { - /// Ignore the [`Record`]. - Ignore, - /// Create a [`Breadcrumb`] from this [`Record`]. - Breadcrumb, - /// Create a message [`Event`] from this [`Record`]. - Event, - /// Create an exception [`Event`] from this [`Record`]. - Exception, - /// Create a [`sentry_core::protocol::Log`] from this [`Record`]. - #[cfg(feature = "logs")] - Log, +bitflags! { + /// The action that Sentry should perform for a [`log::Metadata`]. + #[derive(Debug, Clone, Copy)] + pub struct LogFilter: u32 { + /// Ignore the [`Record`]. + const Ignore = 0b0000; + /// Create a [`Breadcrumb`] from this [`Record`]. + const Breadcrumb = 0b0001; + /// Create a message [`Event`] from this [`Record`]. + const Event = 0b0010; + /// Create an exception [`Event`] from this [`Record`]. + const Exception = 0b0100; + /// Create a [`sentry_core::protocol::Log`] from this [`Record`]. + const Log = 0b1000; + } } /// The type of Data Sentry should ingest for a [`log::Record`]. @@ -34,6 +37,28 @@ pub enum RecordMapping { /// Captures the [`sentry_core::protocol::Log`] to Sentry. #[cfg(feature = "logs")] Log(sentry_core::protocol::Log), + /// Captures multiple items to Sentry. + /// Nesting multiple `RecordMapping::Combined` inside each other will cause the inner mappings to be ignored. + Combined(CombinedRecordMapping), +} + +/// A list of record mappings. +#[derive(Debug)] +pub struct CombinedRecordMapping(Vec); + +impl From for CombinedRecordMapping { + fn from(value: RecordMapping) -> Self { + match value { + RecordMapping::Combined(combined) => combined, + _ => CombinedRecordMapping(vec![value]), + } + } +} + +impl From> for CombinedRecordMapping { + fn from(value: Vec) -> Self { + Self(value) + } } /// The default log filter. @@ -132,30 +157,50 @@ impl SentryLogger { impl log::Log for SentryLogger { fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - self.dest.enabled(metadata) || !matches!((self.filter)(metadata), LogFilter::Ignore) + self.dest.enabled(metadata) || !((self.filter)(metadata).is_empty()) } fn log(&self, record: &log::Record<'_>) { - let item: RecordMapping = match &self.mapper { + let items = match &self.mapper { Some(mapper) => mapper(record), - None => match (self.filter)(record.metadata()) { - LogFilter::Ignore => RecordMapping::Ignore, - LogFilter::Breadcrumb => RecordMapping::Breadcrumb(breadcrumb_from_record(record)), - LogFilter::Event => RecordMapping::Event(event_from_record(record)), - LogFilter::Exception => RecordMapping::Event(exception_from_record(record)), + None => { + let filter = (self.filter)(record.metadata()); + let mut items = vec![]; + if filter.contains(LogFilter::Breadcrumb) { + items.push(RecordMapping::Breadcrumb(breadcrumb_from_record(record))); + } + if filter.contains(LogFilter::Event) { + items.push(RecordMapping::Event(event_from_record(record))); + } + if filter.contains(LogFilter::Exception) { + items.push(RecordMapping::Event(exception_from_record(record))); + } #[cfg(feature = "logs")] - LogFilter::Log => RecordMapping::Log(log_from_record(record)), - }, + if filter.contains(LogFilter::Log) { + items.push(RecordMapping::Log(log_from_record(record))); + } + RecordMapping::Combined(CombinedRecordMapping(items)) + } }; - - match item { - RecordMapping::Ignore => {} - RecordMapping::Breadcrumb(b) => sentry_core::add_breadcrumb(b), - RecordMapping::Event(e) => { - sentry_core::capture_event(e); + let items = CombinedRecordMapping::from(items); + + for item in items.0 { + match item { + RecordMapping::Ignore => {} + RecordMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), + RecordMapping::Event(event) => { + sentry_core::capture_event(event); + } + #[cfg(feature = "logs")] + RecordMapping::Log(log) => { + sentry_core::Hub::with_active(|hub| hub.capture_log(log)) + } + RecordMapping::Combined(_) => { + sentry_core::sentry_debug!( + "[SentryLogger] found nested CombinedEventMapping, ignoring" + ) + } } - #[cfg(feature = "logs")] - RecordMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)), } self.dest.log(record) diff --git a/sentry/tests/test_log.rs b/sentry/tests/test_log.rs index c460cf4ba..e1750e7b6 100644 --- a/sentry/tests/test_log.rs +++ b/sentry/tests/test_log.rs @@ -26,6 +26,86 @@ fn test_log() { assert_eq!(event.breadcrumbs[0].message, Some("Hello World!".into())); } +#[test] +fn test_combined_log_filters() { + let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { + log::Level::Error => sentry_log::LogFilter::Breadcrumb | sentry_log::LogFilter::Event, + log::Level::Warn => sentry_log::LogFilter::Event, + _ => sentry_log::LogFilter::Ignore, + }); + + // Only set logger if not already set to avoid conflicts with other tests + if log::set_boxed_logger(Box::new(logger)).is_ok() { + log::set_max_level(log::LevelFilter::Trace); + } + + let events = sentry::test::with_captured_events(|| { + log::error!("Both a breadcrumb and an event"); + log::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_record_mapper() { + let logger = sentry_log::SentryLogger::new().mapper(|record| match record.metadata().level() { + log::Level::Error => { + let breadcrumb = sentry_log::breadcrumb_from_record(record); + let sentry_event = sentry_log::event_from_record(record); + + sentry_log::RecordMapping::Combined( + vec![ + sentry_log::RecordMapping::Breadcrumb(breadcrumb), + sentry_log::RecordMapping::Event(sentry_event), + ] + .into(), + ) + } + log::Level::Warn => { + let sentry_event = sentry_log::event_from_record(record); + sentry_log::RecordMapping::Event(sentry_event) + } + _ => sentry_log::RecordMapping::Ignore, + }); + + // Only set logger if not already set to avoid conflicts with other tests + if log::set_boxed_logger(Box::new(logger)).is_ok() { + log::set_max_level(log::LevelFilter::Trace); + } + + let events = sentry::test::with_captured_events(|| { + log::error!("Both a breadcrumb and an event"); + log::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_slog() { let drain = sentry_slog::SentryDrain::new(slog::Discard); From 65f7c9724f249a3fef75a0164ae69d8d5fd87adf Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:05:58 +0200 Subject: [PATCH 02/11] improve --- sentry-log/src/logger.rs | 9 +-- sentry/tests/test_log.rs | 80 ----------------------- sentry/tests/test_log_combined_filters.rs | 37 +++++++++++ 3 files changed, 42 insertions(+), 84 deletions(-) create mode 100644 sentry/tests/test_log_combined_filters.rs diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index 804632e09..925e0f25e 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -9,7 +9,7 @@ use crate::converters::{breadcrumb_from_record, event_from_record, exception_fro bitflags! { /// The action that Sentry should perform for a [`log::Metadata`]. - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LogFilter: u32 { /// Ignore the [`Record`]. const Ignore = 0b0000; @@ -38,7 +38,8 @@ pub enum RecordMapping { #[cfg(feature = "logs")] Log(sentry_core::protocol::Log), /// Captures multiple items to Sentry. - /// Nesting multiple `RecordMapping::Combined` inside each other will cause the inner mappings to be ignored. + /// Nesting multiple `RecordMapping::Combined` is not supported and will cause the mappings to + /// be ignored. Combined(CombinedRecordMapping), } @@ -157,11 +158,11 @@ impl SentryLogger { impl log::Log for SentryLogger { fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - self.dest.enabled(metadata) || !((self.filter)(metadata).is_empty()) + self.dest.enabled(metadata) || !(self.filter)(metadata) == LogFilter::Ignore } fn log(&self, record: &log::Record<'_>) { - let items = match &self.mapper { + let items: RecordMapping = match &self.mapper { Some(mapper) => mapper(record), None => { let filter = (self.filter)(record.metadata()); diff --git a/sentry/tests/test_log.rs b/sentry/tests/test_log.rs index e1750e7b6..c460cf4ba 100644 --- a/sentry/tests/test_log.rs +++ b/sentry/tests/test_log.rs @@ -26,86 +26,6 @@ fn test_log() { assert_eq!(event.breadcrumbs[0].message, Some("Hello World!".into())); } -#[test] -fn test_combined_log_filters() { - let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { - log::Level::Error => sentry_log::LogFilter::Breadcrumb | sentry_log::LogFilter::Event, - log::Level::Warn => sentry_log::LogFilter::Event, - _ => sentry_log::LogFilter::Ignore, - }); - - // Only set logger if not already set to avoid conflicts with other tests - if log::set_boxed_logger(Box::new(logger)).is_ok() { - log::set_max_level(log::LevelFilter::Trace); - } - - let events = sentry::test::with_captured_events(|| { - log::error!("Both a breadcrumb and an event"); - log::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_record_mapper() { - let logger = sentry_log::SentryLogger::new().mapper(|record| match record.metadata().level() { - log::Level::Error => { - let breadcrumb = sentry_log::breadcrumb_from_record(record); - let sentry_event = sentry_log::event_from_record(record); - - sentry_log::RecordMapping::Combined( - vec![ - sentry_log::RecordMapping::Breadcrumb(breadcrumb), - sentry_log::RecordMapping::Event(sentry_event), - ] - .into(), - ) - } - log::Level::Warn => { - let sentry_event = sentry_log::event_from_record(record); - sentry_log::RecordMapping::Event(sentry_event) - } - _ => sentry_log::RecordMapping::Ignore, - }); - - // Only set logger if not already set to avoid conflicts with other tests - if log::set_boxed_logger(Box::new(logger)).is_ok() { - log::set_max_level(log::LevelFilter::Trace); - } - - let events = sentry::test::with_captured_events(|| { - log::error!("Both a breadcrumb and an event"); - log::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_slog() { let drain = sentry_slog::SentryDrain::new(slog::Discard); diff --git a/sentry/tests/test_log_combined_filters.rs b/sentry/tests/test_log_combined_filters.rs new file mode 100644 index 000000000..8dd4b74ce --- /dev/null +++ b/sentry/tests/test_log_combined_filters.rs @@ -0,0 +1,37 @@ +#![cfg(feature = "test")] + +// Test `log` integration with combined filters. +// This must be in a separate file because `log::set_boxed_logger` can only be called once. + +#[test] +fn test_log_combined_filters() { + let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { + log::Level::Error => sentry_log::LogFilter::Breadcrumb | sentry_log::LogFilter::Event, + log::Level::Warn => sentry_log::LogFilter::Event, + _ => sentry_log::LogFilter::Ignore, + }); + + log::set_boxed_logger(Box::new(logger)) + .map(|()| log::set_max_level(log::LevelFilter::Trace)) + .unwrap(); + + let events = sentry::test::with_captured_events(|| { + log::error!("Both a breadcrumb and an event"); + log::warn!("An event"); + log::trace!("Ignored"); + }); + + 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()) + ); +} From fb6dc781b5837efd8209a43dc1c5fe1dee1deb82 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:07:26 +0200 Subject: [PATCH 03/11] improve --- sentry/tests/test_log_combined_filters.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/tests/test_log_combined_filters.rs b/sentry/tests/test_log_combined_filters.rs index 8dd4b74ce..3342743ba 100644 --- a/sentry/tests/test_log_combined_filters.rs +++ b/sentry/tests/test_log_combined_filters.rs @@ -1,6 +1,6 @@ #![cfg(feature = "test")] -// Test `log` integration with combined filters. +// Test `log` integration with combined `LogFilter`s. // This must be in a separate file because `log::set_boxed_logger` can only be called once. #[test] From 79261c2fe689b6b51430c298778a4a04f657e78a Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:07:44 +0200 Subject: [PATCH 04/11] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d1db4d2..1c120e8a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Breaking changes -- feat(log): support combined `LogFilter`s and `RecordMapping`s by @lcian +- feat(log): support combined LogFilters and RecordMappings ([#914](https://github.com/getsentry/sentry-rust/pull/914)) by @lcian - `sentry::integrations::log::LogFilter` has been changed to a `bitflags` struct. - It's now possible to map a `log` record to multiple items in Sentry by combining multiple log filters in the filter, e.g. `log::Level::ERROR => LogFilter::Event | LogFilter::Log`. - It's also possible to use `sentry::integrations::log::RecordMapping::Combined` to map a `log` record to multiple items in Sentry. From 7d96f91c412cf51dd7a7a54dac2181e9578f54ea Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:04:36 +0200 Subject: [PATCH 05/11] trigger CI From 979411c06eb6c64d6f6010650b2cca827eb0de4d Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:14:50 +0200 Subject: [PATCH 06/11] gate --- sentry-log/src/logger.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index 925e0f25e..fc2372143 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -20,6 +20,7 @@ bitflags! { /// Create an exception [`Event`] from this [`Record`]. const Exception = 0b0100; /// Create a [`sentry_core::protocol::Log`] from this [`Record`]. + #[cfg(feature = "logs")] const Log = 0b1000; } } From 0da7c5e35895a2e3a83dc10bbc4533f1640be828 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:45:15 +0200 Subject: [PATCH 07/11] fix --- sentry-log/src/logger.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index fc2372143..847d89df7 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -159,7 +159,7 @@ impl SentryLogger { impl log::Log for SentryLogger { fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - self.dest.enabled(metadata) || !(self.filter)(metadata) == LogFilter::Ignore + self.dest.enabled(metadata) || !((self.filter)(metadata) == LogFilter::Ignore) } fn log(&self, record: &log::Record<'_>) { From 14bd54e52a556372b7676ab7ddc59f858f835599 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:31:43 +0200 Subject: [PATCH 08/11] improve --- sentry-log/src/logger.rs | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index 847d89df7..7c41ce97e 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -41,26 +41,7 @@ pub enum RecordMapping { /// Captures multiple items to Sentry. /// Nesting multiple `RecordMapping::Combined` is not supported and will cause the mappings to /// be ignored. - Combined(CombinedRecordMapping), -} - -/// A list of record mappings. -#[derive(Debug)] -pub struct CombinedRecordMapping(Vec); - -impl From for CombinedRecordMapping { - fn from(value: RecordMapping) -> Self { - match value { - RecordMapping::Combined(combined) => combined, - _ => CombinedRecordMapping(vec![value]), - } - } -} - -impl From> for CombinedRecordMapping { - fn from(value: Vec) -> Self { - Self(value) - } + Combined(Vec), } /// The default log filter. @@ -163,7 +144,7 @@ impl log::Log for SentryLogger { } fn log(&self, record: &log::Record<'_>) { - let items: RecordMapping = match &self.mapper { + let items = match &self.mapper { Some(mapper) => mapper(record), None => { let filter = (self.filter)(record.metadata()); @@ -181,13 +162,12 @@ impl log::Log for SentryLogger { if filter.contains(LogFilter::Log) { items.push(RecordMapping::Log(log_from_record(record))); } - RecordMapping::Combined(CombinedRecordMapping(items)) + RecordMapping::Combined(items) } }; - let items = CombinedRecordMapping::from(items); - for item in items.0 { - match item { + fn handle_single_mapping(mapping: RecordMapping) { + match mapping { RecordMapping::Ignore => {} RecordMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), RecordMapping::Event(event) => { @@ -199,12 +179,20 @@ impl log::Log for SentryLogger { } RecordMapping::Combined(_) => { sentry_core::sentry_debug!( - "[SentryLogger] found nested CombinedEventMapping, ignoring" + "[SentryLogger] found nested RecordMapping::Combined, ignoring" ) } } } + if let RecordMapping::Combined(items) = items { + for item in items { + handle_single_mapping(item); + } + } else { + handle_single_mapping(items); + } + self.dest.log(record) } From 139e0ccb766a5ebc5c3853b4da74f5ca44671559 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:35:38 +0200 Subject: [PATCH 09/11] specify latest bitflags --- Cargo.lock | 30 +++++++++++++++--------------- sentry-log/Cargo.toml | 2 +- sentry-tracing/Cargo.toml | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 347312525..ec238430e 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.1", + "bitflags 2.9.4", "bytes", "futures-core", "futures-sink", @@ -30,7 +30,7 @@ dependencies = [ "actix-service", "actix-utils", "base64", - "bitflags 2.9.1", + "bitflags 2.9.4", "brotli", "bytes", "bytestring", @@ -445,7 +445,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools 0.13.0", @@ -467,9 +467,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -2149,7 +2149,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "libc", "redox_syscall", ] @@ -2306,7 +2306,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -2403,7 +2403,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -2842,7 +2842,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] @@ -3018,7 +3018,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3031,7 +3031,7 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.9.4", @@ -3124,7 +3124,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -3264,7 +3264,7 @@ dependencies = [ name = "sentry-log" version = "0.43.0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "log", "pretty_env_logger", "sentry", @@ -3326,7 +3326,7 @@ dependencies = [ name = "sentry-tracing" version = "0.43.0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "log", "sentry", "sentry-backtrace", @@ -4507,7 +4507,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] diff --git a/sentry-log/Cargo.toml b/sentry-log/Cargo.toml index 3c1aaf92b..de92399f3 100644 --- a/sentry-log/Cargo.toml +++ b/sentry-log/Cargo.toml @@ -19,7 +19,7 @@ logs = ["sentry-core/logs"] [dependencies] sentry-core = { version = "0.43.0", path = "../sentry-core" } log = { version = "0.4.8", features = ["std", "kv"] } -bitflags = "2.0.0" +bitflags = "2.9.4" [dev-dependencies] sentry = { path = "../sentry", default-features = false, features = ["test"] } diff --git a/sentry-tracing/Cargo.toml b/sentry-tracing/Cargo.toml index 07a6ec456..be30833e6 100644 --- a/sentry-tracing/Cargo.toml +++ b/sentry-tracing/Cargo.toml @@ -29,7 +29,7 @@ tracing-subscriber = { version = "0.3.20", default-features = false, features = "std", ] } sentry-backtrace = { version = "0.43.0", path = "../sentry-backtrace", optional = true } -bitflags = "2.0.0" +bitflags = "2.9.4" [dev-dependencies] log = "0.4" From 323713da5074d75a6b62ff5cac006b3e9fc08905 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:14:16 +0200 Subject: [PATCH 10/11] remove unnecessary variant --- sentry-log/src/lib.rs | 23 +++++++++++++++++++++-- sentry-log/src/logger.rs | 39 +++++++++++++++------------------------ 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/sentry-log/src/lib.rs b/sentry-log/src/lib.rs index 836fcfc6b..2c2d56c5b 100644 --- a/sentry-log/src/lib.rs +++ b/sentry-log/src/lib.rs @@ -56,13 +56,32 @@ //! use sentry_log::LogFilter; //! //! let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { -//! log::Level::Error => LogFilter::Event | LogFilter::Log, +//! log::Level::Error => LogFilter::Event, //! log::Level::Warn => LogFilter::Breadcrumb | LogFilter::Log, //! _ => LogFilter::Ignore, //! }); //! ``` //! -//! If you're using a custom record mapper instead of a filter, use `RecordMapping::Combined`. +//! If you're using a custom record mapper instead of a filter, you can return a `Vec` +//! from your mapper function to send multiple items to Sentry from a single log record: +//! +//! ``` +//! use sentry_log::{RecordMapping, SentryLogger, event_from_record, breadcrumb_from_record}; +//! +//! let logger = SentryLogger::new().mapper(|record| { +//! match record.level() { +//! log::Level::Error => { +//! // Send both an event and a breadcrumb for errors +//! vec![ +//! RecordMapping::Event(event_from_record(record)), +//! RecordMapping::Breadcrumb(breadcrumb_from_record(record)), +//! ] +//! } +//! log::Level::Warn => RecordMapping::Breadcrumb(breadcrumb_from_record(record)).into(), +//! _ => RecordMapping::Ignore.into(), +//! } +//! }); +//! ``` #![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")] diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index 7c41ce97e..0b5177d1d 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -38,10 +38,12 @@ pub enum RecordMapping { /// Captures the [`sentry_core::protocol::Log`] to Sentry. #[cfg(feature = "logs")] Log(sentry_core::protocol::Log), - /// Captures multiple items to Sentry. - /// Nesting multiple `RecordMapping::Combined` is not supported and will cause the mappings to - /// be ignored. - Combined(Vec), +} + +impl From for Vec { + fn from(mapping: RecordMapping) -> Self { + vec![mapping] + } } /// The default log filter. @@ -81,7 +83,7 @@ pub struct SentryLogger { dest: L, filter: Box) -> LogFilter + Send + Sync>, #[allow(clippy::type_complexity)] - mapper: Option) -> RecordMapping + Send + Sync>>, + mapper: Option) -> Vec + Send + Sync>>, } impl Default for SentryLogger { @@ -127,13 +129,15 @@ impl SentryLogger { /// Sets a custom mapper function. /// /// The mapper is responsible for creating either breadcrumbs or events - /// from [`Record`]s. + /// from [`Record`]s. It can return either a single [`RecordMapping`] or + /// a `Vec` to send multiple items to Sentry from one log record. #[must_use] - pub fn mapper(mut self, mapper: M) -> Self + pub fn mapper(mut self, mapper: M) -> Self where - M: Fn(&Record<'_>) -> RecordMapping + Send + Sync + 'static, + M: Fn(&Record<'_>) -> T + Send + Sync + 'static, + T: Into>, { - self.mapper = Some(Box::new(mapper)); + self.mapper = Some(Box::new(move |record| mapper(record).into())); self } } @@ -162,11 +166,11 @@ impl log::Log for SentryLogger { if filter.contains(LogFilter::Log) { items.push(RecordMapping::Log(log_from_record(record))); } - RecordMapping::Combined(items) + items } }; - fn handle_single_mapping(mapping: RecordMapping) { + for mapping in items { match mapping { RecordMapping::Ignore => {} RecordMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), @@ -177,20 +181,7 @@ impl log::Log for SentryLogger { RecordMapping::Log(log) => { sentry_core::Hub::with_active(|hub| hub.capture_log(log)) } - RecordMapping::Combined(_) => { - sentry_core::sentry_debug!( - "[SentryLogger] found nested RecordMapping::Combined, ignoring" - ) - } - } - } - - if let RecordMapping::Combined(items) = items { - for item in items { - handle_single_mapping(item); } - } else { - handle_single_mapping(items); } self.dest.log(record) From 2272f4d90eb801d566fa1523e37def436aacfec3 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:28:28 +0200 Subject: [PATCH 11/11] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c120e8a6..6654e4013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - feat(log): support combined LogFilters and RecordMappings ([#914](https://github.com/getsentry/sentry-rust/pull/914)) by @lcian - `sentry::integrations::log::LogFilter` has been changed to a `bitflags` struct. - It's now possible to map a `log` record to multiple items in Sentry by combining multiple log filters in the filter, e.g. `log::Level::ERROR => LogFilter::Event | LogFilter::Log`. - - It's also possible to use `sentry::integrations::log::RecordMapping::Combined` to map a `log` record to multiple items in Sentry. + - If using a custom `mapper` instead, it's possible to return a `Vec` to map a `log` record to multiple items in Sentry. ## 0.43.0