Skip to main content

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