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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion sentry-log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ logs = ["sentry-core/logs"]

[dependencies]
sentry-core = { version = "0.41.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"] }
Expand Down
89 changes: 84 additions & 5 deletions sentry-log/src/converters.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -27,23 +28,97 @@ 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<String, Value>,
}

impl AttributeVisitor {
fn record<T: Into<Value>>(&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 contexts = 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());
}
contexts.insert(
"Rust Log Metadata".to_string(),
sentry_core::protocol::Context::Other(metadata_map),
);

if !attributes.is_empty() {
contexts.insert(
"Rust Log Attributes".to_string(),
sentry_core::protocol::Context::Other(attributes),
);
}

Event {
logger: Some(record.target().into()),
level: convert_log_level(record.level()),
message: Some(record.args().to_string()),
contexts,
..Default::default()
}
}
Expand All @@ -60,7 +135,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<String, LogAttribute> = BTreeMap::new();
let visitor = extract_record_attributes(record);

let mut attributes: BTreeMap<String, LogAttribute> = 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() {
Expand All @@ -75,8 +156,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()),
Expand Down
10 changes: 9 additions & 1 deletion sentry/tests/test_log_logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand All @@ -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()
Expand Down