Skip to content

feat(tracing): add support for logs #840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 16, 2025
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

## Unreleased

### Features

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


### Fixes

- fix(logs): send environment in `sentry.environment` default attribute (#837) by @lcian
Expand Down
1 change: 1 addition & 0 deletions sentry-tracing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ all-features = true
[features]
default = []
backtrace = ["dep:sentry-backtrace"]
logs = ["sentry-core/logs"]

[dependencies]
sentry-core = { version = "0.39.0", path = "../sentry-core", features = [
Expand Down
58 changes: 57 additions & 1 deletion sentry-tracing/src/converters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use std::collections::BTreeMap;
use std::error::Error;

use sentry_core::protocol::{Event, Exception, Mechanism, Value};
#[cfg(feature = "logs")]
use sentry_core::protocol::{Log, LogAttribute, LogLevel};
use sentry_core::{event_from_error, Breadcrumb, Level, TransactionOrSpan};
#[cfg(feature = "logs")]
use std::time::SystemTime;
use tracing_core::field::{Field, Visit};
use tracing_core::Subscriber;
use tracing_subscriber::layer::Context;
Expand All @@ -11,7 +15,7 @@ use tracing_subscriber::registry::LookupSpan;
use super::layer::SentrySpanData;
use crate::TAGS_PREFIX;

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

/// Converts a [`tracing_core::Level`] to a Sentry [`LogLevel`], used for logs.
#[cfg(feature = "logs")]
fn level_to_log_level(level: &tracing_core::Level) -> LogLevel {
match *level {
tracing_core::Level::TRACE => LogLevel::Trace,
tracing_core::Level::DEBUG => LogLevel::Debug,
tracing_core::Level::INFO => LogLevel::Info,
tracing_core::Level::WARN => LogLevel::Warn,
tracing_core::Level::ERROR => LogLevel::Error,
}
}

/// Converts a [`tracing_core::Level`] to the corresponding Sentry [`Exception::ty`] entry.
#[allow(unused)]
fn level_to_exception_type(level: &tracing_core::Level) -> &'static str {
Expand Down Expand Up @@ -308,3 +324,43 @@ where
..Default::default()
}
}

/// Creates a [`Log`] from a given [`tracing_core::Event`]
#[cfg(feature = "logs")]
pub fn log_from_event<'context, S>(
event: &tracing_core::Event,
ctx: impl Into<Option<Context<'context, S>>>,
) -> Log
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true);

let mut attributes: BTreeMap<String, LogAttribute> = visitor
.json_values
.into_iter()
.map(|(key, val)| (key, val.into()))
.collect();

let event_meta = event.metadata();
if let Some(module_path) = event_meta.module_path() {
attributes.insert("tracing.module_path".to_owned(), module_path.into());
}
if let Some(file) = event_meta.file() {
attributes.insert("tracing.file".to_owned(), file.into());
}
if let Some(line) = event_meta.line() {
attributes.insert("tracing.line".to_owned(), line.into());
}

attributes.insert("sentry.origin".to_owned(), "auto.tracing".into());

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately we cannot get the sentry.message.template with tracing.

Log {
level: level_to_log_level(event.metadata().level()),
body: message.unwrap_or_default(),
trace_id: None,
timestamp: SystemTime::now(),
severity_number: None,
attributes,
}
}
10 changes: 10 additions & 0 deletions sentry-tracing/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub enum EventFilter {
Breadcrumb,
/// Create a [`sentry_core::protocol::Event`] from this [`Event`]
Event,
/// Create a [`sentry_core::protocol::Log`] from this [`Event`]
#[cfg(feature = "logs")]
Log,
}

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

/// The default event filter.
Expand Down Expand Up @@ -215,6 +221,8 @@ where
EventMapping::Breadcrumb(breadcrumb_from_event(event, span_ctx))
}
EventFilter::Event => EventMapping::Event(event_from_event(event, span_ctx)),
#[cfg(feature = "logs")]
EventFilter::Log => EventMapping::Log(log_from_event(event, span_ctx)),
}
}
};
Expand All @@ -224,6 +232,8 @@ where
sentry_core::capture_event(event);
}
EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb),
#[cfg(feature = "logs")]
EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)),
_ => (),
}
}
Expand Down
22 changes: 21 additions & 1 deletion sentry-tracing/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
//! Support for automatic breadcrumb, event, and trace capturing from `tracing` events and spans.
//!
//! The `tracing` crate is supported in three ways:
//! The `tracing` crate is supported in four ways:
//! - `tracing` events can be captured as Sentry events. These are grouped and show up in the Sentry
//! [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be
//! acted upon.
//! - `tracing` events can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/).
//! Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when
//! an event is captured, either manually through e.g. `sentry::capture_message` or through integrations
//! (e.g. the panic integration is enabled (default) and a panic happens).
//! - `tracing` events can be captured as traditional [structured logs](https://docs.sentry.io/product/explore/logs/).
//! The `tracing` fields are captured as attributes on the logs, which can be queried in the Logs
//! explorer. (Available on crate feature `logs`)
//! - `tracing` spans can be captured as Sentry spans. These can be used to provide more contextual
//! information for errors, diagnose [performance
//! issues](https://docs.sentry.io/product/insights/overview/), and capture additional attributes to
Expand Down Expand Up @@ -79,6 +82,23 @@
//! }
//! ```
//!
//! # Capturing logs
//!
//! Tracing events can be captured as traditional structured logs in Sentry.
//! This is gated by the `logs` feature flag and requires setting up a custom Event filter/mapper
//! to capture logs.
//!
//! ```
//! // assuming `tracing::Level::INFO => EventFilter::Log` in your `event_filter`
//! for i in 0..10 {
//! tracing::info!(number = i, my.key = "val", my.num = 42, "This is a log");
//! }
//! ```
//!
//! The fields of a `tracing` event are captured as attributes of the log.
//! Logs can be viewed and queried in the Logs explorer based on message and attributes.
//! Fields containing dots will be displayed as nested under their common prefix in the UI.
//!
//! # Tracking Errors
//!
//! The easiest way to emit errors is by logging an event with `ERROR` level. This will create a
Expand Down
2 changes: 1 addition & 1 deletion sentry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ opentelemetry = ["sentry-opentelemetry"]
# other features
test = ["sentry-core/test"]
release-health = ["sentry-core/release-health", "sentry-actix?/release-health"]
logs = ["sentry-core/logs"]
logs = ["sentry-core/logs", "sentry-tracing?/logs"]
# transports
transport = ["reqwest", "native-tls"]
reqwest = ["dep:reqwest", "httpdate", "tokio"]
Expand Down
84 changes: 84 additions & 0 deletions sentry/tests/test_tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,87 @@ fn test_set_transaction() {
assert_eq!(transaction.name.as_deref().unwrap(), "new name");
assert!(transaction.request.is_some());
}

#[cfg(feature = "logs")]
#[test]
fn test_tracing_logs() {
let sentry_layer = sentry_tracing::layer().event_filter(|_| sentry_tracing::EventFilter::Log);

let _dispatcher = tracing_subscriber::registry()
.with(sentry_layer)
.set_default();

let options = sentry::ClientOptions {
enable_logs: true,
..Default::default()
};

let envelopes = sentry::test::with_captured_envelopes_options(
|| {
#[derive(Debug)]
struct ConnectionError {
message: String,
source: Option<std::io::Error>,
}

impl std::fmt::Display for ConnectionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}

impl std::error::Error for ConnectionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| e as &(dyn std::error::Error + 'static))
}
}

let io_error =
std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Connection refused");
let connection_error = ConnectionError {
message: "Failed to connect to database server".to_string(),
source: Some(io_error),
};

tracing::error!(
my.key = "hello",
an.error = &connection_error as &dyn std::error::Error,
"This is an error log: {}",
"hello"
);
},
options,
);

assert_eq!(envelopes.len(), 1);
let envelope = envelopes.first().expect("expected envelope");
let item = envelope.items().next().expect("expected envelope item");

match item {
sentry::protocol::EnvelopeItem::ItemContainer(container) => match container {
sentry::protocol::ItemContainer::Logs(logs) => {
assert_eq!(logs.len(), 1);

let log = &logs[0];
assert_eq!(log.level, sentry::protocol::LogLevel::Error);
assert_eq!(log.body, "This is an error log: hello");
assert!(log.trace_id.is_some());
assert_eq!(
log.attributes.get("my.key").unwrap().clone(),
sentry::protocol::LogAttribute::from("hello")
);
assert_eq!(
log.attributes.get("an.error").unwrap().clone(),
sentry::protocol::LogAttribute::from(vec![
"ConnectionError: Failed to connect to database server",
"Custom: Connection refused"
])
);
}
_ => panic!("expected logs container"),
},
_ => panic!("expected item container"),
}
}
Loading