Skip to content

Commit 34db51d

Browse files
authored
feat(log): add support for logs (#841)
1 parent 88b00f3 commit 34db51d

File tree

8 files changed

+154
-14
lines changed

8 files changed

+154
-14
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@
2020
_ => EventFilter::Log,
2121
});
2222
```
23+
- feat(log): add support for logs (#841) by @lcian
24+
- To capture `log` records as Sentry structured logs, enable the `logs` feature of the `sentry` crate.
25+
- Then, initialize the SDK with `enable_logs: true` in your client options.
26+
- Finally, set up a custom event filter to map records to Sentry logs based on criteria such as severity. For example:
27+
```rust
28+
let logger = sentry::integrations::log::SentryLogger::new().filter(|md| match md.level() {
29+
log::Level::Error => LogFilter::Event,
30+
log::Level::Trace => LogFilter::Ignore,
31+
_ => LogFilter::Log,
32+
});
33+
```
2334

2435
### Fixes
2536

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sentry-log/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Sentry integration for log and env_logger crates.
1212
edition = "2021"
1313
rust-version = "1.81"
1414

15+
[features]
16+
default = []
17+
logs = ["sentry-core/logs"]
18+
1519
[dependencies]
1620
sentry-core = { version = "0.39.0", path = "../sentry-core" }
1721
log = { version = "0.4.8", features = ["std"] }

sentry-log/src/converters.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use sentry_core::protocol::Event;
2+
#[cfg(feature = "logs")]
3+
use sentry_core::protocol::{Log, LogAttribute, LogLevel};
24
use sentry_core::{Breadcrumb, Level};
5+
#[cfg(feature = "logs")]
6+
use std::{collections::BTreeMap, time::SystemTime};
37

4-
/// Converts a [`log::Level`] to a Sentry [`Level`]
8+
/// Converts a [`log::Level`] to a Sentry [`Level`], used for [`Event`] and [`Breadcrumb`].
59
pub fn convert_log_level(level: log::Level) -> Level {
610
match level {
711
log::Level::Error => Level::Error,
@@ -11,6 +15,18 @@ pub fn convert_log_level(level: log::Level) -> Level {
1115
}
1216
}
1317

18+
/// Converts a [`log::Level`] to a Sentry [`LogLevel`], used for [`Log`].
19+
#[cfg(feature = "logs")]
20+
pub fn convert_log_level_to_sentry_log_level(level: log::Level) -> LogLevel {
21+
match level {
22+
log::Level::Error => LogLevel::Error,
23+
log::Level::Warn => LogLevel::Warn,
24+
log::Level::Info => LogLevel::Info,
25+
log::Level::Debug => LogLevel::Debug,
26+
log::Level::Trace => LogLevel::Trace,
27+
}
28+
}
29+
1430
/// Creates a [`Breadcrumb`] from a given [`log::Record`].
1531
pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb {
1632
Breadcrumb {
@@ -40,3 +56,33 @@ pub fn exception_from_record(record: &log::Record<'_>) -> Event<'static> {
4056
// an exception record.
4157
event_from_record(record)
4258
}
59+
60+
/// Creates a [`Log`] from a given [`log::Record`].
61+
#[cfg(feature = "logs")]
62+
pub fn log_from_record(record: &log::Record<'_>) -> Log {
63+
let mut attributes: BTreeMap<String, LogAttribute> = BTreeMap::new();
64+
65+
attributes.insert("logger.target".into(), record.target().into());
66+
if let Some(module_path) = record.module_path() {
67+
attributes.insert("logger.module_path".into(), module_path.into());
68+
}
69+
if let Some(file) = record.file() {
70+
attributes.insert("logger.file".into(), file.into());
71+
}
72+
if let Some(line) = record.line() {
73+
attributes.insert("logger.line".into(), line.into());
74+
}
75+
76+
attributes.insert("sentry.origin".into(), "auto.logger.log".into());
77+
78+
// TODO: support the `kv` feature and store key value pairs as attributes
79+
80+
Log {
81+
level: convert_log_level_to_sentry_log_level(record.level()),
82+
body: format!("{}", record.args()),
83+
trace_id: None,
84+
timestamp: SystemTime::now(),
85+
severity_number: None,
86+
attributes,
87+
}
88+
}

sentry-log/src/lib.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1-
//! Adds support for automatic Breadcrumb and Event capturing from logs.
2-
//!
3-
//! The `log` crate is supported in two ways. First, logs can be captured as
4-
//! breadcrumbs for later. Secondly, error logs can be captured as events to
5-
//! Sentry. By default anything above `Info` is recorded as a breadcrumb and
1+
//! Adds support for automatic Breadcrumb, Event, and Log capturing from `log` records.
2+
//!
3+
//! The `log` crate is supported in three ways:
4+
//! - Records can be captured as Sentry events. These are grouped and show up in the Sentry
5+
//! [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be
6+
//! acted upon.
7+
//! - Records can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/).
8+
//! Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when
9+
//! an event is captured, either manually through e.g. `sentry::capture_message` or through integrations
10+
//! (e.g. the panic integration is enabled (default) and a panic happens).
11+
//! - Records can be captured as traditional [logs](https://docs.sentry.io/product/explore/logs/)
12+
//! Logs can be viewed and queried in the Logs explorer.
13+
//!
14+
//! By default anything above `Info` is recorded as a breadcrumb and
615
//! anything above `Error` is captured as error event.
716
//!
17+
//! To capture records as Sentry logs:
18+
//! 1. Enable the `logs` feature of the `sentry` crate.
19+
//! 2. Initialize the SDK with `enable_logs: true` in your client options.
20+
//! 3. Set up a custom filter (see below) to map records to logs (`LogFilter::Log`) based on criteria such as severity.
21+
//!
822
//! # Examples
923
//!
1024
//! ```

sentry-log/src/logger.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use log::Record;
22
use sentry_core::protocol::{Breadcrumb, Event};
33

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

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

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

3139
/// The default log filter.
@@ -135,6 +143,8 @@ impl<L: log::Log> log::Log for SentryLogger<L> {
135143
LogFilter::Breadcrumb => RecordMapping::Breadcrumb(breadcrumb_from_record(record)),
136144
LogFilter::Event => RecordMapping::Event(event_from_record(record)),
137145
LogFilter::Exception => RecordMapping::Event(exception_from_record(record)),
146+
#[cfg(feature = "logs")]
147+
LogFilter::Log => RecordMapping::Log(log_from_record(record)),
138148
},
139149
};
140150

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

149161
self.dest.log(record)

sentry/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ opentelemetry = ["sentry-opentelemetry"]
4848
# other features
4949
test = ["sentry-core/test"]
5050
release-health = ["sentry-core/release-health", "sentry-actix?/release-health"]
51-
logs = ["sentry-core/logs", "sentry-tracing?/logs"]
51+
logs = ["sentry-core/logs", "sentry-tracing?/logs", "sentry-log?/logs"]
5252
# transports
5353
transport = ["reqwest", "native-tls"]
5454
reqwest = ["dep:reqwest", "httpdate", "tokio"]

sentry/tests/test_log_logs.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#![cfg(feature = "test")]
2+
3+
// Test `log` integration <> Sentry structured logging.
4+
// This must be a in a separate file because `log::set_boxed_logger` can only be called once.
5+
#[cfg(feature = "logs")]
6+
#[test]
7+
fn test_log_logs() {
8+
let logger = sentry_log::SentryLogger::new().filter(|_| sentry_log::LogFilter::Log);
9+
10+
log::set_boxed_logger(Box::new(logger))
11+
.map(|()| log::set_max_level(log::LevelFilter::Trace))
12+
.unwrap();
13+
14+
let options = sentry::ClientOptions {
15+
enable_logs: true,
16+
..Default::default()
17+
};
18+
19+
let envelopes = sentry::test::with_captured_envelopes_options(
20+
|| {
21+
log::info!("This is a log");
22+
},
23+
options,
24+
);
25+
26+
assert_eq!(envelopes.len(), 1);
27+
let envelope = envelopes.first().expect("expected envelope");
28+
let item = envelope.items().next().expect("expected envelope item");
29+
30+
match item {
31+
sentry::protocol::EnvelopeItem::ItemContainer(container) => match container {
32+
sentry::protocol::ItemContainer::Logs(logs) => {
33+
assert_eq!(logs.len(), 1);
34+
35+
let info_log = logs
36+
.iter()
37+
.find(|log| log.level == sentry::protocol::LogLevel::Info)
38+
.expect("expected info log");
39+
assert_eq!(info_log.body, "This is a log");
40+
assert_eq!(
41+
info_log.attributes.get("logger.target").unwrap().clone(),
42+
"test_log_logs".into()
43+
);
44+
assert_eq!(
45+
info_log.attributes.get("sentry.origin").unwrap().clone(),
46+
"auto.logger.log".into()
47+
);
48+
}
49+
_ => panic!("expected logs"),
50+
},
51+
_ => panic!("expected item container"),
52+
}
53+
}

0 commit comments

Comments
 (0)