Skip to content
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ members = [
"sentry-failure",
"sentry-log",
"sentry-panic",
"sentry-slog",
"sentry-types",
]
3 changes: 3 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Uncomment and use this with `cargo +nightly fmt`:
# unstable_features = true
# format_code_in_doc_comments = true
20 changes: 20 additions & 0 deletions sentry-slog/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "sentry-slog"
version = "0.18.0"
authors = ["Sentry <hello@sentry.io>"]
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"] }
87 changes: 87 additions & 0 deletions sentry-slog/src/converters.rs
Original file line number Diff line number Diff line change
@@ -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<String, Value>, kv: &impl KV) {
let _ = (map, kv);
// TODO: actually implement this ;-)
Comment thread
Swatinem marked this conversation as resolved.
}

/// Creates a sentry `Breadcrumb` from the `slog::Record`.
Comment thread
Swatinem marked this conversation as resolved.
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
}
40 changes: 40 additions & 0 deletions sentry-slog/src/drain.rs
Original file line number Diff line number Diff line change
@@ -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<D: Drain> {
drain: D,
}

impl<D: Drain> SentryDrain<D> {
/// 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, R>(f: F) -> R
where
F: Fn(&Hub, &SlogIntegration) -> R,
R: Default,
{
Hub::with_active(|hub| hub.with_integration(|integration| f(hub, integration)))
}

impl<D: Drain> slog::Drain for SentryDrain<D> {
type Ok = D::Ok;
type Err = D::Err;

fn log(&self, record: &Record, values: &OwnedKVList) -> Result<Self::Ok, Self::Err> {
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)
}
}
122 changes: 122 additions & 0 deletions sentry-slog/src/integration.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Fn(slog::Level) -> LevelFilter + Send + Sync>,
mapper: Option<Box<dyn Fn(&Record, &OwnedKVList) -> 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<F>(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<M>(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"
}
}
76 changes: 76 additions & 0 deletions sentry-slog/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};