sentry_log/
logger.rs

1use log::Record;
2use sentry_core::protocol::{Breadcrumb, Event};
3
4use crate::converters::{breadcrumb_from_record, event_from_record, exception_from_record};
5
6/// The action that Sentry should perform for a [`log::Metadata`].
7#[derive(Debug)]
8pub enum LogFilter {
9    /// Ignore the [`Record`].
10    Ignore,
11    /// Create a [`Breadcrumb`] from this [`Record`].
12    Breadcrumb,
13    /// Create a message [`Event`] from this [`Record`].
14    Event,
15    /// Create an exception [`Event`] from this [`Record`].
16    Exception,
17}
18
19/// The type of Data Sentry should ingest for a [`log::Record`].
20#[derive(Debug)]
21#[allow(clippy::large_enum_variant)]
22pub enum RecordMapping {
23    /// Ignore the [`Record`].
24    Ignore,
25    /// Adds the [`Breadcrumb`] to the Sentry scope.
26    Breadcrumb(Breadcrumb),
27    /// Captures the [`Event`] to Sentry.
28    Event(Event<'static>),
29}
30
31/// The default log filter.
32///
33/// By default, an exception event is captured for `error`, a breadcrumb for
34/// `warning` and `info`, and `debug` and `trace` logs are ignored.
35pub fn default_filter(metadata: &log::Metadata) -> LogFilter {
36    match metadata.level() {
37        log::Level::Error => LogFilter::Exception,
38        log::Level::Warn | log::Level::Info => LogFilter::Breadcrumb,
39        log::Level::Debug | log::Level::Trace => LogFilter::Ignore,
40    }
41}
42
43/// A noop [`log::Log`] that just ignores everything.
44#[derive(Debug, Default)]
45pub struct NoopLogger;
46
47impl log::Log for NoopLogger {
48    fn enabled(&self, metadata: &log::Metadata) -> bool {
49        let _ = metadata;
50        false
51    }
52
53    fn log(&self, record: &log::Record) {
54        let _ = record;
55    }
56
57    fn flush(&self) {
58        todo!()
59    }
60}
61
62/// Provides a dispatching logger.
63//#[derive(Debug)]
64pub struct SentryLogger<L: log::Log> {
65    dest: L,
66    filter: Box<dyn Fn(&log::Metadata<'_>) -> LogFilter + Send + Sync>,
67    #[allow(clippy::type_complexity)]
68    mapper: Option<Box<dyn Fn(&Record<'_>) -> RecordMapping + Send + Sync>>,
69}
70
71impl Default for SentryLogger<NoopLogger> {
72    fn default() -> Self {
73        Self {
74            dest: NoopLogger,
75            filter: Box::new(default_filter),
76            mapper: None,
77        }
78    }
79}
80
81impl SentryLogger<NoopLogger> {
82    /// Create a new SentryLogger with a [`NoopLogger`] as destination.
83    pub fn new() -> Self {
84        Default::default()
85    }
86}
87
88impl<L: log::Log> SentryLogger<L> {
89    /// Create a new SentryLogger wrapping a destination [`log::Log`].
90    pub fn with_dest(dest: L) -> Self {
91        Self {
92            dest,
93            filter: Box::new(default_filter),
94            mapper: None,
95        }
96    }
97
98    /// Sets a custom filter function.
99    ///
100    /// The filter classifies how sentry should handle [`Record`]s based on
101    /// their [`log::Metadata`].
102    #[must_use]
103    pub fn filter<F>(mut self, filter: F) -> Self
104    where
105        F: Fn(&log::Metadata<'_>) -> LogFilter + Send + Sync + 'static,
106    {
107        self.filter = Box::new(filter);
108        self
109    }
110
111    /// Sets a custom mapper function.
112    ///
113    /// The mapper is responsible for creating either breadcrumbs or events
114    /// from [`Record`]s.
115    #[must_use]
116    pub fn mapper<M>(mut self, mapper: M) -> Self
117    where
118        M: Fn(&Record<'_>) -> RecordMapping + Send + Sync + 'static,
119    {
120        self.mapper = Some(Box::new(mapper));
121        self
122    }
123}
124
125impl<L: log::Log> log::Log for SentryLogger<L> {
126    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
127        self.dest.enabled(metadata) || !matches!((self.filter)(metadata), LogFilter::Ignore)
128    }
129
130    fn log(&self, record: &log::Record<'_>) {
131        let item: RecordMapping = match &self.mapper {
132            Some(mapper) => mapper(record),
133            None => match (self.filter)(record.metadata()) {
134                LogFilter::Ignore => RecordMapping::Ignore,
135                LogFilter::Breadcrumb => RecordMapping::Breadcrumb(breadcrumb_from_record(record)),
136                LogFilter::Event => RecordMapping::Event(event_from_record(record)),
137                LogFilter::Exception => RecordMapping::Event(exception_from_record(record)),
138            },
139        };
140
141        match item {
142            RecordMapping::Ignore => {}
143            RecordMapping::Breadcrumb(b) => sentry_core::add_breadcrumb(b),
144            RecordMapping::Event(e) => {
145                sentry_core::capture_event(e);
146            }
147        }
148
149        self.dest.log(record)
150    }
151
152    fn flush(&self) {
153        self.dest.flush()
154    }
155}