sentry_log/
logger.rs

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