Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<sentry::integrations::log::RecordMapping>` to map a `log` record to multiple items in Sentry.

## 0.43.0

### Breaking changes
Expand Down
29 changes: 15 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sentry-log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
36 changes: 36 additions & 0 deletions sentry-log/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecordMapping>`
//! 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")]
Expand Down
96 changes: 61 additions & 35 deletions sentry-log/src/logger.rs
Original file line number Diff line number Diff line change
@@ -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`].
Expand All @@ -36,6 +40,12 @@ pub enum RecordMapping {
Log(sentry_core::protocol::Log),
}

impl From<RecordMapping> for Vec<RecordMapping> {
fn from(mapping: RecordMapping) -> Self {
vec![mapping]
}
}

/// The default log filter.
///
/// By default, an exception event is captured for `error`, a breadcrumb for
Expand Down Expand Up @@ -73,7 +83,7 @@ pub struct SentryLogger<L: log::Log> {
dest: L,
filter: Box<dyn Fn(&log::Metadata<'_>) -> LogFilter + Send + Sync>,
#[allow(clippy::type_complexity)]
mapper: Option<Box<dyn Fn(&Record<'_>) -> RecordMapping + Send + Sync>>,
mapper: Option<Box<dyn Fn(&Record<'_>) -> Vec<RecordMapping> + Send + Sync>>,
}

impl Default for SentryLogger<NoopLogger> {
Expand Down Expand Up @@ -119,43 +129,59 @@ impl<L: log::Log> SentryLogger<L> {
/// 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<RecordMapping>` to send multiple items to Sentry from one log record.
#[must_use]
pub fn mapper<M>(mut self, mapper: M) -> Self
pub fn mapper<M, T>(mut self, mapper: M) -> Self
where
M: Fn(&Record<'_>) -> RecordMapping + Send + Sync + 'static,
M: Fn(&Record<'_>) -> T + Send + Sync + 'static,
T: Into<Vec<RecordMapping>>,
{
self.mapper = Some(Box::new(mapper));
self.mapper = Some(Box::new(move |record| mapper(record).into()));
self
}
}

impl<L: log::Log> log::Log for SentryLogger<L> {
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)
Expand Down
2 changes: 1 addition & 1 deletion sentry-tracing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 37 additions & 0 deletions sentry/tests/test_log_combined_filters.rs
Original file line number Diff line number Diff line change
@@ -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())
);
}
Loading