Skip to content

Commit 7ee7f6e

Browse files
authored
feat: Add a slog Integration (#217)
This Integration consists of both an `Integration` part, as well as a `Drain` part. The Integration can be configured with custom filters and mappers.
1 parent 1df48f0 commit 7ee7f6e

File tree

7 files changed

+349
-0
lines changed

7 files changed

+349
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ members = [
1111
"sentry-failure",
1212
"sentry-log",
1313
"sentry-panic",
14+
"sentry-slog",
1415
"sentry-types",
1516
]

rustfmt.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Uncomment and use this with `cargo +nightly fmt`:
2+
# unstable_features = true
3+
# format_code_in_doc_comments = true

sentry-slog/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "sentry-slog"
3+
version = "0.18.0"
4+
authors = ["Sentry <hello@sentry.io>"]
5+
license = "Apache-2.0"
6+
readme = "README.md"
7+
repository = "https://github.com/getsentry/sentry-rust"
8+
homepage = "https://github.com/getsentry/sentry-rust"
9+
documentation = "https://getsentry.github.io/sentry-rust"
10+
description = """
11+
Sentry Integration for slog
12+
"""
13+
edition = "2018"
14+
15+
[dependencies]
16+
sentry-core = { version = "0.18.0", path = "../sentry-core" }
17+
slog = "2.5.2"
18+
19+
[dev-dependencies]
20+
sentry = { version = "0.18.0", path = "../sentry", features = ["with_test_support"] }

sentry-slog/src/converters.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use sentry_core::protocol::{Breadcrumb, Event, Exception, Frame, Level, Map, Stacktrace, Value};
2+
use slog::{OwnedKVList, Record, KV};
3+
4+
/// Converts a `slog::Level` to a sentry `Level`
5+
pub fn convert_log_level(level: slog::Level) -> Level {
6+
match level {
7+
slog::Level::Trace | slog::Level::Debug => Level::Debug,
8+
slog::Level::Info => Level::Info,
9+
slog::Level::Warning => Level::Warning,
10+
slog::Level::Error | slog::Level::Critical => Level::Error,
11+
}
12+
}
13+
14+
/// Adds the data from a `slog::KV` into a sentry `Map`.
15+
fn add_kv_to_map(map: &mut Map<String, Value>, kv: &impl KV) {
16+
let _ = (map, kv);
17+
// TODO: actually implement this ;-)
18+
}
19+
20+
/// Creates a sentry `Breadcrumb` from the `slog::Record`.
21+
pub fn breadcrumb_from_record(record: &Record, values: &OwnedKVList) -> Breadcrumb {
22+
let mut data = Map::new();
23+
add_kv_to_map(&mut data, &record.kv());
24+
add_kv_to_map(&mut data, values);
25+
26+
Breadcrumb {
27+
ty: "log".into(),
28+
message: Some(record.msg().to_string()),
29+
level: convert_log_level(record.level()),
30+
data,
31+
..Default::default()
32+
}
33+
}
34+
35+
/// Creates a simple message `Event` from the `slog::Record`.
36+
pub fn event_from_record(record: &Record, values: &OwnedKVList) -> Event<'static> {
37+
let mut extra = Map::new();
38+
add_kv_to_map(&mut extra, &record.kv());
39+
add_kv_to_map(&mut extra, values);
40+
Event {
41+
message: Some(record.msg().to_string()),
42+
level: convert_log_level(record.level()),
43+
..Default::default()
44+
}
45+
}
46+
47+
/// Creates an exception `Event` from the `slog::Record`.
48+
///
49+
/// The exception will have a stacktrace that corresponds to the location
50+
/// information contained in the `slog::Record`.
51+
///
52+
/// # Examples
53+
///
54+
/// ```
55+
/// let args = format_args!("");
56+
/// let record = slog::record!(slog::Level::Error, "", &args, slog::b!());
57+
/// let kv = slog::o!().into();
58+
/// let event = sentry_slog::exception_from_record(&record, &kv);
59+
///
60+
/// let frame = &event.exception.as_ref()[0]
61+
/// .stacktrace
62+
/// .as_ref()
63+
/// .unwrap()
64+
/// .frames[0];
65+
/// assert!(frame.lineno.unwrap() > 0);
66+
/// ```
67+
pub fn exception_from_record(record: &Record, values: &OwnedKVList) -> Event<'static> {
68+
let mut event = event_from_record(record, values);
69+
let frame = Frame {
70+
function: Some(record.function().into()),
71+
module: Some(record.module().into()),
72+
filename: Some(record.file().into()),
73+
lineno: Some(record.line().into()),
74+
colno: Some(record.column().into()),
75+
..Default::default()
76+
};
77+
let exception = Exception {
78+
ty: "slog::Record".into(),
79+
stacktrace: Some(Stacktrace {
80+
frames: vec![frame],
81+
..Default::default()
82+
}),
83+
..Default::default()
84+
};
85+
event.exception = vec![exception].into();
86+
event
87+
}

sentry-slog/src/drain.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use crate::SlogIntegration;
2+
use sentry_core::Hub;
3+
use slog::{Drain, OwnedKVList, Record};
4+
5+
/// A Drain which passes all Records to sentry.
6+
pub struct SentryDrain<D: Drain> {
7+
drain: D,
8+
}
9+
10+
impl<D: Drain> SentryDrain<D> {
11+
/// Creates a new `SentryDrain`, wrapping a `slog::Drain`.
12+
pub fn new(drain: D) -> Self {
13+
Self { drain }
14+
}
15+
}
16+
17+
// TODO: move this into `sentry-core`, as this is generally useful for more
18+
// integrations.
19+
fn with_integration<F, R>(f: F) -> R
20+
where
21+
F: Fn(&Hub, &SlogIntegration) -> R,
22+
R: Default,
23+
{
24+
Hub::with_active(|hub| hub.with_integration(|integration| f(hub, integration)))
25+
}
26+
27+
impl<D: Drain> slog::Drain for SentryDrain<D> {
28+
type Ok = D::Ok;
29+
type Err = D::Err;
30+
31+
fn log(&self, record: &Record, values: &OwnedKVList) -> Result<Self::Ok, Self::Err> {
32+
with_integration(|hub, integration| integration.log(hub, record, values));
33+
self.drain.log(record, values)
34+
}
35+
36+
fn is_enabled(&self, level: slog::Level) -> bool {
37+
with_integration(|_, integration| integration.is_enabled(level))
38+
|| self.drain.is_enabled(level)
39+
}
40+
}

sentry-slog/src/integration.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use sentry_core::protocol::{Breadcrumb, Event};
2+
use sentry_core::{Hub, Integration};
3+
use slog::{OwnedKVList, Record};
4+
5+
use crate::{breadcrumb_from_record, event_from_record, exception_from_record};
6+
7+
/// The Action that Sentry should perform for a `slog::Level`.
8+
pub enum LevelFilter {
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+
/// Custom Mappers
20+
#[allow(clippy::large_enum_variant)]
21+
pub enum RecordMapping {
22+
/// Adds the `Breadcrumb` to the sentry scope.
23+
Breadcrumb(Breadcrumb),
24+
/// Captures the `Event` to sentry.
25+
Event(Event<'static>),
26+
}
27+
28+
/// The default slog filter.
29+
///
30+
/// By default, an exception event is captured for `critical` logs,
31+
/// a regular event for `error` and `warning` logs, and breadcrumbs for
32+
/// everything else.
33+
pub fn default_filter(level: slog::Level) -> LevelFilter {
34+
match level {
35+
slog::Level::Critical => LevelFilter::Exception,
36+
slog::Level::Error | slog::Level::Warning => LevelFilter::Event,
37+
slog::Level::Info | slog::Level::Debug | slog::Level::Trace => LevelFilter::Breadcrumb,
38+
}
39+
}
40+
41+
/// The Sentry `slog` Integration.
42+
///
43+
/// Can be configured with a custom filter and mapper.
44+
pub struct SlogIntegration {
45+
filter: Box<dyn Fn(slog::Level) -> LevelFilter + Send + Sync>,
46+
mapper: Option<Box<dyn Fn(&Record, &OwnedKVList) -> RecordMapping + Send + Sync>>,
47+
}
48+
49+
impl Default for SlogIntegration {
50+
fn default() -> Self {
51+
Self {
52+
filter: Box::new(default_filter),
53+
mapper: None,
54+
}
55+
}
56+
}
57+
58+
impl SlogIntegration {
59+
/// Create a new `slog` Integration.
60+
pub fn new() -> Self {
61+
Self::default()
62+
}
63+
64+
/// Sets a custom filter function.
65+
///
66+
/// The filter classifies how sentry should handle `slog::Record`s based on
67+
/// their level.
68+
pub fn filter<F>(mut self, filter: F) -> Self
69+
where
70+
F: Fn(slog::Level) -> LevelFilter + Send + Sync + 'static,
71+
{
72+
self.filter = Box::new(filter);
73+
self
74+
}
75+
76+
/// Sets a custom mapper function.
77+
///
78+
/// The mapper is responsible for creating either breadcrumbs or events
79+
/// from `slog::Record`s.
80+
pub fn mapper<M>(mut self, mapper: M) -> Self
81+
where
82+
M: Fn(&Record, &OwnedKVList) -> RecordMapping + Send + Sync + 'static,
83+
{
84+
self.mapper = Some(Box::new(mapper));
85+
self
86+
}
87+
88+
pub(crate) fn log(&self, hub: &Hub, record: &Record, values: &OwnedKVList) {
89+
let item: RecordMapping = match &self.mapper {
90+
Some(mapper) => mapper(record, values),
91+
None => match (self.filter)(record.level()) {
92+
LevelFilter::Ignore => return,
93+
LevelFilter::Breadcrumb => {
94+
RecordMapping::Breadcrumb(breadcrumb_from_record(record, values))
95+
}
96+
LevelFilter::Event => RecordMapping::Event(event_from_record(record, values)),
97+
LevelFilter::Exception => {
98+
RecordMapping::Event(exception_from_record(record, values))
99+
}
100+
},
101+
};
102+
match item {
103+
RecordMapping::Breadcrumb(b) => hub.add_breadcrumb(b),
104+
RecordMapping::Event(e) => {
105+
hub.capture_event(e);
106+
}
107+
}
108+
}
109+
110+
pub(crate) fn is_enabled(&self, level: slog::Level) -> bool {
111+
match (self.filter)(level) {
112+
LevelFilter::Ignore => false,
113+
_ => true,
114+
}
115+
}
116+
}
117+
118+
impl Integration for SlogIntegration {
119+
fn name(&self) -> &'static str {
120+
"slog"
121+
}
122+
}

sentry-slog/src/lib.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Sentry `slog` Integration.
2+
//!
3+
//! The sentry `slog` integration consists of two parts, the
4+
//! [`SlogIntegration`] which configures how sentry should treat
5+
//! `slog::Record`s, and the [`SentryDrain`], which can be used to create a
6+
//! `slog::Logger`.
7+
//!
8+
//! *NOTE*: This integration currently does not process any `slog::KV` pairs,
9+
//! but support for this will be added in the future.
10+
//!
11+
//! # Examples
12+
//!
13+
//! ```
14+
//! use sentry::{init, ClientOptions};
15+
//! use sentry_slog::{SentryDrain, SlogIntegration};
16+
//!
17+
//! let integration = SlogIntegration::default();
18+
//! let options = ClientOptions::default().add_integration(integration);
19+
//! let _sentry = sentry::init(options);
20+
//!
21+
//! let drain = SentryDrain::new(slog::Discard);
22+
//! let root = slog::Logger::root(drain, slog::o!());
23+
//!
24+
//! # let options = ClientOptions::default().add_integration(SlogIntegration::default());
25+
//! # let events = sentry::test::with_captured_events_options(|| {
26+
//! slog::info!(root, "recorded as breadcrumb");
27+
//! slog::warn!(root, "recorded as regular event");
28+
//! # }, options.clone());
29+
//! # let captured_event = events.into_iter().next().unwrap();
30+
//!
31+
//! assert_eq!(
32+
//! captured_event.breadcrumbs.as_ref()[0].message.as_deref(),
33+
//! Some("recorded as breadcrumb")
34+
//! );
35+
//! assert_eq!(
36+
//! captured_event.message.as_deref(),
37+
//! Some("recorded as regular event")
38+
//! );
39+
//!
40+
//! # let events = sentry::test::with_captured_events_options(|| {
41+
//! slog::crit!(root, "recorded as exception event");
42+
//! # }, options);
43+
//! # let captured_event = events.into_iter().next().unwrap();
44+
//!
45+
//! assert_eq!(captured_event.exception.len(), 1);
46+
//! ```
47+
//!
48+
//! The integration can also be customized with a `filter`, and a `mapper`:
49+
//!
50+
//! ```
51+
//! use sentry_slog::{exception_from_record, LevelFilter, RecordMapping, SlogIntegration};
52+
//!
53+
//! let integration = SlogIntegration::default()
54+
//! .filter(|level| match level {
55+
//! slog::Level::Critical | slog::Level::Error => LevelFilter::Event,
56+
//! _ => LevelFilter::Ignore,
57+
//! })
58+
//! .mapper(|record, kv| RecordMapping::Event(exception_from_record(record, kv)));
59+
//! ```
60+
//!
61+
//! Please not that the `mapper` can override any classification from the
62+
//! previous `filter`.
63+
//!
64+
//! [`SlogIntegration`]: struct.SlogIntegration.html
65+
//! [`SentryDrain`]: struct.SentryDrain.html
66+
67+
#![deny(missing_docs)]
68+
#![deny(unsafe_code)]
69+
70+
mod converters;
71+
mod drain;
72+
mod integration;
73+
74+
pub use converters::*;
75+
pub use drain::SentryDrain;
76+
pub use integration::{default_filter, LevelFilter, RecordMapping, SlogIntegration};

0 commit comments

Comments
 (0)