Skip to content

Commit 873bb1d

Browse files
authored
feat(log): support kv feature of log (#851)
1 parent f4245f1 commit 873bb1d

File tree

4 files changed

+99
-7
lines changed

4 files changed

+99
-7
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- feat(log): support kv feature of log (#851) by @lcian
6+
- Attributes added to a `log` record using the `kv` feature are now recorded as attributes on the log sent to Sentry.
7+
38
## 0.41.0
49

510
### Breaking changes

sentry-log/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ logs = ["sentry-core/logs"]
1818

1919
[dependencies]
2020
sentry-core = { version = "0.41.0", path = "../sentry-core" }
21-
log = { version = "0.4.8", features = ["std"] }
21+
log = { version = "0.4.8", features = ["std", "kv"] }
2222

2323
[dev-dependencies]
2424
sentry = { path = "../sentry", default-features = false, features = ["test"] }

sentry-log/src/converters.rs

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use sentry_core::protocol::Event;
1+
use sentry_core::protocol::{Event, Value};
22
#[cfg(feature = "logs")]
33
use sentry_core::protocol::{Log, LogAttribute, LogLevel};
44
use sentry_core::{Breadcrumb, Level};
5+
use std::collections::BTreeMap;
56
#[cfg(feature = "logs")]
6-
use std::{collections::BTreeMap, time::SystemTime};
7+
use std::time::SystemTime;
78

89
/// Converts a [`log::Level`] to a Sentry [`Level`], used for [`Event`] and [`Breadcrumb`].
910
pub fn convert_log_level(level: log::Level) -> Level {
@@ -27,23 +28,97 @@ pub fn convert_log_level_to_sentry_log_level(level: log::Level) -> LogLevel {
2728
}
2829
}
2930

31+
/// Visitor to extract key-value pairs from log records
32+
#[derive(Default)]
33+
struct AttributeVisitor {
34+
json_values: BTreeMap<String, Value>,
35+
}
36+
37+
impl AttributeVisitor {
38+
fn record<T: Into<Value>>(&mut self, key: &str, value: T) {
39+
self.json_values.insert(key.to_owned(), value.into());
40+
}
41+
}
42+
43+
impl log::kv::VisitSource<'_> for AttributeVisitor {
44+
fn visit_pair(
45+
&mut self,
46+
key: log::kv::Key,
47+
value: log::kv::Value,
48+
) -> Result<(), log::kv::Error> {
49+
let key = key.as_str();
50+
51+
if let Some(value) = value.to_borrowed_str() {
52+
self.record(key, value);
53+
} else if let Some(value) = value.to_u64() {
54+
self.record(key, value);
55+
} else if let Some(value) = value.to_f64() {
56+
self.record(key, value);
57+
} else if let Some(value) = value.to_bool() {
58+
self.record(key, value);
59+
} else {
60+
self.record(key, format!("{:?}", value));
61+
};
62+
63+
Ok(())
64+
}
65+
}
66+
67+
fn extract_record_attributes(record: &log::Record<'_>) -> AttributeVisitor {
68+
let mut visitor = AttributeVisitor::default();
69+
let _ = record.key_values().visit(&mut visitor);
70+
visitor
71+
}
72+
3073
/// Creates a [`Breadcrumb`] from a given [`log::Record`].
3174
pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb {
75+
let visitor = extract_record_attributes(record);
76+
3277
Breadcrumb {
3378
ty: "log".into(),
3479
level: convert_log_level(record.level()),
3580
category: Some(record.target().into()),
3681
message: Some(record.args().to_string()),
82+
data: visitor.json_values,
3783
..Default::default()
3884
}
3985
}
4086

4187
/// Creates an [`Event`] from a given [`log::Record`].
4288
pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> {
89+
let visitor = extract_record_attributes(record);
90+
let attributes = visitor.json_values;
91+
92+
let mut contexts = BTreeMap::new();
93+
94+
let mut metadata_map = BTreeMap::new();
95+
metadata_map.insert("logger.target".into(), record.target().into());
96+
if let Some(module_path) = record.module_path() {
97+
metadata_map.insert("logger.module_path".into(), module_path.into());
98+
}
99+
if let Some(file) = record.file() {
100+
metadata_map.insert("logger.file".into(), file.into());
101+
}
102+
if let Some(line) = record.line() {
103+
metadata_map.insert("logger.line".into(), line.into());
104+
}
105+
contexts.insert(
106+
"Rust Log Metadata".to_string(),
107+
sentry_core::protocol::Context::Other(metadata_map),
108+
);
109+
110+
if !attributes.is_empty() {
111+
contexts.insert(
112+
"Rust Log Attributes".to_string(),
113+
sentry_core::protocol::Context::Other(attributes),
114+
);
115+
}
116+
43117
Event {
44118
logger: Some(record.target().into()),
45119
level: convert_log_level(record.level()),
46120
message: Some(record.args().to_string()),
121+
contexts,
47122
..Default::default()
48123
}
49124
}
@@ -60,7 +135,13 @@ pub fn exception_from_record(record: &log::Record<'_>) -> Event<'static> {
60135
/// Creates a [`Log`] from a given [`log::Record`].
61136
#[cfg(feature = "logs")]
62137
pub fn log_from_record(record: &log::Record<'_>) -> Log {
63-
let mut attributes: BTreeMap<String, LogAttribute> = BTreeMap::new();
138+
let visitor = extract_record_attributes(record);
139+
140+
let mut attributes: BTreeMap<String, LogAttribute> = visitor
141+
.json_values
142+
.into_iter()
143+
.map(|(key, val)| (key, val.into()))
144+
.collect();
64145

65146
attributes.insert("logger.target".into(), record.target().into());
66147
if let Some(module_path) = record.module_path() {
@@ -75,8 +156,6 @@ pub fn log_from_record(record: &log::Record<'_>) -> Log {
75156

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

78-
// TODO: support the `kv` feature and store key value pairs as attributes
79-
80159
Log {
81160
level: convert_log_level_to_sentry_log_level(record.level()),
82161
body: format!("{}", record.args()),

sentry/tests/test_log_logs.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fn test_log_logs() {
1818

1919
let envelopes = sentry::test::with_captured_envelopes_options(
2020
|| {
21-
log::info!("This is a log");
21+
log::info!(user_id = 42, request_id = "abc123"; "This is a log");
2222
},
2323
options,
2424
);
@@ -37,6 +37,14 @@ fn test_log_logs() {
3737
.find(|log| log.level == sentry::protocol::LogLevel::Info)
3838
.expect("expected info log");
3939
assert_eq!(info_log.body, "This is a log");
40+
assert_eq!(
41+
info_log.attributes.get("user_id").unwrap().clone(),
42+
42.into()
43+
);
44+
assert_eq!(
45+
info_log.attributes.get("request_id").unwrap().clone(),
46+
"abc123".into()
47+
);
4048
assert_eq!(
4149
info_log.attributes.get("logger.target").unwrap().clone(),
4250
"test_log_logs".into()

0 commit comments

Comments
 (0)