diff --git a/Cargo.toml b/Cargo.toml index 1e0d96ac..7491c7af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ with_failure = ["failure", "with_backtrace"] with_log = ["log", "with_backtrace"] with_debug_to_log = ["log"] with_env_logger = ["with_log", "env_logger"] +with_slog = ["slog"] with_error_chain = ["error-chain", "with_backtrace"] with_device_info = ["libc", "hostname", "uname", "with_client_implementation"] with_rust_info = ["rustc_version", "with_client_implementation"] @@ -44,6 +45,7 @@ failure = { version = "0.1.5", optional = true } log = { version = "0.4.6", optional = true, features = ["std"] } sentry-types = "0.11.0" env_logger = { version = "0.6.1", optional = true } +slog = { version = "2.5.2", optional = true } reqwest = { version = "0.9.15", optional = true, default-features = false } lazy_static = "1.3.0" regex = { version = "1.1.6", optional = true } @@ -76,5 +78,9 @@ actix-web = { version = "0.7.19", default-features = false } name = "error-chain-demo" required-features = ["with_error_chain"] +[[example]] +name = "slog-demo" +required-features = ["with_slog"] + [workspace] members = [".", "integrations/sentry-actix"] diff --git a/examples/slog-demo.rs b/examples/slog-demo.rs new file mode 100644 index 00000000..26df1093 --- /dev/null +++ b/examples/slog-demo.rs @@ -0,0 +1,20 @@ +use slog::{debug, error, info, warn}; + +fn main() { + let drain = slog::Discard; + // Default options - breadcrumb from info, event from warnings + let wrapped_drain = sentry::integrations::slog::wrap_drain(drain, Default::default()); + let _sentry = sentry::init(( + "https://a94ae32be2584e0bbd7a4cbb95971fee@sentry.io/1041156", + sentry::ClientOptions { + release: sentry::release_name!(), + ..Default::default() + }, + )); + let root = slog::Logger::root(wrapped_drain, slog::o!("test_slog" => 0)); + + debug!(root, "This should not appear"; "111" => "222"); + info!(root, "Info breadcrumb"; "222" => 333); + warn!(root, "Warning event"; "333" => true); + error!(root, "Error event"; "444" => "555"); +} diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs index 0ba2709c..737861a1 100644 --- a/src/integrations/mod.rs +++ b/src/integrations/mod.rs @@ -13,5 +13,8 @@ pub mod log; #[cfg(feature = "with_env_logger")] pub mod env_logger; +#[cfg(feature = "with_slog")] +pub mod slog; + #[cfg(feature = "with_panic")] pub mod panic; diff --git a/src/integrations/slog.rs b/src/integrations/slog.rs new file mode 100644 index 00000000..0741f5be --- /dev/null +++ b/src/integrations/slog.rs @@ -0,0 +1,266 @@ +//! Adds support for automatic breadcrumb capturing from logs with `slog`. +//! +//! **Feature:** `with_slog` +//! +//! # Configuration +//! +//! In the most trivial version you could proceed like this: +//! +//! ```no_run +//! # extern crate slog; +//! +//! let drain = slog::Discard; +//! let wrapped_drain = sentry::integrations::slog::wrap_drain(drain, Default::default()); +//! let root = slog::Logger::root(drain, slog::o!()); +//! +//! slog::warn!(root, "Log for sentry") +//! ``` +use slog::{Drain, Serializer, KV}; +use std::{collections::BTreeMap, fmt}; + +use crate::{ + api::add_breadcrumb, + hub::Hub, + protocol::{Breadcrumb, Event}, + with_scope, Level, +}; + +// Serializer which stores the serde_json values in BTreeMap +struct StoringSerializer { + result: BTreeMap, +} + +impl StoringSerializer { + #[allow(missing_docs)] + fn emit_serde_json_value(&mut self, key: slog::Key, val: serde_json::Value) -> slog::Result { + self.result.insert(key.to_string(), val); + Ok(()) + } + + #[allow(missing_docs)] + fn emit_serde_json_null(&mut self, key: slog::Key) -> slog::Result { + self.emit_serde_json_value(key, serde_json::Value::Null) + } + + #[allow(missing_docs)] + fn emit_serde_json_bool(&mut self, key: slog::Key, val: bool) -> slog::Result { + self.emit_serde_json_value(key, serde_json::Value::Bool(val)) + } + + #[allow(missing_docs)] + fn emit_serde_json_number(&mut self, key: slog::Key, value: V) -> slog::Result + where + serde_json::Number: From, + { + let num = serde_json::Number::from(value); + self.emit_serde_json_value(key, serde_json::Value::Number(num)) + } + + #[allow(missing_docs)] + fn emit_serde_json_string(&mut self, key: slog::Key, val: String) -> slog::Result { + self.emit_serde_json_value(key, serde_json::Value::String(val)) + } +} + +macro_rules! impl_number { + ( $type:ty => $function_name:ident ) => { + #[allow(missing_docs)] + fn $function_name(&mut self, key: slog::Key, val: $type) -> slog::Result { + self.emit_serde_json_number(key, val) + } + }; +} + +impl Serializer for StoringSerializer { + #[allow(missing_docs)] + fn emit_bool(&mut self, key: slog::Key, val: bool) -> slog::Result { + self.emit_serde_json_bool(key, val) + } + + #[allow(missing_docs)] + fn emit_unit(&mut self, key: slog::Key) -> slog::Result { + self.emit_serde_json_null(key) + } + + #[allow(missing_docs)] + fn emit_none(&mut self, key: slog::Key) -> slog::Result { + self.emit_serde_json_null(key) + } + + #[allow(missing_docs)] + fn emit_char(&mut self, key: slog::Key, val: char) -> slog::Result { + self.emit_serde_json_string(key, val.to_string()) + } + + #[allow(missing_docs)] + fn emit_f64(&mut self, key: slog::Key, val: f64) -> slog::Result { + if let Some(num) = serde_json::Number::from_f64(val) { + self.emit_serde_json_value(key, serde_json::Value::Number(num)) + } else { + self.emit_serde_json_null(key) + } + } + + impl_number!(u8 => emit_u8); + impl_number!(i8 => emit_i8); + impl_number!(u16 => emit_u16); + impl_number!(i16 => emit_i16); + impl_number!(u32 => emit_u32); + impl_number!(i32 => emit_i32); + impl_number!(u64 => emit_u64); + impl_number!(i64 => emit_i64); + + // u128 and i128 should be implemented in serde_json 1.0.40 + // impl_number!(u128 => emit_u128); + // impl_number!(i128 => emit_i128); + + #[allow(missing_docs)] + fn emit_arguments(&mut self, _: slog::Key, _: &fmt::Arguments) -> slog::Result { + Ok(()) + } +} + +/// Converts `slog::Level` to `Level` +fn into_sentry_level(slog_level: slog::Level) -> Level { + match slog_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, + } +} + +/// Options for the slog configuration +#[derive(Debug, Copy, Clone)] +pub struct Options { + /// Level since when the breadcrumbs are created + breadcrumb_level: slog::Level, + /// Level since when the events are sent + event_level: slog::Level, +} + +impl Default for Options { + fn default() -> Self { + Self { + breadcrumb_level: slog::Level::Info, + event_level: slog::Level::Warning, + } + } +} + +/// Wrapped drain for sentry logging +#[derive(Debug, Copy, Clone)] +pub struct WrappedDrain +where + D: Drain, +{ + drain: D, + options: Options, +} + +fn get_record_data(record: &slog::Record) -> BTreeMap { + let mut storing_serializer = StoringSerializer { + result: BTreeMap::new(), + }; + + // slog::KV can be only serialized, but we need to obtain its data + // To do that a Serializer was implemented to store these data to a dict + if record + .kv() + .serialize(record, &mut storing_serializer) + .is_ok() + { + storing_serializer.result + } else { + BTreeMap::new() + } +} + +impl WrappedDrain +where + D: Drain, +{ + /// Creates a new wrapped Drain + fn new(drain: D, options: Options) -> Self { + Self { drain, options } + } + + /// Creates a breadcrumb + fn add_breadcrumb(record: &slog::Record) { + let data = get_record_data(record); + + let breadcrumb = Breadcrumb { + message: Some(record.msg().to_string()), + level: into_sentry_level(record.level()), + data, + ..Breadcrumb::default() + }; + add_breadcrumb(|| breadcrumb); + } + + /// Captures an event + fn capture_event(record: &slog::Record) { + let extra = get_record_data(record); + + let event = Event { + message: Some(record.msg().to_string()), + level: into_sentry_level(record.level()), + ..Event::default() + }; + + with_scope( + |scope| { + for (key, value) in extra { + scope.set_extra(&key, value); + } + }, + || { + Hub::with_active(move |hub| hub.capture_event(event)); + }, + ); + } +} + +impl Drain for WrappedDrain +where + D: Drain, +{ + type Ok = D::Ok; + type Err = D::Err; + + fn log( + &self, + record: &slog::Record, + values: &slog::OwnedKVList, + ) -> Result { + let level = record.level(); + if level <= self.options.event_level { + // log event + Self::capture_event(record); + } else if level <= self.options.breadcrumb_level { + // or log bread crumbs + Self::add_breadcrumb(record); + } + + self.drain.log(record, values) + } +} + +impl std::ops::Deref for WrappedDrain +where + D: Drain, +{ + type Target = D; + + fn deref(&self) -> &Self::Target { + &self.drain + } +} + +/// Wraps `slog::Drain` +pub fn wrap_drain(drain: D, options: Options) -> WrappedDrain +where + D: slog::Drain, +{ + WrappedDrain::new(drain, options) +} diff --git a/src/lib.rs b/src/lib.rs index 7bef8010..a146f4e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,7 @@ //! * `with_reqwest_transport`: enables the reqwest transport explicitly. This //! is currently the default transport. //! * `with_curl_transport`: enables the curl transport. +//! * `with_slog`: enables the `slog` integration #![warn(missing_docs)] #[macro_use]