From 926c7c203660d5fa0b390af822b6aaf4bc398fe1 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 13 Jun 2025 12:44:50 +0200 Subject: [PATCH 1/5] feat(log): add support for logs --- Cargo.lock | 14 ++++----- sentry-log/Cargo.toml | 4 +++ sentry-log/src/converters.rs | 48 ++++++++++++++++++++++++++++++- sentry-log/src/lib.rs | 24 ++++++++++++---- sentry-log/src/logger.rs | 12 ++++++++ sentry/Cargo.toml | 2 +- sentry/tests/test_log_logs.rs | 53 +++++++++++++++++++++++++++++++++++ 7 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 sentry/tests/test_log_logs.rs diff --git a/Cargo.lock b/Cargo.lock index d011b9442..7417360e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1244,7 +1244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2062,7 +2062,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2748,7 +2748,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3032,7 +3032,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3045,7 +3045,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3582,7 +3582,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4265,7 +4265,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/sentry-log/Cargo.toml b/sentry-log/Cargo.toml index 10de5a4ee..cb36c59e8 100644 --- a/sentry-log/Cargo.toml +++ b/sentry-log/Cargo.toml @@ -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"] } diff --git a/sentry-log/src/converters.rs b/sentry-log/src/converters.rs index df4d13a1f..baf7d4640 100644 --- a/sentry-log/src/converters.rs +++ b/sentry-log/src/converters.rs @@ -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, @@ -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 { @@ -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 = 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, + } +} diff --git a/sentry-log/src/lib.rs b/sentry-log/src/lib.rs index bbf667767..260f3bd93 100644 --- a/sentry-log/src/lib.rs +++ b/sentry-log/src/lib.rs @@ -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 events 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 //! //! ``` diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index f0f1e9ab9..a0f233399 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -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`]. @@ -14,6 +16,9 @@ pub enum LogFilter { Event, /// Create an exception [`Event`] from this [`Record`]. Exception, + /// Create a [`Log`] from this [`Record`]. + #[cfg(feature = "logs")] + Log, } /// The type of Data Sentry should ingest for a [`log::Record`]. @@ -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. @@ -135,6 +143,8 @@ impl log::Log for SentryLogger { 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)), }, }; @@ -144,6 +154,8 @@ impl log::Log for SentryLogger { 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) diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml index 72e234d62..e52a5a300 100644 --- a/sentry/Cargo.toml +++ b/sentry/Cargo.toml @@ -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"] +logs = ["sentry-core/logs", "sentry-log?/logs"] # transports transport = ["reqwest", "native-tls"] reqwest = ["dep:reqwest", "httpdate", "tokio"] diff --git a/sentry/tests/test_log_logs.rs b/sentry/tests/test_log_logs.rs new file mode 100644 index 000000000..adaa2993a --- /dev/null +++ b/sentry/tests/test_log_logs.rs @@ -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"), + } +} From 08eebd1fbb927d774290d901ef4bfce22e302f6f Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Fri, 13 Jun 2025 14:16:24 +0200 Subject: [PATCH 2/5] Update lib.rs --- sentry-log/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-log/src/lib.rs b/sentry-log/src/lib.rs index 260f3bd93..56d986539 100644 --- a/sentry-log/src/lib.rs +++ b/sentry-log/src/lib.rs @@ -4,7 +4,7 @@ //! - 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 events can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/). +//! - 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). From ab1b99498355eb6b3e0a1d5f3154e542b31a69c7 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 13 Jun 2025 14:21:28 +0200 Subject: [PATCH 3/5] changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2efa89a02..c618da973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ - chore(deps): upgrade `ureq` to 3.x (#835) by @algesten +### Features + +- 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, + }); + ``` + ## 0.39.0 ### Features From a56573a0abd4d494884d3c1bceec41a8b0bde621 Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Mon, 16 Jun 2025 15:27:19 +0200 Subject: [PATCH 4/5] Update logger.rs --- sentry-log/src/logger.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index a0f233399..7308afa95 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -16,7 +16,7 @@ pub enum LogFilter { Event, /// Create an exception [`Event`] from this [`Record`]. Exception, - /// Create a [`Log`] from this [`Record`]. + /// Create a [`sentry_core::protocol::Log`] from this [`Record`]. #[cfg(feature = "logs")] Log, } From 319433aff6a4166d71ff774c791056b89f08fcfe Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Mon, 16 Jun 2025 15:45:37 +0200 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6cdd67be..f4924ca6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -35,20 +46,6 @@ - chore(deps): upgrade `ureq` to 3.x (#835) by @algesten -### Features - -- 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, - }); - ``` - ## 0.39.0 ### Features