Skip to content

Commit 3b78cf8

Browse files
authored
feat(log): support combined LogFilters and RecordMappings (#914)
1 parent 9ba9a64 commit 3b78cf8

File tree

7 files changed

+160
-50
lines changed

7 files changed

+160
-50
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Breaking changes
6+
7+
- feat(log): support combined LogFilters and RecordMappings ([#914](https://github.com/getsentry/sentry-rust/pull/914)) by @lcian
8+
- `sentry::integrations::log::LogFilter` has been changed to a `bitflags` struct.
9+
- 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`.
10+
- 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.
11+
312
## 0.43.0
413

514
### Breaking changes

Cargo.lock

Lines changed: 15 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sentry-log/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ logs = ["sentry-core/logs"]
1919
[dependencies]
2020
sentry-core = { version = "0.43.0", path = "../sentry-core" }
2121
log = { version = "0.4.8", features = ["std", "kv"] }
22+
bitflags = "2.9.4"
2223

2324
[dev-dependencies]
2425
sentry = { path = "../sentry", default-features = false, features = ["test"] }

sentry-log/src/lib.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,42 @@
4646
//! _ => LogFilter::Ignore,
4747
//! });
4848
//! ```
49+
//!
50+
//! # Sending multiple items to Sentry
51+
//!
52+
//! To map a log record to multiple items in Sentry, you can combine multiple log filters
53+
//! using the bitwise or operator:
54+
//!
55+
//! ```
56+
//! use sentry_log::LogFilter;
57+
//!
58+
//! let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() {
59+
//! log::Level::Error => LogFilter::Event,
60+
//! log::Level::Warn => LogFilter::Breadcrumb | LogFilter::Log,
61+
//! _ => LogFilter::Ignore,
62+
//! });
63+
//! ```
64+
//!
65+
//! If you're using a custom record mapper instead of a filter, you can return a `Vec<RecordMapping>`
66+
//! from your mapper function to send multiple items to Sentry from a single log record:
67+
//!
68+
//! ```
69+
//! use sentry_log::{RecordMapping, SentryLogger, event_from_record, breadcrumb_from_record};
70+
//!
71+
//! let logger = SentryLogger::new().mapper(|record| {
72+
//! match record.level() {
73+
//! log::Level::Error => {
74+
//! // Send both an event and a breadcrumb for errors
75+
//! vec![
76+
//! RecordMapping::Event(event_from_record(record)),
77+
//! RecordMapping::Breadcrumb(breadcrumb_from_record(record)),
78+
//! ]
79+
//! }
80+
//! log::Level::Warn => RecordMapping::Breadcrumb(breadcrumb_from_record(record)).into(),
81+
//! _ => RecordMapping::Ignore.into(),
82+
//! }
83+
//! });
84+
//! ```
4985
5086
#![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")]
5187
#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]

sentry-log/src/logger.rs

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
use log::Record;
22
use sentry_core::protocol::{Breadcrumb, Event};
33

4+
use bitflags::bitflags;
5+
46
#[cfg(feature = "logs")]
57
use crate::converters::log_from_record;
68
use crate::converters::{breadcrumb_from_record, event_from_record, exception_from_record};
79

8-
/// The action that Sentry should perform for a [`log::Metadata`].
9-
#[derive(Debug)]
10-
pub enum LogFilter {
11-
/// Ignore the [`Record`].
12-
Ignore,
13-
/// Create a [`Breadcrumb`] from this [`Record`].
14-
Breadcrumb,
15-
/// Create a message [`Event`] from this [`Record`].
16-
Event,
17-
/// Create an exception [`Event`] from this [`Record`].
18-
Exception,
19-
/// Create a [`sentry_core::protocol::Log`] from this [`Record`].
20-
#[cfg(feature = "logs")]
21-
Log,
10+
bitflags! {
11+
/// The action that Sentry should perform for a [`log::Metadata`].
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13+
pub struct LogFilter: u32 {
14+
/// Ignore the [`Record`].
15+
const Ignore = 0b0000;
16+
/// Create a [`Breadcrumb`] from this [`Record`].
17+
const Breadcrumb = 0b0001;
18+
/// Create a message [`Event`] from this [`Record`].
19+
const Event = 0b0010;
20+
/// Create an exception [`Event`] from this [`Record`].
21+
const Exception = 0b0100;
22+
/// Create a [`sentry_core::protocol::Log`] from this [`Record`].
23+
#[cfg(feature = "logs")]
24+
const Log = 0b1000;
25+
}
2226
}
2327

2428
/// The type of Data Sentry should ingest for a [`log::Record`].
@@ -36,6 +40,12 @@ pub enum RecordMapping {
3640
Log(sentry_core::protocol::Log),
3741
}
3842

43+
impl From<RecordMapping> for Vec<RecordMapping> {
44+
fn from(mapping: RecordMapping) -> Self {
45+
vec![mapping]
46+
}
47+
}
48+
3949
/// The default log filter.
4050
///
4151
/// By default, an exception event is captured for `error`, a breadcrumb for
@@ -73,7 +83,7 @@ pub struct SentryLogger<L: log::Log> {
7383
dest: L,
7484
filter: Box<dyn Fn(&log::Metadata<'_>) -> LogFilter + Send + Sync>,
7585
#[allow(clippy::type_complexity)]
76-
mapper: Option<Box<dyn Fn(&Record<'_>) -> RecordMapping + Send + Sync>>,
86+
mapper: Option<Box<dyn Fn(&Record<'_>) -> Vec<RecordMapping> + Send + Sync>>,
7787
}
7888

7989
impl Default for SentryLogger<NoopLogger> {
@@ -119,43 +129,59 @@ impl<L: log::Log> SentryLogger<L> {
119129
/// Sets a custom mapper function.
120130
///
121131
/// The mapper is responsible for creating either breadcrumbs or events
122-
/// from [`Record`]s.
132+
/// from [`Record`]s. It can return either a single [`RecordMapping`] or
133+
/// a `Vec<RecordMapping>` to send multiple items to Sentry from one log record.
123134
#[must_use]
124-
pub fn mapper<M>(mut self, mapper: M) -> Self
135+
pub fn mapper<M, T>(mut self, mapper: M) -> Self
125136
where
126-
M: Fn(&Record<'_>) -> RecordMapping + Send + Sync + 'static,
137+
M: Fn(&Record<'_>) -> T + Send + Sync + 'static,
138+
T: Into<Vec<RecordMapping>>,
127139
{
128-
self.mapper = Some(Box::new(mapper));
140+
self.mapper = Some(Box::new(move |record| mapper(record).into()));
129141
self
130142
}
131143
}
132144

133145
impl<L: log::Log> log::Log for SentryLogger<L> {
134146
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
135-
self.dest.enabled(metadata) || !matches!((self.filter)(metadata), LogFilter::Ignore)
147+
self.dest.enabled(metadata) || !((self.filter)(metadata) == LogFilter::Ignore)
136148
}
137149

138150
fn log(&self, record: &log::Record<'_>) {
139-
let item: RecordMapping = match &self.mapper {
151+
let items = match &self.mapper {
140152
Some(mapper) => mapper(record),
141-
None => match (self.filter)(record.metadata()) {
142-
LogFilter::Ignore => RecordMapping::Ignore,
143-
LogFilter::Breadcrumb => RecordMapping::Breadcrumb(breadcrumb_from_record(record)),
144-
LogFilter::Event => RecordMapping::Event(event_from_record(record)),
145-
LogFilter::Exception => RecordMapping::Event(exception_from_record(record)),
153+
None => {
154+
let filter = (self.filter)(record.metadata());
155+
let mut items = vec![];
156+
if filter.contains(LogFilter::Breadcrumb) {
157+
items.push(RecordMapping::Breadcrumb(breadcrumb_from_record(record)));
158+
}
159+
if filter.contains(LogFilter::Event) {
160+
items.push(RecordMapping::Event(event_from_record(record)));
161+
}
162+
if filter.contains(LogFilter::Exception) {
163+
items.push(RecordMapping::Event(exception_from_record(record)));
164+
}
146165
#[cfg(feature = "logs")]
147-
LogFilter::Log => RecordMapping::Log(log_from_record(record)),
148-
},
166+
if filter.contains(LogFilter::Log) {
167+
items.push(RecordMapping::Log(log_from_record(record)));
168+
}
169+
items
170+
}
149171
};
150172

151-
match item {
152-
RecordMapping::Ignore => {}
153-
RecordMapping::Breadcrumb(b) => sentry_core::add_breadcrumb(b),
154-
RecordMapping::Event(e) => {
155-
sentry_core::capture_event(e);
173+
for mapping in items {
174+
match mapping {
175+
RecordMapping::Ignore => {}
176+
RecordMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
177+
RecordMapping::Event(event) => {
178+
sentry_core::capture_event(event);
179+
}
180+
#[cfg(feature = "logs")]
181+
RecordMapping::Log(log) => {
182+
sentry_core::Hub::with_active(|hub| hub.capture_log(log))
183+
}
156184
}
157-
#[cfg(feature = "logs")]
158-
RecordMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
159185
}
160186

161187
self.dest.log(record)

sentry-tracing/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ tracing-subscriber = { version = "0.3.20", default-features = false, features =
2929
"std",
3030
] }
3131
sentry-backtrace = { version = "0.43.0", path = "../sentry-backtrace", optional = true }
32-
bitflags = "2.0.0"
32+
bitflags = "2.9.4"
3333

3434
[dev-dependencies]
3535
log = "0.4"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#![cfg(feature = "test")]
2+
3+
// Test `log` integration with combined `LogFilter`s.
4+
// This must be in a separate file because `log::set_boxed_logger` can only be called once.
5+
6+
#[test]
7+
fn test_log_combined_filters() {
8+
let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() {
9+
log::Level::Error => sentry_log::LogFilter::Breadcrumb | sentry_log::LogFilter::Event,
10+
log::Level::Warn => sentry_log::LogFilter::Event,
11+
_ => sentry_log::LogFilter::Ignore,
12+
});
13+
14+
log::set_boxed_logger(Box::new(logger))
15+
.map(|()| log::set_max_level(log::LevelFilter::Trace))
16+
.unwrap();
17+
18+
let events = sentry::test::with_captured_events(|| {
19+
log::error!("Both a breadcrumb and an event");
20+
log::warn!("An event");
21+
log::trace!("Ignored");
22+
});
23+
24+
assert_eq!(events.len(), 2);
25+
26+
assert_eq!(
27+
events[0].message,
28+
Some("Both a breadcrumb and an event".to_owned())
29+
);
30+
31+
assert_eq!(events[1].message, Some("An event".to_owned()));
32+
assert_eq!(events[1].breadcrumbs.len(), 1);
33+
assert_eq!(
34+
events[1].breadcrumbs[0].message,
35+
Some("Both a breadcrumb and an event".into())
36+
);
37+
}

0 commit comments

Comments
 (0)