diff --git a/CHANGELOG.md b/CHANGELOG.md index 8094ca51..8f3ff2a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased + +### Features + +- 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()`](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 ### Features 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/README.md b/sentry-tracing/README.md index 1b5f3cab..ad902e2a 100644 --- a/sentry-tracing/README.md +++ b/sentry-tracing/README.md @@ -6,231 +6,82 @@ # 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), -); +- [`SpanLayer`] captures `tracing` spans as Sentry transactions and spans. +- [`LogLayer`] captures `tracing` events as Sentry structured 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(); -``` +We also provide the following layers, but recommend the span and log layers above for most +applications: -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. - -```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::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(); -``` +- [`ErrorLayer`] captures `tracing` events as Sentry error events, with support for tags and + exception information. +- [`BreadcrumbLayer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. -In addition, a custom event mapper can be provided, to fully customize if and how `tracing` events are converted to Sentry data. +For migration from the legacy combined layer, see [`SentryLayer`]. -Note that if both an event mapper and event filter are set, the mapper takes precedence, thus the -filter has no effect. +## Getting started -## 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. +Enable the `tracing` and `logs` features on `sentry` to use its tracing integration re-export. +Set a traces sample rate to capture transactions, then install the span and log layers with +explicit level filters: ```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", -); -``` - -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 sentry::integrations::tracing::{LogLayer, SpanLayer}; +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::Log, - tracing::Level::TRACE => EventFilter::Ignore, - _ => EventFilter::Log, - }) - .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); +let _guard = sentry::init( + sentry::ClientOptions::new() + // Use a lower sample rate in production. + .traces_sample_rate(1.0), +); tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .with(sentry_layer) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with(LogLayer::new().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. +`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. -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 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( @@ -243,6 +94,15 @@ async fn handle_request(headers: std::collections::HashMap) { } ``` +## Logs + +`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"); +``` + ## Resources License: MIT diff --git a/sentry-tracing/src/converters.rs b/sentry-tracing/src/converters.rs index 1fbe36ab..6b3f991b 100644 --- a/sentry-tracing/src/converters.rs +++ b/sentry-tracing/src/converters.rs @@ -350,7 +350,11 @@ where } /// Creates a [`Log`] from a given [`tracing_core::Event`] +/// +/// 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")] 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..a4f2fd07 --- /dev/null +++ b/sentry-tracing/src/layer/breadcrumb.rs @@ -0,0 +1,131 @@ +use sentry_core::Breadcrumb; +use tracing_core::{Event, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; +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 [`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. +#[must_use] +#[derive(Default)] +pub struct BreadcrumbLayer { + with_span_attributes: bool, +} + +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, + /// this option only has any effect when the `SpanLayer` is installed. + /// + /// 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::{BreadcrumbLayer, SpanLayer}; + /// use tracing::Level; + /// use tracing_subscriber::filter::{filter_fn, LevelFilter}; + /// + /// tracing_subscriber::registry() + /// .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + /// .with( + /// BreadcrumbLayer::new() + /// .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. + /// + pub fn enable_span_attributes(mut self) -> Self { + self.with_span_attributes = true; + self + } + + /// Add a breadcrumb to the current `Hub`'s scope. + pub(super) fn capture(&self, breadcrumb: Breadcrumb) { + sentry_core::add_breadcrumb(breadcrumb); + } +} + +impl BreadcrumbLayer { + /// Capture a tracing event as a breadcrumb. + 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 +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + self.capture_tracing_event(event, ctx); + } +} diff --git a/sentry-tracing/src/layer/error.rs b/sentry-tracing/src/layer/error.rs new file mode 100644 index 00000000..59e571f0 --- /dev/null +++ b/sentry-tracing/src/layer/error.rs @@ -0,0 +1,121 @@ +use sentry_core::protocol::Event as SentryEvent; +use tracing_core::{Event, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; +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. +#[must_use] +#[derive(Default)] +pub struct ErrorLayer { + with_span_attributes: bool, +} + +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, + /// this option only has any effect when the `SpanLayer` is installed. + /// + /// 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::{ErrorLayer, SpanLayer}; + /// use tracing::Level; + /// 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(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 + } + + /// Capture an error event on the current `Hub`. + pub(super) fn capture(&self, event: SentryEvent<'static>) { + sentry_core::capture_event(event); + } + + /// Capture a tracing event as a Sentry error event. + 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 +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + self.capture_tracing_event(event, ctx); + } +} diff --git a/sentry-tracing/src/layer/log.rs b/sentry-tracing/src/layer/log.rs new file mode 100644 index 00000000..c9d80485 --- /dev/null +++ b/sentry-tracing/src/layer/log.rs @@ -0,0 +1,123 @@ +use sentry_core::protocol::Log; +use tracing_core::{Event, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; +use tracing_subscriber::registry::LookupSpan; + +use crate::converters::log_from_event; + +/// Captures tracing events as Sentry logs. +/// +/// Captures all event levels unless a filter is configured; see [`LogLayer::new`] for the recommended +/// 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")))] +#[must_use] +#[derive(Default)] +pub struct LogLayer { + with_span_attributes: bool, +} + +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, + /// this option only has any effect when the `SpanLayer` is installed. + /// + /// # 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 sentry::integrations::tracing::{LogLayer, SpanLayer}; + /// 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(LevelFilter::WARN.or(filter_fn(|m| m.is_span()))), + /// ) + /// .init(); + /// ``` + /// + /// 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 + } + + /// 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| { + let options = client.options(); + #[expect(deprecated, reason = "checking a deprecated field")] + options.enable_logs + }); + if enabled { + hub.capture_log(log); + } + }); + } +} + +impl LogLayer { + /// Capture a tracing event as a log. + 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 +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_event(&self, event: &Event, ctx: Context<'_, S>) { + self.capture_tracing_event(event, ctx); + } +} diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index fb561d23..986b7726 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -1,41 +1,50 @@ -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; -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; +pub use breadcrumb::BreadcrumbLayer; +pub use error::ErrorLayer; +#[cfg(feature = "logs")] +pub use log::LogLayer; +pub(super) use span::SentrySpanData; +pub use span::SpanLayer; + +/// 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 { @@ -47,16 +56,20 @@ 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. + #[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 { @@ -66,16 +79,20 @@ impl From for CombinedEventMapping { } } +#[expect(deprecated)] impl From> for CombinedEventMapping { fn from(value: Vec) -> Self { Self(value) } } -/// 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. +/// 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,10 +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 +/// 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(), @@ -101,18 +118,94 @@ pub fn default_span_filter(metadata: &Metadata) -> bool { ) } +#[expect(deprecated)] type EventMapper = Box) -> EventMapping + Send + Sync>; -/// Provides a tracing layer that dispatches events to sentry +/// Legacy combined tracing layer. +/// +/// 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. +/// +/// 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 +/// 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 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(SpanLayer::new().with_filter(LevelFilter::INFO)) +/// .with( +/// BreadcrumbLayer::new().with_filter(filter_fn(|metadata| { +/// matches!(*metadata.level(), Level::WARN | Level::INFO) +/// })), +/// ) +/// .with(ErrorLayer::new().with_filter(LevelFilter::ERROR)) +/// .with(LogLayer::new().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. +/// - 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" +)] pub struct SentryLayer { + #[expect(deprecated)] 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. /// @@ -155,20 +248,26 @@ impl SentryLayer { 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. + /// + /// Only spans accepted by this layer's span filter provide attributes. Including attributes + /// does not require the transaction to be sampled. /// - /// 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. + /// 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.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 +276,98 @@ where Self { event_filter: Box::new(default_event_filter), event_mapper: None, - span_filter: Box::new(default_span_filter), - - with_span_attributes: false, + span_layer: SpanLayer::new(), + breadcrumb: BreadcrumbLayer::new(), + error: ErrorLayer::new(), + #[cfg(feature = "logs")] + log: LogLayer::new(), } } } -#[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 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 { + let filter = (self.event_filter)(event.metadata()); + if filter.contains(EventFilter::Breadcrumb) { + self.breadcrumb.capture_tracing_event(event, ctx.clone()); + } + if filter.contains(EventFilter::Event) { + self.error.capture_tracing_event(event, ctx.clone()); + } + #[cfg(feature = "logs")] + if filter.contains(EventFilter::Log) { + self.log.capture_tracing_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." - ); - } - - /// 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(); - } - - /// 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); + fn on_new_span( + &self, + attrs: &tracing_span::Attributes<'_>, + id: &tracing_span::Id, + ctx: Context<'_, S>, + ) { + if (self.span_filter)(attrs.metadata()) { + self.span_layer.on_new_span(attrs, id, ctx); } - - record_fields(span, data.json_values); } -} -fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) { - span.set_data("sentry.tracing.target", metadata.target().into()); + fn on_enter(&self, id: &tracing_span::Id, ctx: Context<'_, S>) { + self.span_layer.on_enter(id, ctx); + } - if let Some(module) = metadata.module_path() { - span.set_data("code.module.name", module.into()); + fn on_exit(&self, id: &tracing_span::Id, ctx: Context<'_, S>) { + self.span_layer.on_exit(id, ctx); } - if let Some(file) = metadata.file() { - span.set_data("code.file.path", file.into()); + fn on_close(&self, id: tracing_span::Id, ctx: Context<'_, S>) { + self.span_layer.on_close(id, ctx); } - 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. +/// +/// 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 SpanLayer::new() and LogLayer::new(); see SentryLayer docs for migration" +)] +#[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..fb05196f --- /dev/null +++ b/sentry-tracing/src/layer/span.rs @@ -0,0 +1,360 @@ +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. +/// +/// 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] +#[must_use] +pub struct 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)] +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, 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, + }; + + 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()), + "[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." + ); + } + + /// 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/src/lib.rs b/sentry-tracing/src/lib.rs index 89c8a02b..012c0f87 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -1,228 +1,82 @@ -//! 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. //! -//! ``` -//! 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), -//! ); +//! - [`SpanLayer`] captures `tracing` spans as Sentry transactions and spans. +//! - [`LogLayer`] captures `tracing` events as Sentry structured 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(); -//! ``` +//! We also provide the following layers, but recommend the span and log layers above for most +//! applications: //! -//! 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. -//! -//! ``` -//! 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(); -//! ``` +//! - [`ErrorLayer`] captures `tracing` events as Sentry error events, with support for tags and +//! exception information. +//! - [`BreadcrumbLayer`] adds `tracing` events as breadcrumbs on subsequent Sentry error events. //! -//! In addition, a custom event mapper can be provided, to fully customize if and how `tracing` events are converted to Sentry data. +//! For migration from the legacy combined layer, see [`SentryLayer`]. //! -//! Note that if both an event mapper and event filter are set, the mapper takes precedence, thus the -//! filter has no effect. +//! # Getting started //! -//! # 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. -//! -//! ``` -//! 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. +//! Enable the `tracing` and `logs` features on `sentry` to use its tracing integration re-export. +//! Set a traces sample rate to capture transactions, then install the span and log layers with +//! explicit level filters: //! //! ``` -//! // 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."` -//! -//! ``` -//! 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. -//! -//! ``` -//! 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. -//! -//! ``` -//! 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: -//! -//! ``` -//! use sentry::integrations::tracing::EventFilter; +//! # #[cfg(feature = "logs")] +//! # { +//! use sentry::integrations::tracing::{LogLayer, SpanLayer}; +//! 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::Log, -//! tracing::Level::TRACE => EventFilter::Ignore, -//! _ => EventFilter::Log, -//! }) -//! .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); +//! let _guard = sentry::init( +//! sentry::ClientOptions::new() +//! // Use a lower sample rate in production. +//! .traces_sample_rate(1.0), +//! ); //! //! tracing_subscriber::registry() -//! .with(tracing_subscriber::fmt::layer()) -//! .with(sentry_layer) +//! .with(SpanLayer::new().with_filter(LevelFilter::INFO)) +//! .with(LogLayer::new().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. +//! `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. //! -//! 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: //! //! ``` -//! 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 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( @@ -234,7 +88,17 @@ //! // ... //! } //! ``` +//! +//! # Logs +//! +//! `LogLayer` sends events accepted by its filter as structured logs. Event fields become log +//! attributes. +//! +//! ``` +//! tracing::info!(number = 42, my.key = "value", "Processed 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")] 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/error_breadcrumbs.rs b/sentry-tracing/tests/error_breadcrumbs.rs new file mode 100644 index 00000000..c37d1824 --- /dev/null +++ b/sentry-tracing/tests/error_breadcrumbs.rs @@ -0,0 +1,76 @@ +//! 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::*; + +#[test] +fn error_and_breadcrumb_layers_capture_unsampled_span_context() { + let _subscriber = tracing_subscriber::registry() + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with( + BreadcrumbLayer::new() + .enable_span_attributes() + .with_filter(filter_fn(|metadata| { + metadata.is_span() || matches!(*metadata.level(), Level::INFO | Level::WARN) + })), + ) + .with( + ErrorLayer::new() + .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:?}"), + } +} diff --git a/sentry-tracing/tests/future_cross_thread_info_span.rs b/sentry-tracing/tests/future_cross_thread_info_span.rs index 339d0817..3fcd0755 100644 --- a/sentry-tracing/tests/future_cross_thread_info_span.rs +++ b/sentry-tracing/tests/future_cross_thread_info_span.rs @@ -6,20 +6,26 @@ mod shared; mod transaction_assertions; use sentry::{Hub, HubSwitchGuard}; +use sentry_tracing::SpanLayer; +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. /// /// 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"; let _guard = HubSwitchGuard::new(Hub::new_from_top(Hub::current()).into()); let transport = shared::init_sentry(1.0); + let _subscriber = tracing_subscriber::registry() + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .set_default(); let span = tracing::info_span!(SPAN_NAME); @@ -35,7 +41,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." diff --git a/sentry-tracing/tests/future_cross_thread_trace_span.rs b/sentry-tracing/tests/future_cross_thread_trace_span.rs index 42501db9..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,9 @@ mod future_span_common; mod shared; use sentry::{Hub, HubSwitchGuard}; +use sentry_tracing::SpanLayer; +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 +15,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(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 29432397..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,9 @@ use std::future::Future; use std::thread; use sentry::{Hub, HubSwitchGuard}; +use sentry_tracing::SpanLayer; +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 +19,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(SpanLayer::new().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..6ee60697 --- /dev/null +++ b/sentry-tracing/tests/log_span_attributes.rs @@ -0,0 +1,48 @@ +//! Tests that `LogLayer::enable_span_attributes` attaches parent span attributes to captured logs. + +#![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::*; + +#[test] +fn log_layer_captures_span_attributes() { + let _subscriber = tracing_subscriber::registry() + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with( + LogLayer::new() + .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..d8bde9f1 100644 --- a/sentry-tracing/tests/name_op_updates.rs +++ b/sentry-tracing/tests/name_op_updates.rs @@ -2,6 +2,10 @@ mod shared; +use sentry_tracing::SpanLayer; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::prelude::*; + #[tracing::instrument(fields( some = "value", sentry.name = "updated name", @@ -22,6 +26,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(SpanLayer::new().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 af1fb976..82221b30 100644 --- a/sentry-tracing/tests/shared/mod.rs +++ b/sentry-tracing/tests/shared/mod.rs @@ -4,8 +4,6 @@ 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") @@ -14,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..63d0948f 100644 --- a/sentry-tracing/tests/smoke.rs +++ b/sentry-tracing/tests/smoke.rs @@ -1,6 +1,10 @@ #![expect(missing_docs, reason = "predates lint enforcement")] +#![cfg(feature = "logs")] -mod shared; +use sentry::protocol::{EnvelopeItem, ItemContainer, LogLevel}; +use sentry_tracing::{LogLayer, SpanLayer}; +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 +12,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(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with(LogLayer::new().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 +55,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..ec3141b2 100644 --- a/sentry-tracing/tests/span_cross_thread.rs +++ b/sentry-tracing/tests/span_cross_thread.rs @@ -3,27 +3,39 @@ mod shared; use sentry::protocol::Context; +use sentry_tracing::SpanLayer; 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(SpanLayer::new().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..2f3f965a 100644 --- a/sentry-tracing/tests/span_reentrancy.rs +++ b/sentry-tracing/tests/span_reentrancy.rs @@ -4,11 +4,18 @@ use sentry::protocol::EnvelopeItem; mod shared; +use sentry_tracing::SpanLayer; +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(SpanLayer::new().with_filter(LevelFilter::INFO)) + .set_default(); { // Create a span and enter it, then re-enter the same span to simulate diff --git a/sentry/examples/tracing-demo.rs b/sentry/examples/tracing-demo.rs index 5d05d595..0b7cc41b 100644 --- a/sentry/examples/tracing-demo.rs +++ b/sentry/examples/tracing-demo.rs @@ -3,13 +3,16 @@ use std::thread; use std::time::Duration; +use sentry_tracing::{LogLayer, SpanLayer}; +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::layer()) + .with(SpanLayer::new().with_filter(LevelFilter::INFO)) + .with(LogLayer::new().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 ad10d250..92fc6e43 100644 --- a/sentry/tests/test_tracing.rs +++ b/sentry/tests/test_legacy_tracing.rs @@ -1,11 +1,16 @@ #![expect(missing_docs, reason = "predates lint enforcement")] +#![expect(deprecated, reason = "legacy layer compatibility tests")] #![cfg(feature = "test")] 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::*; #[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()) @@ -118,6 +123,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"); @@ -273,10 +316,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(LogLayer::new().with_filter(LevelFilter::INFO)) .set_default(); let options = sentry::ClientOptions::new();