diff --git a/Cargo.toml b/Cargo.toml index b1765327d..0f9f9640a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,6 @@ members = [ "sentry-failure", "sentry-log", "sentry-panic", + "sentry-slog", "sentry-types", ] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..0d9e5d2e4 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +# Uncomment and use this with `cargo +nightly fmt`: +# unstable_features = true +# format_code_in_doc_comments = true diff --git a/sentry-slog/Cargo.toml b/sentry-slog/Cargo.toml new file mode 100644 index 000000000..ccea223d8 --- /dev/null +++ b/sentry-slog/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "sentry-slog" +version = "0.18.0" +authors = ["Sentry "] +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/getsentry/sentry-rust" +homepage = "https://github.com/getsentry/sentry-rust" +documentation = "https://getsentry.github.io/sentry-rust" +description = """ +Sentry Integration for slog +""" +edition = "2018" + +[dependencies] +sentry-core = { version = "0.18.0", path = "../sentry-core" } +slog = "2.5.2" + +[dev-dependencies] +sentry = { version = "0.18.0", path = "../sentry", features = ["with_test_support"] } \ No newline at end of file diff --git a/sentry-slog/src/converters.rs b/sentry-slog/src/converters.rs new file mode 100644 index 000000000..3af2dd074 --- /dev/null +++ b/sentry-slog/src/converters.rs @@ -0,0 +1,87 @@ +use sentry_core::protocol::{Breadcrumb, Event, Exception, Frame, Level, Map, Stacktrace, Value}; +use slog::{OwnedKVList, Record, KV}; + +/// Converts a `slog::Level` to a sentry `Level` +pub fn convert_log_level(level: slog::Level) -> Level { + match level { + slog::Level::Trace | slog::Level::Debug => Level::Debug, + slog::Level::Info => Level::Info, + slog::Level::Warning => Level::Warning, + slog::Level::Error | slog::Level::Critical => Level::Error, + } +} + +/// Adds the data from a `slog::KV` into a sentry `Map`. +fn add_kv_to_map(map: &mut Map, kv: &impl KV) { + let _ = (map, kv); + // TODO: actually implement this ;-) +} + +/// Creates a sentry `Breadcrumb` from the `slog::Record`. +pub fn breadcrumb_from_record(record: &Record, values: &OwnedKVList) -> Breadcrumb { + let mut data = Map::new(); + add_kv_to_map(&mut data, &record.kv()); + add_kv_to_map(&mut data, values); + + Breadcrumb { + ty: "log".into(), + message: Some(record.msg().to_string()), + level: convert_log_level(record.level()), + data, + ..Default::default() + } +} + +/// Creates a simple message `Event` from the `slog::Record`. +pub fn event_from_record(record: &Record, values: &OwnedKVList) -> Event<'static> { + let mut extra = Map::new(); + add_kv_to_map(&mut extra, &record.kv()); + add_kv_to_map(&mut extra, values); + Event { + message: Some(record.msg().to_string()), + level: convert_log_level(record.level()), + ..Default::default() + } +} + +/// Creates an exception `Event` from the `slog::Record`. +/// +/// The exception will have a stacktrace that corresponds to the location +/// information contained in the `slog::Record`. +/// +/// # Examples +/// +/// ``` +/// let args = format_args!(""); +/// let record = slog::record!(slog::Level::Error, "", &args, slog::b!()); +/// let kv = slog::o!().into(); +/// let event = sentry_slog::exception_from_record(&record, &kv); +/// +/// let frame = &event.exception.as_ref()[0] +/// .stacktrace +/// .as_ref() +/// .unwrap() +/// .frames[0]; +/// assert!(frame.lineno.unwrap() > 0); +/// ``` +pub fn exception_from_record(record: &Record, values: &OwnedKVList) -> Event<'static> { + let mut event = event_from_record(record, values); + let frame = Frame { + function: Some(record.function().into()), + module: Some(record.module().into()), + filename: Some(record.file().into()), + lineno: Some(record.line().into()), + colno: Some(record.column().into()), + ..Default::default() + }; + let exception = Exception { + ty: "slog::Record".into(), + stacktrace: Some(Stacktrace { + frames: vec![frame], + ..Default::default() + }), + ..Default::default() + }; + event.exception = vec![exception].into(); + event +} diff --git a/sentry-slog/src/drain.rs b/sentry-slog/src/drain.rs new file mode 100644 index 000000000..33a864547 --- /dev/null +++ b/sentry-slog/src/drain.rs @@ -0,0 +1,40 @@ +use crate::SlogIntegration; +use sentry_core::Hub; +use slog::{Drain, OwnedKVList, Record}; + +/// A Drain which passes all Records to sentry. +pub struct SentryDrain { + drain: D, +} + +impl SentryDrain { + /// Creates a new `SentryDrain`, wrapping a `slog::Drain`. + pub fn new(drain: D) -> Self { + Self { drain } + } +} + +// TODO: move this into `sentry-core`, as this is generally useful for more +// integrations. +fn with_integration(f: F) -> R +where + F: Fn(&Hub, &SlogIntegration) -> R, + R: Default, +{ + Hub::with_active(|hub| hub.with_integration(|integration| f(hub, integration))) +} + +impl slog::Drain for SentryDrain { + type Ok = D::Ok; + type Err = D::Err; + + fn log(&self, record: &Record, values: &OwnedKVList) -> Result { + with_integration(|hub, integration| integration.log(hub, record, values)); + self.drain.log(record, values) + } + + fn is_enabled(&self, level: slog::Level) -> bool { + with_integration(|_, integration| integration.is_enabled(level)) + || self.drain.is_enabled(level) + } +} diff --git a/sentry-slog/src/integration.rs b/sentry-slog/src/integration.rs new file mode 100644 index 000000000..5d008c4f4 --- /dev/null +++ b/sentry-slog/src/integration.rs @@ -0,0 +1,122 @@ +use sentry_core::protocol::{Breadcrumb, Event}; +use sentry_core::{Hub, Integration}; +use slog::{OwnedKVList, Record}; + +use crate::{breadcrumb_from_record, event_from_record, exception_from_record}; + +/// The Action that Sentry should perform for a `slog::Level`. +pub enum LevelFilter { + /// Ignore the `Record`. + Ignore, + /// Create a `Breadcrumb` from this `Record`. + Breadcrumb, + /// Create a message `Event` from this `Record`. + Event, + /// Create an exception `Event` from this `Record`. + Exception, +} + +/// Custom Mappers +#[allow(clippy::large_enum_variant)] +pub enum RecordMapping { + /// Adds the `Breadcrumb` to the sentry scope. + Breadcrumb(Breadcrumb), + /// Captures the `Event` to sentry. + Event(Event<'static>), +} + +/// The default slog filter. +/// +/// By default, an exception event is captured for `critical` logs, +/// a regular event for `error` and `warning` logs, and breadcrumbs for +/// everything else. +pub fn default_filter(level: slog::Level) -> LevelFilter { + match level { + slog::Level::Critical => LevelFilter::Exception, + slog::Level::Error | slog::Level::Warning => LevelFilter::Event, + slog::Level::Info | slog::Level::Debug | slog::Level::Trace => LevelFilter::Breadcrumb, + } +} + +/// The Sentry `slog` Integration. +/// +/// Can be configured with a custom filter and mapper. +pub struct SlogIntegration { + filter: Box LevelFilter + Send + Sync>, + mapper: Option RecordMapping + Send + Sync>>, +} + +impl Default for SlogIntegration { + fn default() -> Self { + Self { + filter: Box::new(default_filter), + mapper: None, + } + } +} + +impl SlogIntegration { + /// Create a new `slog` Integration. + pub fn new() -> Self { + Self::default() + } + + /// Sets a custom filter function. + /// + /// The filter classifies how sentry should handle `slog::Record`s based on + /// their level. + pub fn filter(mut self, filter: F) -> Self + where + F: Fn(slog::Level) -> LevelFilter + Send + Sync + 'static, + { + self.filter = Box::new(filter); + self + } + + /// Sets a custom mapper function. + /// + /// The mapper is responsible for creating either breadcrumbs or events + /// from `slog::Record`s. + pub fn mapper(mut self, mapper: M) -> Self + where + M: Fn(&Record, &OwnedKVList) -> RecordMapping + Send + Sync + 'static, + { + self.mapper = Some(Box::new(mapper)); + self + } + + pub(crate) fn log(&self, hub: &Hub, record: &Record, values: &OwnedKVList) { + let item: RecordMapping = match &self.mapper { + Some(mapper) => mapper(record, values), + None => match (self.filter)(record.level()) { + LevelFilter::Ignore => return, + LevelFilter::Breadcrumb => { + RecordMapping::Breadcrumb(breadcrumb_from_record(record, values)) + } + LevelFilter::Event => RecordMapping::Event(event_from_record(record, values)), + LevelFilter::Exception => { + RecordMapping::Event(exception_from_record(record, values)) + } + }, + }; + match item { + RecordMapping::Breadcrumb(b) => hub.add_breadcrumb(b), + RecordMapping::Event(e) => { + hub.capture_event(e); + } + } + } + + pub(crate) fn is_enabled(&self, level: slog::Level) -> bool { + match (self.filter)(level) { + LevelFilter::Ignore => false, + _ => true, + } + } +} + +impl Integration for SlogIntegration { + fn name(&self) -> &'static str { + "slog" + } +} diff --git a/sentry-slog/src/lib.rs b/sentry-slog/src/lib.rs new file mode 100644 index 000000000..de5dc4fe9 --- /dev/null +++ b/sentry-slog/src/lib.rs @@ -0,0 +1,76 @@ +//! Sentry `slog` Integration. +//! +//! The sentry `slog` integration consists of two parts, the +//! [`SlogIntegration`] which configures how sentry should treat +//! `slog::Record`s, and the [`SentryDrain`], which can be used to create a +//! `slog::Logger`. +//! +//! *NOTE*: This integration currently does not process any `slog::KV` pairs, +//! but support for this will be added in the future. +//! +//! # Examples +//! +//! ``` +//! use sentry::{init, ClientOptions}; +//! use sentry_slog::{SentryDrain, SlogIntegration}; +//! +//! let integration = SlogIntegration::default(); +//! let options = ClientOptions::default().add_integration(integration); +//! let _sentry = sentry::init(options); +//! +//! let drain = SentryDrain::new(slog::Discard); +//! let root = slog::Logger::root(drain, slog::o!()); +//! +//! # let options = ClientOptions::default().add_integration(SlogIntegration::default()); +//! # let events = sentry::test::with_captured_events_options(|| { +//! slog::info!(root, "recorded as breadcrumb"); +//! slog::warn!(root, "recorded as regular event"); +//! # }, options.clone()); +//! # let captured_event = events.into_iter().next().unwrap(); +//! +//! assert_eq!( +//! captured_event.breadcrumbs.as_ref()[0].message.as_deref(), +//! Some("recorded as breadcrumb") +//! ); +//! assert_eq!( +//! captured_event.message.as_deref(), +//! Some("recorded as regular event") +//! ); +//! +//! # let events = sentry::test::with_captured_events_options(|| { +//! slog::crit!(root, "recorded as exception event"); +//! # }, options); +//! # let captured_event = events.into_iter().next().unwrap(); +//! +//! assert_eq!(captured_event.exception.len(), 1); +//! ``` +//! +//! The integration can also be customized with a `filter`, and a `mapper`: +//! +//! ``` +//! use sentry_slog::{exception_from_record, LevelFilter, RecordMapping, SlogIntegration}; +//! +//! let integration = SlogIntegration::default() +//! .filter(|level| match level { +//! slog::Level::Critical | slog::Level::Error => LevelFilter::Event, +//! _ => LevelFilter::Ignore, +//! }) +//! .mapper(|record, kv| RecordMapping::Event(exception_from_record(record, kv))); +//! ``` +//! +//! Please not that the `mapper` can override any classification from the +//! previous `filter`. +//! +//! [`SlogIntegration`]: struct.SlogIntegration.html +//! [`SentryDrain`]: struct.SentryDrain.html + +#![deny(missing_docs)] +#![deny(unsafe_code)] + +mod converters; +mod drain; +mod integration; + +pub use converters::*; +pub use drain::SentryDrain; +pub use integration::{default_filter, LevelFilter, RecordMapping, SlogIntegration};