diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6cd5e9..6654e401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +### Breaking changes + +- 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`. + - 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 ### Breaking changes diff --git a/Cargo.lock b/Cargo.lock index 6a746990..ec238430 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,6 +3264,7 @@ dependencies = [ name = "sentry-log" version = "0.43.0" dependencies = [ + "bitflags 2.9.4", "log", "pretty_env_logger", "sentry", @@ -3325,7 +3326,7 @@ dependencies = [ name = "sentry-tracing" version = "0.43.0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "log", "sentry", "sentry-backtrace", @@ -4506,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 4559551d..de92399f 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.9.4" [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 56d98653..2c2d56c5 100644 --- a/sentry-log/src/lib.rs +++ b/sentry-log/src/lib.rs @@ -46,6 +46,42 @@ //! _ => 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, +//! log::Level::Warn => LogFilter::Breadcrumb | LogFilter::Log, +//! _ => LogFilter::Ignore, +//! }); +//! ``` +//! +//! 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 7308afa9..0b5177d1 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -1,24 +1,28 @@ 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, PartialEq, Eq)] + 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`]. + #[cfg(feature = "logs")] + const Log = 0b1000; + } } /// The type of Data Sentry should ingest for a [`log::Record`]. @@ -36,6 +40,12 @@ pub enum RecordMapping { Log(sentry_core::protocol::Log), } +impl From for Vec { + fn from(mapping: RecordMapping) -> Self { + vec![mapping] + } +} + /// The default log filter. /// /// By default, an exception event is captured for `error`, a breadcrumb for @@ -73,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 { @@ -119,43 +129,59 @@ 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 } } 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) == LogFilter::Ignore) } 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))); + } + items + } }; - match item { - RecordMapping::Ignore => {} - RecordMapping::Breadcrumb(b) => sentry_core::add_breadcrumb(b), - RecordMapping::Event(e) => { - sentry_core::capture_event(e); + for mapping in items { + match mapping { + 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)) + } } - #[cfg(feature = "logs")] - RecordMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)), } self.dest.log(record) diff --git a/sentry-tracing/Cargo.toml b/sentry-tracing/Cargo.toml index 07a6ec45..be30833e 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" diff --git a/sentry/tests/test_log_combined_filters.rs b/sentry/tests/test_log_combined_filters.rs new file mode 100644 index 00000000..3342743b --- /dev/null +++ b/sentry/tests/test_log_combined_filters.rs @@ -0,0 +1,37 @@ +#![cfg(feature = "test")] + +// 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] +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()) + ); +}