sentry_log4rs/
lib.rs

1//! **sentry-log4rs** is a simple add-on for the **log4rs** logging framework to simplify
2//! integration with the **Sentry** application monitoring system.
3//!
4//! Example usage
5//! =============
6//!
7//! ```no_run
8//! use log::error;
9//! use log4rs;
10//! use sentry_log4rs::SentryAppender;
11//!
12//! fn main() {
13//!     log4rs::init_file("log4rs.yaml", SentryAppender::deserializers()).unwrap();
14//!     error!("Something went wrong!");
15//! }
16//! ```
17//!
18//! `log4rs.yaml` file:
19//! ```yaml
20//! appenders:
21//!   sentry:
22//!     kind: sentry
23//!     encoder:
24//!       pattern: "{m}"
25//!     dsn: "YOUR_SENTRY_DSN_HERE"
26//!     threshold: error
27//!
28//! root:
29//!   appenders:
30//!     - sentry
31//! ```
32//!
33//! You can also constructing the configuration programmatically without using a config file:
34//!
35//! ```no_run
36//! use log::{LevelFilter, error};
37//! use log4rs::{
38//!     config::{Appender, Config, Root},
39//!     encode::pattern::PatternEncoder,
40//! };
41//! use sentry_log4rs::SentryAppender;
42//!
43//! fn main() {
44//!     let sentry = SentryAppender::builder()
45//!         .dsn("YOUR_SENTRY_DSN_HERE")
46//!         .threshold(LevelFilter::Error)
47//!         .encoder(Box::new(PatternEncoder::new("{m}")))
48//!         .build();
49//!
50//!     let config = Config::builder()
51//!         .appender(Appender::builder().build("sentry", Box::new(sentry)))
52//!         .build(
53//!             Root::builder()
54//!                 .appender("sentry")
55//!                 .build(LevelFilter::Info),
56//!         )
57//!         .unwrap();
58//!
59//!     log4rs::init_config(config).unwrap();
60//!
61//!     error!("Something went wrong!");
62//! }
63//! ```
64extern crate log;
65extern crate log4rs;
66extern crate sentry;
67
68use derivative::Derivative;
69use log::{Level, LevelFilter, Record};
70use log4rs::{
71    append::Append,
72    config::{Deserialize, Deserializers},
73    encode::{pattern::PatternEncoder, writer::simple::SimpleWriter, Encode, EncoderConfig},
74};
75use sentry::{
76    protocol::value::{Number, Value},
77    ClientInitGuard, Level as SentryLevel,
78};
79
80/// Configuration for the sentry appender.
81#[derive(Clone, Eq, PartialEq, Hash, Debug, serde::Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct SentryAppenderConfig {
84    dsn: String,
85    encoder: Option<EncoderConfig>,
86    threshold: LevelFilter,
87}
88
89/// An appender which send log message to sentry.
90#[derive(Derivative)]
91#[derivative(Debug)]
92pub struct SentryAppender {
93    #[derivative(Debug = "ignore")]
94    _sentry: ClientInitGuard,
95    encoder: Box<dyn Encode>,
96    threshold: LevelFilter,
97}
98
99impl SentryAppender {
100    /// Creates a new `SentryAppender` builder.
101    pub fn builder() -> SentryAppenderBuilder {
102        SentryAppenderBuilder {
103            encoder: None,
104            dsn: String::default(),
105            threshold: None,
106        }
107    }
108
109    /// Creates a `Deserializers` with sentry appender mapping and the default log4rs mappings.
110    ///  * Appenders
111    ///     * "sentry" -> `SentryAppenderDeserializer`
112    ///  * log4rs default mappings.
113    pub fn deserializers() -> Deserializers {
114        let mut deserializers = Deserializers::new();
115        deserializers.insert("sentry", SentryAppenderDeserializer);
116        deserializers
117    }
118}
119
120impl Append for SentryAppender {
121    fn append(&self, record: &Record) -> anyhow::Result<()> {
122        if record.level() > self.threshold {
123            // Don't send records to sentry if record's level greater than the user defined threshold.
124            // e.g. Info > Error
125            return Ok(());
126        }
127
128        let level = level_mapping(record.level());
129
130        let mut buf: Vec<u8> = Vec::new();
131        self.encoder.encode(&mut SimpleWriter(&mut buf), record)?;
132        let msg = String::from_utf8(buf)?;
133
134        let mut event = sentry::protocol::Event::new();
135        event.level = level;
136        event.message = Some(msg);
137        event.logger = Some(record.metadata().target().to_owned());
138
139        if let Some(file) = record.file() {
140            event
141                .extra
142                .insert("file".to_owned(), Value::String(file.to_owned()));
143        }
144
145        if let Some(line) = record.line() {
146            event
147                .extra
148                .insert("line".to_owned(), Value::Number(Number::from(line)));
149        }
150
151        if let Some(module_path) = record.module_path() {
152            event
153                .tags
154                .insert("module_path".to_owned(), module_path.to_owned());
155        }
156
157        sentry::capture_event(event);
158        Ok(())
159    }
160
161    fn flush(&self) {}
162}
163
164/// A builder for `SentryAppender`s.
165pub struct SentryAppenderBuilder {
166    encoder: Option<Box<dyn Encode>>,
167    dsn: String,
168    threshold: Option<LevelFilter>,
169}
170
171impl SentryAppenderBuilder {
172    pub fn encoder(mut self, encoder: Box<dyn Encode>) -> SentryAppenderBuilder {
173        self.encoder = Some(encoder);
174        self
175    }
176
177    pub fn dsn(mut self, dsn: &str) -> SentryAppenderBuilder {
178        self.dsn = dsn.to_string();
179        self
180    }
181
182    fn dsn_string(mut self, dsn: String) -> SentryAppenderBuilder {
183        self.dsn = dsn;
184        self
185    }
186
187    pub fn threshold(mut self, threshold: LevelFilter) -> SentryAppenderBuilder {
188        self.threshold = Some(threshold);
189        self
190    }
191
192    pub fn build(self) -> SentryAppender {
193        let _sentry: ClientInitGuard = sentry::init(self.dsn);
194        SentryAppender {
195            _sentry,
196            encoder: self
197                .encoder
198                .unwrap_or_else(|| Box::new(PatternEncoder::new("{m}"))),
199            threshold: self.threshold.unwrap_or(LevelFilter::Error),
200        }
201    }
202}
203
204/// A deserializer for the `SentryAppender`.
205///
206/// # Configuration
207///
208/// ```yaml
209/// kind: sentry
210///
211/// # The sentry DSN, e.g. "https://key@sentry.io/42"
212/// dsn: "YOUR_DSN_HERE"
213///
214/// # The log level threshold
215/// threshold: error  # overriding the logging threshold to the ERROR level
216///
217/// # The encoder to use to format output. Defaults to `kind: pattern`.
218/// encoder:
219///   kind: pattern
220/// ```
221#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
222pub struct SentryAppenderDeserializer;
223
224impl Deserialize for SentryAppenderDeserializer {
225    type Trait = dyn Append;
226
227    type Config = SentryAppenderConfig;
228
229    fn deserialize(
230        &self,
231        config: SentryAppenderConfig,
232        deserializers: &Deserializers,
233    ) -> anyhow::Result<Box<dyn Append>> {
234        let mut appender = SentryAppender::builder();
235
236        if let Some(encoder) = config.encoder {
237            appender = appender.encoder(deserializers.deserialize(&encoder.kind, encoder.config)?);
238        }
239
240        appender = appender.dsn_string(config.dsn);
241
242        appender = appender.threshold(config.threshold);
243
244        Ok(Box::new(appender.build()))
245    }
246}
247
248fn level_mapping(level: Level) -> SentryLevel {
249    match level {
250        Level::Error => SentryLevel::Error,
251        Level::Warn => SentryLevel::Warning,
252        Level::Info => SentryLevel::Info,
253        Level::Debug => SentryLevel::Debug,
254        Level::Trace => SentryLevel::Debug,
255    }
256}