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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
_ => EventFilter::Log,
});
```
- feat(log): add support for logs (#841) by @lcian
- To capture `log` records as Sentry structured logs, enable the `logs` feature of the `sentry` crate.
- Then, initialize the SDK with `enable_logs: true` in your client options.
- Finally, set up a custom event filter to map records to Sentry logs based on criteria such as severity. For example:
```rust
let logger = sentry::integrations::log::SentryLogger::new().filter(|md| match md.level() {
log::Level::Error => LogFilter::Event,
log::Level::Trace => LogFilter::Ignore,
_ => LogFilter::Log,
});
```

### Fixes

Expand Down
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions sentry-log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Sentry integration for log and env_logger crates.
edition = "2021"
rust-version = "1.81"

[features]
default = []
logs = ["sentry-core/logs"]

[dependencies]
sentry-core = { version = "0.39.0", path = "../sentry-core" }
log = { version = "0.4.8", features = ["std"] }
Expand Down
48 changes: 47 additions & 1 deletion sentry-log/src/converters.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use sentry_core::protocol::Event;
#[cfg(feature = "logs")]
use sentry_core::protocol::{Log, LogAttribute, LogLevel};
use sentry_core::{Breadcrumb, Level};
#[cfg(feature = "logs")]
use std::{collections::BTreeMap, time::SystemTime};

/// Converts a [`log::Level`] to a Sentry [`Level`]
/// Converts a [`log::Level`] to a Sentry [`Level`], used for [`Event`] and [`Breadcrumb`].
pub fn convert_log_level(level: log::Level) -> Level {
match level {
log::Level::Error => Level::Error,
Expand All @@ -11,6 +15,18 @@ pub fn convert_log_level(level: log::Level) -> Level {
}
}

/// Converts a [`log::Level`] to a Sentry [`LogLevel`], used for [`Log`].
#[cfg(feature = "logs")]
pub fn convert_log_level_to_sentry_log_level(level: log::Level) -> LogLevel {
match level {
log::Level::Error => LogLevel::Error,
log::Level::Warn => LogLevel::Warn,
log::Level::Info => LogLevel::Info,
log::Level::Debug => LogLevel::Debug,
log::Level::Trace => LogLevel::Trace,
}
}

/// Creates a [`Breadcrumb`] from a given [`log::Record`].
pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb {
Breadcrumb {
Expand Down Expand Up @@ -40,3 +56,33 @@ pub fn exception_from_record(record: &log::Record<'_>) -> Event<'static> {
// an exception record.
event_from_record(record)
}

/// Creates a [`Log`] from a given [`log::Record`].
#[cfg(feature = "logs")]
pub fn log_from_record(record: &log::Record<'_>) -> Log {
let mut attributes: BTreeMap<String, LogAttribute> = BTreeMap::new();

attributes.insert("logger.target".into(), record.target().into());
if let Some(module_path) = record.module_path() {
attributes.insert("logger.module_path".into(), module_path.into());
}
if let Some(file) = record.file() {
attributes.insert("logger.file".into(), file.into());
}
if let Some(line) = record.line() {
attributes.insert("logger.line".into(), line.into());
}

attributes.insert("sentry.origin".into(), "auto.logger.log".into());

// TODO: support the `kv` feature and store key value pairs as attributes

Log {
level: convert_log_level_to_sentry_log_level(record.level()),
body: format!("{}", record.args()),
trace_id: None,
timestamp: SystemTime::now(),
severity_number: None,
attributes,
}
}
24 changes: 19 additions & 5 deletions sentry-log/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
//! Adds support for automatic Breadcrumb and Event capturing from logs.
//!
//! The `log` crate is supported in two ways. First, logs can be captured as
//! breadcrumbs for later. Secondly, error logs can be captured as events to
//! Sentry. By default anything above `Info` is recorded as a breadcrumb and
//! Adds support for automatic Breadcrumb, Event, and Log capturing from `log` records.
//!
//! The `log` crate is supported in three ways:
//! - Records can be captured as Sentry events. These are grouped and show up in the Sentry
//! [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be
//! acted upon.
//! - Records can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/).
//! Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when
//! an event is captured, either manually through e.g. `sentry::capture_message` or through integrations
//! (e.g. the panic integration is enabled (default) and a panic happens).
//! - Records can be captured as traditional [logs](https://docs.sentry.io/product/explore/logs/)
//! Logs can be viewed and queried in the Logs explorer.
//!
//! By default anything above `Info` is recorded as a breadcrumb and
//! anything above `Error` is captured as error event.
//!
//! To capture records as Sentry logs:
//! 1. Enable the `logs` feature of the `sentry` crate.
//! 2. Initialize the SDK with `enable_logs: true` in your client options.
//! 3. Set up a custom filter (see below) to map records to logs (`LogFilter::Log`) based on criteria such as severity.
//!
//! # Examples
//!
//! ```
Expand Down
12 changes: 12 additions & 0 deletions sentry-log/src/logger.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use log::Record;
use sentry_core::protocol::{Breadcrumb, Event};

#[cfg(feature = "logs")]
use crate::converters::log_from_record;
use crate::converters::{breadcrumb_from_record, event_from_record, exception_from_record};

/// The action that Sentry should perform for a [`log::Metadata`].
Expand All @@ -14,6 +16,9 @@ pub enum LogFilter {
Event,
/// Create an exception [`Event`] from this [`Record`].
Exception,
/// Create a [`sentry_core::protocol::Log`] from this [`Record`].
#[cfg(feature = "logs")]
Log,
}

/// The type of Data Sentry should ingest for a [`log::Record`].
Expand All @@ -26,6 +31,9 @@ pub enum RecordMapping {
Breadcrumb(Breadcrumb),
/// Captures the [`Event`] to Sentry.
Event(Event<'static>),
/// Captures the [`sentry_core::protocol::Log`] to Sentry.
#[cfg(feature = "logs")]
Log(sentry_core::protocol::Log),
}

/// The default log filter.
Expand Down Expand Up @@ -135,6 +143,8 @@ impl<L: log::Log> log::Log for SentryLogger<L> {
LogFilter::Breadcrumb => RecordMapping::Breadcrumb(breadcrumb_from_record(record)),
LogFilter::Event => RecordMapping::Event(event_from_record(record)),
LogFilter::Exception => RecordMapping::Event(exception_from_record(record)),
#[cfg(feature = "logs")]
LogFilter::Log => RecordMapping::Log(log_from_record(record)),
},
};

Expand All @@ -144,6 +154,8 @@ impl<L: log::Log> log::Log for SentryLogger<L> {
RecordMapping::Event(e) => {
sentry_core::capture_event(e);
}
#[cfg(feature = "logs")]
RecordMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
}

self.dest.log(record)
Expand Down
2 changes: 1 addition & 1 deletion sentry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ opentelemetry = ["sentry-opentelemetry"]
# other features
test = ["sentry-core/test"]
release-health = ["sentry-core/release-health", "sentry-actix?/release-health"]
logs = ["sentry-core/logs", "sentry-tracing?/logs"]
logs = ["sentry-core/logs", "sentry-tracing?/logs", "sentry-log?/logs"]
# transports
transport = ["reqwest", "native-tls"]
reqwest = ["dep:reqwest", "httpdate", "tokio"]
Expand Down
53 changes: 53 additions & 0 deletions sentry/tests/test_log_logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#![cfg(feature = "test")]

// Test `log` integration <> Sentry structured logging.
// This must be a in a separate file because `log::set_boxed_logger` can only be called once.
#[cfg(feature = "logs")]
#[test]
fn test_log_logs() {
let logger = sentry_log::SentryLogger::new().filter(|_| sentry_log::LogFilter::Log);

log::set_boxed_logger(Box::new(logger))
.map(|()| log::set_max_level(log::LevelFilter::Trace))
.unwrap();

let options = sentry::ClientOptions {
enable_logs: true,
..Default::default()
};

let envelopes = sentry::test::with_captured_envelopes_options(
|| {
log::info!("This is a log");
},
options,
);

assert_eq!(envelopes.len(), 1);
let envelope = envelopes.first().expect("expected envelope");
let item = envelope.items().next().expect("expected envelope item");

match item {
sentry::protocol::EnvelopeItem::ItemContainer(container) => match container {
sentry::protocol::ItemContainer::Logs(logs) => {
assert_eq!(logs.len(), 1);

let info_log = logs
.iter()
.find(|log| log.level == sentry::protocol::LogLevel::Info)
.expect("expected info log");
assert_eq!(info_log.body, "This is a log");
assert_eq!(
info_log.attributes.get("logger.target").unwrap().clone(),
"test_log_logs".into()
);
assert_eq!(
info_log.attributes.get("sentry.origin").unwrap().clone(),
"auto.logger.log".into()
);
}
_ => panic!("expected logs"),
},
_ => panic!("expected item container"),
}
}