Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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`.
- 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
Expand Down
1 change: 1 addition & 0 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.0.0"

[dev-dependencies]
sentry = { path = "../sentry", default-features = false, features = ["test"] }
Expand Down
17 changes: 17 additions & 0 deletions sentry-log/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
109 changes: 78 additions & 31 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 @@ -34,6 +38,29 @@ 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(CombinedRecordMapping),
}

/// A list of record mappings.
#[derive(Debug)]
pub struct CombinedRecordMapping(Vec<RecordMapping>);

impl From<RecordMapping> for CombinedRecordMapping {
fn from(value: RecordMapping) -> Self {
match value {
RecordMapping::Combined(combined) => combined,
_ => CombinedRecordMapping(vec![value]),
}
}
}

impl From<Vec<RecordMapping>> for CombinedRecordMapping {
fn from(value: Vec<RecordMapping>) -> Self {
Self(value)
}
}

/// The default log filter.
Expand Down Expand Up @@ -132,30 +159,50 @@ impl<L: log::Log> SentryLogger<L> {

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: RecordMapping = 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)
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())
);
}