Skip to content

feat(tracing): support combined EventFilters and EventMappings #847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

### Breaking changes

- feat(tracing): support combined EventFilters and EventMappings (#847) by @lcian
- `EventFilter` has been changed to a `bitflags` struct.
- It's now possible to map a `tracing` event to multiple items in Sentry by combining multiple event filters in the `event_filter`, e.g. `tracing::Level::ERROR => EventFilter::Event | EventFilter::Log`.
- It's also possible to use `EventMapping::Combined` to map a `tracing` event to multiple items in Sentry.
- `ctx` in the signatures of `event_from_event`, `breadcrumb_from_event` and `log_from_event` has been changed to take `impl Into<Option<&'context Context<'context, S>>>` to avoid cloning the `Context` when mapping to multiple items.

### Fixes

- fix(logs): stringify u64 attributes greater than `i64::MAX` (#846) by @lcian
Expand Down
27 changes: 14 additions & 13 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-tracing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ tracing-subscriber = { version = "0.3.1", default-features = false, features = [
"std",
] }
sentry-backtrace = { version = "0.40.0", path = "../sentry-backtrace", optional = true }
bitflags = "2"

[dev-dependencies]
log = "0.4"
Expand Down
8 changes: 4 additions & 4 deletions sentry-tracing/src/converters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ fn extract_event_data(
/// Extracts the message and metadata from an event, including the data in the current span.
fn extract_event_data_with_context<S>(
event: &tracing_core::Event,
ctx: Option<Context<S>>,
ctx: Option<&Context<S>>,
store_errors_in_values: bool,
) -> (Option<String>, FieldVisitor)
where
Expand Down Expand Up @@ -182,7 +182,7 @@ impl Visit for FieldVisitor {
/// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`].
pub fn breadcrumb_from_event<'context, S>(
event: &tracing_core::Event,
ctx: impl Into<Option<Context<'context, S>>>,
ctx: impl Into<Option<&'context Context<'context, S>>>,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking change here and in the 2 functions below.
This could be avoided by cloning the Context in on_event.

) -> Breadcrumb
where
S: Subscriber + for<'a> LookupSpan<'a>,
Expand Down Expand Up @@ -261,7 +261,7 @@ fn contexts_from_event(
/// Creates an [`Event`] (possibly carrying exceptions) from a given [`tracing_core::Event`].
pub fn event_from_event<'context, S>(
event: &tracing_core::Event,
ctx: impl Into<Option<Context<'context, S>>>,
ctx: impl Into<Option<&'context Context<'context, S>>>,
) -> Event<'static>
where
S: Subscriber + for<'a> LookupSpan<'a>,
Expand Down Expand Up @@ -329,7 +329,7 @@ where
#[cfg(feature = "logs")]
pub fn log_from_event<'context, S>(
event: &tracing_core::Event,
ctx: impl Into<Option<Context<'context, S>>>,
ctx: impl Into<Option<&'context Context<'context, S>>>,
) -> Log
where
S: Subscriber + for<'a> LookupSpan<'a>,
Expand Down
97 changes: 67 additions & 30 deletions sentry-tracing/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::cell::RefCell;
use std::collections::BTreeMap;
use std::sync::Arc;

use bitflags::bitflags;
use sentry_core::protocol::Value;
use sentry_core::{Breadcrumb, TransactionOrSpan};
use tracing_core::field::Visit;
Expand All @@ -13,21 +14,22 @@ use tracing_subscriber::registry::LookupSpan;
use crate::converters::*;
use crate::TAGS_PREFIX;

/// The action that Sentry should perform for a given [`Event`]
#[derive(Debug, Clone, Copy)]
pub enum EventFilter {
/// Ignore the [`Event`]
Ignore,
/// Create a [`Breadcrumb`] from this [`Event`]
Breadcrumb,
/// Create a [`sentry_core::protocol::Event`] from this [`Event`]
Event,
/// Create a [`sentry_core::protocol::Log`] from this [`Event`]
#[cfg(feature = "logs")]
Log,
bitflags! {
Copy link
Member Author

@lcian lcian Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking change because we switch from enum to struct.
This could be avoided by adding a Combined enum variant and possibly implementing BitOr on EventFilter if we want the same syntax that bitflags offers.

/// The action that Sentry should perform for a given [`Event`]
#[derive(Debug, Clone, Copy)]
pub struct EventFilter: u32 {
/// Ignore the [`Event`]
const Ignore = 0b000;
/// Create a [`Breadcrumb`] from this [`Event`]
const Breadcrumb = 0b001;
/// Create a [`sentry_core::protocol::Event`] from this [`Event`]
const Event = 0b010;
/// Create a [`sentry_core::protocol::Log`] from this [`Event`]
const Log = 0b100;
}
}

/// The type of data Sentry should ingest for a [`Event`]
/// The type of data Sentry should ingest for an [`Event`].
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum EventMapping {
Expand All @@ -40,6 +42,27 @@ pub enum EventMapping {
/// Captures the [`sentry_core::protocol::Log`] to Sentry.
#[cfg(feature = "logs")]
Log(sentry_core::protocol::Log),
/// Captures multiple items to Sentry.
Combined(CombinedEventMapping),
}

/// A list of event mappings.
#[derive(Debug)]
pub struct CombinedEventMapping(Vec<EventMapping>);

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

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

/// The default event filter.
Expand Down Expand Up @@ -211,30 +234,44 @@ where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &Event, ctx: Context<'_, S>) {
let item = match &self.event_mapper {
let items = match &self.event_mapper {
Some(mapper) => mapper(event, ctx),
None => {
let span_ctx = self.with_span_attributes.then_some(ctx);
match (self.event_filter)(event.metadata()) {
EventFilter::Ignore => EventMapping::Ignore,
EventFilter::Breadcrumb => {
EventMapping::Breadcrumb(breadcrumb_from_event(event, span_ctx))
}
EventFilter::Event => EventMapping::Event(event_from_event(event, span_ctx)),
#[cfg(feature = "logs")]
EventFilter::Log => EventMapping::Log(log_from_event(event, span_ctx)),
let filter = (self.event_filter)(event.metadata());
let mut items = vec![];
if filter.contains(EventFilter::Breadcrumb) {
items.push(EventMapping::Breadcrumb(breadcrumb_from_event(
event,
span_ctx.as_ref(),
)));
}
if filter.contains(EventFilter::Event) {
items.push(EventMapping::Event(event_from_event(
event,
span_ctx.as_ref(),
)));
}
#[cfg(feature = "logs")]
if filter.contains(EventFilter::Log) {
items.push(EventMapping::Log(log_from_event(event, span_ctx.as_ref())));
}
EventMapping::Combined(CombinedEventMapping(items))
}
};

match item {
EventMapping::Event(event) => {
sentry_core::capture_event(event);
let items = CombinedEventMapping::from(items);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I convert to CombinedEventMapping here so that below I can just handle it with the for loop instead of handling single and combined separately.


for item in items.0 {
match item {
EventMapping::Ignore => (),
EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
EventMapping::Event(event) => {
sentry_core::capture_event(event);
}
#[cfg(feature = "logs")]
EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
_ => (),
}
EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
#[cfg(feature = "logs")]
EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
_ => (),
}
}

Expand Down
81 changes: 81 additions & 0 deletions sentry/tests/test_tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,84 @@ fn test_tracing_logs() {
_ => panic!("expected item container"),
}
}

#[test]
fn test_combined_event_filters() {
let sentry_layer = sentry_tracing::layer().event_filter(|md| match *md.level() {
tracing::Level::ERROR => {
sentry_tracing::EventFilter::Breadcrumb | sentry_tracing::EventFilter::Event
}
tracing::Level::WARN => sentry_tracing::EventFilter::Event,
_ => sentry_tracing::EventFilter::Ignore,
});

let _dispatcher = tracing_subscriber::registry()
.with(sentry_layer)
.set_default();

let events = sentry::test::with_captured_events(|| {
tracing::error!("Both a breadcrumb and an event");
tracing::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_event_mapper() {
let sentry_layer =
sentry_tracing::layer().event_mapper(|event, ctx| match *event.metadata().level() {
tracing::Level::ERROR => {
let breadcrumb = sentry_tracing::breadcrumb_from_event(event, Some(&ctx));
let sentry_event = sentry_tracing::event_from_event(event, Some(&ctx));

sentry_tracing::EventMapping::Combined(
vec![
sentry_tracing::EventMapping::Breadcrumb(breadcrumb),
sentry_tracing::EventMapping::Event(sentry_event),
]
.into(),
)
}
tracing::Level::WARN => {
let sentry_event = sentry_tracing::event_from_event(event, Some(&ctx));
sentry_tracing::EventMapping::Event(sentry_event)
}
_ => sentry_tracing::EventMapping::Ignore,
});

let _dispatcher = tracing_subscriber::registry()
.with(sentry_layer)
.set_default();

let events = sentry::test::with_captured_events(|| {
tracing::error!("Both a breadcrumb and an event");
tracing::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())
);
}