From 5305a238b198a54dba2035b92640bb81c2112d84 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 29 Sep 2026 14:17:49 +0200 Subject: [PATCH 01/15] rewrite docs --- sentry-tracing/src/lib.rs | 247 ++++++++++---------------------------- 1 file changed, 65 insertions(+), 182 deletions(-) diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index 89c8a02b..70aa310a 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -1,240 +1,123 @@ -//! Support for automatic breadcrumb, event, and trace capturing from `tracing` events and spans. +//! Capture `tracing` telemetry with four independently selectable tracing layers: //! -//! 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 -//! aggregate and compute [metrics](https://docs.sentry.io/product/explore/trace-explorer/). +//! - [`span_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.span_layer.html) captures `ERROR`, `WARN`, and `INFO` spans as Sentry transactions and spans. +//! - [`log_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.log_layer.html) captures `ERROR`, `WARN`, and `INFO` events as Sentry structured logs. +//! - [`error_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.error_layer.html) captures `ERROR` events as Sentry error events (issues). +//! - [`breadcrumb_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.breadcrumb_layer.html) adds `WARN` and `INFO` events as breadcrumbs to the current scope. //! -//! By default, events above `Info` are recorded as breadcrumbs, events above `Error` are captured -//! as error events, and spans above `Info` are recorded as spans. +//! For most applications, we recommend using the span and log layers. //! -//! # Configuration +//! # Getting started //! -//! To fully enable the tracing integration, set the traces sample rate and add a layer to the -//! tracing subscriber: +//! Set a traces sample rate to capture transactions, then install the layers you would like to +//! use. //! //! ``` +//! # #[cfg(feature = "logs")] +//! # { //! use tracing_subscriber::prelude::*; //! //! let _guard = sentry::init( //! sentry::ClientOptions::new() -//! // Enable capturing of traces; set this a to lower value in production: +//! // Use a lower sample rate in production. //! .traces_sample_rate(1.0), //! ); //! -//! // Register the Sentry tracing layer to capture breadcrumbs, events, and spans: //! tracing_subscriber::registry() -//! .with(tracing_subscriber::fmt::layer()) -//! .with(sentry::integrations::tracing::layer()) +//! .with(sentry::integrations::tracing::span_layer()) +//! .with(sentry::integrations::tracing::log_layer()) //! .init(); +//! # } //! ``` //! -//! You can customize the behavior of the layer by providing an explicit event filter, to customize which events -//! are captured by Sentry and the data type they are mapped to. -//! Similarly, you can provide a span filter to customize which spans are captured by Sentry. +//! When using the error and breadcrumb layers, we recommend installing the breadcrumb layer before +//! the error layer, so any breadcrumbs added by the breadcrumb layer appear on the correct error. //! -//! ``` -//! use sentry::integrations::tracing::EventFilter; -//! use tracing_subscriber::prelude::*; -//! -//! let sentry_layer = sentry::integrations::tracing::layer() -//! .event_filter(|md| match *md.level() { -//! tracing::Level::ERROR => EventFilter::Event, -//! _ => EventFilter::Ignore, -//! }) -//! .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); -//! -//! tracing_subscriber::registry() -//! .with(tracing_subscriber::fmt::layer()) -//! .with(sentry_layer) -//! .init(); -//! ``` -//! -//! In addition, a custom event mapper can be provided, to fully customize if and how `tracing` events are converted to Sentry data. +//! # Spans //! -//! Note that if both an event mapper and event filter are set, the mapper takes precedence, thus the -//! filter has no effect. -//! -//! # Capturing breadcrumbs -//! -//! Tracing events automatically create breadcrumbs that are attached to the current scope in -//! Sentry. They show up on errors and transactions captured within this scope as shown in the -//! examples below. -//! -//! Fields passed to the event macro are automatically tracked as structured data in Sentry. For -//! breadcrumbs, they are shown directly with the breadcrumb message. For other types of data, read -//! below. +//! `span_layer()` creates a Sentry transaction for a root `tracing` span and Sentry spans for +//! captured children. For example, [`tracing::instrument`](https://docs.rs/tracing/latest/tracing/attr.instrument.html) +//! creates spans for instrumented functions; arguments become span data unless skipped. //! //! ``` -//! for i in 0..10 { -//! tracing::debug!(number = i, "Generates a breadcrumb"); +//! #[tracing::instrument] +//! async fn outer() { +//! inner(42).await; //! } //! ``` //! -//! # 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. +//! If you also have the recommended `log_layer` installed, then the following will create an +//! info-level log associated with the same trace as the span for the function: //! //! ``` -//! // 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"); +//! #[tracing::instrument] +//! async fn inner(value: i32) { +//! tracing::info!(value, "completed step"); //! } //! ``` //! -//! 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. +//! By default, the Sentry span name matches the `tracing` span name, and its operation is +//! `::`. Spans at `DEBUG` and `TRACE` are not captured by default. //! -//! # Tracking Errors +//! ## Special span fields //! -//! The easiest way to emit errors is by logging an event with `ERROR` level. This will create a -//! grouped issue in Sentry. To add custom information, prepend the message with fields. It is also -//! possible to add Sentry tags if a field is prefixed with `"tags."` +//! - `sentry.name` overrides the Sentry span name, including when recorded after creation. +//! - `sentry.op` overrides the Sentry span operation, including when recorded after creation. +//! - `sentry.trace` continues the trace from a `sentry-trace` header. It must be set when a +//! **root** span is created; recording it later has no effect. //! //! ``` -//! tracing::error!( -//! field = "value", // will become a context field -//! tags.custom = "value", // will become a tag in Sentry -//! "this is an error with a custom tag", -//! ); -//! ``` -//! -//! To track [error structs](std::error::Error), assign a reference to error trait object as field -//! in one of the logging macros. By convention, it is recommended to use the `ERROR` level and -//! assign it to a field called `error`, although the integration will also work with all other -//! levels and field names. -//! -//! All other fields passed to the macro are captured in a custom "Tracing Fields" context in -//! Sentry. -//! +//! #[tracing::instrument(skip_all, fields( +//! sentry.name = "GET /payments", +//! sentry.op = "http.server", +//! sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()), +//! ))] +//! async fn handle_request(headers: std::collections::HashMap) { +//! // ... +//! } //! ``` -//! use std::error::Error; -//! use std::io; //! -//! let custom_error = io::Error::new(io::ErrorKind::Other, "oh no"); -//! tracing::error!(error = &custom_error as &dyn Error); -//! ``` +//! # Logs //! -//! It is also possible to combine error messages with error structs. In Sentry, this creates issues -//! grouped by the message and location of the error log, and adds the passed error as nested -//! source. +//! `log_layer()` sends `ERROR`, `WARN`, and `INFO` events as structured logs. Event fields +//! become searchable log attributes; fields with dots appear nested under their common +//! prefix in the Sentry Logs explorer. //! //! ``` -//! use std::error::Error; -//! use std::io; -//! -//! let custom_error = io::Error::new(io::ErrorKind::Other, "oh no"); -//! tracing::error!(error = &custom_error as &dyn Error, "my operation failed"); +//! tracing::info!(number = 42, my.key = "value", "Processed request"); //! ``` //! -//! # Sending multiple items to Sentry +//! # Errors //! -//! To map a `tracing` event to multiple items in Sentry, you can combine multiple event filters -//! using the bitwise or operator: +//! `error_layer()` captures an `ERROR` event as a Sentry error event. Fields become context; +//! fields prefixed with `tags.` become Sentry tags. //! //! ``` -//! use sentry::integrations::tracing::EventFilter; -//! use tracing_subscriber::prelude::*; -//! -//! let sentry_layer = sentry::integrations::tracing::layer() -//! .event_filter(|md| match *md.level() { -//! tracing::Level::ERROR => EventFilter::Event | EventFilter::Log, -//! tracing::Level::TRACE => EventFilter::Ignore, -//! _ => EventFilter::Log, -//! }) -//! .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); -//! -//! tracing_subscriber::registry() -//! .with(tracing_subscriber::fmt::layer()) -//! .with(sentry_layer) -//! .init(); +//! tracing::error!(field = "value", tags.custom = "value", "Request failed"); //! ``` //! -//! If you're using a custom event mapper instead of an event filter, use `EventMapping::Combined`. -//! -//! # Tracing Spans -//! -//! The integration automatically tracks `tracing` spans as spans in Sentry. A convenient way to do -//! this is with the `#[instrument]` attribute macro, which creates a span/transaction for the function -//! in Sentry. -//! -//! Function arguments are added as context fields automatically, which can be configured through -//! attribute arguments. Refer to documentation of the macro for more information. +//! To capture an error chain as Sentry exceptions, record an error trait object. A message +//! can also be provided for grouping and issue context: //! //! ``` -//! use std::time::Duration; -//! -//! use tracing_subscriber::prelude::*; -//! -//! // Functions instrumented by tracing automatically -//! // create spans/transactions around their execution. -//! #[tracing::instrument] -//! async fn outer() { -//! for i in 0..10 { -//! inner(i).await; -//! } -//! } -//! -//! // This creates spans inside the outer transaction, unless called directly. -//! #[tracing::instrument] -//! async fn inner(i: u32) { -//! // Also works, since log events are ingested by the tracing system -//! tracing::debug!(number = i, "Generates a breadcrumb"); +//! use std::error::Error; //! -//! tokio::time::sleep(Duration::from_millis(100)).await; -//! } +//! let error = std::io::Error::other("connection refused"); +//! tracing::error!(error = &error as &dyn Error, "Request failed"); //! ``` //! -//! By default, the name of the span sent to Sentry matches the name of the `tracing` span, which -//! is the name of the function when using `tracing::instrument`, or the name passed to the -//! `tracing::_span` macros. -//! -//! By default, the `op` of the span sent to Sentry is `default`. +//! # Breadcrumbs //! -//! ## Special Span Fields -//! -//! Some fields on spans are treated specially by the Sentry tracing integration: -//! - `sentry.name`: overrides the span name sent to Sentry. -//! This is useful to customize the span name when using `#[tracing::instrument]`, or to update -//! it retroactively (using `span.record`) after the span has been created. -//! - `sentry.op`: overrides the span `op` sent to Sentry. -//! - `sentry.trace`: in Sentry, the `sentry-trace` header is sent with HTTP requests to achieve distributed tracing. -//! If the value of this field is set to the value of a valid `sentry-trace` header, which -//! other Sentry SDKs send automatically with outgoing requests, then the SDK will continue the trace using the given distributed tracing information. -//! This is useful to achieve distributed tracing at service boundaries by using only the -//! `tracing` API. -//! Note that `sentry.trace` will only be effective on span creation (it cannot be applied retroactively) -//! and requires the span it's applied to to be a root span, i.e. no span should active upon its -//! creation. -//! -//! -//! Example: +//! `breadcrumb_layer()` adds `WARN` and `INFO` events to the current Sentry scope. Breadcrumbs +//! are sent with subsequent captured errors; they are not sent independently. Event fields +//! become breadcrumb data. //! //! ``` -//! #[tracing::instrument(skip_all, fields( -//! sentry.name = "GET /payments", -//! sentry.op = "http.server", -//! sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()), -//! ))] -//! async fn handle_request(headers: std::collections::HashMap) { -//! // ... -//! } +//! tracing::info!(request_id = 42, "Starting request"); +//! tracing::warn!("Retrying request"); //! ``` +#![cfg_attr(doc_cfg, feature(doc_cfg))] #![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")] #![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")] From 6cfd927dfa18d04e29eb96335a720fe1efccf77a Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 29 Sep 2026 14:26:49 +0200 Subject: [PATCH 02/15] WIP: Initial clanker version --- sentry-tracing/Cargo.toml | 1 + sentry-tracing/src/converters.rs | 1 + sentry-tracing/src/layer/breadcrumb.rs | 52 +++ sentry-tracing/src/layer/error.rs | 52 +++ sentry-tracing/src/layer/log.rs | 68 ++++ sentry-tracing/src/layer/mod.rs | 494 +++++------------------ sentry-tracing/src/layer/span.rs | 352 ++++++++++++++++ sentry-tracing/tests/shared/mod.rs | 2 + sentry-tracing/tests/telemetry_layers.rs | 127 ++++++ sentry/examples/tracing-demo.rs | 3 +- sentry/tests/test_tracing.rs | 1 + 11 files changed, 755 insertions(+), 398 deletions(-) create mode 100644 sentry-tracing/src/layer/breadcrumb.rs create mode 100644 sentry-tracing/src/layer/error.rs create mode 100644 sentry-tracing/src/layer/log.rs create mode 100644 sentry-tracing/src/layer/span.rs create mode 100644 sentry-tracing/tests/telemetry_layers.rs diff --git a/sentry-tracing/Cargo.toml b/sentry-tracing/Cargo.toml index 3cf36594..e84e759f 100644 --- a/sentry-tracing/Cargo.toml +++ b/sentry-tracing/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "doc_cfg"] [features] default = [] diff --git a/sentry-tracing/src/converters.rs b/sentry-tracing/src/converters.rs index 1fbe36ab..81160a84 100644 --- a/sentry-tracing/src/converters.rs +++ b/sentry-tracing/src/converters.rs @@ -351,6 +351,7 @@ where /// Creates a [`Log`] from a given [`tracing_core::Event`] #[cfg(feature = "logs")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] #[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")] pub fn log_from_event<'context, S>( event: &tracing_core::Event, diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs new file mode 100644 index 00000000..13227bc1 --- /dev/null +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -0,0 +1,52 @@ +use sentry_core::Breadcrumb; +use tracing_core::{Event, Level, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; +use tracing_subscriber::registry::LookupSpan; + +use crate::converters::breadcrumb_from_event; + +/// Adds `WARN` and `INFO` tracing events as Sentry breadcrumbs. +#[derive(Default)] +pub struct BreadcrumbLayer { + with_span_attributes: bool, +} + +impl BreadcrumbLayer { + /// Include attributes of captured parent spans in breadcrumbs. + /// Requires a [`super::SpanLayer`] installed on the same subscriber. + #[must_use] + pub fn enable_span_attributes(mut self) -> Self { + self.with_span_attributes = true; + self + } + + pub(super) fn capture_event(&self, event: &Event, ctx: &Context<'_, S>) + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + self.capture(breadcrumb_from_event( + event, + self.with_span_attributes.then_some(ctx), + )); + } + + pub(super) fn capture(&self, breadcrumb: Breadcrumb) { + sentry_core::add_breadcrumb(breadcrumb); + } +} + +impl Layer for BreadcrumbLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + if matches!(event.metadata().level(), &Level::WARN | &Level::INFO) { + self.capture_event(event, &ctx); + } + } +} + +/// Creates a layer that captures only breadcrumbs. +pub fn breadcrumb_layer() -> BreadcrumbLayer { + BreadcrumbLayer::default() +} diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs new file mode 100644 index 00000000..d5ee6419 --- /dev/null +++ b/sentry-tracing/src/layer/error.rs @@ -0,0 +1,52 @@ +use sentry_core::protocol::Event as SentryEvent; +use tracing_core::{Event, Level, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; +use tracing_subscriber::registry::LookupSpan; + +use crate::converters::event_from_event; + +/// Captures `ERROR` tracing events as Sentry error events. +#[derive(Default)] +pub struct ErrorLayer { + with_span_attributes: bool, +} + +impl ErrorLayer { + /// Include attributes of captured parent spans in error events. + /// Requires a [`super::SpanLayer`] installed on the same subscriber. + #[must_use] + pub fn enable_span_attributes(mut self) -> Self { + self.with_span_attributes = true; + self + } + + pub(super) fn capture_event(&self, event: &Event, ctx: &Context<'_, S>) + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + self.capture(event_from_event( + event, + self.with_span_attributes.then_some(ctx), + )); + } + + pub(super) fn capture(&self, event: SentryEvent<'static>) { + sentry_core::capture_event(event); + } +} + +impl Layer for ErrorLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + if event.metadata().level() == &Level::ERROR { + self.capture_event(event, &ctx); + } + } +} + +/// Creates a layer that captures only Sentry error events. +pub fn error_layer() -> ErrorLayer { + ErrorLayer::default() +} diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs new file mode 100644 index 00000000..387543e7 --- /dev/null +++ b/sentry-tracing/src/layer/log.rs @@ -0,0 +1,68 @@ +use sentry_core::protocol::Log; +use tracing_core::{Event, Level, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; +use tracing_subscriber::registry::LookupSpan; + +use crate::converters::log_from_event; + +/// Captures `ERROR`, `WARN`, and `INFO` tracing events as Sentry logs. +/// +/// Requires a client with logs enabled. +#[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] +#[derive(Default)] +pub struct LogLayer { + with_span_attributes: bool, +} + +impl LogLayer { + /// Include attributes of captured parent spans in logs. + /// Requires a [`super::SpanLayer`] installed on the same subscriber. + #[must_use] + pub fn enable_span_attributes(mut self) -> Self { + self.with_span_attributes = true; + self + } + + pub(super) fn capture_event(&self, event: &Event, ctx: &Context<'_, S>) + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + self.capture(log_from_event( + event, + self.with_span_attributes.then_some(ctx), + )); + } + + pub(super) fn capture(&self, log: Log) { + sentry_core::Hub::with_active(|hub| { + let enabled = hub.client().is_none_or(|client| { + let options = client.options(); + #[expect(deprecated, reason = "checking a deprecated field")] + options.enable_logs + }); + if enabled { + hub.capture_log(log); + } + }); + } +} + +impl Layer for LogLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + if matches!( + event.metadata().level(), + &Level::ERROR | &Level::WARN | &Level::INFO + ) { + self.capture_event(event, &ctx); + } + } +} + +/// Creates a layer that captures only Sentry logs. +#[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] +pub fn log_layer() -> LogLayer { + LogLayer::default() +} diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index fb561d23..c131a81f 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -1,25 +1,27 @@ -use std::borrow::Cow; -use std::cell::RefCell; -use std::collections::BTreeMap; -use std::sync::Arc; - use bitflags::bitflags; -use sentry_core::protocol::Value; -use sentry_core::{Breadcrumb, Hub, HubSwitchGuard, TransactionOrSpan}; -use tracing_core::field::Visit; -use tracing_core::{span, Event, Field, Level, Metadata, Subscriber}; +use sentry_core::Breadcrumb; +use tracing_core::{span as tracing_span, Event, Level, Metadata, Subscriber}; use tracing_subscriber::layer::{Context, Layer}; use tracing_subscriber::registry::LookupSpan; -use crate::converters::*; -use crate::SENTRY_NAME_FIELD; -use crate::SENTRY_OP_FIELD; -use crate::SENTRY_TRACE_FIELD; -use crate::TAGS_PREFIX; -use span_guard_stack::SpanGuardStack; - +mod breadcrumb; +mod error; +#[cfg(feature = "logs")] +mod log; +mod span; mod span_guard_stack; +pub use breadcrumb::{breadcrumb_layer, BreadcrumbLayer}; +pub use error::{error_layer, ErrorLayer}; +#[cfg(feature = "logs")] +pub use log::{log_layer, LogLayer}; +pub(super) use span::SentrySpanData; +pub use span::{span_layer, SpanLayer}; + +// TODO: Design new-layer filtering without hiding filtered spans from event +// context lookups. A per-layer tracing-subscriber Filter only narrows what +// that layer receives; it is not a replacement for legacy custom mappings. + bitflags! { /// The action that Sentry should perform for a given [`Event`] #[derive(Debug, Clone, Copy)] @@ -47,6 +49,7 @@ pub enum EventMapping { Event(Box>), /// Captures the [`sentry_core::protocol::Log`] to Sentry. #[cfg(feature = "logs")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] Log(sentry_core::protocol::Log), /// Captures multiple items to Sentry. /// Nesting multiple `EventMapping::Combined` inside each other will cause the inner mappings to be ignored. @@ -72,7 +75,7 @@ impl From> for CombinedEventMapping { } } -/// The default event filter. +/// The default event filter for the legacy combined layer. /// /// By default, an exception event is captured for `error`, a breadcrumb for /// `warning` and `info`, and `debug` and `trace` logs are ignored. @@ -92,8 +95,7 @@ pub fn default_event_filter(metadata: &Metadata) -> EventFilter { /// The default span filter. /// -/// By default, spans at the `error`, `warning`, and `info` -/// levels are captured +/// By default, spans at the `error`, `warning`, and `info` levels are captured. pub fn default_span_filter(metadata: &Metadata) -> bool { matches!( metadata.level(), @@ -103,16 +105,22 @@ pub fn default_span_filter(metadata: &Metadata) -> bool { type EventMapper = Box) -> EventMapping + Send + Sync>; -/// Provides a tracing layer that dispatches events to sentry +/// Legacy combined tracing layer; use [`span_layer`], [`breadcrumb_layer`], +/// [`error_layer`], and `log_layer()` instead. +#[deprecated( + note = "Use span_layer(), breadcrumb_layer(), error_layer(), and optionally log_layer() instead" +)] pub struct SentryLayer { event_filter: Box EventFilter + Send + Sync>, event_mapper: Option>, - - span_filter: Box bool + Send + Sync>, - - with_span_attributes: bool, + span_layer: SpanLayer, + breadcrumb: BreadcrumbLayer, + error: ErrorLayer, + #[cfg(feature = "logs")] + log: LogLayer, } +#[expect(deprecated, reason = "implementing the legacy layer")] impl SentryLayer { /// Sets a custom event filter function. /// @@ -151,7 +159,7 @@ impl SentryLayer { where F: Fn(&Metadata) -> bool + Send + Sync + 'static, { - self.span_filter = Box::new(filter); + self.span_layer.span_filter(filter); self } @@ -164,11 +172,17 @@ impl SentryLayer { /// while configuring your sentry client. #[must_use] pub fn enable_span_attributes(mut self) -> Self { - self.with_span_attributes = true; + self.breadcrumb = self.breadcrumb.enable_span_attributes(); + self.error = self.error.enable_span_attributes(); + #[cfg(feature = "logs")] + { + self.log = self.log.enable_span_attributes(); + } self } } +#[expect(deprecated, reason = "implementing the legacy layer")] impl Default for SentryLayer where S: Subscriber + for<'a> LookupSpan<'a>, @@ -177,407 +191,93 @@ where Self { event_filter: Box::new(default_event_filter), event_mapper: None, - - span_filter: Box::new(default_span_filter), - - with_span_attributes: false, - } - } -} - -#[inline(always)] -fn record_fields<'a, K: AsRef + Into>>( - span: &TransactionOrSpan, - data: BTreeMap, -) { - match span { - TransactionOrSpan::Span(span) => { - let mut span = span.data(); - for (key, value) in data { - if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) { - match value { - Value::Bool(value) => { - span.set_tag(stripped_key.to_owned(), value.to_string()) - } - Value::Number(value) => { - span.set_tag(stripped_key.to_owned(), value.to_string()) - } - Value::String(value) => span.set_tag(stripped_key.to_owned(), value), - _ => span.set_data(key.into().into_owned(), value), - } - } else { - span.set_data(key.into().into_owned(), value); - } - } - } - TransactionOrSpan::Transaction(transaction) => { - let mut transaction = transaction.data(); - for (key, value) in data { - if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) { - match value { - Value::Bool(value) => { - transaction.set_tag(stripped_key.into(), value.to_string()) - } - Value::Number(value) => { - transaction.set_tag(stripped_key.into(), value.to_string()) - } - Value::String(value) => transaction.set_tag(stripped_key.into(), value), - _ => transaction.set_data(key.into(), value), - } - } else { - transaction.set_data(key.into(), value); - } - } + span_layer: span_layer(), + breadcrumb: breadcrumb_layer(), + error: error_layer(), + #[cfg(feature = "logs")] + log: log_layer(), } } } -/// Data that is attached to the tracing Spans `extensions`, in order to -/// `finish` the corresponding sentry span `on_close`, and re-set its parent as -/// the *current* span. -pub(super) struct SentrySpanData { - pub(super) sentry_span: TransactionOrSpan, - hub: Arc, -} - +#[expect(deprecated, reason = "implementing the legacy layer")] impl Layer for SentryLayer where S: Subscriber + for<'a> LookupSpan<'a>, { fn on_event(&self, event: &Event, ctx: Context<'_, S>) { - let items = match &self.event_mapper { - Some(mapper) => mapper(event, ctx), - None => { - let span_ctx = self.with_span_attributes.then_some(ctx); - let filter = (self.event_filter)(event.metadata()); - let mut items = vec![]; - if filter.contains(EventFilter::Breadcrumb) { - items.push(EventMapping::Breadcrumb(breadcrumb_from_event( - event, - span_ctx.as_ref(), - ))); - } - if filter.contains(EventFilter::Event) { - items.push(EventMapping::Event( - event_from_event(event, span_ctx.as_ref()).into(), - )); - } - #[cfg(feature = "logs")] - if filter.contains(EventFilter::Log) { - items.push(EventMapping::Log(log_from_event(event, span_ctx.as_ref()))); - } - EventMapping::Combined(CombinedEventMapping(items)) - } - }; - let items = CombinedEventMapping::from(items); - - for item in items.0 { - match item { - EventMapping::Ignore => (), - EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), - EventMapping::Event(event) => { - sentry_core::capture_event(*event); - } - #[cfg(feature = "logs")] - EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| { - let enabled = hub.client().is_none_or(|client| { - let options = client.options(); - - #[expect(deprecated, reason = "checking a deprecated field")] - options.enable_logs - }); - if enabled { - hub.capture_log(log); - } - }), - EventMapping::Combined(_) => { - sentry_core::sentry_debug!( + if let Some(mapper) = &self.event_mapper { + // A custom mapper overrides the filter and supplies already converted + // items in the order they must be captured. + let items = CombinedEventMapping::from(mapper(event, ctx)); + for item in items.0 { + match item { + EventMapping::Ignore => (), + EventMapping::Breadcrumb(breadcrumb) => self.breadcrumb.capture(breadcrumb), + EventMapping::Event(event) => self.error.capture(*event), + #[cfg(feature = "logs")] + EventMapping::Log(log) => self.log.capture(log), + EventMapping::Combined(_) => sentry_core::sentry_debug!( "[SentryLayer] found nested CombinedEventMapping, ignoring" - ) + ), } } - } - } - - /// When a new Span gets created, run the filter and start a new sentry span - /// if it passes, setting it as the *current* sentry span. - fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) { - let span = match ctx.span(id) { - Some(span) => span, - None => return, - }; - - if !(self.span_filter)(span.metadata()) { - return; - } - - let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs); - let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name()); - let sentry_op = - sentry_op.unwrap_or_else(|| format!("{}::{}", span.metadata().target(), span.name())); - - let hub = sentry_core::Hub::current(); - let parent_sentry_span = hub.configure_scope(|scope| scope.get_span()); - - let mut sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span { - Some(parent) => parent.start_child(&sentry_op, sentry_name).into(), - None => { - let ctx = if let Some(trace_header) = sentry_trace { - sentry_core::TransactionContext::continue_from_headers( - sentry_name, - &sentry_op, - [("sentry-trace", trace_header.as_str())], - ) - } else { - sentry_core::TransactionContext::new(sentry_name, &sentry_op) - }; - - let tx = sentry_core::start_transaction(ctx); - tx.set_origin("auto.tracing"); - tx.into() + } else { + // The legacy filter may choose nondefault levels. Do not reapply the + // standalone event layers' default level checks here. + let filter = (self.event_filter)(event.metadata()); + if filter.contains(EventFilter::Breadcrumb) { + self.breadcrumb.capture_event(event, &ctx); + } + if filter.contains(EventFilter::Event) { + self.error.capture_event(event, &ctx); + } + #[cfg(feature = "logs")] + if filter.contains(EventFilter::Log) { + self.log.capture_event(event, &ctx); } - }; - // Add the data from the original span to the sentry span. - // This comes from typically the `fields` in `tracing::instrument`. - record_fields(&sentry_span, data); - - set_default_attributes(&mut sentry_span, span.metadata()); - - let mut extensions = span.extensions_mut(); - extensions.insert(SentrySpanData { sentry_span, hub }); - } - - /// Sets the entered span as *current* sentry span. - /// - /// A tracing span can be entered and exited multiple times, for example, - /// when using a `tracing::Instrumented` future. - /// - /// Spans must be exited on the same thread that they are entered. The - /// `sentry-tracing` integration's behavior is undefined if spans are - /// exited on threads other than the one they are entered from; - /// specifically, doing so will likely cause data to bleed between - /// [`Hub`]s in unexpected ways. - fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) { - let span = match ctx.span(id) { - Some(span) => span, - None => return, - }; - - let extensions = span.extensions(); - if let Some(data) = extensions.get::() { - // We fork the hub (based on the hub associated with the span) - // upon entering the span. This prevents data leakage if the span - // is entered and exited multiple times. - // - // Further, Hubs are meant to manage thread-local state, even - // though they can be shared across threads. As the span may being - // entered on a different thread than where it was created, we need - // to use a new hub to avoid altering state on the original thread. - let hub = Arc::new(Hub::new_from_top(&data.hub)); - - hub.configure_scope(|scope| { - scope.set_span(Some(data.sentry_span.clone())); - }); - - let guard = HubSwitchGuard::new(hub); - - SPAN_GUARDS.with(|guards| { - guards.borrow_mut().push(id.clone(), guard); - }); } } - /// Drop the current span's [`HubSwitchGuard`] to restore the parent [`Hub`]. - fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) { - let popped = SPAN_GUARDS.with(|guards| guards.borrow_mut().pop(id.clone())); - - // We should have popped a guard if the tracing span has `SentrySpanData` extensions. - sentry_core::debug_assert_or_log!( - popped.is_some() - || ctx - .span(id) - .is_none_or(|span| span.extensions().get::().is_none()), - "[SentryLayer] missing HubSwitchGuard on exit for span {id:?}. \ - This span has been exited more times on this thread than it has been entered, \ - likely due to dropping an `Entered` guard in a different thread than where it was \ - entered. This mismatch will likely cause the sentry-tracing layer to leak memory." - ); + fn on_new_span( + &self, + attrs: &tracing_span::Attributes<'_>, + id: &tracing_span::Id, + ctx: Context<'_, S>, + ) { + self.span_layer.on_new_span(attrs, id, ctx); } - /// When a span gets closed, finish the underlying sentry span, and set back - /// its parent as the *current* sentry span. - fn on_close(&self, id: span::Id, ctx: Context<'_, S>) { - let span = match ctx.span(&id) { - Some(span) => span, - None => return, - }; - - let mut extensions = span.extensions_mut(); - let SentrySpanData { sentry_span, .. } = match extensions.remove::() { - Some(data) => data, - None => return, - }; - - sentry_span.finish(); + fn on_enter(&self, id: &tracing_span::Id, ctx: Context<'_, S>) { + self.span_layer.on_enter(id, ctx); } - /// Implement the writing of extra data to span - fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) { - let span = match ctx.span(span) { - Some(s) => s, - _ => return, - }; - - let mut extensions = span.extensions_mut(); - let span = match extensions.get_mut::() { - Some(t) => &t.sentry_span, - _ => return, - }; - - let mut data = FieldVisitor::default(); - values.record(&mut data); - - let sentry_name = data - .json_values - .remove(SENTRY_NAME_FIELD) - .and_then(|v| match v { - Value::String(s) => Some(s), - _ => None, - }); - - let sentry_op = data - .json_values - .remove(SENTRY_OP_FIELD) - .and_then(|v| match v { - Value::String(s) => Some(s), - _ => None, - }); - - // `sentry.trace` cannot be applied retroactively - data.json_values.remove(SENTRY_TRACE_FIELD); - - if let Some(name) = sentry_name { - span.set_name(&name); - } - if let Some(op) = sentry_op { - span.set_op(&op); - } - - record_fields(span, data.json_values); + fn on_exit(&self, id: &tracing_span::Id, ctx: Context<'_, S>) { + self.span_layer.on_exit(id, ctx); } -} - -fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) { - span.set_data("sentry.tracing.target", metadata.target().into()); - if let Some(module) = metadata.module_path() { - span.set_data("code.module.name", module.into()); + fn on_close(&self, id: tracing_span::Id, ctx: Context<'_, S>) { + self.span_layer.on_close(id, ctx); } - if let Some(file) = metadata.file() { - span.set_data("code.file.path", file.into()); - } - - if let Some(line) = metadata.line() { - span.set_data("code.line.number", line.into()); + fn on_record( + &self, + id: &tracing_span::Id, + values: &tracing_span::Record<'_>, + ctx: Context<'_, S>, + ) { + self.span_layer.on_record(id, values, ctx); } } -/// Creates a default Sentry layer +/// Creates a legacy combined Sentry layer. Use individual telemetry layers instead. +#[deprecated( + note = "Use span_layer(), breadcrumb_layer(), error_layer(), and optionally log_layer() instead" +)] +#[expect(deprecated, reason = "returning the legacy layer")] pub fn layer() -> SentryLayer where S: Subscriber + for<'a> LookupSpan<'a>, { Default::default() } - -/// Extracts the attributes from a span, -/// returning the values of SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD separately -fn extract_span_data( - attrs: &span::Attributes, -) -> ( - BTreeMap<&'static str, Value>, - Option, - Option, - Option, -) { - let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| { - let mut visitor = SpanFieldVisitor { - debug_buffer, - json_values: Default::default(), - }; - attrs.record(&mut visitor); - visitor.json_values - }); - - let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v { - Value::String(s) => Some(s), - _ => None, - }); - - let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v { - Value::String(s) => Some(s), - _ => None, - }); - - let sentry_trace = json_values - .remove(SENTRY_TRACE_FIELD) - .and_then(|v| match v { - Value::String(s) => Some(s), - _ => None, - }); - - (json_values, name, op, sentry_trace) -} - -thread_local! { - static VISITOR_BUFFER: RefCell = const { RefCell::new(String::new()) }; - /// Hub switch guards keyed by span ID. - /// - /// Guard bookkeeping is thread-local by design. Correctness expects - /// balanced enter/exit callbacks on the same thread. - static SPAN_GUARDS: RefCell = RefCell::new(SpanGuardStack::new()); -} - -/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer. -struct SpanFieldVisitor<'s> { - debug_buffer: &'s mut String, - json_values: BTreeMap<&'static str, Value>, -} - -impl SpanFieldVisitor<'_> { - fn record>(&mut self, field: &Field, value: T) { - self.json_values.insert(field.name(), value.into()); - } -} - -impl Visit for SpanFieldVisitor<'_> { - fn record_i64(&mut self, field: &Field, value: i64) { - self.record(field, value); - } - - fn record_u64(&mut self, field: &Field, value: u64) { - self.record(field, value); - } - - fn record_bool(&mut self, field: &Field, value: bool) { - self.record(field, value); - } - - fn record_f64(&mut self, field: &Field, value: f64) { - self.record(field, value); - } - - fn record_str(&mut self, field: &Field, value: &str) { - self.record(field, value); - } - - fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { - use std::fmt::Write; - self.debug_buffer.reserve(128); - write!(self.debug_buffer, "{value:?}").unwrap(); - self.json_values - .insert(field.name(), self.debug_buffer.as_str().into()); - self.debug_buffer.clear(); - } -} diff --git a/sentry-tracing/src/layer/span.rs b/sentry-tracing/src/layer/span.rs new file mode 100644 index 00000000..27d86a63 --- /dev/null +++ b/sentry-tracing/src/layer/span.rs @@ -0,0 +1,352 @@ +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::sync::Arc; + +use sentry_core::protocol::Value; +use sentry_core::{Hub, HubSwitchGuard, TransactionOrSpan}; +use tracing_core::field::Visit; +use tracing_core::{span, Field, Metadata, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; +use tracing_subscriber::registry::LookupSpan; + +use super::span_guard_stack::SpanGuardStack; +use crate::converters::FieldVisitor; +use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX}; + +/// Captures tracing spans as Sentry transactions and spans. +/// +/// By default, spans at `ERROR`, `WARN`, and `INFO` are captured. Install this +/// layer to capture spans; event layers do not manage the span lifecycle. +pub struct SpanLayer { + span_filter: Box bool + Send + Sync>, +} + +impl Default for SpanLayer { + fn default() -> Self { + Self { + span_filter: Box::new(super::default_span_filter), + } + } +} + +impl SpanLayer { + pub(super) fn span_filter(&mut self, filter: F) + where + F: Fn(&Metadata) -> bool + Send + Sync + 'static, + { + self.span_filter = Box::new(filter); + } +} + +/// Creates a layer that captures only Sentry transactions and spans. +pub fn span_layer() -> SpanLayer { + SpanLayer::default() +} + +#[inline(always)] +fn record_fields<'a, K: AsRef + Into>>( + span: &TransactionOrSpan, + data: BTreeMap, +) { + match span { + TransactionOrSpan::Span(span) => { + let mut span = span.data(); + for (key, value) in data { + if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) { + match value { + Value::Bool(value) => { + span.set_tag(stripped_key.to_owned(), value.to_string()) + } + Value::Number(value) => { + span.set_tag(stripped_key.to_owned(), value.to_string()) + } + Value::String(value) => span.set_tag(stripped_key.to_owned(), value), + _ => span.set_data(key.into().into_owned(), value), + } + } else { + span.set_data(key.into().into_owned(), value); + } + } + } + TransactionOrSpan::Transaction(transaction) => { + let mut transaction = transaction.data(); + for (key, value) in data { + if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) { + match value { + Value::Bool(value) => { + transaction.set_tag(stripped_key.into(), value.to_string()) + } + Value::Number(value) => { + transaction.set_tag(stripped_key.into(), value.to_string()) + } + Value::String(value) => transaction.set_tag(stripped_key.into(), value), + _ => transaction.set_data(key.into(), value), + } + } else { + transaction.set_data(key.into(), value); + } + } + } + } +} + +/// Data attached to tracing span extensions, read by the event converters and +/// finished when the corresponding tracing span closes. +pub(crate) struct SentrySpanData { + pub(crate) sentry_span: TransactionOrSpan, + hub: Arc, +} + +impl Layer for SpanLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + /// When a new Span gets created, run the filter and start a new sentry span + /// if it passes, setting it as the *current* sentry span. + fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) { + let span = match ctx.span(id) { + Some(span) => span, + None => return, + }; + + if !(self.span_filter)(span.metadata()) { + return; + } + + let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs); + let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name()); + let sentry_op = + sentry_op.unwrap_or_else(|| format!("{}::{}", span.metadata().target(), span.name())); + + let hub = Hub::current(); + let parent_sentry_span = hub.configure_scope(|scope| scope.get_span()); + + let mut sentry_span: TransactionOrSpan = match &parent_sentry_span { + Some(parent) => parent.start_child(&sentry_op, sentry_name).into(), + None => { + let ctx = if let Some(trace_header) = sentry_trace { + sentry_core::TransactionContext::continue_from_headers( + sentry_name, + &sentry_op, + [("sentry-trace", trace_header.as_str())], + ) + } else { + sentry_core::TransactionContext::new(sentry_name, &sentry_op) + }; + + let tx = sentry_core::start_transaction(ctx); + tx.set_origin("auto.tracing"); + tx.into() + } + }; + record_fields(&sentry_span, data); + set_default_attributes(&mut sentry_span, span.metadata()); + + let mut extensions = span.extensions_mut(); + extensions.insert(SentrySpanData { sentry_span, hub }); + } + + /// Sets the entered span as *current* sentry span. + /// + /// A tracing span can be entered and exited multiple times, for example, + /// when using a `tracing::Instrumented` future. + /// + /// Spans must be exited on the same thread that they are entered. The + /// `sentry-tracing` integration's behavior is undefined if spans are + /// exited on threads other than the one they are entered from; + /// specifically, doing so will likely cause data to bleed between + /// [`Hub`]s in unexpected ways. + fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) { + let span = match ctx.span(id) { + Some(span) => span, + None => return, + }; + + let extensions = span.extensions(); + if let Some(data) = extensions.get::() { + let hub = Arc::new(Hub::new_from_top(&data.hub)); + hub.configure_scope(|scope| { + scope.set_span(Some(data.sentry_span.clone())); + }); + + let guard = HubSwitchGuard::new(hub); + SPAN_GUARDS.with(|guards| { + guards.borrow_mut().push(id.clone(), guard); + }); + } + } + + /// Drop the current span's [`HubSwitchGuard`] to restore the parent [`Hub`]. + fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) { + let popped = SPAN_GUARDS.with(|guards| guards.borrow_mut().pop(id.clone())); + + sentry_core::debug_assert_or_log!( + popped.is_some() + || ctx + .span(id) + .is_none_or(|span| span.extensions().get::().is_none()), + "[SentryLayer] missing HubSwitchGuard on exit for span {id:?}. \ + This span has been exited more times on this thread than it has been entered, \ + likely due to dropping an `Entered` guard in a different thread than where it was \ + entered. This mismatch will likely cause the sentry-tracing layer to leak memory." + ); + } + + /// Finish the underlying sentry span when the tracing span closes. + fn on_close(&self, id: span::Id, ctx: Context<'_, S>) { + let span = match ctx.span(&id) { + Some(span) => span, + None => return, + }; + + let mut extensions = span.extensions_mut(); + let SentrySpanData { sentry_span, .. } = match extensions.remove::() { + Some(data) => data, + None => return, + }; + sentry_span.finish(); + } + + /// Implement the writing of extra data to span. + fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) { + let span = match ctx.span(span) { + Some(s) => s, + _ => return, + }; + + let mut extensions = span.extensions_mut(); + let span = match extensions.get_mut::() { + Some(t) => &t.sentry_span, + _ => return, + }; + + let mut data = FieldVisitor::default(); + values.record(&mut data); + + let sentry_name = data + .json_values + .remove(SENTRY_NAME_FIELD) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + let sentry_op = data + .json_values + .remove(SENTRY_OP_FIELD) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + // `sentry.trace` cannot be applied retroactively + data.json_values.remove(SENTRY_TRACE_FIELD); + + if let Some(name) = sentry_name { + span.set_name(&name); + } + if let Some(op) = sentry_op { + span.set_op(&op); + } + + record_fields(span, data.json_values); + } +} + +fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) { + span.set_data("sentry.tracing.target", metadata.target().into()); + + if let Some(module) = metadata.module_path() { + span.set_data("code.module.name", module.into()); + } + if let Some(file) = metadata.file() { + span.set_data("code.file.path", file.into()); + } + if let Some(line) = metadata.line() { + span.set_data("code.line.number", line.into()); + } +} + +/// Extracts the attributes from a span, returning Sentry's special fields separately. +fn extract_span_data( + attrs: &span::Attributes, +) -> ( + BTreeMap<&'static str, Value>, + Option, + Option, + Option, +) { + let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| { + let mut visitor = SpanFieldVisitor { + debug_buffer, + json_values: Default::default(), + }; + attrs.record(&mut visitor); + visitor.json_values + }); + + let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + let sentry_trace = json_values + .remove(SENTRY_TRACE_FIELD) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + (json_values, name, op, sentry_trace) +} + +thread_local! { + static VISITOR_BUFFER: RefCell = const { RefCell::new(String::new()) }; + /// Hub switch guards keyed by span ID. + /// + /// Guard bookkeeping is thread-local by design. Correctness expects + /// balanced enter/exit callbacks on the same thread. + static SPAN_GUARDS: RefCell = RefCell::new(SpanGuardStack::new()); +} + +/// Records all span fields into a `BTreeMap`, reusing a mutable `String` as buffer. +struct SpanFieldVisitor<'s> { + debug_buffer: &'s mut String, + json_values: BTreeMap<&'static str, Value>, +} + +impl SpanFieldVisitor<'_> { + fn record>(&mut self, field: &Field, value: T) { + self.json_values.insert(field.name(), value.into()); + } +} + +impl Visit for SpanFieldVisitor<'_> { + fn record_i64(&mut self, field: &Field, value: i64) { + self.record(field, value); + } + fn record_u64(&mut self, field: &Field, value: u64) { + self.record(field, value); + } + fn record_bool(&mut self, field: &Field, value: bool) { + self.record(field, value); + } + fn record_f64(&mut self, field: &Field, value: f64) { + self.record(field, value); + } + fn record_str(&mut self, field: &Field, value: &str) { + self.record(field, value); + } + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + use std::fmt::Write; + self.debug_buffer.reserve(128); + write!(self.debug_buffer, "{value:?}").unwrap(); + self.json_values + .insert(field.name(), self.debug_buffer.as_str().into()); + self.debug_buffer.clear(); + } +} diff --git a/sentry-tracing/tests/shared/mod.rs b/sentry-tracing/tests/shared/mod.rs index af1fb976..b47d2f04 100644 --- a/sentry-tracing/tests/shared/mod.rs +++ b/sentry-tracing/tests/shared/mod.rs @@ -1,3 +1,5 @@ +#![expect(deprecated, reason = "legacy layer compatibility tests")] + use sentry::{ClientOptions, Hub}; use sentry_core::test::TestTransport; diff --git a/sentry-tracing/tests/telemetry_layers.rs b/sentry-tracing/tests/telemetry_layers.rs new file mode 100644 index 00000000..1e86285e --- /dev/null +++ b/sentry-tracing/tests/telemetry_layers.rs @@ -0,0 +1,127 @@ +#![expect(missing_docs, reason = "integration tests")] + +use sentry::protocol::EnvelopeItem; +#[cfg(feature = "logs")] +use sentry::protocol::{Context, ItemContainer, LogLevel, Value}; +use tracing_subscriber::prelude::*; + +#[test] +fn span_layer_does_not_capture_event_telemetry() { + let _dispatcher = tracing_subscriber::registry() + .with(sentry_tracing::span_layer()) + .set_default(); + + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let _span = tracing::info_span!("operation").entered(); + tracing::info!("info"); + tracing::error!("error"); + }, + sentry::ClientOptions::new().traces_sample_rate(1.0), + ); + + assert_eq!(envelopes.len(), 1); + assert!(matches!( + envelopes[0].items().next(), + Some(EnvelopeItem::Transaction(_)) + )); + assert_eq!(envelopes[0].items().count(), 1); +} + +#[cfg(feature = "logs")] +#[test] +fn spans_and_logs_do_not_capture_errors_or_breadcrumbs() { + let _dispatcher = tracing_subscriber::registry() + .with(sentry_tracing::span_layer()) + .with(sentry_tracing::log_layer()) + .set_default(); + + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let _span = tracing::info_span!("operation").entered(); + tracing::debug!("ignored"); + tracing::info!("info"); + tracing::error!("error"); + }, + sentry::ClientOptions::new().traces_sample_rate(1.0), + ); + + let mut transactions = 0; + let mut logs = Vec::new(); + for item in envelopes.iter().flat_map(|envelope| envelope.items()) { + match item { + EnvelopeItem::Transaction(_) => transactions += 1, + EnvelopeItem::ItemContainer(ItemContainer::Logs(items)) => logs.extend(items), + other => panic!("unexpected item: {other:?}"), + } + } + assert_eq!(transactions, 1); + assert_eq!(logs.len(), 2); + assert_eq!(logs[0].body, "info"); + assert_eq!(logs[1].body, "error"); +} + +#[cfg(feature = "logs")] +#[test] +fn all_layers_capture_their_default_levels_and_parent_fields() { + let _dispatcher = tracing_subscriber::registry() + .with(sentry_tracing::span_layer()) + .with(sentry_tracing::breadcrumb_layer().enable_span_attributes()) + .with(sentry_tracing::error_layer().enable_span_attributes()) + .with(sentry_tracing::log_layer().enable_span_attributes()) + .set_default(); + + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let _span = tracing::info_span!("operation", contextual_value = 42).entered(); + tracing::debug!("ignored"); + tracing::info!("info"); + tracing::warn!("warning"); + tracing::error!("error"); + }, + sentry::ClientOptions::new().traces_sample_rate(1.0), + ); + + let mut transactions = 0; + let mut events = Vec::new(); + let mut logs = Vec::new(); + for item in envelopes.iter().flat_map(|envelope| envelope.items()) { + match item { + EnvelopeItem::Transaction(_) => transactions += 1, + EnvelopeItem::Event(event) => events.push(event), + EnvelopeItem::ItemContainer(ItemContainer::Logs(items)) => logs.extend(items), + other => panic!("unexpected item: {other:?}"), + } + } + assert_eq!(transactions, 1); + assert_eq!(logs.len(), 3); + assert_eq!( + logs.iter().map(|log| log.level).collect::>(), + [LogLevel::Info, LogLevel::Warn, LogLevel::Error,] + ); + assert_eq!( + logs[0] + .attributes + .get("operation:contextual_value") + .cloned(), + Some(42.into()) + ); + + assert_eq!(events.len(), 1); + let event = events[0]; + assert_eq!(event.message.as_deref(), Some("error")); + assert_eq!(event.breadcrumbs.len(), 2); + assert_eq!(event.breadcrumbs[0].message.as_deref(), Some("info")); + assert_eq!(event.breadcrumbs[1].message.as_deref(), Some("warning")); + assert_eq!( + event.breadcrumbs[0].data.get("operation:contextual_value"), + Some(&Value::from(42)) + ); + match event.contexts.get("Rust Tracing Fields") { + Some(Context::Other(fields)) => assert_eq!( + fields.get("operation:contextual_value"), + Some(&Value::from(42)) + ), + other => panic!("missing span fields: {other:?}"), + } +} diff --git a/sentry/examples/tracing-demo.rs b/sentry/examples/tracing-demo.rs index 5d05d595..25057b0b 100644 --- a/sentry/examples/tracing-demo.rs +++ b/sentry/examples/tracing-demo.rs @@ -9,7 +9,8 @@ use tracing_subscriber::prelude::*; fn main() { tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) - .with(sentry_tracing::layer()) + .with(sentry_tracing::span_layer()) + .with(sentry_tracing::log_layer()) .try_init() .unwrap(); diff --git a/sentry/tests/test_tracing.rs b/sentry/tests/test_tracing.rs index ad10d250..5d5ba975 100644 --- a/sentry/tests/test_tracing.rs +++ b/sentry/tests/test_tracing.rs @@ -1,4 +1,5 @@ #![expect(missing_docs, reason = "predates lint enforcement")] +#![expect(deprecated, reason = "legacy layer compatibility tests")] #![cfg(feature = "test")] use sentry::protocol::{Context, Request, Value}; From adbe9fe3dfa4fa1f6377e68345ec3a6dda6b93ee Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 5 Oct 2026 11:34:56 +0200 Subject: [PATCH 03/15] more WIP: logs layer refactor --- sentry-tracing/src/converters.rs | 2 + sentry-tracing/src/layer/log.rs | 92 +++++++++++++++++++++++++------- sentry-tracing/src/layer/mod.rs | 4 +- 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/sentry-tracing/src/converters.rs b/sentry-tracing/src/converters.rs index 81160a84..d1095213 100644 --- a/sentry-tracing/src/converters.rs +++ b/sentry-tracing/src/converters.rs @@ -350,6 +350,8 @@ where } /// Creates a [`Log`] from a given [`tracing_core::Event`] +/// +/// If a context is provided, the data from the context is set on the span. #[cfg(feature = "logs")] #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] #[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")] diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 387543e7..f18595b3 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -1,5 +1,5 @@ use sentry_core::protocol::Log; -use tracing_core::{Event, Level, Subscriber}; +use tracing_core::{Event, Subscriber}; use tracing_subscriber::layer::{Context, Layer}; use tracing_subscriber::registry::LookupSpan; @@ -9,30 +9,32 @@ use crate::converters::log_from_event; /// /// Requires a client with logs enabled. #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] -#[derive(Default)] -pub struct LogLayer { +pub struct LogLayer { with_span_attributes: bool, + event_mapper: Option>>, } -impl LogLayer { +impl LogLayer { /// Include attributes of captured parent spans in logs. /// Requires a [`super::SpanLayer`] installed on the same subscriber. + /// + /// This option has no effect when [`Self::mapper`] is set. #[must_use] pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } - pub(super) fn capture_event(&self, event: &Event, ctx: &Context<'_, S>) + /// Sets a custom mapping from the tracing event to the log object. + pub fn mapper(mut self, mapper: M) -> Self where - S: Subscriber + for<'a> LookupSpan<'a>, + M: EventToLogMapper + 'static, { - self.capture(log_from_event( - event, - self.with_span_attributes.then_some(ctx), - )); + self.event_mapper = Some(Box::from(mapper)); + self } + /// Capture a log on the current `Hub`. pub(super) fn capture(&self, log: Log) { sentry_core::Hub::with_active(|hub| { let enabled = hub.client().is_none_or(|client| { @@ -47,22 +49,72 @@ impl LogLayer { } } -impl Layer for LogLayer +impl LogLayer where S: Subscriber + for<'a> LookupSpan<'a>, { - fn on_event(&self, event: &Event, ctx: Context<'_, S>) { - if matches!( - event.metadata().level(), - &Level::ERROR | &Level::WARN | &Level::INFO - ) { - self.capture_event(event, &ctx); + /// Capture a tracing event as a log. + pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) { + if let Some(log) = self.event_to_log(event, ctx) { + self.capture(log); + } + } + + /// Convert the + fn event_to_log(&self, event: &Event, ctx: Context<'_, S>) -> Option { + match &self.event_mapper { + Some(mapper) => mapper(event, ctx), + None => Some(log_from_event( + event, + self.with_span_attributes.then_some(&ctx), + )), } } } -/// Creates a layer that captures only Sentry logs. +impl Layer for LogLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + self.capture_tracing_event(event, ctx); + } +} + +/// A mapper function which convernts a tracing event to a [`Log`]. +/// +/// This is a pretty advanced API; when using it, callers must handle constructing the log +/// object manually from the tracing event. +/// +/// The function can also return [`None`], in which case, no log is created from the tracing event. +pub trait EventToLogMapper: Fn(&Event, Context<'_, S>) -> Option + Send + Sync {} + +impl EventToLogMapper for F where F: Fn(&Event, Context<'_, S>) -> Option + Send + Sync +{} + +/// Creates a layer that captures Sentry logs from tracing events. +/// +/// It is highly recommended to configure a level filter on the layer to limit log volume. We +/// recommend capturing logs at the `INFO` level, and above. +/// +/// ```rust +/// # use tracing_subscriber::prelude::*; +/// use tracing_subscriber::filter::LevelFilter; +/// +/// tracing_subscriber::registry() +/// .with(sentry::integrations::tracing::log_layer().with_filter(LevelFilter::INFO)) +/// .init(); +/// +/// // This will be captured ... +/// tracing::info!("INFO log"); +/// +/// // ... but this will not be, due to LevelFilter::INFO being set. +/// tracing::debug!("DEBUG log"); +/// ``` #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] -pub fn log_layer() -> LogLayer { - LogLayer::default() +pub fn log_layer() -> LogLayer { + LogLayer { + with_span_attributes: false, + event_mapper: None, + } } diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index c131a81f..cfbdbbf3 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -117,7 +117,7 @@ pub struct SentryLayer { breadcrumb: BreadcrumbLayer, error: ErrorLayer, #[cfg(feature = "logs")] - log: LogLayer, + log: LogLayer, } #[expect(deprecated, reason = "implementing the legacy layer")] @@ -234,7 +234,7 @@ where } #[cfg(feature = "logs")] if filter.contains(EventFilter::Log) { - self.log.capture_event(event, &ctx); + self.log.capture_tracing_event(event, ctx); } } } From 2b44dd71a1a5e86cf7927951df923b6fa585d978 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 5 Oct 2026 13:42:24 +0200 Subject: [PATCH 04/15] WIP: Expand changes to other filters, and improve docs --- sentry-tracing/src/layer/breadcrumb.rs | 151 ++++++++++++++++++++--- sentry-tracing/src/layer/error.rs | 136 +++++++++++++++++--- sentry-tracing/src/layer/log.rs | 38 +++++- sentry-tracing/src/layer/mod.rs | 18 +-- sentry-tracing/src/layer/span.rs | 42 +++++-- sentry-tracing/tests/telemetry_layers.rs | 30 ++++- 6 files changed, 343 insertions(+), 72 deletions(-) diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index 13227bc1..be80a5fe 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -1,52 +1,163 @@ use sentry_core::Breadcrumb; -use tracing_core::{Event, Level, Subscriber}; +use tracing_core::{Event, Subscriber}; use tracing_subscriber::layer::{Context, Layer}; use tracing_subscriber::registry::LookupSpan; use crate::converters::breadcrumb_from_event; -/// Adds `WARN` and `INFO` tracing events as Sentry breadcrumbs. -#[derive(Default)] -pub struct BreadcrumbLayer { +/// Adds tracing events as Sentry breadcrumbs. +pub struct BreadcrumbLayer { with_span_attributes: bool, + event_mapper: Option>>, } -impl BreadcrumbLayer { - /// Include attributes of captured parent spans in breadcrumbs. - /// Requires a [`super::SpanLayer`] installed on the same subscriber. +impl BreadcrumbLayer { + /// Include attributes of parent spans in breadcrumbs. + /// + /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, + /// this option only has any effect when the `SpanLayer` is installed. + /// + /// Furthermore, only spans accepted by the breadcrumb layer's + /// [filter](tracing_subscriber::layer#per-layer-filtering) are considered when attaching + /// attributes. When using this option, therefore, we recommend building a custom filter that + /// accepts all spans, while filtering events to the desired levels, like so: + /// + /// ```rust + /// # use tracing_subscriber::prelude::*; + /// use tracing::Level; + /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; + /// + /// tracing_subscriber::registry() + /// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + /// .with( + /// sentry_tracing::breadcrumb_layer() + /// .enable_span_attributes() + /// .with_filter(filter_fn(|metadata| { + /// // Accept all spans, but only `WARN` and `INFO` events. + /// metadata.is_span() || matches!(*metadata.level(), Level::WARN | Level::INFO) + /// })), + /// ) + /// .init(); + /// ``` + /// + /// Including all spans in the breadcrumb layer filter is safe, because this layer does not send + /// any spans to Sentry. + /// + /// This option has no effect when [`Self::mapper`] is set. #[must_use] pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } - pub(super) fn capture_event(&self, event: &Event, ctx: &Context<'_, S>) + /// Sets a custom mapping from the tracing event to the breadcrumb object. + #[must_use] + pub fn mapper(mut self, mapper: M) -> Self where - S: Subscriber + for<'a> LookupSpan<'a>, + M: EventToBreadcrumbMapper + 'static, { - self.capture(breadcrumb_from_event( - event, - self.with_span_attributes.then_some(ctx), - )); + self.event_mapper = Some(Box::from(mapper)); + self } + /// Add a breadcrumb to the current `Hub`'s scope. pub(super) fn capture(&self, breadcrumb: Breadcrumb) { sentry_core::add_breadcrumb(breadcrumb); } } -impl Layer for BreadcrumbLayer +impl BreadcrumbLayer where S: Subscriber + for<'a> LookupSpan<'a>, { - fn on_event(&self, event: &Event, ctx: Context<'_, S>) { - if matches!(event.metadata().level(), &Level::WARN | &Level::INFO) { - self.capture_event(event, &ctx); + /// Capture a tracing event as a breadcrumb. + pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) { + if let Some(breadcrumb) = self.event_to_breadcrumb(event, ctx) { + self.capture(breadcrumb); + } + } + + /// Convert a tracing event to a breadcrumb, using the custom mapper if one is set. + fn event_to_breadcrumb(&self, event: &Event, ctx: Context<'_, S>) -> Option { + match &self.event_mapper { + Some(mapper) => mapper(event, ctx), + None => Some(breadcrumb_from_event( + event, + self.with_span_attributes.then_some(&ctx), + )), } } } -/// Creates a layer that captures only breadcrumbs. -pub fn breadcrumb_layer() -> BreadcrumbLayer { - BreadcrumbLayer::default() +impl Layer for BreadcrumbLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + self.capture_tracing_event(event, ctx); + } +} + +/// A mapper function which converts a tracing event to a [`Breadcrumb`]. +/// +/// This is a pretty advanced API; when using it, callers must handle constructing the breadcrumb +/// object manually from the tracing event. +/// +/// The function can also return [`None`], in which case, no breadcrumb is created from the +/// tracing event. +pub trait EventToBreadcrumbMapper: + Fn(&Event, Context<'_, S>) -> Option + Send + Sync +{ +} + +impl EventToBreadcrumbMapper for F where + F: Fn(&Event, Context<'_, S>) -> Option + Send + Sync +{ +} + +/// Creates a layer that adds tracing events as Sentry breadcrumbs. +/// +/// For most users, we would recommend using the [`log_layer`](super::log_layer) instead of this +/// layer, as logs are trace-connected and sent regardless of whether an error occurs. +/// +/// Breadcrumbs are only sent attached to Sentry error events. Users using this layer may therefore +/// also wish to enable the [`error_layer`](super::error_layer). +/// +/// # Filtering +/// +/// It is highly recommended to configure a filter on the layer to limit breadcrumb volume. We +/// recommend capturing `WARN` and `INFO` events. If you also use the error layer, exclude `ERROR` +/// events, which would otherwise duplicate the error event as a breadcrumb. Install the breadcrumb +/// layer before the error layer, so the breadcrumbs are added before the error event is captured. +/// +/// ```rust +/// # use tracing_subscriber::prelude::*; +/// use tracing::Level; +/// use tracing_subscriber::filter::{filter_fn, LevelFilter}; +/// +/// tracing_subscriber::registry() +/// .with( +/// sentry_tracing::breadcrumb_layer().with_filter(filter_fn(|metadata| { +/// matches!(*metadata.level(), Level::WARN | Level::INFO) +/// })), +/// ) +/// .with(sentry_tracing::error_layer().with_filter(LevelFilter::ERROR)) +/// .init(); +/// +/// // These will be captured as breadcrumbs ... +/// tracing::info!("INFO breadcrumb"); +/// tracing::warn!("WARN breadcrumb"); +/// +/// // ... but this will not be. +/// tracing::debug!("DEBUG breadcrumb"); +/// +/// // This will not be captured as a breadcrumb, but as an error event by the error layer. The +/// // breadcrumbs above will be contained in that error event. +/// tracing::error!("ERROR event"); +/// ``` +pub fn breadcrumb_layer() -> BreadcrumbLayer { + BreadcrumbLayer { + with_span_attributes: false, + event_mapper: None, + } } diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index d5ee6419..f66b5e56 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -1,52 +1,148 @@ use sentry_core::protocol::Event as SentryEvent; -use tracing_core::{Event, Level, Subscriber}; +use tracing_core::{Event, Subscriber}; use tracing_subscriber::layer::{Context, Layer}; use tracing_subscriber::registry::LookupSpan; use crate::converters::event_from_event; -/// Captures `ERROR` tracing events as Sentry error events. -#[derive(Default)] -pub struct ErrorLayer { +/// Captures tracing events as Sentry error events. +pub struct ErrorLayer { with_span_attributes: bool, + event_mapper: Option>>, } -impl ErrorLayer { - /// Include attributes of captured parent spans in error events. - /// Requires a [`super::SpanLayer`] installed on the same subscriber. +impl ErrorLayer { + /// Include attributes of parent spans in error events. + /// + /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, + /// this option only has any effect when the `SpanLayer` is installed. + /// + /// Furthermore, only spans accepted by the error layer's + /// [filter](tracing_subscriber::layer#per-layer-filtering) are considered when attaching + /// attributes. When using this option, therefore, we recommend building a custom filter that + /// accepts all spans, while filtering errors to the desired level, like so: + /// + /// ```rust + /// # use tracing_subscriber::prelude::*; + /// use tracing::Level; + /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; + /// + /// tracing_subscriber::registry() + /// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + /// .with( + /// sentry_tracing::error_layer() + /// .enable_span_attributes() + /// .with_filter(filter_fn(|metadata| { + /// // Accept all spans, but only `ERROR` events. + /// metadata.is_span() || *metadata.level() <= Level::ERROR + /// })), + /// ) + /// .init(); + /// ``` + /// + /// Including all spans in the error layer filter is safe, because this layer does not send + /// any spans to Sentry. + /// + /// This option has no effect when [`Self::mapper`] is set. #[must_use] pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } - pub(super) fn capture_event(&self, event: &Event, ctx: &Context<'_, S>) + /// Sets a custom mapping from the tracing event to the Sentry error event object. + #[must_use] + pub fn mapper(mut self, mapper: M) -> Self where - S: Subscriber + for<'a> LookupSpan<'a>, + M: EventToErrorMapper + 'static, { - self.capture(event_from_event( - event, - self.with_span_attributes.then_some(ctx), - )); + self.event_mapper = Some(Box::from(mapper)); + self } + /// Capture an error event on the current `Hub`. pub(super) fn capture(&self, event: SentryEvent<'static>) { sentry_core::capture_event(event); } } -impl Layer for ErrorLayer +impl ErrorLayer where S: Subscriber + for<'a> LookupSpan<'a>, { - fn on_event(&self, event: &Event, ctx: Context<'_, S>) { - if event.metadata().level() == &Level::ERROR { - self.capture_event(event, &ctx); + /// Capture a tracing event as a Sentry error event. + pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) { + if let Some(sentry_event) = self.event_to_error(event, ctx) { + self.capture(sentry_event); + } + } + + /// Convert a tracing event to a Sentry error event, using the custom mapper if one is set. + fn event_to_error(&self, event: &Event, ctx: Context<'_, S>) -> Option> { + match &self.event_mapper { + Some(mapper) => mapper(event, ctx), + None => Some(event_from_event( + event, + self.with_span_attributes.then_some(&ctx), + )), } } } -/// Creates a layer that captures only Sentry error events. -pub fn error_layer() -> ErrorLayer { - ErrorLayer::default() +impl Layer for ErrorLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + self.capture_tracing_event(event, ctx); + } +} + +/// A mapper function which converts a tracing event to a Sentry error event. +/// +/// This is a pretty advanced API; when using it, callers must handle constructing the Sentry +/// event object manually from the tracing event. +/// +/// The function can also return [`None`], in which case, no error event is created from the +/// tracing event. +pub trait EventToErrorMapper: + Fn(&Event, Context<'_, S>) -> Option> + Send + Sync +{ +} + +impl EventToErrorMapper for F where + F: Fn(&Event, Context<'_, S>) -> Option> + Send + Sync +{ +} + +/// Creates a layer that captures tracing events as Sentry error events. +/// +/// Most applications should start with [`log_layer`](super::log_layer), and add this layer if +/// `ERROR` events should also create Sentry issues. Combine it with +/// [`breadcrumb_layer`](super::breadcrumb_layer) to attach the events leading up to each error. +/// +/// # Filtering +/// +/// It is highly recommended to configure a filter on the layer, so that only `ERROR` events +/// create Sentry issues. +/// +/// ```rust +/// # use tracing_subscriber::prelude::*; +/// use tracing_subscriber::filter::LevelFilter; +/// +/// tracing_subscriber::registry() +/// .with(sentry_tracing::error_layer().with_filter(LevelFilter::ERROR)) +/// .init(); +/// +/// // This will be captured ... +/// tracing::error!("ERROR event"); +/// +/// // ... but this will not be, due to LevelFilter::ERROR being set. +/// tracing::warn!("WARN event"); +/// ``` +pub fn error_layer() -> ErrorLayer { + ErrorLayer { + with_span_attributes: false, + event_mapper: None, + } } diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index f18595b3..daec014b 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -15,8 +15,36 @@ pub struct LogLayer { } impl LogLayer { - /// Include attributes of captured parent spans in logs. - /// Requires a [`super::SpanLayer`] installed on the same subscriber. + /// Include attributes of parent spans in logs. + /// + /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, + /// this option only has any effect when the `SpanLayer` is installed. + /// + /// Furthermore, only spans accepted by the log layer's + /// [filter](tracing_subscriber::layer#per-layer-filtering) are considered when attaching + /// attributes. When using this option, therefore, we recommend building a custom filter that + /// accepts all spans, while filtering events to the desired level, like so: + /// + /// ```rust + /// # use tracing_subscriber::prelude::*; + /// use tracing::Level; + /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; + /// + /// tracing_subscriber::registry() + /// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + /// .with( + /// sentry_tracing::log_layer() + /// .enable_span_attributes() + /// .with_filter(filter_fn(|metadata| { + /// // Accept all spans, but only events at `INFO` and more severe. + /// metadata.is_span() || *metadata.level() <= Level::INFO + /// })), + /// ) + /// .init(); + /// ``` + /// + /// Including all spans in the log layer filter is safe, because this layer does not send any + /// spans to Sentry. /// /// This option has no effect when [`Self::mapper`] is set. #[must_use] @@ -94,15 +122,17 @@ impl EventToLogMapper for F where F: Fn(&Event, Context<'_, S>) -> Opti /// Creates a layer that captures Sentry logs from tracing events. /// +/// # Filtering +/// /// It is highly recommended to configure a level filter on the layer to limit log volume. We -/// recommend capturing logs at the `INFO` level, and above. +/// recommend capturing logs at the `INFO` level, and more severe. /// /// ```rust /// # use tracing_subscriber::prelude::*; /// use tracing_subscriber::filter::LevelFilter; /// /// tracing_subscriber::registry() -/// .with(sentry::integrations::tracing::log_layer().with_filter(LevelFilter::INFO)) +/// .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) /// .init(); /// /// // This will be captured ... diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index cfbdbbf3..fd86fdc9 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -11,10 +11,10 @@ mod log; mod span; mod span_guard_stack; -pub use breadcrumb::{breadcrumb_layer, BreadcrumbLayer}; -pub use error::{error_layer, ErrorLayer}; +pub use breadcrumb::{breadcrumb_layer, BreadcrumbLayer, EventToBreadcrumbMapper}; +pub use error::{error_layer, ErrorLayer, EventToErrorMapper}; #[cfg(feature = "logs")] -pub use log::{log_layer, LogLayer}; +pub use log::{log_layer, EventToLogMapper, LogLayer}; pub(super) use span::SentrySpanData; pub use span::{span_layer, SpanLayer}; @@ -114,8 +114,8 @@ pub struct SentryLayer { event_filter: Box EventFilter + Send + Sync>, event_mapper: Option>, span_layer: SpanLayer, - breadcrumb: BreadcrumbLayer, - error: ErrorLayer, + breadcrumb: BreadcrumbLayer, + error: ErrorLayer, #[cfg(feature = "logs")] log: LogLayer, } @@ -159,7 +159,7 @@ impl SentryLayer { where F: Fn(&Metadata) -> bool + Send + Sync + 'static, { - self.span_layer.span_filter(filter); + self.span_layer = self.span_layer.span_filter(filter); self } @@ -191,7 +191,7 @@ where Self { event_filter: Box::new(default_event_filter), event_mapper: None, - span_layer: span_layer(), + span_layer: span_layer().span_filter(default_span_filter), breadcrumb: breadcrumb_layer(), error: error_layer(), #[cfg(feature = "logs")] @@ -227,10 +227,10 @@ where // standalone event layers' default level checks here. let filter = (self.event_filter)(event.metadata()); if filter.contains(EventFilter::Breadcrumb) { - self.breadcrumb.capture_event(event, &ctx); + self.breadcrumb.capture_tracing_event(event, ctx.clone()); } if filter.contains(EventFilter::Event) { - self.error.capture_event(event, &ctx); + self.error.capture_tracing_event(event, ctx.clone()); } #[cfg(feature = "logs")] if filter.contains(EventFilter::Log) { diff --git a/sentry-tracing/src/layer/span.rs b/sentry-tracing/src/layer/span.rs index 27d86a63..855d189b 100644 --- a/sentry-tracing/src/layer/span.rs +++ b/sentry-tracing/src/layer/span.rs @@ -16,32 +16,48 @@ use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX} /// Captures tracing spans as Sentry transactions and spans. /// -/// By default, spans at `ERROR`, `WARN`, and `INFO` are captured. Install this -/// layer to capture spans; event layers do not manage the span lifecycle. +/// Install this layer to capture spans; event layers do not manage the span lifecycle. pub struct SpanLayer { span_filter: Box bool + Send + Sync>, } -impl Default for SpanLayer { - fn default() -> Self { - Self { - span_filter: Box::new(super::default_span_filter), - } - } -} - impl SpanLayer { - pub(super) fn span_filter(&mut self, filter: F) + /// Sets the filter deciding which spans are captured, replacing any previous filter. + #[must_use] + pub(super) fn span_filter(mut self, filter: F) -> Self where F: Fn(&Metadata) -> bool + Send + Sync + 'static, { self.span_filter = Box::new(filter); + self } } -/// Creates a layer that captures only Sentry transactions and spans. +/// Creates a layer that captures tracing spans as Sentry transactions and spans. +/// +/// # Filtering +/// +/// It is highly recommended to configure a level filter on the layer to limit span volume. We +/// recommend capturing spans at the `INFO` level, and more severe. +/// +/// ```rust +/// # use tracing_subscriber::prelude::*; +/// use tracing_subscriber::filter::LevelFilter; +/// +/// tracing_subscriber::registry() +/// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) +/// .init(); +/// +/// // This will be captured ... +/// let _info = tracing::info_span!("INFO span").entered(); +/// +/// // ... but this will not be, due to LevelFilter::INFO being set. +/// let _debug = tracing::debug_span!("DEBUG span").entered(); +/// ``` pub fn span_layer() -> SpanLayer { - SpanLayer::default() + SpanLayer { + span_filter: Box::new(|_| true), + } } #[inline(always)] diff --git a/sentry-tracing/tests/telemetry_layers.rs b/sentry-tracing/tests/telemetry_layers.rs index 1e86285e..504cd8a6 100644 --- a/sentry-tracing/tests/telemetry_layers.rs +++ b/sentry-tracing/tests/telemetry_layers.rs @@ -3,6 +3,7 @@ use sentry::protocol::EnvelopeItem; #[cfg(feature = "logs")] use sentry::protocol::{Context, ItemContainer, LogLevel, Value}; +use tracing_subscriber::filter::{filter_fn, LevelFilter}; use tracing_subscriber::prelude::*; #[test] @@ -32,8 +33,8 @@ fn span_layer_does_not_capture_event_telemetry() { #[test] fn spans_and_logs_do_not_capture_errors_or_breadcrumbs() { let _dispatcher = tracing_subscriber::registry() - .with(sentry_tracing::span_layer()) - .with(sentry_tracing::log_layer()) + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) .set_default(); let envelopes = sentry::test::with_captured_envelopes_options( @@ -65,10 +66,27 @@ fn spans_and_logs_do_not_capture_errors_or_breadcrumbs() { #[test] fn all_layers_capture_their_default_levels_and_parent_fields() { let _dispatcher = tracing_subscriber::registry() - .with(sentry_tracing::span_layer()) - .with(sentry_tracing::breadcrumb_layer().enable_span_attributes()) - .with(sentry_tracing::error_layer().enable_span_attributes()) - .with(sentry_tracing::log_layer().enable_span_attributes()) + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with( + sentry_tracing::breadcrumb_layer() + .enable_span_attributes() + // `WARN` and `INFO` only; `ERROR` is captured by the error layer. + .with_filter(filter_fn(|m| { + matches!(*m.level(), tracing::Level::WARN | tracing::Level::INFO) + })), + ) + .with( + sentry_tracing::error_layer() + .enable_span_attributes() + .with_filter(filter_fn(|m| { + m.is_span() || *m.level() == tracing::Level::ERROR + })), + ) + .with( + sentry_tracing::log_layer() + .enable_span_attributes() + .with_filter(LevelFilter::INFO), + ) .set_default(); let envelopes = sentry::test::with_captured_envelopes_options( From d2231f7297885c49f29cea4fd7274315ecb6c3f1 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 5 Oct 2026 15:08:32 +0200 Subject: [PATCH 05/15] move tests to new api --- CHANGELOG.md | 10 + sentry-tracing/README.md | 244 ++++-------------- sentry-tracing/src/layer/breadcrumb.rs | 5 +- sentry-tracing/src/layer/log.rs | 7 +- sentry-tracing/src/layer/mod.rs | 109 ++++++-- sentry-tracing/src/layer/span.rs | 29 +-- sentry-tracing/src/lib.rs | 81 ++---- sentry-tracing/tests/breadcrumbs.rs | 36 --- .../tests/future_cross_thread_info_span.rs | 7 +- .../tests/future_cross_thread_trace_span.rs | 5 + .../tests/futures_same_thread_info_span.rs | 5 + sentry-tracing/tests/log_span_attributes.rs | 47 ++++ sentry-tracing/tests/name_op_updates.rs | 6 + sentry-tracing/tests/shared/mod.rs | 8 - sentry-tracing/tests/smoke.rs | 79 +++--- sentry-tracing/tests/span_cross_thread.rs | 25 +- sentry-tracing/tests/span_reentrancy.rs | 6 + sentry-tracing/tests/telemetry_layers.rs | 145 ----------- sentry/examples/tracing-demo.rs | 5 +- ...test_tracing.rs => test_legacy_tracing.rs} | 46 +++- 20 files changed, 359 insertions(+), 546 deletions(-) delete mode 100644 sentry-tracing/tests/breadcrumbs.rs create mode 100644 sentry-tracing/tests/log_span_attributes.rs delete mode 100644 sentry-tracing/tests/telemetry_layers.rs rename sentry/tests/{test_tracing.rs => test_legacy_tracing.rs} (90%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8094ca51..119741dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### Features + +- Added independently selectable `SpanLayer`, `LogLayer` (`logs` feature), `ErrorLayer`, and `BreadcrumbLayer` to `sentry-tracing`. Most applications should use the span and log layers. The new layers do not filter by level internally; configure per-layer filters with `.with_filter(...)` to control capture. + +### Deprecations + +- Deprecated `sentry_tracing::layer()` and `SentryLayer` in favor of the individual layers. Legacy filtering defaults are unchanged. See the [migration guide](sentry-tracing/src/layer/mod.rs) for the equivalent four-layer configuration and custom-mapper differences. + ## 0.49.3 ### Features diff --git a/sentry-tracing/README.md b/sentry-tracing/README.md index 1b5f3cab..754f28f7 100644 --- a/sentry-tracing/README.md +++ b/sentry-tracing/README.md @@ -6,231 +6,73 @@ # Sentry Rust SDK: sentry-tracing -Support for automatic breadcrumb, event, and trace capturing from `tracing` events and spans. - -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 - aggregate and compute [metrics](https://docs.sentry.io/product/explore/trace-explorer/). - -By default, events above `Info` are recorded as breadcrumbs, events above `Error` are captured -as error events, and spans above `Info` are recorded as spans. - -## Configuration - -To fully enable the tracing integration, set the traces sample rate and add a layer to the -tracing subscriber: +Capture `tracing` spans and events as Sentry traces and structured logs. -```rust -use tracing_subscriber::prelude::*; +For most applications, we recommend installing only these two layers: -let _guard = sentry::init( - sentry::ClientOptions::new() - // Enable capturing of traces; set this a to lower value in production: - .traces_sample_rate(1.0), -); +- [`span_layer`] captures `tracing` spans as Sentry transactions and spans. +- [`log_layer`] captures `tracing` events as + [Sentry Logs](https://docs.sentry.io/product/explore/logs/). Requires the `logs` feature. -// Register the Sentry tracing layer to capture breadcrumbs, events, and spans: -tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .with(sentry::integrations::tracing::layer()) - .init(); -``` +## Getting started -You can customize the behavior of the layer by providing an explicit event filter, to customize which events -are captured by Sentry and the data type they are mapped to. -Similarly, you can provide a span filter to customize which spans are captured by Sentry. +Enable the `logs` feature on `sentry-tracing` (or the `tracing` and `logs` features on +`sentry`). +Set a traces sample rate to capture transactions, then install the span and log layers with +explicit level filters: ```rust -use sentry::integrations::tracing::EventFilter; +use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; -let sentry_layer = sentry::integrations::tracing::layer() - .event_filter(|md| match *md.level() { - tracing::Level::ERROR => EventFilter::Event, - _ => EventFilter::Ignore, - }) - .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); - -tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .with(sentry_layer) - .init(); -``` - -In addition, a custom event mapper can be provided, to fully customize if and how `tracing` events are converted to Sentry data. - -Note that if both an event mapper and event filter are set, the mapper takes precedence, thus the -filter has no effect. - -## Capturing breadcrumbs - -Tracing events automatically create breadcrumbs that are attached to the current scope in -Sentry. They show up on errors and transactions captured within this scope as shown in the -examples below. - -Fields passed to the event macro are automatically tracked as structured data in Sentry. For -breadcrumbs, they are shown directly with the breadcrumb message. For other types of data, read -below. - -```rust -for i in 0..10 { - tracing::debug!(number = i, "Generates a breadcrumb"); -} -``` - -## 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. - -```rust -// 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 -grouped issue in Sentry. To add custom information, prepend the message with fields. It is also -possible to add Sentry tags if a field is prefixed with `"tags."` - -```rust -tracing::error!( - field = "value", // will become a context field - tags.custom = "value", // will become a tag in Sentry - "this is an error with a custom tag", +let _guard = sentry::init( + sentry::ClientOptions::new() + // Use a lower sample rate in production. + .traces_sample_rate(1.0), ); -``` - -To track [error structs](https://docs.rs/sentry-tracing/0.49.3/sentry_tracing/std::error::Error), assign a reference to error trait object as field -in one of the logging macros. By convention, it is recommended to use the `ERROR` level and -assign it to a field called `error`, although the integration will also work with all other -levels and field names. - -All other fields passed to the macro are captured in a custom "Tracing Fields" context in -Sentry. - -```rust -use std::error::Error; -use std::io; - -let custom_error = io::Error::new(io::ErrorKind::Other, "oh no"); -tracing::error!(error = &custom_error as &dyn Error); -``` - -It is also possible to combine error messages with error structs. In Sentry, this creates issues -grouped by the message and location of the error log, and adds the passed error as nested -source. - -```rust -use std::error::Error; -use std::io; - -let custom_error = io::Error::new(io::ErrorKind::Other, "oh no"); -tracing::error!(error = &custom_error as &dyn Error, "my operation failed"); -``` - -## Sending multiple items to Sentry - -To map a `tracing` event to multiple items in Sentry, you can combine multiple event filters -using the bitwise or operator: - -```rust -use sentry::integrations::tracing::EventFilter; -use tracing_subscriber::prelude::*; - -let sentry_layer = sentry::integrations::tracing::layer() - .event_filter(|md| match *md.level() { - tracing::Level::ERROR => EventFilter::Event | EventFilter::Log, - tracing::Level::TRACE => EventFilter::Ignore, - _ => EventFilter::Log, - }) - .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .with(sentry_layer) + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) .init(); ``` -If you're using a custom event mapper instead of an event filter, use `EventMapping::Combined`. +The layers do not filter by level internally. The filters above capture `INFO`, `WARN`, and +`ERROR` telemetry, excluding `DEBUG` and `TRACE`. Adjust each layer's filter independently to +control volume. Global subscriber filters also apply and can suppress telemetry before the +Sentry layers receive it. -## Tracing Spans +## Spans -The integration automatically tracks `tracing` spans as spans in Sentry. A convenient way to do -this is with the `#[instrument]` attribute macro, which creates a span/transaction for the function -in Sentry. +`span_layer()` creates a Sentry transaction when no Sentry span is active, or a child span +otherwise. For example, +[`tracing::instrument`](https://docs.rs/tracing/latest/tracing/attr.instrument.html) creates +spans for instrumented functions; arguments become span data unless skipped. -Function arguments are added as context fields automatically, which can be configured through -attribute arguments. Refer to documentation of the macro for more information. +With the log layer installed, the `info!` event below produces a log associated with the same +trace as the instrumented functions: ```rust -use std::time::Duration; - -use tracing_subscriber::prelude::*; - -// Functions instrumented by tracing automatically -// create spans/transactions around their execution. #[tracing::instrument] async fn outer() { - for i in 0..10 { - inner(i).await; - } + inner(42).await; } -// This creates spans inside the outer transaction, unless called directly. #[tracing::instrument] -async fn inner(i: u32) { - // Also works, since log events are ingested by the tracing system - tracing::debug!(number = i, "Generates a breadcrumb"); - - tokio::time::sleep(Duration::from_millis(100)).await; +async fn inner(value: i32) { + tracing::info!(value, "completed step"); } ``` -By default, the name of the span sent to Sentry matches the name of the `tracing` span, which -is the name of the function when using `tracing::instrument`, or the name passed to the -`tracing::_span` macros. +By default, the Sentry span name matches the `tracing` span name, and its operation is +`::`. -By default, the `op` of the span sent to Sentry is `default`. +### Special span fields -### Special Span Fields - -Some fields on spans are treated specially by the Sentry tracing integration: -- `sentry.name`: overrides the span name sent to Sentry. - This is useful to customize the span name when using `#[tracing::instrument]`, or to update - it retroactively (using `span.record`) after the span has been created. -- `sentry.op`: overrides the span `op` sent to Sentry. -- `sentry.trace`: in Sentry, the `sentry-trace` header is sent with HTTP requests to achieve distributed tracing. - If the value of this field is set to the value of a valid `sentry-trace` header, which - other Sentry SDKs send automatically with outgoing requests, then the SDK will continue the trace using the given distributed tracing information. - This is useful to achieve distributed tracing at service boundaries by using only the - `tracing` API. - Note that `sentry.trace` will only be effective on span creation (it cannot be applied retroactively) - and requires the span it's applied to to be a root span, i.e. no span should active upon its - creation. - - -Example: +- `sentry.name` overrides the Sentry span name, including when recorded after creation. +- `sentry.op` overrides the Sentry span operation, including when recorded after creation. +- `sentry.trace` continues the trace from a `sentry-trace` header. It must be set when a + **root** span is created; recording it later has no effect. ```rust #[tracing::instrument(skip_all, fields( @@ -243,6 +85,16 @@ async fn handle_request(headers: std::collections::HashMap) { } ``` +## Logs + +`log_layer()` sends events accepted by its filter as structured logs. Event fields become +searchable log attributes; fields with dots appear nested under their common prefix in the +Sentry Logs explorer. + +```rust +tracing::info!(number = 42, my.key = "value", "Processed request"); +``` + ## Resources License: MIT diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index be80a5fe..d3663e26 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -127,8 +127,9 @@ impl EventToBreadcrumbMapper for F where /// /// It is highly recommended to configure a filter on the layer to limit breadcrumb volume. We /// recommend capturing `WARN` and `INFO` events. If you also use the error layer, exclude `ERROR` -/// events, which would otherwise duplicate the error event as a breadcrumb. Install the breadcrumb -/// layer before the error layer, so the breadcrumbs are added before the error event is captured. +/// events, which would otherwise duplicate the error event as a breadcrumb. If a custom filter +/// sends the same event to both layers, install the breadcrumb layer before the error layer so +/// that breadcrumb is included in the error. /// /// ```rust /// # use tracing_subscriber::prelude::*; diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index daec014b..0dbcce88 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -5,7 +5,10 @@ use tracing_subscriber::registry::LookupSpan; use crate::converters::log_from_event; -/// Captures `ERROR`, `WARN`, and `INFO` tracing events as Sentry logs. +/// Captures tracing events as Sentry logs. +/// +/// Captures all event levels unless a filter is configured; see [`log_layer`] for the recommended +/// level filter. /// /// Requires a client with logs enabled. #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] @@ -109,7 +112,7 @@ where } } -/// A mapper function which convernts a tracing event to a [`Log`]. +/// A mapper function which converts a tracing event to a [`Log`]. /// /// This is a pretty advanced API; when using it, callers must handle constructing the log /// object manually from the tracing event. diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index fd86fdc9..0f7b6d03 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -18,10 +18,6 @@ pub use log::{log_layer, EventToLogMapper, LogLayer}; pub(super) use span::SentrySpanData; pub use span::{span_layer, SpanLayer}; -// TODO: Design new-layer filtering without hiding filtered spans from event -// context lookups. A per-layer tracing-subscriber Filter only narrows what -// that layer receives; it is not a replacement for legacy custom mappings. - bitflags! { /// The action that Sentry should perform for a given [`Event`] #[derive(Debug, Clone, Copy)] @@ -77,8 +73,9 @@ impl From> for CombinedEventMapping { /// The default event filter for the legacy combined layer. /// -/// By default, an exception event is captured for `error`, a breadcrumb for -/// `warning` and `info`, and `debug` and `trace` logs are ignored. +/// Captures `ERROR` events as Sentry error events and `WARN` and `INFO` events as breadcrumbs. +/// With the `logs` feature, these three levels are also captured as logs. `DEBUG` and `TRACE` +/// events are ignored. pub fn default_event_filter(metadata: &Metadata) -> EventFilter { match metadata.level() { #[cfg(feature = "logs")] @@ -105,14 +102,73 @@ pub fn default_span_filter(metadata: &Metadata) -> bool { type EventMapper = Box) -> EventMapping + Send + Sync>; -/// Legacy combined tracing layer; use [`span_layer`], [`breadcrumb_layer`], -/// [`error_layer`], and `log_layer()` instead. -#[deprecated( - note = "Use span_layer(), breadcrumb_layer(), error_layer(), and optionally log_layer() instead" -)] +/// Legacy combined tracing layer. +/// +/// For most applications, use [`span_layer`] and [`log_layer`] instead. +/// Add [`error_layer`] if tracing events should also create Sentry issues, and [`breadcrumb_layer`] +/// if errors need breadcrumb context. +/// +/// # Migrating the default configuration +/// +/// The individual layers do not filter by level internally. The following configuration +/// reproduces the default level selection of `SentryLayer` with the `logs` feature enabled: +/// +/// - `ERROR`, `WARN`, and `INFO` spans become Sentry transactions and spans. +/// - `ERROR`, `WARN`, and `INFO` events become structured logs. +/// - `ERROR` events also become Sentry error events. +/// - `WARN` and `INFO` events also become breadcrumbs on the current scope, sent with subsequent +/// captured errors. +/// +/// ```rust +/// # #[cfg(feature = "logs")] +/// # { +/// use tracing::Level; +/// use tracing_subscriber::filter::{filter_fn, LevelFilter}; +/// use tracing_subscriber::prelude::*; +/// +/// tracing_subscriber::registry() +/// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) +/// .with( +/// sentry_tracing::breadcrumb_layer().with_filter(filter_fn(|metadata| { +/// matches!(*metadata.level(), Level::WARN | Level::INFO) +/// })), +/// ) +/// .with(sentry_tracing::error_layer().with_filter(LevelFilter::ERROR)) +/// .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) +/// .init(); +/// # } +/// ``` +/// +/// Without the `logs` feature, omit the log layer. Client configuration, including trace sampling, +/// is unchanged. +/// +/// If the legacy layer used [`enable_span_attributes`](Self::enable_span_attributes), enable +/// span attributes on each individual event layer that needs them. See the option docs for +/// [breadcrumbs](BreadcrumbLayer::enable_span_attributes), +/// [errors](ErrorLayer::enable_span_attributes), and +/// [logs](LogLayer::enable_span_attributes) for filtering requirements. +/// +/// # Migrating custom configuration +/// +/// - Replace [`span_filter`](Self::span_filter) with a filter on the span layer. +/// - Split [`event_filter`](Self::event_filter) into filters on the individual event layers. An +/// event can be accepted by multiple layers, replacing combined [`EventFilter`] flags. +/// - Replace [`event_mapper`](Self::event_mapper) with each event layer's `mapper()` callback. +/// Unlike the legacy mapper, these callbacks do not override filters: filters run first and +/// can prevent a mapper from receiving an event. Keep value-dependent routing in the mapper. +/// - Each mapper returns at most one item of its telemetry type. There is no direct equivalent +/// for a legacy [`EventMapping::Combined`] that emits multiple items of the same type or changes +/// their capture order per event. +/// +/// If a custom configuration sends the same event to both the breadcrumb and error layers, +/// install the breadcrumb layer before the error layer to include that breadcrumb in the error. +/// The default configuration above excludes `ERROR` events from breadcrumbs, so the order does +/// not affect their inclusion. +#[deprecated(note = "Prefer span_layer() and log_layer(); see SentryLayer docs for migration")] pub struct SentryLayer { event_filter: Box EventFilter + Send + Sync>, event_mapper: Option>, + span_filter: Box bool + Send + Sync>, span_layer: SpanLayer, breadcrumb: BreadcrumbLayer, error: ErrorLayer, @@ -159,17 +215,17 @@ impl SentryLayer { where F: Fn(&Metadata) -> bool + Send + Sync + 'static, { - self.span_layer = self.span_layer.span_filter(filter); + self.span_filter = Box::new(filter); self } - /// Enable every parent span's attributes to be sent along with own event's attributes. + /// Include attributes of parent spans in breadcrumbs, error events, and logs. /// - /// Note that the root span is considered a [transaction][sentry_core::protocol::Transaction] - /// so its context will only be grabbed only if you set the transaction to be sampled. - /// The most straightforward way to do this is to set - /// the [traces_sample_rate][sentry_core::ClientOptions::traces_sample_rate] to `1.0` - /// while configuring your sentry client. + /// Only spans accepted by this layer's span filter provide attributes. Including attributes + /// does not require the transaction to be sampled. + /// + /// When migrating, enable span attributes on each individual event layer that needs them; + /// see the [migration guide](SentryLayer#migrating-the-default-configuration). #[must_use] pub fn enable_span_attributes(mut self) -> Self { self.breadcrumb = self.breadcrumb.enable_span_attributes(); @@ -191,7 +247,8 @@ where Self { event_filter: Box::new(default_event_filter), event_mapper: None, - span_layer: span_layer().span_filter(default_span_filter), + span_filter: Box::new(default_span_filter), + span_layer: span_layer(), breadcrumb: breadcrumb_layer(), error: error_layer(), #[cfg(feature = "logs")] @@ -245,7 +302,9 @@ where id: &tracing_span::Id, ctx: Context<'_, S>, ) { - self.span_layer.on_new_span(attrs, id, ctx); + if (self.span_filter)(attrs.metadata()) { + self.span_layer.on_new_span(attrs, id, ctx); + } } fn on_enter(&self, id: &tracing_span::Id, ctx: Context<'_, S>) { @@ -270,10 +329,12 @@ where } } -/// Creates a legacy combined Sentry layer. Use individual telemetry layers instead. -#[deprecated( - note = "Use span_layer(), breadcrumb_layer(), error_layer(), and optionally log_layer() instead" -)] +/// Creates a legacy combined Sentry layer. +/// +/// For most applications, use [`span_layer`] and [`log_layer`] instead. +/// See [`SentryLayer`] for a migration example that preserves the legacy default level selection +/// using all four individual layers, and for differences when migrating custom configuration. +#[deprecated(note = "Prefer span_layer() and log_layer(); see SentryLayer docs for migration")] #[expect(deprecated, reason = "returning the legacy layer")] pub fn layer() -> SentryLayer where diff --git a/sentry-tracing/src/layer/span.rs b/sentry-tracing/src/layer/span.rs index 855d189b..a90f9631 100644 --- a/sentry-tracing/src/layer/span.rs +++ b/sentry-tracing/src/layer/span.rs @@ -17,21 +17,8 @@ use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX} /// Captures tracing spans as Sentry transactions and spans. /// /// Install this layer to capture spans; event layers do not manage the span lifecycle. -pub struct SpanLayer { - span_filter: Box bool + Send + Sync>, -} - -impl SpanLayer { - /// Sets the filter deciding which spans are captured, replacing any previous filter. - #[must_use] - pub(super) fn span_filter(mut self, filter: F) -> Self - where - F: Fn(&Metadata) -> bool + Send + Sync + 'static, - { - self.span_filter = Box::new(filter); - self - } -} +#[non_exhaustive] +pub struct SpanLayer {} /// Creates a layer that captures tracing spans as Sentry transactions and spans. /// @@ -55,9 +42,7 @@ impl SpanLayer { /// let _debug = tracing::debug_span!("DEBUG span").entered(); /// ``` pub fn span_layer() -> SpanLayer { - SpanLayer { - span_filter: Box::new(|_| true), - } + SpanLayer {} } #[inline(always)] @@ -118,18 +103,14 @@ impl Layer for SpanLayer where S: Subscriber + for<'a> LookupSpan<'a>, { - /// When a new Span gets created, run the filter and start a new sentry span - /// if it passes, setting it as the *current* sentry span. + /// When a new Span gets created, start a new sentry span, setting it as the *current* sentry + /// span. fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) { let span = match ctx.span(id) { Some(span) => span, None => return, }; - if !(self.span_filter)(span.metadata()) { - return; - } - let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs); let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name()); let sentry_op = diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index 70aa310a..723e7ac2 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -1,20 +1,22 @@ -//! Capture `tracing` telemetry with four independently selectable tracing layers: +//! Capture `tracing` spans and events as Sentry traces and structured logs. //! -//! - [`span_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.span_layer.html) captures `ERROR`, `WARN`, and `INFO` spans as Sentry transactions and spans. -//! - [`log_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.log_layer.html) captures `ERROR`, `WARN`, and `INFO` events as Sentry structured logs. -//! - [`error_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.error_layer.html) captures `ERROR` events as Sentry error events (issues). -//! - [`breadcrumb_layer`](https://docs.rs/sentry-tracing/latest/sentry_tracing/fn.breadcrumb_layer.html) adds `WARN` and `INFO` events as breadcrumbs to the current scope. +//! For most applications, we recommend installing only these two layers: //! -//! For most applications, we recommend using the span and log layers. +//! - [`span_layer`] captures `tracing` spans as Sentry transactions and spans. +//! - [`log_layer`] captures `tracing` events as +//! [Sentry Logs](https://docs.sentry.io/product/explore/logs/). Requires the `logs` feature. //! //! # Getting started //! -//! Set a traces sample rate to capture transactions, then install the layers you would like to -//! use. +//! Enable the `logs` feature on `sentry-tracing` (or the `tracing` and `logs` features on +//! `sentry`). +//! Set a traces sample rate to capture transactions, then install the span and log layers with +//! explicit level filters: //! //! ``` //! # #[cfg(feature = "logs")] //! # { +//! use tracing_subscriber::filter::LevelFilter; //! use tracing_subscriber::prelude::*; //! //! let _guard = sentry::init( @@ -24,32 +26,33 @@ //! ); //! //! tracing_subscriber::registry() -//! .with(sentry::integrations::tracing::span_layer()) -//! .with(sentry::integrations::tracing::log_layer()) +//! .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) +//! .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) //! .init(); //! # } //! ``` //! -//! When using the error and breadcrumb layers, we recommend installing the breadcrumb layer before -//! the error layer, so any breadcrumbs added by the breadcrumb layer appear on the correct error. +//! The layers do not filter by level internally. The filters above capture `INFO`, `WARN`, and +//! `ERROR` telemetry, excluding `DEBUG` and `TRACE`. Adjust each layer's filter independently to +//! control volume. Global subscriber filters also apply and can suppress telemetry before the +//! Sentry layers receive it. //! //! # Spans //! -//! `span_layer()` creates a Sentry transaction for a root `tracing` span and Sentry spans for -//! captured children. For example, [`tracing::instrument`](https://docs.rs/tracing/latest/tracing/attr.instrument.html) -//! creates spans for instrumented functions; arguments become span data unless skipped. +//! `span_layer()` creates a Sentry transaction when no Sentry span is active, or a child span +//! otherwise. For example, +//! [`tracing::instrument`](https://docs.rs/tracing/latest/tracing/attr.instrument.html) creates +//! spans for instrumented functions; arguments become span data unless skipped. +//! +//! With the log layer installed, the `info!` event below produces a log associated with the same +//! trace as the instrumented functions: //! //! ``` //! #[tracing::instrument] //! async fn outer() { //! inner(42).await; //! } -//! ``` -//! -//! If you also have the recommended `log_layer` installed, then the following will create an -//! info-level log associated with the same trace as the span for the function: //! -//! ``` //! #[tracing::instrument] //! async fn inner(value: i32) { //! tracing::info!(value, "completed step"); @@ -57,7 +60,7 @@ //! ``` //! //! By default, the Sentry span name matches the `tracing` span name, and its operation is -//! `::`. Spans at `DEBUG` and `TRACE` are not captured by default. +//! `::`. //! //! ## Special span fields //! @@ -79,43 +82,13 @@ //! //! # Logs //! -//! `log_layer()` sends `ERROR`, `WARN`, and `INFO` events as structured logs. Event fields -//! become searchable log attributes; fields with dots appear nested under their common -//! prefix in the Sentry Logs explorer. +//! `log_layer()` sends events accepted by its filter as structured logs. Event fields become +//! searchable log attributes; fields with dots appear nested under their common prefix in the +//! Sentry Logs explorer. //! //! ``` //! tracing::info!(number = 42, my.key = "value", "Processed request"); //! ``` -//! -//! # Errors -//! -//! `error_layer()` captures an `ERROR` event as a Sentry error event. Fields become context; -//! fields prefixed with `tags.` become Sentry tags. -//! -//! ``` -//! tracing::error!(field = "value", tags.custom = "value", "Request failed"); -//! ``` -//! -//! To capture an error chain as Sentry exceptions, record an error trait object. A message -//! can also be provided for grouping and issue context: -//! -//! ``` -//! use std::error::Error; -//! -//! let error = std::io::Error::other("connection refused"); -//! tracing::error!(error = &error as &dyn Error, "Request failed"); -//! ``` -//! -//! # Breadcrumbs -//! -//! `breadcrumb_layer()` adds `WARN` and `INFO` events to the current Sentry scope. Breadcrumbs -//! are sent with subsequent captured errors; they are not sent independently. Event fields -//! become breadcrumb data. -//! -//! ``` -//! tracing::info!(request_id = 42, "Starting request"); -//! tracing::warn!("Retrying request"); -//! ``` #![cfg_attr(doc_cfg, feature(doc_cfg))] #![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")] diff --git a/sentry-tracing/tests/breadcrumbs.rs b/sentry-tracing/tests/breadcrumbs.rs deleted file mode 100644 index 7b972e0e..00000000 --- a/sentry-tracing/tests/breadcrumbs.rs +++ /dev/null @@ -1,36 +0,0 @@ -#![expect(missing_docs, reason = "predates lint enforcement")] - -mod shared; - -#[test] -fn breadcrumbs_should_capture_span_fields() { - let transport = shared::init_sentry(0.0); // This test should work even if we are not sampling transactions. - - foo(); - - let data = transport.fetch_and_clear_envelopes(); - assert_eq!(data.len(), 1); - - let event = data.first().expect("should have 1 event"); - let event = match event.items().next().unwrap() { - sentry::protocol::EnvelopeItem::Event(event) => event, - unexpected => panic!("Expected event, but got {unexpected:#?}"), - }; - - assert_eq!(event.breadcrumbs.len(), 1); - assert_eq!( - event.breadcrumbs[0].data["foo:contextual_value"], - serde_json::Value::from(42) - ); - assert_eq!( - event.breadcrumbs[0].message, - Some("executing foo".to_owned()) - ); -} - -#[tracing::instrument(fields(contextual_value = 42))] -fn foo() { - tracing::info!("executing foo"); - - tracing::error!("boom!"); -} diff --git a/sentry-tracing/tests/future_cross_thread_info_span.rs b/sentry-tracing/tests/future_cross_thread_info_span.rs index 339d0817..7980a280 100644 --- a/sentry-tracing/tests/future_cross_thread_info_span.rs +++ b/sentry-tracing/tests/future_cross_thread_info_span.rs @@ -6,8 +6,10 @@ mod shared; mod transaction_assertions; use sentry::{Hub, HubSwitchGuard}; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; -/// Tests that `SentryLayer`'s `on_exit` implementation panics (only when +/// Tests that `SpanLayer`'s `on_exit` implementation panics (only when /// `debug_assertions` are enabled) if a Sentry-captured span is exited on a /// different thread than where it was entered. /// @@ -20,6 +22,9 @@ fn future_cross_thread_info_span() { let _guard = HubSwitchGuard::new(Hub::new_from_top(Hub::current()).into()); let transport = shared::init_sentry(1.0); + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .set_default(); let span = tracing::info_span!(SPAN_NAME); diff --git a/sentry-tracing/tests/future_cross_thread_trace_span.rs b/sentry-tracing/tests/future_cross_thread_trace_span.rs index 42501db9..66b26531 100644 --- a/sentry-tracing/tests/future_cross_thread_trace_span.rs +++ b/sentry-tracing/tests/future_cross_thread_trace_span.rs @@ -5,6 +5,8 @@ mod future_span_common; mod shared; use sentry::{Hub, HubSwitchGuard}; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; /// Tests that no panic occurs when a future containing a span that is not /// captured by Sentry is awaited across threads. @@ -12,6 +14,9 @@ use sentry::{Hub, HubSwitchGuard}; fn future_cross_thread_trace_span() { let _guard = HubSwitchGuard::new(Hub::new_from_top(Hub::current()).into()); let transport = shared::init_sentry(1.0); + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .set_default(); let span = tracing::trace_span!("future_cross_thread_trace_span"); diff --git a/sentry-tracing/tests/futures_same_thread_info_span.rs b/sentry-tracing/tests/futures_same_thread_info_span.rs index 29432397..b4a2a20f 100644 --- a/sentry-tracing/tests/futures_same_thread_info_span.rs +++ b/sentry-tracing/tests/futures_same_thread_info_span.rs @@ -8,6 +8,8 @@ use std::future::Future; use std::thread; use sentry::{Hub, HubSwitchGuard}; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; /// Tests that the panic asserted in `future_cross_thread_info_span` is not /// triggered when the span is exited on the same thread that it was entered @@ -16,6 +18,9 @@ use sentry::{Hub, HubSwitchGuard}; fn futures_same_thread_info_span() { let _guard = HubSwitchGuard::new(Hub::new_from_top(Hub::current()).into()); let transport = shared::init_sentry(1.0); + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .set_default(); let span = tracing::info_span!("futures_same_thread_info_span"); let mut future = Box::pin(future_span_common::span_across_await(span)); diff --git a/sentry-tracing/tests/log_span_attributes.rs b/sentry-tracing/tests/log_span_attributes.rs new file mode 100644 index 00000000..a53be2dc --- /dev/null +++ b/sentry-tracing/tests/log_span_attributes.rs @@ -0,0 +1,47 @@ +//! Tests that `LogLayer::enable_span_attributes` attaches parent span attributes to captured logs. + +#![cfg(feature = "logs")] + +use sentry::protocol::{EnvelopeItem, ItemContainer, LogLevel}; +use tracing::Level; +use tracing_subscriber::filter::{filter_fn, LevelFilter}; +use tracing_subscriber::prelude::*; + +#[test] +fn log_layer_captures_span_attributes() { + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with( + sentry_tracing::log_layer() + .enable_span_attributes() + .with_filter(filter_fn(|metadata| { + metadata.is_span() || *metadata.level() == Level::INFO + })), + ) + .set_default(); + + let envelopes = sentry::test::with_captured_envelopes_options( + foo, + sentry::ClientOptions::new().traces_sample_rate(0.0), + ); + let mut logs = Vec::new(); + for item in envelopes.iter().flat_map(|envelope| envelope.items()) { + if let EnvelopeItem::ItemContainer(ItemContainer::Logs(items)) = item { + logs.extend(items.iter()); + } + } + + assert_eq!(logs.len(), 1); + let log = logs[0]; + assert_eq!(log.level, LogLevel::Info); + assert_eq!(log.body, "executing foo"); + assert_eq!( + log.attributes.get("foo:contextual_value").cloned(), + Some(42.into()) + ); +} + +#[tracing::instrument(fields(contextual_value = 42))] +fn foo() { + tracing::info!("executing foo"); +} diff --git a/sentry-tracing/tests/name_op_updates.rs b/sentry-tracing/tests/name_op_updates.rs index 925f10e2..632d337b 100644 --- a/sentry-tracing/tests/name_op_updates.rs +++ b/sentry-tracing/tests/name_op_updates.rs @@ -2,6 +2,9 @@ mod shared; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; + #[tracing::instrument(fields( some = "value", sentry.name = "updated name", @@ -22,6 +25,9 @@ fn test_fun_record_later() { #[test] fn should_update_sentry_op_and_name_based_on_fields() { let transport = shared::init_sentry(1.0); + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .set_default(); for f in [test_fun_record_on_creation, test_fun_record_later] { f(); diff --git a/sentry-tracing/tests/shared/mod.rs b/sentry-tracing/tests/shared/mod.rs index b47d2f04..82221b30 100644 --- a/sentry-tracing/tests/shared/mod.rs +++ b/sentry-tracing/tests/shared/mod.rs @@ -1,13 +1,9 @@ -#![expect(deprecated, reason = "legacy layer compatibility tests")] - use sentry::{ClientOptions, Hub}; use sentry_core::test::TestTransport; use std::sync::Arc; pub fn init_sentry(traces_sample_rate: f32) -> Arc { - use tracing_subscriber::prelude::*; - let transport = TestTransport::new(); let options = ClientOptions::new() .dsn("https://test@sentry-tracing.com/test") @@ -16,9 +12,5 @@ pub fn init_sentry(traces_sample_rate: f32) -> Arc { .traces_sample_rate(traces_sample_rate); Hub::current().bind_client(Some(Arc::new(options.into()))); - let _ = tracing_subscriber::registry() - .with(sentry_tracing::layer().enable_span_attributes()) - .try_init(); - transport } diff --git a/sentry-tracing/tests/smoke.rs b/sentry-tracing/tests/smoke.rs index aca9bf0c..698024ed 100644 --- a/sentry-tracing/tests/smoke.rs +++ b/sentry-tracing/tests/smoke.rs @@ -1,6 +1,9 @@ #![expect(missing_docs, reason = "predates lint enforcement")] +#![cfg(feature = "logs")] -mod shared; +use sentry::protocol::{EnvelopeItem, ItemContainer, LogLevel}; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; #[tracing::instrument(fields(tags.tag = "key", not_tag = "value"))] fn function_with_tags(value: i32) { @@ -8,52 +11,39 @@ fn function_with_tags(value: i32) { } #[test] -fn should_instrument_function_with_event() { - let transport = shared::init_sentry(1.0); // Sample all spans. +fn should_instrument_function_with_log() { + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) + .set_default(); - function_with_tags(1); + let envelopes = sentry::test::with_captured_envelopes_options( + || function_with_tags(1), + sentry::ClientOptions::new().traces_sample_rate(1.0), + ); + let mut transaction = None; + let mut logs = Vec::new(); + for item in envelopes.iter().flat_map(|envelope| envelope.items()) { + match item { + EnvelopeItem::Transaction(item) => assert!(transaction.replace(item).is_none()), + EnvelopeItem::ItemContainer(ItemContainer::Logs(items)) => logs.extend(items.iter()), + _ => (), + } + } - let data = transport.fetch_and_clear_envelopes(); - assert_eq!(data.len(), 2); - let event = data.first().expect("should have 1 event"); - let event = match event.items().next().unwrap() { - sentry::protocol::EnvelopeItem::Event(event) => event, - unexpected => panic!("Expected event, but got {unexpected:#?}"), - }; + let transaction = transaction.expect("transaction"); + assert_eq!(transaction.name.as_deref(), Some("function_with_tags")); + assert_eq!(transaction.tags.len(), 1); + assert_eq!(transaction.tags.get("tag").map(String::as_str), Some("key")); - //Validate transaction is created - let trace = match event.contexts.get("trace").expect("to get 'trace' context") { + let trace = match transaction.contexts.get("trace").expect("trace context") { sentry::protocol::Context::Trace(trace) => trace, - unexpected => panic!("Expected trace context but got {unexpected:?}"), - }; - assert_eq!(trace.op.as_deref().unwrap(), "smoke::function_with_tags"); - - //Confirm transaction values - let transaction = data.get(1).expect("should have 1 transaction"); - let transaction = match transaction.items().next().unwrap() { - sentry::protocol::EnvelopeItem::Transaction(transaction) => transaction, - unexpected => panic!("Expected transaction, but got {unexpected:#?}"), + unexpected => panic!("Expected trace context, but got {unexpected:?}"), }; - assert_eq!(transaction.name, Some("function_with_tags".into())); - assert_eq!(transaction.tags.len(), 1); + assert_eq!(trace.op.as_deref(), Some("smoke::function_with_tags")); + assert_eq!(trace.data.get("not_tag"), Some(&"value".into())); + assert_eq!(trace.data.get("value"), Some(&1.into())); assert_eq!(trace.data.len(), 6); - - let tag = transaction - .tags - .get("tag") - .expect("to have tag with name 'tag'"); - assert_eq!(tag, "key"); - let not_tag = trace - .data - .get("not_tag") - .expect("to have data attribute with name 'not_tag'"); - assert_eq!(not_tag, "value"); - let value = trace - .data - .get("value") - .expect("to have data attribute with name 'value'"); - assert_eq!(value, 1); - assert_eq!( trace.data.get("sentry.tracing.target"), Some("smoke".into()).as_ref() @@ -64,4 +54,11 @@ fn should_instrument_function_with_event() { ); assert!(trace.data.contains_key("code.file.path")); assert!(trace.data.contains_key("code.line.number")); + + assert_eq!(logs.len(), 1); + let log = logs[0]; + assert_eq!(log.level, LogLevel::Error); + assert_eq!(log.body, "event"); + assert_eq!(log.trace_id.as_ref(), Some(&trace.trace_id)); + assert_eq!(log.attributes.get("value"), Some(&1.into())); } diff --git a/sentry-tracing/tests/span_cross_thread.rs b/sentry-tracing/tests/span_cross_thread.rs index 0fb449e8..9243fdd6 100644 --- a/sentry-tracing/tests/span_cross_thread.rs +++ b/sentry-tracing/tests/span_cross_thread.rs @@ -5,25 +5,36 @@ mod shared; use sentry::protocol::Context; use std::thread; use std::time::Duration; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; #[test] fn cross_thread_span_entries_share_transaction() { let transport = shared::init_sentry(1.0); + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .set_default(); let span = tracing::info_span!("foo"); let span2 = span.clone(); + let dispatch1 = tracing::dispatcher::get_default(|dispatch| dispatch.clone()); + let dispatch2 = dispatch1.clone(); let handle1 = thread::spawn(move || { - let _guard = span.enter(); - let _bar_span = tracing::info_span!("bar").entered(); - thread::sleep(Duration::from_millis(100)); + tracing::dispatcher::with_default(&dispatch1, || { + let _guard = span.enter(); + let _bar_span = tracing::info_span!("bar").entered(); + thread::sleep(Duration::from_millis(100)); + }); }); let handle2 = thread::spawn(move || { - thread::sleep(Duration::from_millis(10)); - let _guard = span2.enter(); - let _baz_span = tracing::info_span!("baz").entered(); - thread::sleep(Duration::from_millis(50)); + tracing::dispatcher::with_default(&dispatch2, || { + thread::sleep(Duration::from_millis(10)); + let _guard = span2.enter(); + let _baz_span = tracing::info_span!("baz").entered(); + thread::sleep(Duration::from_millis(50)); + }); }); handle1.join().unwrap(); diff --git a/sentry-tracing/tests/span_reentrancy.rs b/sentry-tracing/tests/span_reentrancy.rs index c1ad6e14..13c0639a 100644 --- a/sentry-tracing/tests/span_reentrancy.rs +++ b/sentry-tracing/tests/span_reentrancy.rs @@ -4,11 +4,17 @@ use sentry::protocol::EnvelopeItem; mod shared; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; + /// Ensures re-entering the same span does not corrupt the current tracing state, /// so subsequent spans are still recorded under a single transaction. #[test] fn reentering_span_preserves_parent() { let transport = shared::init_sentry(1.0); + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .set_default(); { // Create a span and enter it, then re-enter the same span to simulate diff --git a/sentry-tracing/tests/telemetry_layers.rs b/sentry-tracing/tests/telemetry_layers.rs deleted file mode 100644 index 504cd8a6..00000000 --- a/sentry-tracing/tests/telemetry_layers.rs +++ /dev/null @@ -1,145 +0,0 @@ -#![expect(missing_docs, reason = "integration tests")] - -use sentry::protocol::EnvelopeItem; -#[cfg(feature = "logs")] -use sentry::protocol::{Context, ItemContainer, LogLevel, Value}; -use tracing_subscriber::filter::{filter_fn, LevelFilter}; -use tracing_subscriber::prelude::*; - -#[test] -fn span_layer_does_not_capture_event_telemetry() { - let _dispatcher = tracing_subscriber::registry() - .with(sentry_tracing::span_layer()) - .set_default(); - - let envelopes = sentry::test::with_captured_envelopes_options( - || { - let _span = tracing::info_span!("operation").entered(); - tracing::info!("info"); - tracing::error!("error"); - }, - sentry::ClientOptions::new().traces_sample_rate(1.0), - ); - - assert_eq!(envelopes.len(), 1); - assert!(matches!( - envelopes[0].items().next(), - Some(EnvelopeItem::Transaction(_)) - )); - assert_eq!(envelopes[0].items().count(), 1); -} - -#[cfg(feature = "logs")] -#[test] -fn spans_and_logs_do_not_capture_errors_or_breadcrumbs() { - let _dispatcher = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) - .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) - .set_default(); - - let envelopes = sentry::test::with_captured_envelopes_options( - || { - let _span = tracing::info_span!("operation").entered(); - tracing::debug!("ignored"); - tracing::info!("info"); - tracing::error!("error"); - }, - sentry::ClientOptions::new().traces_sample_rate(1.0), - ); - - let mut transactions = 0; - let mut logs = Vec::new(); - for item in envelopes.iter().flat_map(|envelope| envelope.items()) { - match item { - EnvelopeItem::Transaction(_) => transactions += 1, - EnvelopeItem::ItemContainer(ItemContainer::Logs(items)) => logs.extend(items), - other => panic!("unexpected item: {other:?}"), - } - } - assert_eq!(transactions, 1); - assert_eq!(logs.len(), 2); - assert_eq!(logs[0].body, "info"); - assert_eq!(logs[1].body, "error"); -} - -#[cfg(feature = "logs")] -#[test] -fn all_layers_capture_their_default_levels_and_parent_fields() { - let _dispatcher = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) - .with( - sentry_tracing::breadcrumb_layer() - .enable_span_attributes() - // `WARN` and `INFO` only; `ERROR` is captured by the error layer. - .with_filter(filter_fn(|m| { - matches!(*m.level(), tracing::Level::WARN | tracing::Level::INFO) - })), - ) - .with( - sentry_tracing::error_layer() - .enable_span_attributes() - .with_filter(filter_fn(|m| { - m.is_span() || *m.level() == tracing::Level::ERROR - })), - ) - .with( - sentry_tracing::log_layer() - .enable_span_attributes() - .with_filter(LevelFilter::INFO), - ) - .set_default(); - - let envelopes = sentry::test::with_captured_envelopes_options( - || { - let _span = tracing::info_span!("operation", contextual_value = 42).entered(); - tracing::debug!("ignored"); - tracing::info!("info"); - tracing::warn!("warning"); - tracing::error!("error"); - }, - sentry::ClientOptions::new().traces_sample_rate(1.0), - ); - - let mut transactions = 0; - let mut events = Vec::new(); - let mut logs = Vec::new(); - for item in envelopes.iter().flat_map(|envelope| envelope.items()) { - match item { - EnvelopeItem::Transaction(_) => transactions += 1, - EnvelopeItem::Event(event) => events.push(event), - EnvelopeItem::ItemContainer(ItemContainer::Logs(items)) => logs.extend(items), - other => panic!("unexpected item: {other:?}"), - } - } - assert_eq!(transactions, 1); - assert_eq!(logs.len(), 3); - assert_eq!( - logs.iter().map(|log| log.level).collect::>(), - [LogLevel::Info, LogLevel::Warn, LogLevel::Error,] - ); - assert_eq!( - logs[0] - .attributes - .get("operation:contextual_value") - .cloned(), - Some(42.into()) - ); - - assert_eq!(events.len(), 1); - let event = events[0]; - assert_eq!(event.message.as_deref(), Some("error")); - assert_eq!(event.breadcrumbs.len(), 2); - assert_eq!(event.breadcrumbs[0].message.as_deref(), Some("info")); - assert_eq!(event.breadcrumbs[1].message.as_deref(), Some("warning")); - assert_eq!( - event.breadcrumbs[0].data.get("operation:contextual_value"), - Some(&Value::from(42)) - ); - match event.contexts.get("Rust Tracing Fields") { - Some(Context::Other(fields)) => assert_eq!( - fields.get("operation:contextual_value"), - Some(&Value::from(42)) - ), - other => panic!("missing span fields: {other:?}"), - } -} diff --git a/sentry/examples/tracing-demo.rs b/sentry/examples/tracing-demo.rs index 25057b0b..51639629 100644 --- a/sentry/examples/tracing-demo.rs +++ b/sentry/examples/tracing-demo.rs @@ -3,14 +3,15 @@ use std::thread; use std::time::Duration; +use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; // cargo run --example tracing-demo fn main() { tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) - .with(sentry_tracing::span_layer()) - .with(sentry_tracing::log_layer()) + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) .try_init() .unwrap(); diff --git a/sentry/tests/test_tracing.rs b/sentry/tests/test_legacy_tracing.rs similarity index 90% rename from sentry/tests/test_tracing.rs rename to sentry/tests/test_legacy_tracing.rs index 5d5ba975..fccf5f98 100644 --- a/sentry/tests/test_tracing.rs +++ b/sentry/tests/test_legacy_tracing.rs @@ -3,10 +3,12 @@ #![cfg(feature = "test")] use sentry::protocol::{Context, Request, Value}; +#[cfg(feature = "logs")] +use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; #[test] -fn test_tracing() { +fn test_legacy_tracing() { // Don't configure the fmt layer to avoid logging to test output let _dispatcher = tracing_subscriber::registry() .with(sentry_tracing::layer()) @@ -119,6 +121,44 @@ fn test_tracing() { assert_eq!(event.message, Some("I'm broken!".to_string())); } +#[tracing::instrument(fields(contextual_value = 42))] +fn legacy_span_attributes_function() { + tracing::info!("executing function"); + tracing::error!("boom!"); +} + +#[test] +fn test_legacy_span_attributes() { + let _dispatcher = tracing_subscriber::registry() + .with(sentry_tracing::layer().enable_span_attributes()) + .set_default(); + + let envelopes = sentry::test::with_captured_envelopes_options( + legacy_span_attributes_function, + sentry::ClientOptions::new().traces_sample_rate(0.0), + ); + let event = envelopes + .iter() + .flat_map(|envelope| envelope.items()) + .find_map(|item| match item { + sentry::protocol::EnvelopeItem::Event(event) => Some(event), + _ => None, + }) + .expect("expected Sentry error event"); + + assert_eq!(event.breadcrumbs.len(), 1); + assert_eq!( + event.breadcrumbs[0] + .data + .get("legacy_span_attributes_function:contextual_value"), + Some(&Value::from(42)) + ); + assert_eq!( + event.breadcrumbs[0].message.as_deref(), + Some("executing function") + ); +} + #[tracing::instrument(fields(span_field))] fn function() { tracing::Span::current().record("span_field", "some data"); @@ -274,10 +314,8 @@ fn test_tracing_logs() { #[cfg(feature = "logs")] #[test] fn test_tracing_log_floating_point_field() { - let sentry_layer = sentry_tracing::layer().event_filter(|_| sentry_tracing::EventFilter::Log); - let _dispatcher = tracing_subscriber::registry() - .with(sentry_layer) + .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) .set_default(); let options = sentry::ClientOptions::new(); From 46a1c639f856c21ba7ceb2bf62903b1fe6ac9746 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 5 Oct 2026 15:21:57 +0200 Subject: [PATCH 06/15] some fixes --- sentry-tracing/src/layer/log.rs | 2 +- sentry-tracing/src/layer/mod.rs | 48 +++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 0dbcce88..7458a7ec 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -91,7 +91,7 @@ where } } - /// Convert the + /// Convert the tracing event to a Sentry log. fn event_to_log(&self, event: &Event, ctx: Context<'_, S>) -> Option { match &self.event_mapper { Some(mapper) => mapper(event, ctx), diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index 0f7b6d03..7898ca9c 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -18,22 +18,33 @@ pub use log::{log_layer, EventToLogMapper, LogLayer}; pub(super) use span::SentrySpanData; pub use span::{span_layer, SpanLayer}; -bitflags! { - /// The action that Sentry should perform for a given [`Event`] - #[derive(Debug, Clone, Copy)] - pub struct EventFilter: u32 { - /// Ignore the [`Event`] - const Ignore = 0b000; - /// Create a [`Breadcrumb`] from this [`Event`] - const Breadcrumb = 0b001; - /// Create a [`sentry_core::protocol::Event`] from this [`Event`] - const Event = 0b010; - /// Create a [`sentry_core::protocol::Log`] from this [`Event`] - const Log = 0b100; +/// Separate submodule with expect(deprecated) needed because bitflags! uses the deprecated +/// EventFilter. +#[expect(deprecated)] +mod event_filter { + use super::*; + bitflags! { + /// The action that Sentry should perform for a given [`Event`] + #[deprecated = "only used for the legacy combined layer"] + #[derive(Debug, Clone, Copy)] + pub struct EventFilter: u32 { + /// Ignore the [`Event`] + const Ignore = 0b000; + /// Create a [`Breadcrumb`] from this [`Event`] + const Breadcrumb = 0b001; + /// Create a [`sentry_core::protocol::Event`] from this [`Event`] + const Event = 0b010; + /// Create a [`sentry_core::protocol::Log`] from this [`Event`] + const Log = 0b100; + } } } +#[expect(deprecated)] +pub use event_filter::EventFilter; + /// The type of data Sentry should ingest for an [`Event`]. +#[deprecated = "only used for the legacy combined layer"] #[derive(Debug)] #[non_exhaustive] pub enum EventMapping { @@ -49,13 +60,16 @@ pub enum EventMapping { Log(sentry_core::protocol::Log), /// Captures multiple items to Sentry. /// Nesting multiple `EventMapping::Combined` inside each other will cause the inner mappings to be ignored. + #[expect(deprecated)] Combined(CombinedEventMapping), } /// A list of event mappings. +#[deprecated = "only used for the legacy combined layer"] #[derive(Debug)] -pub struct CombinedEventMapping(Vec); +pub struct CombinedEventMapping(#[expect(deprecated)] Vec); +#[expect(deprecated)] impl From for CombinedEventMapping { fn from(value: EventMapping) -> Self { match value { @@ -65,6 +79,7 @@ impl From for CombinedEventMapping { } } +#[expect(deprecated)] impl From> for CombinedEventMapping { fn from(value: Vec) -> Self { Self(value) @@ -76,6 +91,8 @@ impl From> for CombinedEventMapping { /// Captures `ERROR` events as Sentry error events and `WARN` and `INFO` events as breadcrumbs. /// With the `logs` feature, these three levels are also captured as logs. `DEBUG` and `TRACE` /// events are ignored. +#[deprecated = "only used for the legacy combined layer"] +#[expect(deprecated)] pub fn default_event_filter(metadata: &Metadata) -> EventFilter { match metadata.level() { #[cfg(feature = "logs")] @@ -90,9 +107,10 @@ pub fn default_event_filter(metadata: &Metadata) -> EventFilter { } } -/// The default span filter. +/// The default span filter for the legacy combined layer. /// /// By default, spans at the `error`, `warning`, and `info` levels are captured. +#[deprecated = "only used for the legacy combined layer"] pub fn default_span_filter(metadata: &Metadata) -> bool { matches!( metadata.level(), @@ -100,6 +118,7 @@ pub fn default_span_filter(metadata: &Metadata) -> bool { ) } +#[expect(deprecated)] type EventMapper = Box) -> EventMapping + Send + Sync>; /// Legacy combined tracing layer. @@ -166,6 +185,7 @@ type EventMapper = Box) -> EventMapping + Send /// not affect their inclusion. #[deprecated(note = "Prefer span_layer() and log_layer(); see SentryLayer docs for migration")] pub struct SentryLayer { + #[expect(deprecated)] event_filter: Box EventFilter + Send + Sync>, event_mapper: Option>, span_filter: Box bool + Send + Sync>, From b0ef1223c7281a3b91b96a90194ba470adf7d789 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 6 Oct 2026 18:52:35 +0200 Subject: [PATCH 07/15] update docs --- CHANGELOG.md | 2 +- sentry-tracing/README.md | 18 ++++++++++-- sentry-tracing/src/converters.rs | 3 +- sentry-tracing/src/layer/breadcrumb.rs | 18 +++++++----- sentry-tracing/src/layer/error.rs | 28 +++++++++++++++---- sentry-tracing/src/layer/log.rs | 13 +++++---- sentry-tracing/src/layer/mod.rs | 7 ++--- sentry-tracing/src/layer/span.rs | 9 ++++-- sentry-tracing/src/lib.rs | 18 ++++++++++-- .../tests/future_cross_thread_info_span.rs | 4 +-- 10 files changed, 84 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 119741dc..d75bf887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Deprecations -- Deprecated `sentry_tracing::layer()` and `SentryLayer` in favor of the individual layers. Legacy filtering defaults are unchanged. See the [migration guide](sentry-tracing/src/layer/mod.rs) for the equivalent four-layer configuration and custom-mapper differences. +- Deprecated `sentry_tracing::layer()` and `SentryLayer`, along with their associated filtering/mapping types and helpers, in favor of the individual layers. Legacy filtering defaults are unchanged. See the [migration guide](sentry-tracing/src/layer/mod.rs) for the equivalent four-layer configuration and custom-mapper differences. ## 0.49.3 diff --git a/sentry-tracing/README.md b/sentry-tracing/README.md index 754f28f7..641fd5e1 100644 --- a/sentry-tracing/README.md +++ b/sentry-tracing/README.md @@ -14,14 +14,25 @@ For most applications, we recommend installing only these two layers: - [`log_layer`] captures `tracing` events as [Sentry Logs](https://docs.sentry.io/product/explore/logs/). Requires the `logs` feature. +We also provide the following layers, but recommend the span and log layers above for most +applications: + +- [`error_layer`] captures `tracing` events as Sentry error events, with support for tags and + exception information. +- [`breadcrumb_layer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. + +For migration from the legacy combined layer, see [`SentryLayer`]. + ## Getting started -Enable the `logs` feature on `sentry-tracing` (or the `tracing` and `logs` features on -`sentry`). +Enable the `tracing` and `logs` features on `sentry` to use its tracing integration re-export. +Alternatively, enable `logs` on a direct `sentry-tracing` dependency and omit the alias import +below. Set a traces sample rate to capture transactions, then install the span and log layers with explicit level filters: ```rust +use sentry::integrations::tracing as sentry_tracing; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -72,7 +83,8 @@ By default, the Sentry span name matches the `tracing` span name, and its operat - `sentry.name` overrides the Sentry span name, including when recorded after creation. - `sentry.op` overrides the Sentry span operation, including when recorded after creation. - `sentry.trace` continues the trace from a `sentry-trace` header. It must be set when a - **root** span is created; recording it later has no effect. + **root Sentry span** is created, meaning no Sentry span is active in the current scope. + Applying it to a child Sentry span or recording it after creation has no effect. ```rust #[tracing::instrument(skip_all, fields( diff --git a/sentry-tracing/src/converters.rs b/sentry-tracing/src/converters.rs index d1095213..6b3f991b 100644 --- a/sentry-tracing/src/converters.rs +++ b/sentry-tracing/src/converters.rs @@ -351,7 +351,8 @@ where /// Creates a [`Log`] from a given [`tracing_core::Event`] /// -/// If a context is provided, the data from the context is set on the span. +/// If a context is provided, include available parent-span data in the returned log's attributes. +/// See [`crate::LogLayer::enable_span_attributes`] for span-capture and filtering requirements. #[cfg(feature = "logs")] #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] #[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")] diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index d3663e26..4b181ab3 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -6,6 +6,9 @@ use tracing_subscriber::registry::LookupSpan; use crate::converters::breadcrumb_from_event; /// Adds tracing events as Sentry breadcrumbs. +/// +/// Without a filter, events at every level become breadcrumbs. Configure a filter to limit +/// breadcrumb volume; see [`breadcrumb_layer`] for the recommended levels. pub struct BreadcrumbLayer { with_span_attributes: bool, event_mapper: Option>>, @@ -17,13 +20,14 @@ impl BreadcrumbLayer { /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, /// this option only has any effect when the `SpanLayer` is installed. /// - /// Furthermore, only spans accepted by the breadcrumb layer's - /// [filter](tracing_subscriber::layer#per-layer-filtering) are considered when attaching + /// Furthermore, only spans accepted by the breadcrumb layer's filter (see + /// [`tracing_subscriber::layer`]) are considered when attaching /// attributes. When using this option, therefore, we recommend building a custom filter that /// accepts all spans, while filtering events to the desired levels, like so: /// /// ```rust /// # use tracing_subscriber::prelude::*; + /// use sentry::integrations::tracing as sentry_tracing; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// @@ -100,8 +104,7 @@ where /// A mapper function which converts a tracing event to a [`Breadcrumb`]. /// -/// This is a pretty advanced API; when using it, callers must handle constructing the breadcrumb -/// object manually from the tracing event. +/// Fully customizes if and how `tracing` events are converted to Sentry data. /// /// The function can also return [`None`], in which case, no breadcrumb is created from the /// tracing event. @@ -118,21 +121,22 @@ impl EventToBreadcrumbMapper for F where /// Creates a layer that adds tracing events as Sentry breadcrumbs. /// /// For most users, we would recommend using the [`log_layer`](super::log_layer) instead of this -/// layer, as logs are trace-connected and sent regardless of whether an error occurs. +/// layer, as logs are sent regardless of whether an error occurs. /// /// Breadcrumbs are only sent attached to Sentry error events. Users using this layer may therefore /// also wish to enable the [`error_layer`](super::error_layer). /// /// # Filtering /// -/// It is highly recommended to configure a filter on the layer to limit breadcrumb volume. We -/// recommend capturing `WARN` and `INFO` events. If you also use the error layer, exclude `ERROR` +/// Without a filter, events at every level become breadcrumbs. Configure a filter to limit +/// breadcrumb volume. We recommend capturing `WARN` and `INFO` events. If you also use the error layer, exclude `ERROR` /// events, which would otherwise duplicate the error event as a breadcrumb. If a custom filter /// sends the same event to both layers, install the breadcrumb layer before the error layer so /// that breadcrumb is included in the error. /// /// ```rust /// # use tracing_subscriber::prelude::*; +/// use sentry::integrations::tracing as sentry_tracing; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index f66b5e56..38be7b9f 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -6,6 +6,11 @@ use tracing_subscriber::registry::LookupSpan; use crate::converters::event_from_event; /// Captures tracing events as Sentry error events. +/// +/// # Warning +/// +/// Without a filter, events at every level become Sentry error events. This can quickly exhaust +/// your Sentry error quota. Configure a filter to select the events to capture; see [`error_layer`]. pub struct ErrorLayer { with_span_attributes: bool, event_mapper: Option>>, @@ -17,13 +22,14 @@ impl ErrorLayer { /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, /// this option only has any effect when the `SpanLayer` is installed. /// - /// Furthermore, only spans accepted by the error layer's - /// [filter](tracing_subscriber::layer#per-layer-filtering) are considered when attaching + /// Furthermore, only spans accepted by the error layer's filter (see + /// [`tracing_subscriber::layer`]) are considered when attaching /// attributes. When using this option, therefore, we recommend building a custom filter that /// accepts all spans, while filtering errors to the desired level, like so: /// /// ```rust /// # use tracing_subscriber::prelude::*; + /// use sentry::integrations::tracing as sentry_tracing; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// @@ -100,8 +106,7 @@ where /// A mapper function which converts a tracing event to a Sentry error event. /// -/// This is a pretty advanced API; when using it, callers must handle constructing the Sentry -/// event object manually from the tracing event. +/// Fully customizes if and how `tracing` events are converted to Sentry data. /// /// The function can also return [`None`], in which case, no error event is created from the /// tracing event. @@ -123,11 +128,13 @@ impl EventToErrorMapper for F where /// /// # Filtering /// -/// It is highly recommended to configure a filter on the layer, so that only `ERROR` events -/// create Sentry issues. +/// **Warning:** Without a filter, events at every level, including `DEBUG` and `TRACE`, become +/// Sentry error events. This can quickly exhaust your Sentry error quota. Configure a filter +/// so that only `ERROR` events create Sentry issues, as shown below. /// /// ```rust /// # use tracing_subscriber::prelude::*; +/// use sentry::integrations::tracing as sentry_tracing; /// use tracing_subscriber::filter::LevelFilter; /// /// tracing_subscriber::registry() @@ -140,6 +147,15 @@ impl EventToErrorMapper for F where /// // ... but this will not be, due to LevelFilter::ERROR being set. /// tracing::warn!("WARN event"); /// ``` +/// +/// # Event conversion +/// +/// With the default converter: +/// +/// - String, numeric, and boolean fields prefixed with `tags.` become Sentry tags, with the +/// prefix removed. +/// - Other data fields are stored in the `Rust Tracing Fields` context on the Sentry error event. +/// - Record an error as `&dyn std::error::Error` to capture its exception type and source chain. pub fn error_layer() -> ErrorLayer { ErrorLayer { with_span_attributes: false, diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 7458a7ec..047b4cc7 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -23,13 +23,14 @@ impl LogLayer { /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, /// this option only has any effect when the `SpanLayer` is installed. /// - /// Furthermore, only spans accepted by the log layer's - /// [filter](tracing_subscriber::layer#per-layer-filtering) are considered when attaching + /// Furthermore, only spans accepted by the log layer's filter (see + /// [`tracing_subscriber::layer`]) are considered when attaching /// attributes. When using this option, therefore, we recommend building a custom filter that /// accepts all spans, while filtering events to the desired level, like so: /// /// ```rust /// # use tracing_subscriber::prelude::*; + /// use sentry::integrations::tracing as sentry_tracing; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// @@ -114,8 +115,7 @@ where /// A mapper function which converts a tracing event to a [`Log`]. /// -/// This is a pretty advanced API; when using it, callers must handle constructing the log -/// object manually from the tracing event. +/// Fully customizes if and how `tracing` events are converted to Sentry data. /// /// The function can also return [`None`], in which case, no log is created from the tracing event. pub trait EventToLogMapper: Fn(&Event, Context<'_, S>) -> Option + Send + Sync {} @@ -127,11 +127,12 @@ impl EventToLogMapper for F where F: Fn(&Event, Context<'_, S>) -> Opti /// /// # Filtering /// -/// It is highly recommended to configure a level filter on the layer to limit log volume. We -/// recommend capturing logs at the `INFO` level, and more severe. +/// Without a filter, this layer captures logs at every level. Configure a level filter to limit +/// log volume. We recommend capturing logs at the `INFO` level, and more severe. /// /// ```rust /// # use tracing_subscriber::prelude::*; +/// use sentry::integrations::tracing as sentry_tracing; /// use tracing_subscriber::filter::LevelFilter; /// /// tracing_subscriber::registry() diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index 7898ca9c..b09cd521 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -123,7 +123,7 @@ type EventMapper = Box) -> EventMapping + Send /// Legacy combined tracing layer. /// -/// For most applications, use [`span_layer`] and [`log_layer`] instead. +/// For most applications, use [`span_layer`] and [`log_layer`] with explicit filters instead. /// Add [`error_layer`] if tracing events should also create Sentry issues, and [`breadcrumb_layer`] /// if errors need breadcrumb context. /// @@ -141,6 +141,7 @@ type EventMapper = Box) -> EventMapping + Send /// ```rust /// # #[cfg(feature = "logs")] /// # { +/// use sentry::integrations::tracing as sentry_tracing; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// use tracing_subscriber::prelude::*; @@ -300,8 +301,6 @@ where } } } else { - // The legacy filter may choose nondefault levels. Do not reapply the - // standalone event layers' default level checks here. let filter = (self.event_filter)(event.metadata()); if filter.contains(EventFilter::Breadcrumb) { self.breadcrumb.capture_tracing_event(event, ctx.clone()); @@ -351,7 +350,7 @@ where /// Creates a legacy combined Sentry layer. /// -/// For most applications, use [`span_layer`] and [`log_layer`] instead. +/// For most applications, use [`span_layer`] and [`log_layer`] with explicit filters instead. /// See [`SentryLayer`] for a migration example that preserves the legacy default level selection /// using all four individual layers, and for differences when migrating custom configuration. #[deprecated(note = "Prefer span_layer() and log_layer(); see SentryLayer docs for migration")] diff --git a/sentry-tracing/src/layer/span.rs b/sentry-tracing/src/layer/span.rs index a90f9631..4f10876e 100644 --- a/sentry-tracing/src/layer/span.rs +++ b/sentry-tracing/src/layer/span.rs @@ -17,6 +17,8 @@ use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX} /// Captures tracing spans as Sentry transactions and spans. /// /// Install this layer to capture spans; event layers do not manage the span lifecycle. +/// Without a filter, this layer captures spans at every level. Configure a filter to limit span +/// volume; see [`span_layer`] for the recommended levels. #[non_exhaustive] pub struct SpanLayer {} @@ -24,11 +26,12 @@ pub struct SpanLayer {} /// /// # Filtering /// -/// It is highly recommended to configure a level filter on the layer to limit span volume. We -/// recommend capturing spans at the `INFO` level, and more severe. +/// Without a filter, this layer captures spans at every level. Configure a level filter to limit +/// span volume. We recommend capturing spans at the `INFO` level, and more severe. /// /// ```rust /// # use tracing_subscriber::prelude::*; +/// use sentry::integrations::tracing as sentry_tracing; /// use tracing_subscriber::filter::LevelFilter; /// /// tracing_subscriber::registry() @@ -183,7 +186,7 @@ where || ctx .span(id) .is_none_or(|span| span.extensions().get::().is_none()), - "[SentryLayer] missing HubSwitchGuard on exit for span {id:?}. \ + "[SentrySpanLayer] missing HubSwitchGuard on exit for span {id:?}. \ This span has been exited more times on this thread than it has been entered, \ likely due to dropping an `Entered` guard in a different thread than where it was \ entered. This mismatch will likely cause the sentry-tracing layer to leak memory." diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index 723e7ac2..5fd2ec56 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -6,16 +6,27 @@ //! - [`log_layer`] captures `tracing` events as //! [Sentry Logs](https://docs.sentry.io/product/explore/logs/). Requires the `logs` feature. //! +//! We also provide the following layers, but recommend the span and log layers above for most +//! applications: +//! +//! - [`error_layer`] captures `tracing` events as Sentry error events, with support for tags and +//! exception information. +//! - [`breadcrumb_layer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. +//! +//! For migration from the legacy combined layer, see [`SentryLayer`]. +//! //! # Getting started //! -//! Enable the `logs` feature on `sentry-tracing` (or the `tracing` and `logs` features on -//! `sentry`). +//! Enable the `tracing` and `logs` features on `sentry` to use its tracing integration re-export. +//! Alternatively, enable `logs` on a direct `sentry-tracing` dependency and omit the alias import +//! below. //! Set a traces sample rate to capture transactions, then install the span and log layers with //! explicit level filters: //! //! ``` //! # #[cfg(feature = "logs")] //! # { +//! use sentry::integrations::tracing as sentry_tracing; //! use tracing_subscriber::filter::LevelFilter; //! use tracing_subscriber::prelude::*; //! @@ -67,7 +78,8 @@ //! - `sentry.name` overrides the Sentry span name, including when recorded after creation. //! - `sentry.op` overrides the Sentry span operation, including when recorded after creation. //! - `sentry.trace` continues the trace from a `sentry-trace` header. It must be set when a -//! **root** span is created; recording it later has no effect. +//! **root Sentry span** is created, meaning no Sentry span is active in the current scope. +//! Applying it to a child Sentry span or recording it after creation has no effect. //! //! ``` //! #[tracing::instrument(skip_all, fields( diff --git a/sentry-tracing/tests/future_cross_thread_info_span.rs b/sentry-tracing/tests/future_cross_thread_info_span.rs index 7980a280..81f57abc 100644 --- a/sentry-tracing/tests/future_cross_thread_info_span.rs +++ b/sentry-tracing/tests/future_cross_thread_info_span.rs @@ -15,7 +15,7 @@ use tracing_subscriber::prelude::*; /// /// This specifically tests a future awaited across threads, which is probably /// the most common scenario where this can occur. The span is at info level, -/// the lowest level captured by Sentry by default. +/// the least-severe level accepted by the filter configured in this test. #[test] fn future_cross_thread_info_span() { const SPAN_NAME: &str = "future_cross_thread_info_span"; @@ -40,7 +40,7 @@ fn future_cross_thread_info_span() { assert!( thread2_panic_message.starts_with( - "[SentryLayer] missing HubSwitchGuard on exit for span" + "[SentrySpanLayer] missing HubSwitchGuard on exit for span" ), "Thread 2 panicked, but not for the expected reason. It is also possible that the panic \ message was changed without updating this test." From cfcb9ad28603725d7f74a966fa8cfad7e6bda8b3 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 6 Oct 2026 18:57:15 +0200 Subject: [PATCH 08/15] add a test --- sentry-tracing/tests/error_breadcrumbs.rs | 75 +++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 sentry-tracing/tests/error_breadcrumbs.rs diff --git a/sentry-tracing/tests/error_breadcrumbs.rs b/sentry-tracing/tests/error_breadcrumbs.rs new file mode 100644 index 00000000..c9dd716a --- /dev/null +++ b/sentry-tracing/tests/error_breadcrumbs.rs @@ -0,0 +1,75 @@ +//! Tests standalone error/breadcrumb capture and span context without transaction sampling. + +use sentry::protocol::{Context, EnvelopeItem}; +use tracing::Level; +use tracing_subscriber::filter::{filter_fn, LevelFilter}; +use tracing_subscriber::prelude::*; + +#[test] +fn error_and_breadcrumb_layers_capture_unsampled_span_context() { + let _subscriber = tracing_subscriber::registry() + .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with( + sentry_tracing::breadcrumb_layer() + .enable_span_attributes() + .with_filter(filter_fn(|metadata| { + metadata.is_span() || matches!(*metadata.level(), Level::INFO | Level::WARN) + })), + ) + .with( + sentry_tracing::error_layer() + .enable_span_attributes() + .with_filter(filter_fn(|metadata| { + metadata.is_span() || *metadata.level() == Level::ERROR + })), + ) + .set_default(); + + let mut expected_trace = None; + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let _span = tracing::info_span!("request", order_id = 42).entered(); + expected_trace = sentry::configure_scope(|scope| { + scope.get_span().map(|span| span.get_trace_context()) + }); + tracing::info!("processing order"); + tracing::error!("order failed"); + }, + sentry::ClientOptions::new().traces_sample_rate(0.0), + ); + + let mut errors = Vec::new(); + for item in envelopes.iter().flat_map(|envelope| envelope.items()) { + match item { + EnvelopeItem::Event(error) => errors.push(error), + unexpected => panic!("expected only an error event, got {unexpected:?}"), + } + } + assert_eq!(errors.len(), 1); + let error = errors[0]; + assert_eq!(error.level, sentry::Level::Error); + assert_eq!(error.message.as_deref(), Some("order failed")); + + assert_eq!(error.breadcrumbs.len(), 1); + let breadcrumb = &error.breadcrumbs[0]; + assert_eq!(breadcrumb.level, sentry::Level::Info); + assert_eq!(breadcrumb.message.as_deref(), Some("processing order")); + assert_eq!(breadcrumb.data.get("request:order_id"), Some(&42.into())); + + match error.contexts.get("Rust Tracing Fields") { + Some(Context::Other(fields)) => { + assert_eq!(fields.get("request:order_id"), Some(&42.into())); + } + unexpected => panic!("expected tracing fields, got {unexpected:?}"), + } + + let expected_trace = expected_trace.expect("expected an active Sentry span"); + match error.contexts.get("trace") { + Some(Context::Trace(trace)) => { + assert_eq!(trace.trace_id, expected_trace.trace_id); + assert_eq!(trace.span_id, expected_trace.span_id); + assert_eq!(trace.op, expected_trace.op); + } + unexpected => panic!("expected trace context, got {unexpected:?}"), + } +} From 227cc9a4d92603694424d7dc9bc029e4d8fd1ded Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 7 Oct 2026 15:32:43 +0200 Subject: [PATCH 09/15] Improved API --- sentry-tracing/README.md | 24 ++-- sentry-tracing/src/layer/breadcrumb.rs | 110 ++++++++++-------- sentry-tracing/src/layer/error.rs | 108 +++++++++-------- sentry-tracing/src/layer/log.rs | 72 +++++++----- sentry-tracing/src/layer/mod.rs | 42 ++++--- sentry-tracing/src/layer/span.rs | 53 +++++---- sentry-tracing/src/lib.rs | 24 ++-- sentry-tracing/tests/error_breadcrumbs.rs | 7 +- .../tests/future_cross_thread_info_span.rs | 3 +- .../tests/future_cross_thread_trace_span.rs | 3 +- .../tests/futures_same_thread_info_span.rs | 3 +- sentry-tracing/tests/log_span_attributes.rs | 5 +- sentry-tracing/tests/name_op_updates.rs | 3 +- sentry-tracing/tests/smoke.rs | 5 +- sentry-tracing/tests/span_cross_thread.rs | 3 +- sentry-tracing/tests/span_reentrancy.rs | 3 +- sentry/examples/tracing-demo.rs | 5 +- sentry/tests/test_legacy_tracing.rs | 4 +- 18 files changed, 257 insertions(+), 220 deletions(-) diff --git a/sentry-tracing/README.md b/sentry-tracing/README.md index 641fd5e1..ad902e2a 100644 --- a/sentry-tracing/README.md +++ b/sentry-tracing/README.md @@ -10,29 +10,26 @@ Capture `tracing` spans and events as Sentry traces and structured logs. For most applications, we recommend installing only these two layers: -- [`span_layer`] captures `tracing` spans as Sentry transactions and spans. -- [`log_layer`] captures `tracing` events as - [Sentry Logs](https://docs.sentry.io/product/explore/logs/). Requires the `logs` feature. +- [`SpanLayer`] captures `tracing` spans as Sentry transactions and spans. +- [`LogLayer`] captures `tracing` events as Sentry structured logs. Requires the `logs` feature. We also provide the following layers, but recommend the span and log layers above for most applications: -- [`error_layer`] captures `tracing` events as Sentry error events, with support for tags and +- [`ErrorLayer`] captures `tracing` events as Sentry error events, with support for tags and exception information. -- [`breadcrumb_layer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. +- [`BreadcrumbLayer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. For migration from the legacy combined layer, see [`SentryLayer`]. ## Getting started Enable the `tracing` and `logs` features on `sentry` to use its tracing integration re-export. -Alternatively, enable `logs` on a direct `sentry-tracing` dependency and omit the alias import -below. Set a traces sample rate to capture transactions, then install the span and log layers with explicit level filters: ```rust -use sentry::integrations::tracing as sentry_tracing; +use sentry::integrations::tracing::{LogLayer, SpanLayer}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -43,8 +40,8 @@ let _guard = sentry::init( ); tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) - .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with(LogLayer::new().with_filter(LevelFilter::INFO)) .init(); ``` @@ -55,7 +52,7 @@ Sentry layers receive it. ## Spans -`span_layer()` creates a Sentry transaction when no Sentry span is active, or a child span +`SpanLayer` creates a Sentry transaction when no Sentry span is active, or a child span otherwise. For example, [`tracing::instrument`](https://docs.rs/tracing/latest/tracing/attr.instrument.html) creates spans for instrumented functions; arguments become span data unless skipped. @@ -99,9 +96,8 @@ async fn handle_request(headers: std::collections::HashMap) { ## Logs -`log_layer()` sends events accepted by its filter as structured logs. Event fields become -searchable log attributes; fields with dots appear nested under their common prefix in the -Sentry Logs explorer. +`LogLayer` sends events accepted by its filter as structured logs. Event fields become log +attributes. ```rust tracing::info!(number = 42, my.key = "value", "Processed request"); diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index 4b181ab3..cedf0422 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -8,13 +8,59 @@ use crate::converters::breadcrumb_from_event; /// Adds tracing events as Sentry breadcrumbs. /// /// Without a filter, events at every level become breadcrumbs. Configure a filter to limit -/// breadcrumb volume; see [`breadcrumb_layer`] for the recommended levels. +/// breadcrumb volume; see [`BreadcrumbLayer::new`] for the recommended levels. pub struct BreadcrumbLayer { with_span_attributes: bool, event_mapper: Option>>, } impl BreadcrumbLayer { + /// Creates a layer that adds tracing events as Sentry breadcrumbs. + /// + /// For most users, we would recommend using the [`LogLayer`](super::LogLayer) instead of this + /// layer, as logs are sent regardless of whether an error occurs. + /// + /// Breadcrumbs are only sent attached to Sentry error events. Users using this layer may therefore + /// also wish to enable the [`ErrorLayer`](super::ErrorLayer). + /// + /// # Filtering + /// + /// Without a filter, events at every level become breadcrumbs. Configure a filter to limit + /// breadcrumb volume. We recommend capturing `WARN` and `INFO` events. If you also use the error layer, exclude `ERROR` + /// events, which would otherwise duplicate the error event as a breadcrumb. If a custom filter + /// sends the same event to both layers, install the breadcrumb layer before the error layer so + /// that breadcrumb is included in the error. + /// + /// ```rust + /// # use tracing_subscriber::prelude::*; + /// use sentry::integrations::tracing::{BreadcrumbLayer, ErrorLayer}; + /// use tracing::Level; + /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; + /// + /// tracing_subscriber::registry() + /// .with( + /// BreadcrumbLayer::new().with_filter(filter_fn(|metadata| { + /// matches!(*metadata.level(), Level::WARN | Level::INFO) + /// })), + /// ) + /// .with(ErrorLayer::new().with_filter(LevelFilter::ERROR)) + /// .init(); + /// + /// // These will be captured as breadcrumbs ... + /// tracing::info!("INFO breadcrumb"); + /// tracing::warn!("WARN breadcrumb"); + /// + /// // ... but this will not be. + /// tracing::debug!("DEBUG breadcrumb"); + /// + /// // This will not be captured as a breadcrumb, but as an error event by the error layer. The + /// // breadcrumbs above will be contained in that error event. + /// tracing::error!("ERROR event"); + /// ``` + pub fn new() -> Self { + Self::default() + } + /// Include attributes of parent spans in breadcrumbs. /// /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, @@ -27,14 +73,14 @@ impl BreadcrumbLayer { /// /// ```rust /// # use tracing_subscriber::prelude::*; - /// use sentry::integrations::tracing as sentry_tracing; + /// use sentry::integrations::tracing::{BreadcrumbLayer, SpanLayer}; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// /// tracing_subscriber::registry() - /// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + /// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) /// .with( - /// sentry_tracing::breadcrumb_layer() + /// BreadcrumbLayer::new() /// .enable_span_attributes() /// .with_filter(filter_fn(|metadata| { /// // Accept all spans, but only `WARN` and `INFO` events. @@ -104,7 +150,9 @@ where /// A mapper function which converts a tracing event to a [`Breadcrumb`]. /// -/// Fully customizes if and how `tracing` events are converted to Sentry data. +/// This advanced API fully customizes whether and how `tracing` events become Sentry breadcrumbs. +/// A mapper can call [`breadcrumb_from_event`](crate::breadcrumb_from_event) to perform the conversion +/// alongside its custom filtering or mapping logic. /// /// The function can also return [`None`], in which case, no breadcrumb is created from the /// tracing event. @@ -118,51 +166,11 @@ impl EventToBreadcrumbMapper for F where { } -/// Creates a layer that adds tracing events as Sentry breadcrumbs. -/// -/// For most users, we would recommend using the [`log_layer`](super::log_layer) instead of this -/// layer, as logs are sent regardless of whether an error occurs. -/// -/// Breadcrumbs are only sent attached to Sentry error events. Users using this layer may therefore -/// also wish to enable the [`error_layer`](super::error_layer). -/// -/// # Filtering -/// -/// Without a filter, events at every level become breadcrumbs. Configure a filter to limit -/// breadcrumb volume. We recommend capturing `WARN` and `INFO` events. If you also use the error layer, exclude `ERROR` -/// events, which would otherwise duplicate the error event as a breadcrumb. If a custom filter -/// sends the same event to both layers, install the breadcrumb layer before the error layer so -/// that breadcrumb is included in the error. -/// -/// ```rust -/// # use tracing_subscriber::prelude::*; -/// use sentry::integrations::tracing as sentry_tracing; -/// use tracing::Level; -/// use tracing_subscriber::filter::{filter_fn, LevelFilter}; -/// -/// tracing_subscriber::registry() -/// .with( -/// sentry_tracing::breadcrumb_layer().with_filter(filter_fn(|metadata| { -/// matches!(*metadata.level(), Level::WARN | Level::INFO) -/// })), -/// ) -/// .with(sentry_tracing::error_layer().with_filter(LevelFilter::ERROR)) -/// .init(); -/// -/// // These will be captured as breadcrumbs ... -/// tracing::info!("INFO breadcrumb"); -/// tracing::warn!("WARN breadcrumb"); -/// -/// // ... but this will not be. -/// tracing::debug!("DEBUG breadcrumb"); -/// -/// // This will not be captured as a breadcrumb, but as an error event by the error layer. The -/// // breadcrumbs above will be contained in that error event. -/// tracing::error!("ERROR event"); -/// ``` -pub fn breadcrumb_layer() -> BreadcrumbLayer { - BreadcrumbLayer { - with_span_attributes: false, - event_mapper: None, +impl Default for BreadcrumbLayer { + fn default() -> Self { + Self { + with_span_attributes: false, + event_mapper: None, + } } } diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index 38be7b9f..c025d84b 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -6,17 +6,59 @@ use tracing_subscriber::registry::LookupSpan; use crate::converters::event_from_event; /// Captures tracing events as Sentry error events. -/// -/// # Warning -/// -/// Without a filter, events at every level become Sentry error events. This can quickly exhaust -/// your Sentry error quota. Configure a filter to select the events to capture; see [`error_layer`]. pub struct ErrorLayer { with_span_attributes: bool, event_mapper: Option>>, } impl ErrorLayer { + /// Creates a layer that captures tracing events as Sentry error events. + /// + /// Most applications should start with [`LogLayer`](super::LogLayer), and add this layer if + /// `ERROR` events should also be captured as Sentry error events. Combine it with + /// [`BreadcrumbLayer`](super::BreadcrumbLayer) to attach the events leading up to each error. + /// + /// # Filtering + /// + /// Configure a level filter to limit error-event volume. We recommend capturing only `ERROR` + /// events, as shown below. + /// + ///
+ /// + /// **Do not use this layer without a filter.** An unfiltered layer captures every tracing + /// event, including `DEBUG` and `TRACE`, as a Sentry error event. This can quickly exhaust + /// your Sentry error quota. + /// + ///
+ /// + /// ```rust + /// # use tracing_subscriber::prelude::*; + /// use sentry::integrations::tracing::ErrorLayer; + /// use tracing_subscriber::filter::LevelFilter; + /// + /// tracing_subscriber::registry() + /// .with(ErrorLayer::new().with_filter(LevelFilter::ERROR)) + /// .init(); + /// + /// // This will be captured ... + /// tracing::error!("ERROR event"); + /// + /// // ... but this will not be, due to LevelFilter::ERROR being set. + /// tracing::warn!("WARN event"); + /// ``` + /// + /// # Event conversion + /// + /// With the default converter: + /// + /// - String, numeric, and boolean fields prefixed with `tags.` become Sentry tags, with the + /// prefix removed. + /// - Other data fields are stored in the `Rust Tracing Fields` context on the Sentry error event. + /// - Record an error as `&dyn std::error::Error` to capture its exception type and source chain. + pub fn new() -> Self { + Self::default() + } + /// Include attributes of parent spans in error events. /// /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, @@ -29,14 +71,14 @@ impl ErrorLayer { /// /// ```rust /// # use tracing_subscriber::prelude::*; - /// use sentry::integrations::tracing as sentry_tracing; + /// use sentry::integrations::tracing::{ErrorLayer, SpanLayer}; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// /// tracing_subscriber::registry() - /// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + /// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) /// .with( - /// sentry_tracing::error_layer() + /// ErrorLayer::new() /// .enable_span_attributes() /// .with_filter(filter_fn(|metadata| { /// // Accept all spans, but only `ERROR` events. @@ -106,7 +148,9 @@ where /// A mapper function which converts a tracing event to a Sentry error event. /// -/// Fully customizes if and how `tracing` events are converted to Sentry data. +/// This advanced API fully customizes whether and how `tracing` events become Sentry error events. +/// A mapper can call [`event_from_event`](crate::event_from_event) to perform the conversion +/// alongside its custom filtering or mapping logic. /// /// The function can also return [`None`], in which case, no error event is created from the /// tracing event. @@ -120,45 +164,11 @@ impl EventToErrorMapper for F where { } -/// Creates a layer that captures tracing events as Sentry error events. -/// -/// Most applications should start with [`log_layer`](super::log_layer), and add this layer if -/// `ERROR` events should also create Sentry issues. Combine it with -/// [`breadcrumb_layer`](super::breadcrumb_layer) to attach the events leading up to each error. -/// -/// # Filtering -/// -/// **Warning:** Without a filter, events at every level, including `DEBUG` and `TRACE`, become -/// Sentry error events. This can quickly exhaust your Sentry error quota. Configure a filter -/// so that only `ERROR` events create Sentry issues, as shown below. -/// -/// ```rust -/// # use tracing_subscriber::prelude::*; -/// use sentry::integrations::tracing as sentry_tracing; -/// use tracing_subscriber::filter::LevelFilter; -/// -/// tracing_subscriber::registry() -/// .with(sentry_tracing::error_layer().with_filter(LevelFilter::ERROR)) -/// .init(); -/// -/// // This will be captured ... -/// tracing::error!("ERROR event"); -/// -/// // ... but this will not be, due to LevelFilter::ERROR being set. -/// tracing::warn!("WARN event"); -/// ``` -/// -/// # Event conversion -/// -/// With the default converter: -/// -/// - String, numeric, and boolean fields prefixed with `tags.` become Sentry tags, with the -/// prefix removed. -/// - Other data fields are stored in the `Rust Tracing Fields` context on the Sentry error event. -/// - Record an error as `&dyn std::error::Error` to capture its exception type and source chain. -pub fn error_layer() -> ErrorLayer { - ErrorLayer { - with_span_attributes: false, - event_mapper: None, +impl Default for ErrorLayer { + fn default() -> Self { + Self { + with_span_attributes: false, + event_mapper: None, + } } } diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 047b4cc7..16e3e23d 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -7,7 +7,7 @@ use crate::converters::log_from_event; /// Captures tracing events as Sentry logs. /// -/// Captures all event levels unless a filter is configured; see [`log_layer`] for the recommended +/// Captures all event levels unless a filter is configured; see [`LogLayer::new`] for the recommended /// level filter. /// /// Requires a client with logs enabled. @@ -18,6 +18,33 @@ pub struct LogLayer { } impl LogLayer { + /// Creates a layer that captures Sentry logs from tracing events. + /// + /// # Filtering + /// + /// Without a filter, this layer captures logs at every level. Configure a level filter to limit + /// log volume. We recommend capturing logs at the `INFO` level, and more severe. + /// + /// ```rust + /// # use tracing_subscriber::prelude::*; + /// use sentry::integrations::tracing::LogLayer; + /// use tracing_subscriber::filter::LevelFilter; + /// + /// tracing_subscriber::registry() + /// .with(LogLayer::new().with_filter(LevelFilter::INFO)) + /// .init(); + /// + /// // This will be captured ... + /// tracing::info!("INFO log"); + /// + /// // ... but this will not be, due to LevelFilter::INFO being set. + /// tracing::debug!("DEBUG log"); + /// ``` + #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] + pub fn new() -> Self { + Self::default() + } + /// Include attributes of parent spans in logs. /// /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, @@ -30,14 +57,14 @@ impl LogLayer { /// /// ```rust /// # use tracing_subscriber::prelude::*; - /// use sentry::integrations::tracing as sentry_tracing; + /// use sentry::integrations::tracing::{LogLayer, SpanLayer}; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// /// tracing_subscriber::registry() - /// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + /// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) /// .with( - /// sentry_tracing::log_layer() + /// LogLayer::new() /// .enable_span_attributes() /// .with_filter(filter_fn(|metadata| { /// // Accept all spans, but only events at `INFO` and more severe. @@ -115,7 +142,9 @@ where /// A mapper function which converts a tracing event to a [`Log`]. /// -/// Fully customizes if and how `tracing` events are converted to Sentry data. +/// This advanced API fully customizes whether and how `tracing` events become Sentry logs. +/// A mapper can call [`log_from_event`](crate::log_from_event) to perform the conversion +/// alongside its custom filtering or mapping logic. /// /// The function can also return [`None`], in which case, no log is created from the tracing event. pub trait EventToLogMapper: Fn(&Event, Context<'_, S>) -> Option + Send + Sync {} @@ -123,32 +152,11 @@ pub trait EventToLogMapper: Fn(&Event, Context<'_, S>) -> Option + Send impl EventToLogMapper for F where F: Fn(&Event, Context<'_, S>) -> Option + Send + Sync {} -/// Creates a layer that captures Sentry logs from tracing events. -/// -/// # Filtering -/// -/// Without a filter, this layer captures logs at every level. Configure a level filter to limit -/// log volume. We recommend capturing logs at the `INFO` level, and more severe. -/// -/// ```rust -/// # use tracing_subscriber::prelude::*; -/// use sentry::integrations::tracing as sentry_tracing; -/// use tracing_subscriber::filter::LevelFilter; -/// -/// tracing_subscriber::registry() -/// .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) -/// .init(); -/// -/// // This will be captured ... -/// tracing::info!("INFO log"); -/// -/// // ... but this will not be, due to LevelFilter::INFO being set. -/// tracing::debug!("DEBUG log"); -/// ``` -#[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] -pub fn log_layer() -> LogLayer { - LogLayer { - with_span_attributes: false, - event_mapper: None, +impl Default for LogLayer { + fn default() -> Self { + Self { + with_span_attributes: false, + event_mapper: None, + } } } diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index b09cd521..2921cda8 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -11,12 +11,12 @@ mod log; mod span; mod span_guard_stack; -pub use breadcrumb::{breadcrumb_layer, BreadcrumbLayer, EventToBreadcrumbMapper}; -pub use error::{error_layer, ErrorLayer, EventToErrorMapper}; +pub use breadcrumb::{BreadcrumbLayer, EventToBreadcrumbMapper}; +pub use error::{ErrorLayer, EventToErrorMapper}; #[cfg(feature = "logs")] -pub use log::{log_layer, EventToLogMapper, LogLayer}; +pub use log::{EventToLogMapper, LogLayer}; pub(super) use span::SentrySpanData; -pub use span::{span_layer, SpanLayer}; +pub use span::SpanLayer; /// Separate submodule with expect(deprecated) needed because bitflags! uses the deprecated /// EventFilter. @@ -123,9 +123,9 @@ type EventMapper = Box) -> EventMapping + Send /// Legacy combined tracing layer. /// -/// For most applications, use [`span_layer`] and [`log_layer`] with explicit filters instead. -/// Add [`error_layer`] if tracing events should also create Sentry issues, and [`breadcrumb_layer`] -/// if errors need breadcrumb context. +/// For most applications, use [`SpanLayer`] and [`LogLayer`] with explicit filters instead. +/// Add [`ErrorLayer`] if tracing events should also be captured as Sentry error events, and +/// [`BreadcrumbLayer`] if errors need breadcrumb context. /// /// # Migrating the default configuration /// @@ -141,20 +141,20 @@ type EventMapper = Box) -> EventMapping + Send /// ```rust /// # #[cfg(feature = "logs")] /// # { -/// use sentry::integrations::tracing as sentry_tracing; +/// use sentry::integrations::tracing::{BreadcrumbLayer, ErrorLayer, LogLayer, SpanLayer}; /// use tracing::Level; /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; /// use tracing_subscriber::prelude::*; /// /// tracing_subscriber::registry() -/// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) +/// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) /// .with( -/// sentry_tracing::breadcrumb_layer().with_filter(filter_fn(|metadata| { +/// BreadcrumbLayer::new().with_filter(filter_fn(|metadata| { /// matches!(*metadata.level(), Level::WARN | Level::INFO) /// })), /// ) -/// .with(sentry_tracing::error_layer().with_filter(LevelFilter::ERROR)) -/// .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) +/// .with(ErrorLayer::new().with_filter(LevelFilter::ERROR)) +/// .with(LogLayer::new().with_filter(LevelFilter::INFO)) /// .init(); /// # } /// ``` @@ -184,7 +184,9 @@ type EventMapper = Box) -> EventMapping + Send /// install the breadcrumb layer before the error layer to include that breadcrumb in the error. /// The default configuration above excludes `ERROR` events from breadcrumbs, so the order does /// not affect their inclusion. -#[deprecated(note = "Prefer span_layer() and log_layer(); see SentryLayer docs for migration")] +#[deprecated( + note = "Prefer SpanLayer::new() and LogLayer::new(); see SentryLayer docs for migration" +)] pub struct SentryLayer { #[expect(deprecated)] event_filter: Box EventFilter + Send + Sync>, @@ -269,11 +271,11 @@ where event_filter: Box::new(default_event_filter), event_mapper: None, span_filter: Box::new(default_span_filter), - span_layer: span_layer(), - breadcrumb: breadcrumb_layer(), - error: error_layer(), + span_layer: SpanLayer::new(), + breadcrumb: BreadcrumbLayer::new(), + error: ErrorLayer::new(), #[cfg(feature = "logs")] - log: log_layer(), + log: LogLayer::new(), } } } @@ -350,10 +352,12 @@ where /// Creates a legacy combined Sentry layer. /// -/// For most applications, use [`span_layer`] and [`log_layer`] with explicit filters instead. +/// For most applications, use [`SpanLayer`] and [`LogLayer`] with explicit filters instead. /// See [`SentryLayer`] for a migration example that preserves the legacy default level selection /// using all four individual layers, and for differences when migrating custom configuration. -#[deprecated(note = "Prefer span_layer() and log_layer(); see SentryLayer docs for migration")] +#[deprecated( + note = "Prefer SpanLayer::new() and LogLayer::new(); see SentryLayer docs for migration" +)] #[expect(deprecated, reason = "returning the legacy layer")] pub fn layer() -> SentryLayer where diff --git a/sentry-tracing/src/layer/span.rs b/sentry-tracing/src/layer/span.rs index 4f10876e..d31d9db8 100644 --- a/sentry-tracing/src/layer/span.rs +++ b/sentry-tracing/src/layer/span.rs @@ -18,34 +18,37 @@ use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX} /// /// Install this layer to capture spans; event layers do not manage the span lifecycle. /// Without a filter, this layer captures spans at every level. Configure a filter to limit span -/// volume; see [`span_layer`] for the recommended levels. +/// volume; see [`SpanLayer::new`] for the recommended levels. +#[derive(Default)] #[non_exhaustive] pub struct SpanLayer {} -/// Creates a layer that captures tracing spans as Sentry transactions and spans. -/// -/// # Filtering -/// -/// Without a filter, this layer captures spans at every level. Configure a level filter to limit -/// span volume. We recommend capturing spans at the `INFO` level, and more severe. -/// -/// ```rust -/// # use tracing_subscriber::prelude::*; -/// use sentry::integrations::tracing as sentry_tracing; -/// use tracing_subscriber::filter::LevelFilter; -/// -/// tracing_subscriber::registry() -/// .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) -/// .init(); -/// -/// // This will be captured ... -/// let _info = tracing::info_span!("INFO span").entered(); -/// -/// // ... but this will not be, due to LevelFilter::INFO being set. -/// let _debug = tracing::debug_span!("DEBUG span").entered(); -/// ``` -pub fn span_layer() -> SpanLayer { - SpanLayer {} +impl SpanLayer { + /// Creates a layer that captures tracing spans as Sentry transactions and spans. + /// + /// # Filtering + /// + /// Without a filter, this layer captures spans at every level. Configure a level filter to limit + /// span volume. We recommend capturing spans at the `INFO` level, and more severe. + /// + /// ```rust + /// # use tracing_subscriber::prelude::*; + /// use sentry::integrations::tracing::SpanLayer; + /// use tracing_subscriber::filter::LevelFilter; + /// + /// tracing_subscriber::registry() + /// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + /// .init(); + /// + /// // This will be captured ... + /// let _info = tracing::info_span!("INFO span").entered(); + /// + /// // ... but this will not be, due to LevelFilter::INFO being set. + /// let _debug = tracing::debug_span!("DEBUG span").entered(); + /// ``` + pub fn new() -> Self { + Self::default() + } } #[inline(always)] diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index 5fd2ec56..012c0f87 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -2,31 +2,28 @@ //! //! For most applications, we recommend installing only these two layers: //! -//! - [`span_layer`] captures `tracing` spans as Sentry transactions and spans. -//! - [`log_layer`] captures `tracing` events as -//! [Sentry Logs](https://docs.sentry.io/product/explore/logs/). Requires the `logs` feature. +//! - [`SpanLayer`] captures `tracing` spans as Sentry transactions and spans. +//! - [`LogLayer`] captures `tracing` events as Sentry structured logs. Requires the `logs` feature. //! //! We also provide the following layers, but recommend the span and log layers above for most //! applications: //! -//! - [`error_layer`] captures `tracing` events as Sentry error events, with support for tags and +//! - [`ErrorLayer`] captures `tracing` events as Sentry error events, with support for tags and //! exception information. -//! - [`breadcrumb_layer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. +//! - [`BreadcrumbLayer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. //! //! For migration from the legacy combined layer, see [`SentryLayer`]. //! //! # Getting started //! //! Enable the `tracing` and `logs` features on `sentry` to use its tracing integration re-export. -//! Alternatively, enable `logs` on a direct `sentry-tracing` dependency and omit the alias import -//! below. //! Set a traces sample rate to capture transactions, then install the span and log layers with //! explicit level filters: //! //! ``` //! # #[cfg(feature = "logs")] //! # { -//! use sentry::integrations::tracing as sentry_tracing; +//! use sentry::integrations::tracing::{LogLayer, SpanLayer}; //! use tracing_subscriber::filter::LevelFilter; //! use tracing_subscriber::prelude::*; //! @@ -37,8 +34,8 @@ //! ); //! //! tracing_subscriber::registry() -//! .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) -//! .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) +//! .with(SpanLayer::new().with_filter(LevelFilter::INFO)) +//! .with(LogLayer::new().with_filter(LevelFilter::INFO)) //! .init(); //! # } //! ``` @@ -50,7 +47,7 @@ //! //! # Spans //! -//! `span_layer()` creates a Sentry transaction when no Sentry span is active, or a child span +//! `SpanLayer` creates a Sentry transaction when no Sentry span is active, or a child span //! otherwise. For example, //! [`tracing::instrument`](https://docs.rs/tracing/latest/tracing/attr.instrument.html) creates //! spans for instrumented functions; arguments become span data unless skipped. @@ -94,9 +91,8 @@ //! //! # Logs //! -//! `log_layer()` sends events accepted by its filter as structured logs. Event fields become -//! searchable log attributes; fields with dots appear nested under their common prefix in the -//! Sentry Logs explorer. +//! `LogLayer` sends events accepted by its filter as structured logs. Event fields become log +//! attributes. //! //! ``` //! tracing::info!(number = 42, my.key = "value", "Processed request"); diff --git a/sentry-tracing/tests/error_breadcrumbs.rs b/sentry-tracing/tests/error_breadcrumbs.rs index c9dd716a..c37d1824 100644 --- a/sentry-tracing/tests/error_breadcrumbs.rs +++ b/sentry-tracing/tests/error_breadcrumbs.rs @@ -1,6 +1,7 @@ //! Tests standalone error/breadcrumb capture and span context without transaction sampling. use sentry::protocol::{Context, EnvelopeItem}; +use sentry_tracing::{BreadcrumbLayer, ErrorLayer, SpanLayer}; use tracing::Level; use tracing_subscriber::filter::{filter_fn, LevelFilter}; use tracing_subscriber::prelude::*; @@ -8,16 +9,16 @@ use tracing_subscriber::prelude::*; #[test] fn error_and_breadcrumb_layers_capture_unsampled_span_context() { let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .with( - sentry_tracing::breadcrumb_layer() + BreadcrumbLayer::new() .enable_span_attributes() .with_filter(filter_fn(|metadata| { metadata.is_span() || matches!(*metadata.level(), Level::INFO | Level::WARN) })), ) .with( - sentry_tracing::error_layer() + ErrorLayer::new() .enable_span_attributes() .with_filter(filter_fn(|metadata| { metadata.is_span() || *metadata.level() == Level::ERROR diff --git a/sentry-tracing/tests/future_cross_thread_info_span.rs b/sentry-tracing/tests/future_cross_thread_info_span.rs index 81f57abc..3fcd0755 100644 --- a/sentry-tracing/tests/future_cross_thread_info_span.rs +++ b/sentry-tracing/tests/future_cross_thread_info_span.rs @@ -6,6 +6,7 @@ mod shared; mod transaction_assertions; use sentry::{Hub, HubSwitchGuard}; +use sentry_tracing::SpanLayer; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -23,7 +24,7 @@ fn future_cross_thread_info_span() { let _guard = HubSwitchGuard::new(Hub::new_from_top(Hub::current()).into()); let transport = shared::init_sentry(1.0); let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .set_default(); let span = tracing::info_span!(SPAN_NAME); diff --git a/sentry-tracing/tests/future_cross_thread_trace_span.rs b/sentry-tracing/tests/future_cross_thread_trace_span.rs index 66b26531..92b6f807 100644 --- a/sentry-tracing/tests/future_cross_thread_trace_span.rs +++ b/sentry-tracing/tests/future_cross_thread_trace_span.rs @@ -5,6 +5,7 @@ mod future_span_common; mod shared; use sentry::{Hub, HubSwitchGuard}; +use sentry_tracing::SpanLayer; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -15,7 +16,7 @@ fn future_cross_thread_trace_span() { let _guard = HubSwitchGuard::new(Hub::new_from_top(Hub::current()).into()); let transport = shared::init_sentry(1.0); let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .set_default(); let span = tracing::trace_span!("future_cross_thread_trace_span"); diff --git a/sentry-tracing/tests/futures_same_thread_info_span.rs b/sentry-tracing/tests/futures_same_thread_info_span.rs index b4a2a20f..2b7e87f5 100644 --- a/sentry-tracing/tests/futures_same_thread_info_span.rs +++ b/sentry-tracing/tests/futures_same_thread_info_span.rs @@ -8,6 +8,7 @@ use std::future::Future; use std::thread; use sentry::{Hub, HubSwitchGuard}; +use sentry_tracing::SpanLayer; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -19,7 +20,7 @@ fn futures_same_thread_info_span() { let _guard = HubSwitchGuard::new(Hub::new_from_top(Hub::current()).into()); let transport = shared::init_sentry(1.0); let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .set_default(); let span = tracing::info_span!("futures_same_thread_info_span"); diff --git a/sentry-tracing/tests/log_span_attributes.rs b/sentry-tracing/tests/log_span_attributes.rs index a53be2dc..6ee60697 100644 --- a/sentry-tracing/tests/log_span_attributes.rs +++ b/sentry-tracing/tests/log_span_attributes.rs @@ -3,6 +3,7 @@ #![cfg(feature = "logs")] use sentry::protocol::{EnvelopeItem, ItemContainer, LogLevel}; +use sentry_tracing::{LogLayer, SpanLayer}; use tracing::Level; use tracing_subscriber::filter::{filter_fn, LevelFilter}; use tracing_subscriber::prelude::*; @@ -10,9 +11,9 @@ use tracing_subscriber::prelude::*; #[test] fn log_layer_captures_span_attributes() { let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .with( - sentry_tracing::log_layer() + LogLayer::new() .enable_span_attributes() .with_filter(filter_fn(|metadata| { metadata.is_span() || *metadata.level() == Level::INFO diff --git a/sentry-tracing/tests/name_op_updates.rs b/sentry-tracing/tests/name_op_updates.rs index 632d337b..d8bde9f1 100644 --- a/sentry-tracing/tests/name_op_updates.rs +++ b/sentry-tracing/tests/name_op_updates.rs @@ -2,6 +2,7 @@ mod shared; +use sentry_tracing::SpanLayer; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -26,7 +27,7 @@ fn test_fun_record_later() { fn should_update_sentry_op_and_name_based_on_fields() { let transport = shared::init_sentry(1.0); let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .set_default(); for f in [test_fun_record_on_creation, test_fun_record_later] { diff --git a/sentry-tracing/tests/smoke.rs b/sentry-tracing/tests/smoke.rs index 698024ed..63d0948f 100644 --- a/sentry-tracing/tests/smoke.rs +++ b/sentry-tracing/tests/smoke.rs @@ -2,6 +2,7 @@ #![cfg(feature = "logs")] use sentry::protocol::{EnvelopeItem, ItemContainer, LogLevel}; +use sentry_tracing::{LogLayer, SpanLayer}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -13,8 +14,8 @@ fn function_with_tags(value: i32) { #[test] fn should_instrument_function_with_log() { let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) - .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with(LogLayer::new().with_filter(LevelFilter::INFO)) .set_default(); let envelopes = sentry::test::with_captured_envelopes_options( diff --git a/sentry-tracing/tests/span_cross_thread.rs b/sentry-tracing/tests/span_cross_thread.rs index 9243fdd6..ec3141b2 100644 --- a/sentry-tracing/tests/span_cross_thread.rs +++ b/sentry-tracing/tests/span_cross_thread.rs @@ -3,6 +3,7 @@ mod shared; use sentry::protocol::Context; +use sentry_tracing::SpanLayer; use std::thread; use std::time::Duration; use tracing_subscriber::filter::LevelFilter; @@ -12,7 +13,7 @@ use tracing_subscriber::prelude::*; fn cross_thread_span_entries_share_transaction() { let transport = shared::init_sentry(1.0); let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .set_default(); let span = tracing::info_span!("foo"); diff --git a/sentry-tracing/tests/span_reentrancy.rs b/sentry-tracing/tests/span_reentrancy.rs index 13c0639a..2f3f965a 100644 --- a/sentry-tracing/tests/span_reentrancy.rs +++ b/sentry-tracing/tests/span_reentrancy.rs @@ -4,6 +4,7 @@ use sentry::protocol::EnvelopeItem; mod shared; +use sentry_tracing::SpanLayer; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -13,7 +14,7 @@ use tracing_subscriber::prelude::*; fn reentering_span_preserves_parent() { let transport = shared::init_sentry(1.0); let _subscriber = tracing_subscriber::registry() - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) .set_default(); { diff --git a/sentry/examples/tracing-demo.rs b/sentry/examples/tracing-demo.rs index 51639629..0b7cc41b 100644 --- a/sentry/examples/tracing-demo.rs +++ b/sentry/examples/tracing-demo.rs @@ -3,6 +3,7 @@ use std::thread; use std::time::Duration; +use sentry_tracing::{LogLayer, SpanLayer}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -10,8 +11,8 @@ use tracing_subscriber::prelude::*; fn main() { tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) - .with(sentry_tracing::span_layer().with_filter(LevelFilter::INFO)) - .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with(LogLayer::new().with_filter(LevelFilter::INFO)) .try_init() .unwrap(); diff --git a/sentry/tests/test_legacy_tracing.rs b/sentry/tests/test_legacy_tracing.rs index fccf5f98..92fc6e43 100644 --- a/sentry/tests/test_legacy_tracing.rs +++ b/sentry/tests/test_legacy_tracing.rs @@ -4,6 +4,8 @@ use sentry::protocol::{Context, Request, Value}; #[cfg(feature = "logs")] +use sentry_tracing::LogLayer; +#[cfg(feature = "logs")] use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; @@ -315,7 +317,7 @@ fn test_tracing_logs() { #[test] fn test_tracing_log_floating_point_field() { let _dispatcher = tracing_subscriber::registry() - .with(sentry_tracing::log_layer().with_filter(LevelFilter::INFO)) + .with(LogLayer::new().with_filter(LevelFilter::INFO)) .set_default(); let options = sentry::ClientOptions::new(); From 81d511a101da2ca2fa14df602b4aa5a967835fa1 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 7 Oct 2026 15:56:03 +0200 Subject: [PATCH 10/15] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d75bf887..b8a0696d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,11 @@ ### Features -- Added independently selectable `SpanLayer`, `LogLayer` (`logs` feature), `ErrorLayer`, and `BreadcrumbLayer` to `sentry-tracing`. Most applications should use the span and log layers. The new layers do not filter by level internally; configure per-layer filters with `.with_filter(...)` to control capture. +- Added independently selectable `SpanLayer`, `LogLayer`, `ErrorLayer`, and `BreadcrumbLayer` to `sentry-tracing`. Most applications should use the span and log layers. The new layers do not filter by level internally; configure per-layer filters with `.with_filter(...)` to control capture ([#1342](https://github.com/getsentry/sentry-rust/pull/1342)). ### Deprecations -- Deprecated `sentry_tracing::layer()` and `SentryLayer`, along with their associated filtering/mapping types and helpers, in favor of the individual layers. Legacy filtering defaults are unchanged. See the [migration guide](sentry-tracing/src/layer/mod.rs) for the equivalent four-layer configuration and custom-mapper differences. +- Deprecated `sentry_tracing::layer()` and `SentryLayer`, along with their associated filtering/mapping types and helpers, in favor of the individual layers. Legacy filtering defaults are unchanged. See the [migration guide](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.SentryLayer.html) ([#1342](https://github.com/getsentry/sentry-rust/pull/1342)). ## 0.49.3 From 927c0312e5b8771862ae99d36524df845f519700 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 7 Oct 2026 16:05:08 +0200 Subject: [PATCH 11/15] fix ci --- sentry-tracing/src/layer/breadcrumb.rs | 2 +- sentry-tracing/src/layer/error.rs | 2 +- sentry-tracing/src/layer/log.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index cedf0422..61759d9a 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -151,7 +151,7 @@ where /// A mapper function which converts a tracing event to a [`Breadcrumb`]. /// /// This advanced API fully customizes whether and how `tracing` events become Sentry breadcrumbs. -/// A mapper can call [`breadcrumb_from_event`](crate::breadcrumb_from_event) to perform the conversion +/// A mapper can call [`breadcrumb_from_event`] to perform the conversion /// alongside its custom filtering or mapping logic. /// /// The function can also return [`None`], in which case, no breadcrumb is created from the diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index c025d84b..13e4b3cc 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -149,7 +149,7 @@ where /// A mapper function which converts a tracing event to a Sentry error event. /// /// This advanced API fully customizes whether and how `tracing` events become Sentry error events. -/// A mapper can call [`event_from_event`](crate::event_from_event) to perform the conversion +/// A mapper can call [`event_from_event`] to perform the conversion /// alongside its custom filtering or mapping logic. /// /// The function can also return [`None`], in which case, no error event is created from the diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 16e3e23d..ca7af085 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -143,7 +143,7 @@ where /// A mapper function which converts a tracing event to a [`Log`]. /// /// This advanced API fully customizes whether and how `tracing` events become Sentry logs. -/// A mapper can call [`log_from_event`](crate::log_from_event) to perform the conversion +/// A mapper can call [`log_from_event`] to perform the conversion /// alongside its custom filtering or mapping logic. /// /// The function can also return [`None`], in which case, no log is created from the tracing event. From cf30f2d12a8c2adaa2c0053282023a1e3f163aef Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 7 Oct 2026 16:24:51 +0200 Subject: [PATCH 12/15] indicate unsupported mixing of legacy layer --- sentry-tracing/src/layer/breadcrumb.rs | 3 +++ sentry-tracing/src/layer/error.rs | 3 +++ sentry-tracing/src/layer/log.rs | 3 +++ sentry-tracing/src/layer/mod.rs | 4 ++++ sentry-tracing/src/layer/span.rs | 4 ++++ 5 files changed, 17 insertions(+) diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index 61759d9a..82265e3a 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -9,6 +9,9 @@ use crate::converters::breadcrumb_from_event; /// /// Without a filter, events at every level become breadcrumbs. Configure a filter to limit /// breadcrumb volume; see [`BreadcrumbLayer::new`] for the recommended levels. +/// +/// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. +/// Use either the legacy layer or the individual layers, not both. pub struct BreadcrumbLayer { with_span_attributes: bool, event_mapper: Option>>, diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index 13e4b3cc..28ad5c0b 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -6,6 +6,9 @@ use tracing_subscriber::registry::LookupSpan; use crate::converters::event_from_event; /// Captures tracing events as Sentry error events. +/// +/// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. +/// Use either the legacy layer or the individual layers, not both. pub struct ErrorLayer { with_span_attributes: bool, event_mapper: Option>>, diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index ca7af085..55e72bfb 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -11,6 +11,9 @@ use crate::converters::log_from_event; /// level filter. /// /// Requires a client with logs enabled. +/// +/// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. +/// Use either the legacy layer or the individual layers, not both. #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] pub struct LogLayer { with_span_attributes: bool, diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index 2921cda8..37e0161d 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -127,6 +127,10 @@ type EventMapper = Box) -> EventMapping + Send /// Add [`ErrorLayer`] if tracing events should also be captured as Sentry error events, and /// [`BreadcrumbLayer`] if errors need breadcrumb context. /// +/// Mixing this legacy layer with any individual Sentry layer is unsupported. +/// **Combining it with [`SpanLayer`] can panic when a span is created.** When migrating, +/// replace the legacy layer rather than adding individual layers alongside it. +/// /// # Migrating the default configuration /// /// The individual layers do not filter by level internally. The following configuration diff --git a/sentry-tracing/src/layer/span.rs b/sentry-tracing/src/layer/span.rs index d31d9db8..2440474a 100644 --- a/sentry-tracing/src/layer/span.rs +++ b/sentry-tracing/src/layer/span.rs @@ -19,6 +19,10 @@ use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX} /// Install this layer to capture spans; event layers do not manage the span lifecycle. /// Without a filter, this layer captures spans at every level. Configure a filter to limit span /// volume; see [`SpanLayer::new`] for the recommended levels. +/// +/// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. +/// **Combining the two layers can panic when a span is created.** Use either the legacy layer +/// or the individual layers, not both. #[derive(Default)] #[non_exhaustive] pub struct SpanLayer {} From 863a1c821058cd0906b69c4d5e65860307b3a10d Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 7 Oct 2026 16:28:33 +0200 Subject: [PATCH 13/15] must_use --- sentry-tracing/src/layer/breadcrumb.rs | 3 +-- sentry-tracing/src/layer/error.rs | 3 +-- sentry-tracing/src/layer/log.rs | 2 +- sentry-tracing/src/layer/span.rs | 1 + 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index 82265e3a..d2b6ba17 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -12,6 +12,7 @@ use crate::converters::breadcrumb_from_event; /// /// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. /// Use either the legacy layer or the individual layers, not both. +#[must_use] pub struct BreadcrumbLayer { with_span_attributes: bool, event_mapper: Option>>, @@ -97,14 +98,12 @@ impl BreadcrumbLayer { /// any spans to Sentry. /// /// This option has no effect when [`Self::mapper`] is set. - #[must_use] pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } /// Sets a custom mapping from the tracing event to the breadcrumb object. - #[must_use] pub fn mapper(mut self, mapper: M) -> Self where M: EventToBreadcrumbMapper + 'static, diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index 28ad5c0b..f07ad8fd 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -9,6 +9,7 @@ use crate::converters::event_from_event; /// /// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. /// Use either the legacy layer or the individual layers, not both. +#[must_use] pub struct ErrorLayer { with_span_attributes: bool, event_mapper: Option>>, @@ -95,14 +96,12 @@ impl ErrorLayer { /// any spans to Sentry. /// /// This option has no effect when [`Self::mapper`] is set. - #[must_use] pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } /// Sets a custom mapping from the tracing event to the Sentry error event object. - #[must_use] pub fn mapper(mut self, mapper: M) -> Self where M: EventToErrorMapper + 'static, diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 55e72bfb..0a695193 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -15,6 +15,7 @@ use crate::converters::log_from_event; /// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. /// Use either the legacy layer or the individual layers, not both. #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] +#[must_use] pub struct LogLayer { with_span_attributes: bool, event_mapper: Option>>, @@ -81,7 +82,6 @@ impl LogLayer { /// spans to Sentry. /// /// This option has no effect when [`Self::mapper`] is set. - #[must_use] pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self diff --git a/sentry-tracing/src/layer/span.rs b/sentry-tracing/src/layer/span.rs index 2440474a..fb05196f 100644 --- a/sentry-tracing/src/layer/span.rs +++ b/sentry-tracing/src/layer/span.rs @@ -25,6 +25,7 @@ use crate::{SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD, TAGS_PREFIX} /// or the individual layers, not both. #[derive(Default)] #[non_exhaustive] +#[must_use] pub struct SpanLayer {} impl SpanLayer { From cc530e51ae02f467920810df23ce856550b76b84 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 12 Oct 2026 21:46:59 +0200 Subject: [PATCH 14/15] no more mapper, update docs and changelog --- CHANGELOG.md | 5 +- sentry-tracing/src/layer/breadcrumb.rs | 73 +++++--------------------- sentry-tracing/src/layer/error.rs | 71 ++++--------------------- sentry-tracing/src/layer/log.rs | 67 +++++------------------ sentry-tracing/src/layer/mod.rs | 26 ++++----- 5 files changed, 53 insertions(+), 189 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a0696d..8f3ff2a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,12 @@ ### Features -- Added independently selectable `SpanLayer`, `LogLayer`, `ErrorLayer`, and `BreadcrumbLayer` to `sentry-tracing`. Most applications should use the span and log layers. The new layers do not filter by level internally; configure per-layer filters with `.with_filter(...)` to control capture ([#1342](https://github.com/getsentry/sentry-rust/pull/1342)). +- Redesigned [`sentry-tracing`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/) around independently selectable [`SpanLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.SpanLayer.html), [`LogLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.LogLayer.html), [`ErrorLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.ErrorLayer.html), and [`BreadcrumbLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.BreadcrumbLayer.html). Most applications should use the span and log layers ([#1342](https://github.com/getsentry/sentry-rust/pull/1342)). ### Deprecations -- Deprecated `sentry_tracing::layer()` and `SentryLayer`, along with their associated filtering/mapping types and helpers, in favor of the individual layers. Legacy filtering defaults are unchanged. See the [migration guide](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.SentryLayer.html) ([#1342](https://github.com/getsentry/sentry-rust/pull/1342)). +- Deprecated [`sentry_tracing::layer()`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/fn.layer.html) and [`SentryLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.SentryLayer.html), along with their associated filtering and mapping types and helpers, in favor of independently selectable [`SpanLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.SpanLayer.html), [`LogLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.LogLayer.html), [`ErrorLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.ErrorLayer.html), and [`BreadcrumbLayer`](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.BreadcrumbLayer.html). A [migration guide](https://docs.rs/sentry-tracing/0.49.4/sentry_tracing/struct.SentryLayer.html) is available ([#1342](https://github.com/getsentry/sentry-rust/pull/1342)). + - As this is a larger change, we would love to hear your feedback about the new API if you have trouble migrating. Please [open a GitHub issue](https://github.com/getsentry/sentry-rust/issues/new?template=02-improvement.yml), and we will help identify a solution. ## 0.49.3 diff --git a/sentry-tracing/src/layer/breadcrumb.rs b/sentry-tracing/src/layer/breadcrumb.rs index d2b6ba17..a4f2fd07 100644 --- a/sentry-tracing/src/layer/breadcrumb.rs +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -13,12 +13,12 @@ use crate::converters::breadcrumb_from_event; /// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. /// Use either the legacy layer or the individual layers, not both. #[must_use] -pub struct BreadcrumbLayer { +#[derive(Default)] +pub struct BreadcrumbLayer { with_span_attributes: bool, - event_mapper: Option>>, } -impl BreadcrumbLayer { +impl BreadcrumbLayer { /// Creates a layer that adds tracing events as Sentry breadcrumbs. /// /// For most users, we would recommend using the [`LogLayer`](super::LogLayer) instead of this @@ -97,51 +97,31 @@ impl BreadcrumbLayer { /// Including all spans in the breadcrumb layer filter is safe, because this layer does not send /// any spans to Sentry. /// - /// This option has no effect when [`Self::mapper`] is set. pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } - /// Sets a custom mapping from the tracing event to the breadcrumb object. - pub fn mapper(mut self, mapper: M) -> Self - where - M: EventToBreadcrumbMapper + 'static, - { - self.event_mapper = Some(Box::from(mapper)); - self - } - /// Add a breadcrumb to the current `Hub`'s scope. pub(super) fn capture(&self, breadcrumb: Breadcrumb) { sentry_core::add_breadcrumb(breadcrumb); } } -impl BreadcrumbLayer -where - S: Subscriber + for<'a> LookupSpan<'a>, -{ +impl BreadcrumbLayer { /// Capture a tracing event as a breadcrumb. - pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) { - if let Some(breadcrumb) = self.event_to_breadcrumb(event, ctx) { - self.capture(breadcrumb); - } - } - - /// Convert a tracing event to a breadcrumb, using the custom mapper if one is set. - fn event_to_breadcrumb(&self, event: &Event, ctx: Context<'_, S>) -> Option { - match &self.event_mapper { - Some(mapper) => mapper(event, ctx), - None => Some(breadcrumb_from_event( - event, - self.with_span_attributes.then_some(&ctx), - )), - } + pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + self.capture(breadcrumb_from_event( + event, + self.with_span_attributes.then_some(&ctx), + )); } } -impl Layer for BreadcrumbLayer +impl Layer for BreadcrumbLayer where S: Subscriber + for<'a> LookupSpan<'a>, { @@ -149,30 +129,3 @@ where self.capture_tracing_event(event, ctx); } } - -/// A mapper function which converts a tracing event to a [`Breadcrumb`]. -/// -/// This advanced API fully customizes whether and how `tracing` events become Sentry breadcrumbs. -/// A mapper can call [`breadcrumb_from_event`] to perform the conversion -/// alongside its custom filtering or mapping logic. -/// -/// The function can also return [`None`], in which case, no breadcrumb is created from the -/// tracing event. -pub trait EventToBreadcrumbMapper: - Fn(&Event, Context<'_, S>) -> Option + Send + Sync -{ -} - -impl EventToBreadcrumbMapper for F where - F: Fn(&Event, Context<'_, S>) -> Option + Send + Sync -{ -} - -impl Default for BreadcrumbLayer { - fn default() -> Self { - Self { - with_span_attributes: false, - event_mapper: None, - } - } -} diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index f07ad8fd..488f9935 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -10,12 +10,12 @@ use crate::converters::event_from_event; /// Using this layer alongside the legacy [`SentryLayer`](super::SentryLayer) is unsupported. /// Use either the legacy layer or the individual layers, not both. #[must_use] -pub struct ErrorLayer { +#[derive(Default)] +pub struct ErrorLayer { with_span_attributes: bool, - event_mapper: Option>>, } -impl ErrorLayer { +impl ErrorLayer { /// Creates a layer that captures tracing events as Sentry error events. /// /// Most applications should start with [`LogLayer`](super::LogLayer), and add this layer if @@ -95,51 +95,27 @@ impl ErrorLayer { /// Including all spans in the error layer filter is safe, because this layer does not send /// any spans to Sentry. /// - /// This option has no effect when [`Self::mapper`] is set. pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } - /// Sets a custom mapping from the tracing event to the Sentry error event object. - pub fn mapper(mut self, mapper: M) -> Self - where - M: EventToErrorMapper + 'static, - { - self.event_mapper = Some(Box::from(mapper)); - self - } - /// Capture an error event on the current `Hub`. pub(super) fn capture(&self, event: SentryEvent<'static>) { sentry_core::capture_event(event); } -} -impl ErrorLayer -where - S: Subscriber + for<'a> LookupSpan<'a>, -{ /// Capture a tracing event as a Sentry error event. - pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) { - if let Some(sentry_event) = self.event_to_error(event, ctx) { - self.capture(sentry_event); - } - } - - /// Convert a tracing event to a Sentry error event, using the custom mapper if one is set. - fn event_to_error(&self, event: &Event, ctx: Context<'_, S>) -> Option> { - match &self.event_mapper { - Some(mapper) => mapper(event, ctx), - None => Some(event_from_event( - event, - self.with_span_attributes.then_some(&ctx), - )), - } + pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + let sentry_event = event_from_event(event, self.with_span_attributes.then_some(&ctx)); + self.capture(sentry_event); } } -impl Layer for ErrorLayer +impl Layer for ErrorLayer where S: Subscriber + for<'a> LookupSpan<'a>, { @@ -147,30 +123,3 @@ where self.capture_tracing_event(event, ctx); } } - -/// A mapper function which converts a tracing event to a Sentry error event. -/// -/// This advanced API fully customizes whether and how `tracing` events become Sentry error events. -/// A mapper can call [`event_from_event`] to perform the conversion -/// alongside its custom filtering or mapping logic. -/// -/// The function can also return [`None`], in which case, no error event is created from the -/// tracing event. -pub trait EventToErrorMapper: - Fn(&Event, Context<'_, S>) -> Option> + Send + Sync -{ -} - -impl EventToErrorMapper for F where - F: Fn(&Event, Context<'_, S>) -> Option> + Send + Sync -{ -} - -impl Default for ErrorLayer { - fn default() -> Self { - Self { - with_span_attributes: false, - event_mapper: None, - } - } -} diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 0a695193..0a5e65b5 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -16,12 +16,12 @@ use crate::converters::log_from_event; /// Use either the legacy layer or the individual layers, not both. #[cfg_attr(doc_cfg, doc(cfg(feature = "logs")))] #[must_use] -pub struct LogLayer { +#[derive(Default)] +pub struct LogLayer { with_span_attributes: bool, - event_mapper: Option>>, } -impl LogLayer { +impl LogLayer { /// Creates a layer that captures Sentry logs from tracing events. /// /// # Filtering @@ -81,21 +81,11 @@ impl LogLayer { /// Including all spans in the log layer filter is safe, because this layer does not send any /// spans to Sentry. /// - /// This option has no effect when [`Self::mapper`] is set. pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self } - /// Sets a custom mapping from the tracing event to the log object. - pub fn mapper(mut self, mapper: M) -> Self - where - M: EventToLogMapper + 'static, - { - self.event_mapper = Some(Box::from(mapper)); - self - } - /// Capture a log on the current `Hub`. pub(super) fn capture(&self, log: Log) { sentry_core::Hub::with_active(|hub| { @@ -111,30 +101,20 @@ impl LogLayer { } } -impl LogLayer -where - S: Subscriber + for<'a> LookupSpan<'a>, -{ +impl LogLayer { /// Capture a tracing event as a log. - pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) { - if let Some(log) = self.event_to_log(event, ctx) { - self.capture(log); - } - } - - /// Convert the tracing event to a Sentry log. - fn event_to_log(&self, event: &Event, ctx: Context<'_, S>) -> Option { - match &self.event_mapper { - Some(mapper) => mapper(event, ctx), - None => Some(log_from_event( - event, - self.with_span_attributes.then_some(&ctx), - )), - } + pub(super) fn capture_tracing_event(&self, event: &Event, ctx: Context<'_, S>) + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + self.capture(log_from_event( + event, + self.with_span_attributes.then_some(&ctx), + )); } } -impl Layer for LogLayer +impl Layer for LogLayer where S: Subscriber + for<'a> LookupSpan<'a>, { @@ -142,24 +122,3 @@ where self.capture_tracing_event(event, ctx); } } - -/// A mapper function which converts a tracing event to a [`Log`]. -/// -/// This advanced API fully customizes whether and how `tracing` events become Sentry logs. -/// A mapper can call [`log_from_event`] to perform the conversion -/// alongside its custom filtering or mapping logic. -/// -/// The function can also return [`None`], in which case, no log is created from the tracing event. -pub trait EventToLogMapper: Fn(&Event, Context<'_, S>) -> Option + Send + Sync {} - -impl EventToLogMapper for F where F: Fn(&Event, Context<'_, S>) -> Option + Send + Sync -{} - -impl Default for LogLayer { - fn default() -> Self { - Self { - with_span_attributes: false, - event_mapper: None, - } - } -} diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index 37e0161d..986b7726 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -11,10 +11,10 @@ mod log; mod span; mod span_guard_stack; -pub use breadcrumb::{BreadcrumbLayer, EventToBreadcrumbMapper}; -pub use error::{ErrorLayer, EventToErrorMapper}; +pub use breadcrumb::BreadcrumbLayer; +pub use error::ErrorLayer; #[cfg(feature = "logs")] -pub use log::{EventToLogMapper, LogLayer}; +pub use log::LogLayer; pub(super) use span::SentrySpanData; pub use span::SpanLayer; @@ -177,17 +177,19 @@ type EventMapper = Box) -> EventMapping + Send /// - Replace [`span_filter`](Self::span_filter) with a filter on the span layer. /// - Split [`event_filter`](Self::event_filter) into filters on the individual event layers. An /// event can be accepted by multiple layers, replacing combined [`EventFilter`] flags. -/// - Replace [`event_mapper`](Self::event_mapper) with each event layer's `mapper()` callback. -/// Unlike the legacy mapper, these callbacks do not override filters: filters run first and -/// can prevent a mapper from receiving an event. Keep value-dependent routing in the mapper. -/// - Each mapper returns at most one item of its telemetry type. There is no direct equivalent -/// for a legacy [`EventMapping::Combined`] that emits multiple items of the same type or changes -/// their capture order per event. +/// - Individual layers do not provide mapper callbacks. Some [`SentryLayer::event_mapper`] usages +/// may be addressed by other existing APIs, such as filter callbacks on the layers or +/// `before_send*` callbacks provided by the Sentry SDK. However, we recognize it may not be +/// possible to migrate all existing usages. If you are unable to migrate to the new API due to +/// this change, we would love to hear from you. Please [open a GitHub issue] explaining your +/// use case, and we will work with you to find a solution. /// /// If a custom configuration sends the same event to both the breadcrumb and error layers, /// install the breadcrumb layer before the error layer to include that breadcrumb in the error. /// The default configuration above excludes `ERROR` events from breadcrumbs, so the order does /// not affect their inclusion. +/// +/// [open a GitHub issue]: https://github.com/getsentry/sentry-rust/issues/new?template=02-improvement.yml #[deprecated( note = "Prefer SpanLayer::new() and LogLayer::new(); see SentryLayer docs for migration" )] @@ -197,10 +199,10 @@ pub struct SentryLayer { event_mapper: Option>, span_filter: Box bool + Send + Sync>, span_layer: SpanLayer, - breadcrumb: BreadcrumbLayer, - error: ErrorLayer, + breadcrumb: BreadcrumbLayer, + error: ErrorLayer, #[cfg(feature = "logs")] - log: LogLayer, + log: LogLayer, } #[expect(deprecated, reason = "implementing the legacy layer")] From 6de850c9c1417e0543ed268fb132374b31842474 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 13 Oct 2026 13:59:02 +0200 Subject: [PATCH 15/15] make recommended enable_span_attributes filters simpler --- sentry-tracing/src/layer/error.rs | 8 ++------ sentry-tracing/src/layer/log.rs | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs index 488f9935..59e571f0 100644 --- a/sentry-tracing/src/layer/error.rs +++ b/sentry-tracing/src/layer/error.rs @@ -77,24 +77,20 @@ impl ErrorLayer { /// # use tracing_subscriber::prelude::*; /// use sentry::integrations::tracing::{ErrorLayer, SpanLayer}; /// use tracing::Level; - /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; + /// use tracing_subscriber::filter::{filter_fn, LevelFilter, FilterExt as _}; /// /// tracing_subscriber::registry() /// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) /// .with( /// ErrorLayer::new() /// .enable_span_attributes() - /// .with_filter(filter_fn(|metadata| { - /// // Accept all spans, but only `ERROR` events. - /// metadata.is_span() || *metadata.level() <= Level::ERROR - /// })), + /// .with_filter(LevelFilter::ERROR.or(filter_fn(|m| m.is_span()))), /// ) /// .init(); /// ``` /// /// Including all spans in the error layer filter is safe, because this layer does not send /// any spans to Sentry. - /// pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs index 0a5e65b5..c9d80485 100644 --- a/sentry-tracing/src/layer/log.rs +++ b/sentry-tracing/src/layer/log.rs @@ -54,33 +54,32 @@ impl LogLayer { /// We can only include spans captured by a [`super::SpanLayer`] on the same subscriber. So, /// this option only has any effect when the `SpanLayer` is installed. /// - /// Furthermore, only spans accepted by the log layer's filter (see - /// [`tracing_subscriber::layer`]) are considered when attaching - /// attributes. When using this option, therefore, we recommend building a custom filter that - /// accepts all spans, while filtering events to the desired level, like so: + /// # Filtering + /// + /// Parent-span attributes are included only when the span is accepted by both `SpanLayer` + /// and `LogLayer`. + /// + /// If `LogLayer` uses a stricter level filter, attributes from less severe spans are omitted. + /// For example, this captures logs at `WARN` and more severe levels while allowing attributes + /// from `INFO` parent spans to appear on those logs: /// /// ```rust - /// # use tracing_subscriber::prelude::*; /// use sentry::integrations::tracing::{LogLayer, SpanLayer}; - /// use tracing::Level; - /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; + /// use tracing_subscriber::prelude::*; + /// use tracing_subscriber::filter::{filter_fn, FilterExt as _, LevelFilter}; /// /// tracing_subscriber::registry() /// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) /// .with( /// LogLayer::new() /// .enable_span_attributes() - /// .with_filter(filter_fn(|metadata| { - /// // Accept all spans, but only events at `INFO` and more severe. - /// metadata.is_span() || *metadata.level() <= Level::INFO - /// })), + /// .with_filter(LevelFilter::WARN.or(filter_fn(|m| m.is_span()))), /// ) /// .init(); /// ``` /// - /// Including all spans in the log layer filter is safe, because this layer does not send any - /// spans to Sentry. - /// + /// No filter adjustment is needed when both layers use the same level filter, such as the + /// `INFO` level recommended for most applications. pub fn enable_span_attributes(mut self) -> Self { self.with_span_attributes = true; self