diff --git a/Cargo.toml b/Cargo.toml index 1e0d96ac1..6df12359c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ with_backtrace = ["backtrace", "regex"] with_panic = ["with_backtrace"] with_failure = ["failure", "with_backtrace"] with_log = ["log", "with_backtrace"] +with_slog = ["slog", "with_backtrace"] with_debug_to_log = ["log"] with_env_logger = ["with_log", "env_logger"] with_error_chain = ["error-chain", "with_backtrace"] @@ -42,6 +43,7 @@ backtrace = { version = "0.3.15", optional = true } url = { version = "1.7.2", optional = true } failure = { version = "0.1.5", optional = true } log = { version = "0.4.6", optional = true, features = ["std"] } +slog = { version = "2.5.2", optional = true, features = ["std"] } sentry-types = "0.11.0" env_logger = { version = "0.6.1", optional = true } reqwest = { version = "0.9.15", optional = true, default-features = false } diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs index 0ba2709ce..5283f2ee0 100644 --- a/src/integrations/mod.rs +++ b/src/integrations/mod.rs @@ -10,6 +10,9 @@ pub mod error_chain; #[cfg(feature = "with_log")] pub mod log; +#[cfg(feature = "with_slog")] +pub mod slog; + #[cfg(feature = "with_env_logger")] pub mod env_logger; diff --git a/src/integrations/slog.rs b/src/integrations/slog.rs new file mode 100644 index 000000000..aee212180 --- /dev/null +++ b/src/integrations/slog.rs @@ -0,0 +1,171 @@ +//! Adds support for automatic breadcrumb capturing from logs +//! by implementing the `slog::Drain` +//! +//! **Feature:** `with_slog` +use slog::{Drain, Level as SlogLevel, Never, OwnedKVList, Record}; + +use crate::api::add_breadcrumb; +use crate::backtrace_support::current_stacktrace; +use crate::hub::Hub; +use crate::protocol::{Breadcrumb, Event, Exception, Level}; + +/// Logger specific options. +pub struct LoggerOptions { + /// The global filter that should be used (also used before dispatching + /// to the nested logger). + pub global_filter: Option, + /// The sentry specific log level filter (defaults to `Info`) + pub filter: log::LevelFilter, + /// If set to `true`, breadcrumbs will be emitted. (defaults to `true`) + pub emit_breadcrumbs: bool, + /// If set to `true` error events will be sent for errors in the log. (defaults to `true`) + pub emit_error_events: bool, + /// If set to `true` warning events will be sent for warnings in the log. (defaults to `false`) + pub emit_warning_events: bool, + /// If set to `true` current stacktrace will be resolved and attached + /// to each event. (expensive, defaults to `true`) + pub attach_stacktraces: bool, +} + +impl Default for LoggerOptions { + fn default() -> LoggerOptions { + LoggerOptions { + global_filter: None, + filter: log::LevelFilter::Info, + emit_breadcrumbs: true, + emit_error_events: true, + emit_warning_events: false, + attach_stacktraces: true, + } + } +} + +impl LoggerOptions { + /// Checks if an issue should be created. + fn create_issue_for_record(&self, record: &log::Record<'_>) -> bool { + match record.level() { + log::Level::Warn => self.emit_warning_events, + log::Level::Error => self.emit_error_events, + _ => false, + } + } +} + +/// Provides a logger that wraps the sentry communication. +pub struct Logger { + options: LoggerOptions, +} + +impl Logger { + /// Initializes a new logger. + pub fn new(options: LoggerOptions) -> Logger { + Logger { options } + } + + /// Returns the options of the logger. + pub fn options(&self) -> &LoggerOptions { + &self.options + } +} + +/// Creates a breadcrumb from a given log record. +pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb { + Breadcrumb { + ty: "log".into(), + level: convert_log_level(record.level()), + category: Some(record.target().into()), + message: Some(format!("{}", record.args())), + ..Default::default() + } +} + +/// Creates an event from a given log record. +/// +/// If `with_stacktrace` is set to `true` then a stacktrace is attached +/// from the current frame. +pub fn event_from_record(record: &log::Record<'_>, with_stacktrace: bool) -> Event<'static> { + let culprit = format!( + "{}:{}", + record.file().unwrap_or(""), + record.line().unwrap_or(0) + ); + Event { + logger: Some(record.target().into()), + level: convert_log_level(record.level()), + exception: vec![Exception { + ty: record.target().into(), + value: Some(format!("{}", record.args())), + stacktrace: if with_stacktrace { + current_stacktrace() + } else { + None + }, + ..Default::default() + }] + .into(), + culprit: Some(culprit), + ..Default::default() + } +} + +fn convert_log_level(level: log::Level) -> Level { + match level { + log::Level::Error => Level::Error, + log::Level::Warn => Level::Warning, + log::Level::Info => Level::Info, + log::Level::Debug | log::Level::Trace => Level::Debug, + } +} + +impl Drain for Logger { + type Ok = (); + type Err = Never; + + fn log(&self, record: &Record, _values: &OwnedKVList) -> Result { + let level = to_log_level(record.level()); + let md = log::MetadataBuilder::new() + .level(level) + .target(record.tag()) + .build(); + let args = *record.msg(); + + let record = log::RecordBuilder::new() + .metadata(md) + .args(args) + .module_path(Some(record.module())) + .line(Some(record.line())) + .file(Some(record.file())) + .build(); + + if self.options.create_issue_for_record(&record) { + Hub::with_active(|hub| { + hub.capture_event(event_from_record(&record, self.options.attach_stacktraces)) + }); + } + if self.options.emit_breadcrumbs && record.level() <= self.options.filter { + add_breadcrumb(|| breadcrumb_from_record(&record)) + } + + Ok(()) + } + + fn is_enabled(&self, level: SlogLevel) -> bool { + let level = to_log_level(level); + if let Some(global_filter) = self.options.global_filter { + if level > global_filter { + return false; + } + } + level <= self.options.filter + } +} + +fn to_log_level(level: SlogLevel) -> log::Level { + match level { + SlogLevel::Trace => log::Level::Trace, + SlogLevel::Debug => log::Level::Debug, + SlogLevel::Info => log::Level::Info, + SlogLevel::Warning => log::Level::Warn, + SlogLevel::Error | SlogLevel::Critical => log::Level::Error, + } +} diff --git a/src/transport.rs b/src/transport.rs index ce7926f86..f5c1d010e 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -138,7 +138,7 @@ macro_rules! implement_http_transport { shutdown_signal: Arc, shutdown_immediately: Arc, queue_size: Arc>, - _handle: Option>, + handle: Option>, } impl $typename { @@ -164,7 +164,7 @@ macro_rules! implement_http_transport { #[allow(clippy::mutex_atomic)] let queue_size = Arc::new(Mutex::new(0)); let http_client = http_client(options, $hc_client); - let _handle = Some(spawn( + let handle = Some(spawn( options, receiver, shutdown_signal.clone(), @@ -177,7 +177,7 @@ macro_rules! implement_http_transport { shutdown_signal, shutdown_immediately, queue_size, - _handle, + handle, } } } @@ -195,14 +195,22 @@ macro_rules! implement_http_transport { fn shutdown(&self, timeout: Duration) -> bool { sentry_debug!("shutting down http transport"); - let guard = self.queue_size.lock().unwrap(); - if *guard == 0 { + + // prevent deadlock with `spawn` thread by creating temporary variable + if *self.queue_size.lock().unwrap() == 0 { true } else { if let Ok(sender) = self.sender.lock() { sender.send(None).ok(); } - self.shutdown_signal.wait_timeout(guard, timeout).is_ok() + + let guard = self.queue_size.lock().unwrap(); + if *guard > 0 { + self.shutdown_signal.wait_timeout(guard, timeout).is_ok() + } + else { + true + } } } } @@ -212,7 +220,13 @@ macro_rules! implement_http_transport { sentry_debug!("dropping http transport"); self.shutdown_immediately.store(true, Ordering::SeqCst); if let Ok(sender) = self.sender.lock() { - sender.send(None).ok(); + // send stop signal + if sender.send(None).is_ok() { + // and wait for actual stopping + if let Some(handle) = self.handle.take() { + handle.join().ok(); + } + } } } }