sentry_log/
logger.rs

1use log::Record;
2use sentry_core::protocol::{Breadcrumb, Event};
3
4use bitflags::bitflags;
5
6#[cfg(feature = "logs")]
7use crate::converters::log_from_record;
8use crate::converters::{breadcrumb_from_record, event_from_record, exception_from_record};
9
10bitflags! {
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    }
26}
27
28/// The type of Data Sentry should ingest for a [`log::Record`].
29#[derive(Debug)]
30#[allow(clippy::large_enum_variant)]
31pub enum RecordMapping {
32    /// Ignore the [`Record`].
33    Ignore,
34    /// Adds the [`Breadcrumb`] to the Sentry scope.
35    Breadcrumb(Breadcrumb),
36    /// Captures the [`Event`] to Sentry.
37    Event(Event<'static>),
38    /// Captures the [`sentry_core::protocol::Log`] to Sentry.
39    #[cfg(feature = "logs")]
40    Log(sentry_core::protocol::Log),
41}
42
43impl From<RecordMapping> for Vec<RecordMapping> {
44    fn from(mapping: RecordMapping) -> Self {
45        vec![mapping]
46    }
47}
48
49/// The default log filter.
50///
51/// By default, an exception event is captured for `error`, a breadcrumb for
52/// `warning` and `info`, and `debug` and `trace` logs are ignored.
53pub fn default_filter(metadata: &log::Metadata) -> LogFilter {
54    match metadata.level() {
55        #[cfg(feature = "logs")]
56        log::Level::Error => LogFilter::Exception | LogFilter::Log,
57        #[cfg(not(feature = "logs"))]
58        log::Level::Error => LogFilter::Exception,
59        #[cfg(feature = "logs")]
60        log::Level::Warn | log::Level::Info => LogFilter::Breadcrumb | LogFilter::Log,
61        #[cfg(not(feature = "logs"))]
62        log::Level::Warn | log::Level::Info => LogFilter::Breadcrumb,
63        log::Level::Debug | log::Level::Trace => LogFilter::Ignore,
64    }
65}
66
67/// A noop [`log::Log`] that just ignores everything.
68#[derive(Debug, Default)]
69pub struct NoopLogger;
70
71impl log::Log for NoopLogger {
72    fn enabled(&self, metadata: &log::Metadata) -> bool {
73        let _ = metadata;
74        false
75    }
76
77    fn log(&self, record: &log::Record) {
78        let _ = record;
79    }
80
81    fn flush(&self) {
82        todo!()
83    }
84}
85
86/// Provides a dispatching logger.
87//#[derive(Debug)]
88pub struct SentryLogger<L: log::Log> {
89    dest: L,
90    filter: Box<dyn Fn(&log::Metadata<'_>) -> LogFilter + Send + Sync>,
91    #[allow(clippy::type_complexity)]
92    mapper: Option<Box<dyn Fn(&Record<'_>) -> Vec<RecordMapping> + Send + Sync>>,
93}
94
95impl Default for SentryLogger<NoopLogger> {
96    fn default() -> Self {
97        Self {
98            dest: NoopLogger,
99            filter: Box::new(default_filter),
100            mapper: None,
101        }
102    }
103}
104
105impl SentryLogger<NoopLogger> {
106    /// Create a new SentryLogger with a [`NoopLogger`] as destination.
107    pub fn new() -> Self {
108        Default::default()
109    }
110}
111
112impl<L: log::Log> SentryLogger<L> {
113    /// Create a new SentryLogger wrapping a destination [`log::Log`].
114    pub fn with_dest(dest: L) -> Self {
115        Self {
116            dest,
117            filter: Box::new(default_filter),
118            mapper: None,
119        }
120    }
121
122    /// Sets a custom filter function.
123    ///
124    /// The filter classifies how sentry should handle [`Record`]s based on
125    /// their [`log::Metadata`].
126    #[must_use]
127    pub fn filter<F>(mut self, filter: F) -> Self
128    where
129        F: Fn(&log::Metadata<'_>) -> LogFilter + Send + Sync + 'static,
130    {
131        self.filter = Box::new(filter);
132        self
133    }
134
135    /// Sets a custom mapper function.
136    ///
137    /// The mapper is responsible for creating either breadcrumbs or events
138    /// from [`Record`]s. It can return either a single [`RecordMapping`] or
139    /// a `Vec<RecordMapping>` to send multiple items to Sentry from one log record.
140    #[must_use]
141    pub fn mapper<M, T>(mut self, mapper: M) -> Self
142    where
143        M: Fn(&Record<'_>) -> T + Send + Sync + 'static,
144        T: Into<Vec<RecordMapping>>,
145    {
146        self.mapper = Some(Box::new(move |record| mapper(record).into()));
147        self
148    }
149}
150
151impl<L: log::Log> log::Log for SentryLogger<L> {
152    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
153        self.dest.enabled(metadata) || !((self.filter)(metadata) == LogFilter::Ignore)
154    }
155
156    fn log(&self, record: &log::Record<'_>) {
157        let items = match &self.mapper {
158            Some(mapper) => mapper(record),
159            None => {
160                let filter = (self.filter)(record.metadata());
161                let mut items = vec![];
162                if filter.contains(LogFilter::Breadcrumb) {
163                    items.push(RecordMapping::Breadcrumb(breadcrumb_from_record(record)));
164                }
165                if filter.contains(LogFilter::Event) {
166                    items.push(RecordMapping::Event(event_from_record(record)));
167                }
168                if filter.contains(LogFilter::Exception) {
169                    items.push(RecordMapping::Event(exception_from_record(record)));
170                }
171                #[cfg(feature = "logs")]
172                if filter.contains(LogFilter::Log) {
173                    items.push(RecordMapping::Log(log_from_record(record)));
174                }
175                items
176            }
177        };
178
179        for mapping in items {
180            match mapping {
181                RecordMapping::Ignore => {}
182                RecordMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
183                RecordMapping::Event(event) => {
184                    sentry_core::capture_event(event);
185                }
186                #[cfg(feature = "logs")]
187                RecordMapping::Log(log) => {
188                    sentry_core::Hub::with_active(|hub| hub.capture_log(log))
189                }
190            }
191        }
192
193        self.dest.log(record)
194    }
195
196    fn flush(&self) {
197        self.dest.flush()
198    }
199}