Skip to content

Commit 0b251dc

Browse files
authored
feat(tracing): add support for logs (#840)
1 parent c42ed3f commit 0b251dc

File tree

7 files changed

+189
-3
lines changed

7 files changed

+189
-3
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- feat(tracing): add support for logs (#840) by @lcian
8+
- To capture `tracing` events as Sentry structured logs, enable the `logs` feature of the `sentry` crate.
9+
- Then, initialize the SDK with `enable_logs: true` in your client options.
10+
- Finally, set up a custom event filter to map events to logs based on criteria such as severity. For example:
11+
```rust
12+
let sentry_layer = sentry_tracing::layer().event_filter(|md| match *md.level() {
13+
tracing::Level::ERROR => EventFilter::Event,
14+
tracing::Level::TRACE => EventFilter::Ignore,
15+
_ => EventFilter::Log,
16+
});
17+
```
18+
19+
520
### Fixes
621

722
- fix(logs): send environment in `sentry.environment` default attribute (#837) by @lcian

sentry-tracing/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ all-features = true
1818
[features]
1919
default = []
2020
backtrace = ["dep:sentry-backtrace"]
21+
logs = ["sentry-core/logs"]
2122

2223
[dependencies]
2324
sentry-core = { version = "0.39.0", path = "../sentry-core", features = [

sentry-tracing/src/converters.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ use std::collections::BTreeMap;
22
use std::error::Error;
33

44
use sentry_core::protocol::{Event, Exception, Mechanism, Value};
5+
#[cfg(feature = "logs")]
6+
use sentry_core::protocol::{Log, LogAttribute, LogLevel};
57
use sentry_core::{event_from_error, Breadcrumb, Level, TransactionOrSpan};
8+
#[cfg(feature = "logs")]
9+
use std::time::SystemTime;
610
use tracing_core::field::{Field, Visit};
711
use tracing_core::Subscriber;
812
use tracing_subscriber::layer::Context;
@@ -11,7 +15,7 @@ use tracing_subscriber::registry::LookupSpan;
1115
use super::layer::SentrySpanData;
1216
use crate::TAGS_PREFIX;
1317

14-
/// Converts a [`tracing_core::Level`] to a Sentry [`Level`].
18+
/// Converts a [`tracing_core::Level`] to a Sentry [`Level`], used for events and breadcrumbs.
1519
fn level_to_sentry_level(level: &tracing_core::Level) -> Level {
1620
match *level {
1721
tracing_core::Level::TRACE | tracing_core::Level::DEBUG => Level::Debug,
@@ -21,6 +25,18 @@ fn level_to_sentry_level(level: &tracing_core::Level) -> Level {
2125
}
2226
}
2327

28+
/// Converts a [`tracing_core::Level`] to a Sentry [`LogLevel`], used for logs.
29+
#[cfg(feature = "logs")]
30+
fn level_to_log_level(level: &tracing_core::Level) -> LogLevel {
31+
match *level {
32+
tracing_core::Level::TRACE => LogLevel::Trace,
33+
tracing_core::Level::DEBUG => LogLevel::Debug,
34+
tracing_core::Level::INFO => LogLevel::Info,
35+
tracing_core::Level::WARN => LogLevel::Warn,
36+
tracing_core::Level::ERROR => LogLevel::Error,
37+
}
38+
}
39+
2440
/// Converts a [`tracing_core::Level`] to the corresponding Sentry [`Exception::ty`] entry.
2541
#[allow(unused)]
2642
fn level_to_exception_type(level: &tracing_core::Level) -> &'static str {
@@ -308,3 +324,43 @@ where
308324
..Default::default()
309325
}
310326
}
327+
328+
/// Creates a [`Log`] from a given [`tracing_core::Event`]
329+
#[cfg(feature = "logs")]
330+
pub fn log_from_event<'context, S>(
331+
event: &tracing_core::Event,
332+
ctx: impl Into<Option<Context<'context, S>>>,
333+
) -> Log
334+
where
335+
S: Subscriber + for<'a> LookupSpan<'a>,
336+
{
337+
let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true);
338+
339+
let mut attributes: BTreeMap<String, LogAttribute> = visitor
340+
.json_values
341+
.into_iter()
342+
.map(|(key, val)| (key, val.into()))
343+
.collect();
344+
345+
let event_meta = event.metadata();
346+
if let Some(module_path) = event_meta.module_path() {
347+
attributes.insert("tracing.module_path".to_owned(), module_path.into());
348+
}
349+
if let Some(file) = event_meta.file() {
350+
attributes.insert("tracing.file".to_owned(), file.into());
351+
}
352+
if let Some(line) = event_meta.line() {
353+
attributes.insert("tracing.line".to_owned(), line.into());
354+
}
355+
356+
attributes.insert("sentry.origin".to_owned(), "auto.tracing".into());
357+
358+
Log {
359+
level: level_to_log_level(event.metadata().level()),
360+
body: message.unwrap_or_default(),
361+
trace_id: None,
362+
timestamp: SystemTime::now(),
363+
severity_number: None,
364+
attributes,
365+
}
366+
}

sentry-tracing/src/layer.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub enum EventFilter {
2222
Breadcrumb,
2323
/// Create a [`sentry_core::protocol::Event`] from this [`Event`]
2424
Event,
25+
/// Create a [`sentry_core::protocol::Log`] from this [`Event`]
26+
#[cfg(feature = "logs")]
27+
Log,
2528
}
2629

2730
/// The type of data Sentry should ingest for a [`Event`]
@@ -34,6 +37,9 @@ pub enum EventMapping {
3437
Breadcrumb(Breadcrumb),
3538
/// Captures the [`sentry_core::protocol::Event`] to Sentry.
3639
Event(sentry_core::protocol::Event<'static>),
40+
/// Captures the [`sentry_core::protocol::Log`] to Sentry.
41+
#[cfg(feature = "logs")]
42+
Log(sentry_core::protocol::Log),
3743
}
3844

3945
/// The default event filter.
@@ -215,6 +221,8 @@ where
215221
EventMapping::Breadcrumb(breadcrumb_from_event(event, span_ctx))
216222
}
217223
EventFilter::Event => EventMapping::Event(event_from_event(event, span_ctx)),
224+
#[cfg(feature = "logs")]
225+
EventFilter::Log => EventMapping::Log(log_from_event(event, span_ctx)),
218226
}
219227
}
220228
};
@@ -224,6 +232,8 @@ where
224232
sentry_core::capture_event(event);
225233
}
226234
EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
235+
#[cfg(feature = "logs")]
236+
EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
227237
_ => (),
228238
}
229239
}

sentry-tracing/src/lib.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
//! Support for automatic breadcrumb, event, and trace capturing from `tracing` events and spans.
22
//!
3-
//! The `tracing` crate is supported in three ways:
3+
//! The `tracing` crate is supported in four ways:
44
//! - `tracing` events can be captured as Sentry events. These are grouped and show up in the Sentry
55
//! [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be
66
//! acted upon.
77
//! - `tracing` events can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/).
88
//! Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when
99
//! an event is captured, either manually through e.g. `sentry::capture_message` or through integrations
1010
//! (e.g. the panic integration is enabled (default) and a panic happens).
11+
//! - `tracing` events can be captured as traditional [structured logs](https://docs.sentry.io/product/explore/logs/).
12+
//! The `tracing` fields are captured as attributes on the logs, which can be queried in the Logs
13+
//! explorer. (Available on crate feature `logs`)
1114
//! - `tracing` spans can be captured as Sentry spans. These can be used to provide more contextual
1215
//! information for errors, diagnose [performance
1316
//! issues](https://docs.sentry.io/product/insights/overview/), and capture additional attributes to
@@ -79,6 +82,23 @@
7982
//! }
8083
//! ```
8184
//!
85+
//! # Capturing logs
86+
//!
87+
//! Tracing events can be captured as traditional structured logs in Sentry.
88+
//! This is gated by the `logs` feature flag and requires setting up a custom Event filter/mapper
89+
//! to capture logs.
90+
//!
91+
//! ```
92+
//! // assuming `tracing::Level::INFO => EventFilter::Log` in your `event_filter`
93+
//! for i in 0..10 {
94+
//! tracing::info!(number = i, my.key = "val", my.num = 42, "This is a log");
95+
//! }
96+
//! ```
97+
//!
98+
//! The fields of a `tracing` event are captured as attributes of the log.
99+
//! Logs can be viewed and queried in the Logs explorer based on message and attributes.
100+
//! Fields containing dots will be displayed as nested under their common prefix in the UI.
101+
//!
82102
//! # Tracking Errors
83103
//!
84104
//! The easiest way to emit errors is by logging an event with `ERROR` level. This will create a

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"]
51+
logs = ["sentry-core/logs", "sentry-tracing?/logs"]
5252
# transports
5353
transport = ["reqwest", "native-tls"]
5454
reqwest = ["dep:reqwest", "httpdate", "tokio"]

sentry/tests/test_tracing.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,87 @@ fn test_set_transaction() {
193193
assert_eq!(transaction.name.as_deref().unwrap(), "new name");
194194
assert!(transaction.request.is_some());
195195
}
196+
197+
#[cfg(feature = "logs")]
198+
#[test]
199+
fn test_tracing_logs() {
200+
let sentry_layer = sentry_tracing::layer().event_filter(|_| sentry_tracing::EventFilter::Log);
201+
202+
let _dispatcher = tracing_subscriber::registry()
203+
.with(sentry_layer)
204+
.set_default();
205+
206+
let options = sentry::ClientOptions {
207+
enable_logs: true,
208+
..Default::default()
209+
};
210+
211+
let envelopes = sentry::test::with_captured_envelopes_options(
212+
|| {
213+
#[derive(Debug)]
214+
struct ConnectionError {
215+
message: String,
216+
source: Option<std::io::Error>,
217+
}
218+
219+
impl std::fmt::Display for ConnectionError {
220+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221+
write!(f, "{}", self.message)
222+
}
223+
}
224+
225+
impl std::error::Error for ConnectionError {
226+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
227+
self.source
228+
.as_ref()
229+
.map(|e| e as &(dyn std::error::Error + 'static))
230+
}
231+
}
232+
233+
let io_error =
234+
std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Connection refused");
235+
let connection_error = ConnectionError {
236+
message: "Failed to connect to database server".to_string(),
237+
source: Some(io_error),
238+
};
239+
240+
tracing::error!(
241+
my.key = "hello",
242+
an.error = &connection_error as &dyn std::error::Error,
243+
"This is an error log: {}",
244+
"hello"
245+
);
246+
},
247+
options,
248+
);
249+
250+
assert_eq!(envelopes.len(), 1);
251+
let envelope = envelopes.first().expect("expected envelope");
252+
let item = envelope.items().next().expect("expected envelope item");
253+
254+
match item {
255+
sentry::protocol::EnvelopeItem::ItemContainer(container) => match container {
256+
sentry::protocol::ItemContainer::Logs(logs) => {
257+
assert_eq!(logs.len(), 1);
258+
259+
let log = &logs[0];
260+
assert_eq!(log.level, sentry::protocol::LogLevel::Error);
261+
assert_eq!(log.body, "This is an error log: hello");
262+
assert!(log.trace_id.is_some());
263+
assert_eq!(
264+
log.attributes.get("my.key").unwrap().clone(),
265+
sentry::protocol::LogAttribute::from("hello")
266+
);
267+
assert_eq!(
268+
log.attributes.get("an.error").unwrap().clone(),
269+
sentry::protocol::LogAttribute::from(vec![
270+
"ConnectionError: Failed to connect to database server",
271+
"Custom: Connection refused"
272+
])
273+
);
274+
}
275+
_ => panic!("expected logs container"),
276+
},
277+
_ => panic!("expected item container"),
278+
}
279+
}

0 commit comments

Comments
 (0)