From d2504493df3c69e13c71f799c34a87fec5b40c9c Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 18 Jun 2025 10:41:20 +0200 Subject: [PATCH 1/5] feat(log): support `kv` feature of `log` --- sentry-log/Cargo.toml | 2 +- sentry-log/src/converters.rs | 88 ++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/sentry-log/Cargo.toml b/sentry-log/Cargo.toml index 51e07960..a5a4d504 100644 --- a/sentry-log/Cargo.toml +++ b/sentry-log/Cargo.toml @@ -18,7 +18,7 @@ logs = ["sentry-core/logs"] [dependencies] sentry-core = { version = "0.40.0", path = "../sentry-core" } -log = { version = "0.4.8", features = ["std"] } +log = { version = "0.4.8", features = ["std", "kv"] } [dev-dependencies] sentry = { path = "../sentry", default-features = false, features = ["test"] } diff --git a/sentry-log/src/converters.rs b/sentry-log/src/converters.rs index baf7d464..f8c198a2 100644 --- a/sentry-log/src/converters.rs +++ b/sentry-log/src/converters.rs @@ -1,9 +1,10 @@ -use sentry_core::protocol::Event; +use sentry_core::protocol::{Event, Value}; #[cfg(feature = "logs")] use sentry_core::protocol::{Log, LogAttribute, LogLevel}; use sentry_core::{Breadcrumb, Level}; +use std::collections::BTreeMap; #[cfg(feature = "logs")] -use std::{collections::BTreeMap, time::SystemTime}; +use std::time::SystemTime; /// Converts a [`log::Level`] to a Sentry [`Level`], used for [`Event`] and [`Breadcrumb`]. pub fn convert_log_level(level: log::Level) -> Level { @@ -27,19 +28,92 @@ pub fn convert_log_level_to_sentry_log_level(level: log::Level) -> LogLevel { } } +/// Visitor to extract key-value pairs from log records +#[derive(Default)] +struct AttributeVisitor { + json_values: BTreeMap, +} + +impl AttributeVisitor { + fn record>(&mut self, key: &str, value: T) { + self.json_values.insert(key.to_owned(), value.into()); + } +} + +impl log::kv::VisitSource<'_> for AttributeVisitor { + fn visit_pair( + &mut self, + key: log::kv::Key, + value: log::kv::Value, + ) -> Result<(), log::kv::Error> { + let key = key.as_str(); + + if let Some(value) = value.to_borrowed_str() { + self.record(key, value); + } else if let Some(value) = value.to_u64() { + self.record(key, value); + } else if let Some(value) = value.to_f64() { + self.record(key, value); + } else if let Some(value) = value.to_bool() { + self.record(key, value); + } else { + self.record(key, format!("{:?}", value)); + }; + + Ok(()) + } +} + +fn extract_record_attributes(record: &log::Record<'_>) -> AttributeVisitor { + let mut visitor = AttributeVisitor::default(); + let _ = record.key_values().visit(&mut visitor); + visitor +} + /// Creates a [`Breadcrumb`] from a given [`log::Record`]. pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb { + let visitor = extract_record_attributes(record); + Breadcrumb { ty: "log".into(), level: convert_log_level(record.level()), category: Some(record.target().into()), message: Some(record.args().to_string()), + data: visitor.json_values, ..Default::default() } } /// Creates an [`Event`] from a given [`log::Record`]. pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> { + let visitor = extract_record_attributes(record); + let attributes = visitor.json_values; + + let mut context = BTreeMap::new(); + + let mut metadata_map = BTreeMap::new(); + metadata_map.insert("logger.target".into(), record.target().into()); + if let Some(module_path) = record.module_path() { + metadata_map.insert("logger.module_path".into(), module_path.into()); + } + if let Some(file) = record.file() { + metadata_map.insert("logger.file".into(), file.into()); + } + if let Some(line) = record.line() { + metadata_map.insert("logger.line".into(), line.into()); + } + context.insert( + "Rust Log Metadata".to_string(), + sentry_core::protocol::Context::Other(metadata_map), + ); + + if !attributes.is_empty() { + context.insert( + "Rust Log Attributes".to_string(), + sentry_core::protocol::Context::Other(attributes), + ); + } + Event { logger: Some(record.target().into()), level: convert_log_level(record.level()), @@ -60,7 +134,13 @@ pub fn exception_from_record(record: &log::Record<'_>) -> Event<'static> { /// 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(); + let visitor = extract_record_attributes(record); + + let mut attributes: BTreeMap = visitor + .json_values + .into_iter() + .map(|(key, val)| (key, val.into())) + .collect(); attributes.insert("logger.target".into(), record.target().into()); if let Some(module_path) = record.module_path() { @@ -75,8 +155,6 @@ pub fn log_from_record(record: &log::Record<'_>) -> Log { 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()), From 6bf13cb3412bd66eb9f69c3778a3355f85eaab45 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 18 Jun 2025 10:54:08 +0200 Subject: [PATCH 2/5] updategs --- sentry-log/src/converters.rs | 3 ++- sentry/tests/test_log.rs | 13 +++++++++++-- sentry/tests/test_log_logs.rs | 10 +++++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/sentry-log/src/converters.rs b/sentry-log/src/converters.rs index f8c198a2..151f4573 100644 --- a/sentry-log/src/converters.rs +++ b/sentry-log/src/converters.rs @@ -118,6 +118,7 @@ pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> { logger: Some(record.target().into()), level: convert_log_level(record.level()), message: Some(record.args().to_string()), + contexts: context, ..Default::default() } } @@ -163,4 +164,4 @@ pub fn log_from_record(record: &log::Record<'_>) -> Log { severity_number: None, attributes, } -} +} \ No newline at end of file diff --git a/sentry/tests/test_log.rs b/sentry/tests/test_log.rs index c460cf4b..607da6c2 100644 --- a/sentry/tests/test_log.rs +++ b/sentry/tests/test_log.rs @@ -13,8 +13,8 @@ fn test_log() { scope.set_tag("worker", "worker1"); }); - log::info!("Hello World!"); - log::error!("Shit's on fire yo"); + log::info!(user_id = 42, request_id = "abc123"; "Hello World!"); + log::error!(error_code = 500, retry_count = 3; "Shit's on fire yo"); }); assert_eq!(events.len(), 1); @@ -22,8 +22,17 @@ fn test_log() { assert_eq!(event.tags["worker"], "worker1"); assert_eq!(event.level, sentry::Level::Error); + if let Some(sentry::protocol::Context::Other(attributes)) = event.contexts.get("Rust Log Attributes") { + assert_eq!(attributes.get("error_code"), 500.into()); + assert_eq!(attributes.get("retry_count"), 3.into()); + } else { + panic!("Expected 'Rust Log Attributes' context to be present"); + } + assert_eq!(event.breadcrumbs[0].level, sentry::Level::Info); assert_eq!(event.breadcrumbs[0].message, Some("Hello World!".into())); + assert_eq!(event.breadcrumbs[0].data.get("user_id"), 42.into()); + assert_eq!(event.breadcrumbs[0].data.get("request_id"), "abc123".into()); } #[test] diff --git a/sentry/tests/test_log_logs.rs b/sentry/tests/test_log_logs.rs index adaa2993..1d631a5b 100644 --- a/sentry/tests/test_log_logs.rs +++ b/sentry/tests/test_log_logs.rs @@ -18,7 +18,7 @@ fn test_log_logs() { let envelopes = sentry::test::with_captured_envelopes_options( || { - log::info!("This is a log"); + log::info!(user_id = 42, request_id = "abc123"; "This is a log"); }, options, ); @@ -37,6 +37,14 @@ fn test_log_logs() { .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("user_id").unwrap().clone(), + 42.into() + ); + assert_eq!( + info_log.attributes.get("request_id").unwrap().clone(), + "abc123".into() + ); assert_eq!( info_log.attributes.get("logger.target").unwrap().clone(), "test_log_logs".into() From 7cc95aed103e1ec15074203929a484dd4e666bad Mon Sep 17 00:00:00 2001 From: lcian Date: Sun, 22 Jun 2025 23:27:13 +0200 Subject: [PATCH 3/5] improve --- sentry-log/src/converters.rs | 10 +++++----- sentry/tests/test_log.rs | 14 +++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/sentry-log/src/converters.rs b/sentry-log/src/converters.rs index 151f4573..f7a4356e 100644 --- a/sentry-log/src/converters.rs +++ b/sentry-log/src/converters.rs @@ -89,7 +89,7 @@ pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> { let visitor = extract_record_attributes(record); let attributes = visitor.json_values; - let mut context = BTreeMap::new(); + let mut contexts = BTreeMap::new(); let mut metadata_map = BTreeMap::new(); metadata_map.insert("logger.target".into(), record.target().into()); @@ -102,13 +102,13 @@ pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> { if let Some(line) = record.line() { metadata_map.insert("logger.line".into(), line.into()); } - context.insert( + contexts.insert( "Rust Log Metadata".to_string(), sentry_core::protocol::Context::Other(metadata_map), ); if !attributes.is_empty() { - context.insert( + contexts.insert( "Rust Log Attributes".to_string(), sentry_core::protocol::Context::Other(attributes), ); @@ -118,7 +118,7 @@ pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> { logger: Some(record.target().into()), level: convert_log_level(record.level()), message: Some(record.args().to_string()), - contexts: context, + contexts, ..Default::default() } } @@ -164,4 +164,4 @@ pub fn log_from_record(record: &log::Record<'_>) -> Log { severity_number: None, attributes, } -} \ No newline at end of file +} diff --git a/sentry/tests/test_log.rs b/sentry/tests/test_log.rs index 607da6c2..f6c384c4 100644 --- a/sentry/tests/test_log.rs +++ b/sentry/tests/test_log.rs @@ -13,8 +13,8 @@ fn test_log() { scope.set_tag("worker", "worker1"); }); - log::info!(user_id = 42, request_id = "abc123"; "Hello World!"); - log::error!(error_code = 500, retry_count = 3; "Shit's on fire yo"); + log::info!("Hello World!"); + log::error!("Shit's on fire yo"); }); assert_eq!(events.len(), 1); @@ -22,17 +22,9 @@ fn test_log() { assert_eq!(event.tags["worker"], "worker1"); assert_eq!(event.level, sentry::Level::Error); - if let Some(sentry::protocol::Context::Other(attributes)) = event.contexts.get("Rust Log Attributes") { - assert_eq!(attributes.get("error_code"), 500.into()); - assert_eq!(attributes.get("retry_count"), 3.into()); - } else { - panic!("Expected 'Rust Log Attributes' context to be present"); - } - + assert_eq!(event.breadcrumbs[0].level, sentry::Level::Info); assert_eq!(event.breadcrumbs[0].message, Some("Hello World!".into())); - assert_eq!(event.breadcrumbs[0].data.get("user_id"), 42.into()); - assert_eq!(event.breadcrumbs[0].data.get("request_id"), "abc123".into()); } #[test] From f5ce2a00e8128f67071ee51426bfbd608e20bc58 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 23 Jun 2025 12:57:33 +0200 Subject: [PATCH 4/5] improve --- sentry/tests/test_log.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry/tests/test_log.rs b/sentry/tests/test_log.rs index f6c384c4..c460cf4b 100644 --- a/sentry/tests/test_log.rs +++ b/sentry/tests/test_log.rs @@ -22,7 +22,6 @@ fn test_log() { assert_eq!(event.tags["worker"], "worker1"); assert_eq!(event.level, sentry::Level::Error); - assert_eq!(event.breadcrumbs[0].level, sentry::Level::Info); assert_eq!(event.breadcrumbs[0].message, Some("Hello World!".into())); } From c90af4964a0444bc25b70c667e8ebb95016da28b Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 25 Jun 2025 11:14:09 +0200 Subject: [PATCH 5/5] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72fb129c..bc697884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- feat(log): support kv feature of log (#851) by @lcian + - Attributes added to a `log` record using the `kv` feature are now recorded as attributes on the log sent to Sentry. + ## 0.41.0 ### Breaking changes