Skip to content

ref: Rearchitect the log and slog Integrations #268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- Removed all deprecated exports and deprecated feature flags.
- The `failure` integration / feature is now off-by-default along with its deprecation.
- The `log` and `slog` integrations were re-designed, they now offer types that wrap a `log::Log` or `slog::Drain` and forward log events to the currently active sentry `Hub` based on an optional filter and an optional mapper.
- The new `log` integration will not implicitly call `log::set_max_level_filter` anymore, and users need to do so manually.

**Deprecations**:

Expand Down
3 changes: 1 addition & 2 deletions sentry-anyhow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
//! Err(anyhow::anyhow!("some kind of error"))
//! }
//!
//! let _sentry =
//! sentry::init(sentry::ClientOptions::new().add_integration(AnyhowIntegration));
//! let _sentry = sentry::init(sentry::ClientOptions::new().add_integration(AnyhowIntegration));
//!
//! if let Err(err) = function_that_might_fail() {
//! capture_anyhow(&err);
Expand Down
6 changes: 0 additions & 6 deletions sentry-log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,10 @@ Sentry integration for log and env_logger crates.
"""
edition = "2018"

[package.metadata.docs.rs]
all-features = true

[dependencies]
sentry-core = { version = "0.20.1", path = "../sentry-core" }
sentry-backtrace = { version = "0.20.1", path = "../sentry-backtrace" }
log = { version = "0.4.8", features = ["std"] }
env_logger = { version = "0.7.1", optional = true }

[dev-dependencies]
sentry = { version = "0.20.1", path = "../sentry", default-features = false, features = ["test"] }
pretty_env_logger = "0.4.0"
env_logger = "0.7.1"
49 changes: 29 additions & 20 deletions sentry-log/src/converters.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use sentry_backtrace::current_stacktrace;
use sentry_core::protocol::{Event, Exception};
use sentry_core::protocol::{Event, Exception, Frame, Stacktrace};
use sentry_core::{Breadcrumb, Level};

fn convert_log_level(level: log::Level) -> Level {
/// Converts a [`log::Level`] to a Sentry [`Level`]
pub fn convert_log_level(level: log::Level) -> Level {
match level {
log::Level::Error => Level::Error,
log::Level::Warn => Level::Warning,
Expand All @@ -11,7 +11,7 @@ fn convert_log_level(level: log::Level) -> Level {
}
}

/// Creates a breadcrumb from a given log record.
/// Creates a [`Breadcrumb`] from a given [`log::Record`].
pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb {
Breadcrumb {
ty: "log".into(),
Expand All @@ -22,25 +22,34 @@ pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb {
}
}

/// 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> {
/// Creates an [`Event`] from a given [`log::Record`].
pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> {
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(),
message: Some(format!("{}", record.args())),
..Default::default()
}
}

/// Creates an exception [`Event`] from a given [`log::Record`].
pub fn exception_from_record(record: &log::Record<'_>) -> Event<'static> {
let mut event = event_from_record(record);
let frame = Frame {
module: record.module_path().map(ToOwned::to_owned),
filename: record.file().map(ToOwned::to_owned),
lineno: record.line().map(Into::into),
..Default::default()
};
let exception = Exception {
ty: record.target().into(),
value: event.message.clone(),
stacktrace: Some(Stacktrace {
frames: vec![frame],
..Default::default()
}),
..Default::default()
};
event.exception = vec![exception].into();
event
}
161 changes: 6 additions & 155 deletions sentry-log/src/integration.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,8 @@
use std::sync::Once;

use log::{Level, LevelFilter, Record};
use sentry_core::{ClientOptions, Integration};

use crate::logger::Logger;

/// Logger specific options.
pub struct LogIntegration {
/// The global filter that should be used (also used before dispatching
/// to the nested logger).
pub global_filter: Option<LevelFilter>,
/// The sentry specific log level filter (defaults to `Info`)
pub filter: 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,
/// The destination log.
pub dest_log: Option<Box<dyn log::Log>>,
}

static INIT: Once = Once::new();
/// The Sentry [`log`] Integration.
#[derive(Default)]
pub struct LogIntegration;

impl Integration for LogIntegration {
fn name(&self) -> &'static str {
Expand All @@ -36,138 +13,12 @@ impl Integration for LogIntegration {
cfg.in_app_exclude.push("log::");
cfg.extra_border_frames
.push("<sentry_log::Logger as log::Log>::log");

let filter = self.effective_global_filter();
if filter > log::max_level() {
log::set_max_level(filter);
}

INIT.call_once(|| {
log::set_boxed_logger(Box::new(Logger::default())).ok();
});
}
}

impl Default for LogIntegration {
fn default() -> Self {
Self {
global_filter: None,
filter: LevelFilter::Info,
emit_breadcrumbs: true,
emit_error_events: true,
emit_warning_events: false,
attach_stacktraces: true,
dest_log: None,
}
}
}

impl std::fmt::Debug for LogIntegration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[derive(Debug)]
struct DestLog;
let dest_log = self.dest_log.as_ref().map(|_| DestLog);

f.debug_struct("LogIntegration")
.field("global_filter", &self.global_filter)
.field("filter", &self.filter)
.field("emit_breadcrumbs", &self.emit_breadcrumbs)
.field("emit_error_events", &self.emit_error_events)
.field("emit_warning_events", &self.emit_warning_events)
.field("attach_stacktraces", &self.attach_stacktraces)
.field("dest_log", &dest_log)
.finish()
}
}

impl LogIntegration {
/// Initializes an env logger as destination target.
#[cfg(feature = "env_logger")]
pub fn with_env_logger_dest(mut self, logger: Option<env_logger::Logger>) -> Self {
let logger = logger
.unwrap_or_else(|| env_logger::Builder::from_env(env_logger::Env::default()).build());
let filter = logger.filter();
if self.global_filter.is_none() {
self.global_filter = Some(filter);
}
self.dest_log = Some(Box::new(logger));
self
}

/// Returns the effective global filter.
///
/// This is what is set for these logger options when the log level
/// needs to be set globally. This is the greater of `global_filter`
/// and `filter`.
#[inline(always)]
pub(crate) fn effective_global_filter(&self) -> LevelFilter {
let filter = if let Some(filter) = self.global_filter {
if filter < self.filter {
self.filter
} else {
filter
}
} else {
self.filter
};
std::cmp::max(filter, self.issue_filter())
}

/// Returns the level for which issues should be created.
///
/// This is controlled by `emit_error_events` and `emit_warning_events`.
#[inline(always)]
fn issue_filter(&self) -> LevelFilter {
if self.emit_warning_events {
LevelFilter::Warn
} else if self.emit_error_events {
LevelFilter::Error
} else {
LevelFilter::Off
}
}

/// Checks if an issue should be created.
pub(crate) fn create_issue_for_record(&self, record: &Record<'_>) -> bool {
match record.level() {
Level::Warn => self.emit_warning_events,
Level::Error => self.emit_error_events,
_ => false,
}
/// Creates a new `log` Integration.
pub fn new() -> Self {
Self::default()
}
}

#[test]
fn test_filters() {
let opt_warn = LogIntegration {
filter: LevelFilter::Warn,
..Default::default()
};
assert_eq!(opt_warn.effective_global_filter(), LevelFilter::Warn);
assert_eq!(opt_warn.issue_filter(), LevelFilter::Error);

let opt_debug = LogIntegration {
global_filter: Some(LevelFilter::Debug),
filter: LevelFilter::Warn,
..Default::default()
};
assert_eq!(opt_debug.effective_global_filter(), LevelFilter::Debug);

let opt_debug_inverse = LogIntegration {
global_filter: Some(LevelFilter::Warn),
filter: LevelFilter::Debug,
..Default::default()
};
assert_eq!(
opt_debug_inverse.effective_global_filter(),
LevelFilter::Debug
);

let opt_weird = LogIntegration {
filter: LevelFilter::Error,
emit_warning_events: true,
..Default::default()
};
assert_eq!(opt_weird.issue_filter(), LevelFilter::Warn);
assert_eq!(opt_weird.effective_global_filter(), LevelFilter::Warn);
}
31 changes: 21 additions & 10 deletions sentry-log/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,43 @@
//! # Examples
//!
//! ```
//! let mut log_builder = pretty_env_logger::formatted_builder();
//! log_builder.parse_filters("info");
//! let logger = sentry_log::SentryLogger::with_dest(log_builder.build());
//!
//! log::set_boxed_logger(Box::new(logger)).unwrap();
//! log::set_max_level(log::LevelFilter::Info);
//!
//! let log_integration = sentry_log::LogIntegration::default();
//! let _sentry = sentry::init(sentry::ClientOptions::default().add_integration(log_integration));
//! let _sentry = sentry::init(
//! sentry::ClientOptions::new().add_integration(sentry_log::LogIntegration::new()),
//! );
//!
//! log::info!("Generates a breadcrumb");
//! log::error!("Generates an event");
//! ```
//!
//! Or optionally with env_logger support:
//! Or one might also set an explicit filter, to customize how to treat log
//! records:
//!
//! ```
//! let mut log_builder = pretty_env_logger::formatted_builder();
//! log_builder.parse_filters("info");
//! let log_integration =
//! sentry_log::LogIntegration::default().with_env_logger_dest(Some(log_builder.build()));
//! let _sentry = sentry::init(sentry::ClientOptions::default().add_integration(log_integration));
//! use sentry_log::LogFilter;
//!
//! log::error!("Generates an event");
//! let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() {
//! log::Level::Error => LogFilter::Event,
//! _ => LogFilter::Ignore,
//! });
//! ```

#![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")]
#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
#![warn(missing_docs)]
#![allow(clippy::match_like_matches_macro)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of allowing this, can we just fix this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not unless we break the MSRV, since matches! is a 1.42 feature.


mod converters;
mod integration;
mod logger;

pub use converters::{breadcrumb_from_record, event_from_record};
pub use converters::*;
pub use integration::LogIntegration;
pub use logger::Logger;
pub use logger::*;
Loading