From d1e64e6b2e8708e1f17245fcb7b27b71ae71585b Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 27 Aug 2025 19:32:01 +0200 Subject: [PATCH 01/13] ref(tracing): rework tracing to Sentry span name/op conversion --- CHANGELOG.md | 18 ++++++ sentry-core/src/futures.rs | 4 ++ sentry-core/src/performance.rs | 42 +++++++++++++ sentry-tracing/src/layer.rs | 83 +++++++++++++++++++------ sentry-tracing/src/lib.rs | 42 ++++++++++++- sentry-tracing/tests/dynamic_updates.rs | 49 +++++++++++++++ sentry-tracing/tests/smoke.rs | 2 +- 7 files changed, 217 insertions(+), 23 deletions(-) create mode 100644 sentry-tracing/tests/dynamic_updates.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 03acbefec..9f976151e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,24 @@ .contexts .insert("response".to_owned(), response.into()); ``` +- feat(tracing): enhance span control with special attributes and distributed tracing support + - The tracing integration now uses the tracing span name as the Sentry span name by default + - Span operation defaults to "default" instead of using the span name + - Added support for special span attributes: + - `sentry.name`: Override the span name in Sentry + - `sentry.op`: Override the span operation in Sentry + - `sentry.trace`: Enable distributed tracing by continuing from upstream trace headers + - Example: + ```rust + #[tracing::instrument(fields( + sentry.name = "payment_processing", + sentry.op = "payment.process", + sentry.trace = %incoming_trace_header + ))] + async fn process_payment(amount: u64) { + // Custom span name, operation, and distributed tracing + } + ``` ### Fixes diff --git a/sentry-core/src/futures.rs b/sentry-core/src/futures.rs index 5d8e7f05e..519714c3e 100644 --- a/sentry-core/src/futures.rs +++ b/sentry-core/src/futures.rs @@ -3,6 +3,8 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use futures_core::stream::Stream; + use crate::Hub; /// A future that binds a `Hub` to its execution. @@ -66,6 +68,8 @@ pub trait SentryFutureExt: Sized { impl SentryFutureExt for F where F: Future {} +impl SentryFutureExt for F where F: Stream {} + #[cfg(all(test, feature = "test"))] mod tests { use crate::test::with_captured_events; diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 18a96d1f3..07d3544e2 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -465,6 +465,22 @@ impl TransactionOrSpan { } } + /// Set the operation for this Transaction/Span. + pub fn set_op(&self, op: &str) { + match self { + TransactionOrSpan::Transaction(transaction) => transaction.set_op(op), + TransactionOrSpan::Span(span) => span.set_op(op), + } + } + + /// Set the name (description) for this Transaction/Span. + pub fn set_name(&self, name: &str) { + match self { + TransactionOrSpan::Transaction(transaction) => transaction.set_name(name), + TransactionOrSpan::Span(span) => span.set_name(name), + } + } + /// Set the HTTP request information for this Transaction/Span. pub fn set_request(&self, request: protocol::Request) { match self { @@ -781,6 +797,20 @@ impl Transaction { inner.context.status = Some(status); } + /// Set the operation of the Transaction. + pub fn set_op(&self, op: &str) { + let mut inner = self.inner.lock().unwrap(); + inner.context.op = Some(op.to_string()); + } + + /// Set the name of the Transaction. + pub fn set_name(&self, name: &str) { + let mut inner = self.inner.lock().unwrap(); + if let Some(transaction) = inner.transaction.as_mut() { + transaction.name = Some(name.to_string()); + } + } + /// Set the HTTP request information for this Transaction. pub fn set_request(&self, request: protocol::Request) { let mut inner = self.inner.lock().unwrap(); @@ -1018,6 +1048,18 @@ impl Span { span.status = Some(status); } + /// Set the operation of the Span. + pub fn set_op(&self, op: &str) { + let mut span = self.span.lock().unwrap(); + span.op = Some(op.to_string()); + } + + /// Set the name (description) of the Span. + pub fn set_name(&self, name: &str) { + let mut span = self.span.lock().unwrap(); + span.description = Some(name.to_string()); + } + /// Set the HTTP request information for this Span. pub fn set_request(&self, request: protocol::Request) { let mut span = self.span.lock().unwrap(); diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs index efc4da540..4ace8367e 100644 --- a/sentry-tracing/src/layer.rs +++ b/sentry-tracing/src/layer.rs @@ -14,6 +14,10 @@ use tracing_subscriber::registry::LookupSpan; use crate::converters::*; use crate::TAGS_PREFIX; +const SENTRY_NAME_ATTR: &str = "sentry.name"; +const SENTRY_OP_ATTR: &str = "sentry.op"; +const SENTRY_TRACE_ATTR: &str = "sentry.trace"; + bitflags! { /// The action that Sentry should perform for a given [`Event`] #[derive(Debug, Clone, Copy)] @@ -292,27 +296,26 @@ where return; } - let (description, data) = extract_span_data(attrs); - let op = span.name(); - - // Spans don't always have a description, this ensures our data is not empty, - // therefore the Sentry UI will be a lot more valuable for navigating spans. - let description = description.unwrap_or_else(|| { - let target = span.metadata().target(); - if target.is_empty() { - op.to_string() - } else { - format!("{target}::{op}") - } - }); + 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.as_deref().unwrap_or("default"); let hub = sentry_core::Hub::current(); let parent_sentry_span = hub.configure_scope(|scope| scope.get_span()); let sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span { - Some(parent) => parent.start_child(op, &description).into(), + Some(parent) => parent.start_child(sentry_op, sentry_name).into(), None => { - let ctx = sentry_core::TransactionContext::new(&description, op); + 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())].into_iter(), + ) + } else { + sentry_core::TransactionContext::new(sentry_name, sentry_op) + }; + let tx = sentry_core::start_transaction(ctx); tx.set_data("origin", "auto.tracing".into()); tx.into() @@ -397,6 +400,32 @@ where let mut data = FieldVisitor::default(); values.record(&mut data); + let sentry_name = data + .json_values + .remove(SENTRY_NAME_ATTR) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + let sentry_op = data + .json_values + .remove(SENTRY_OP_ATTR) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + // `sentry.trace` cannot be applied retroactively + data.json_values.remove(SENTRY_TRACE_ATTR); + + 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); } } @@ -410,7 +439,14 @@ where } /// Extracts the message and attributes from a span -fn extract_span_data(attrs: &span::Attributes) -> (Option, BTreeMap<&'static str, Value>) { +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, @@ -420,13 +456,22 @@ fn extract_span_data(attrs: &span::Attributes) -> (Option, BTreeMap<&'st visitor.json_values }); - // Find message of the span, if any - let message = json_values.remove("message").and_then(|v| match v { + let name = json_values.remove(SENTRY_NAME_ATTR).and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + let op = json_values.remove(SENTRY_OP_ATTR).and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + let sentry_trace = json_values.remove(SENTRY_TRACE_ATTR).and_then(|v| match v { Value::String(s) => Some(s), _ => None, }); - (message, json_values) + (json_values, name, op, sentry_trace) } thread_local! { diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index df6887180..0915daac6 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -169,7 +169,7 @@ //! # Tracing Spans //! //! The integration automatically tracks `tracing` spans as spans in Sentry. A convenient way to do -//! this is with the `#[instrument]` attribute macro, which creates a transaction for the function +//! this is with the `#[instrument]` attribute macro, which creates a span/transaction for the function //! in Sentry. //! //! Function arguments are added as context fields automatically, which can be configured through @@ -180,8 +180,8 @@ //! //! use tracing_subscriber::prelude::*; //! -//! // Functions instrumented by tracing automatically report -//! // their span as transactions. +//! // Functions instrumented by tracing automatically +//! // create spans/transactions around their execution. //! #[tracing::instrument] //! async fn outer() { //! for i in 0..10 { @@ -198,6 +198,42 @@ //! tokio::time::sleep(Duration::from_millis(100)).await; //! } //! ``` +//! +//! By default, the name of the span sent to Sentry matches the name of the `tracing` span, which +//! is the name of the function when using `tracing::instrument`, or the name passed to the +//! `tracing::_span` macros. +//! +//! By default, the `op` of the span sent to Sentry is `default`. +//! +//! ## 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, as `tracing` doesn't allow doing it. +//! - `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 +//! frontend 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 this will only be effective on span creation (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: +//! +//! ```no_run +//! #[tracing::instrument(skip_all, fields( +//! sentry.name = "GET /payments", +//! sentry.op = "http.server", +//! sentry.trace = req.headers.get("sentry-trace").unwrap_or(""), +//! ))] +//! async fn handle_request(amount: u64) { +//! // ... +//! } +//! ``` +//! #![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/dynamic_updates.rs b/sentry-tracing/tests/dynamic_updates.rs new file mode 100644 index 000000000..7a4b9b94d --- /dev/null +++ b/sentry-tracing/tests/dynamic_updates.rs @@ -0,0 +1,49 @@ +mod shared; + +#[tracing::instrument(fields(initial_field = "value", sentry.name = tracing::field::Empty, sentry.op = tracing::field::Empty))] +fn function_with_dynamic_updates() { + // Record new sentry attributes dynamically + tracing::Span::current().record("sentry.name", "updated_span_name"); + tracing::Span::current().record("sentry.op", "updated_operation"); + tracing::error!("event in updated span"); +} + +#[test] +fn should_update_span_name_and_op_dynamically() { + let transport = shared::init_sentry(1.0); // Sample all spans. + + function_with_dynamic_updates(); + + 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:#?}"), + }; + + // Validate transaction trace context shows updated operation + let trace = match event.contexts.get("trace").expect("to get 'trace' context") { + sentry::protocol::Context::Trace(trace) => trace, + unexpected => panic!("Expected trace context but got {unexpected:?}"), + }; + assert_eq!(trace.op.as_deref().unwrap(), "updated_operation"); + + // Confirm transaction has updated 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:#?}"), + }; + + // Check that the transaction name was updated + assert_eq!(transaction.name.as_deref().unwrap(), "updated_span_name"); + + // Verify the initial field is still present + let initial_field = trace + .data + .get("initial_field") + .expect("to have data attribute with name 'initial_field'"); + assert_eq!(initial_field, "value"); +} diff --git a/sentry-tracing/tests/smoke.rs b/sentry-tracing/tests/smoke.rs index cb4f04e34..f7c158924 100644 --- a/sentry-tracing/tests/smoke.rs +++ b/sentry-tracing/tests/smoke.rs @@ -24,7 +24,7 @@ fn should_instrument_function_with_event() { sentry::protocol::Context::Trace(trace) => trace, unexpected => panic!("Expected trace context but got {unexpected:?}"), }; - assert_eq!(trace.op.as_deref().unwrap(), "function_with_tags"); + assert_eq!(trace.op.as_deref().unwrap(), "default"); //Confirm transaction values let transaction = data.get(1).expect("should have 1 transaction"); From e0b4c50598adf5221c47c9633bc5dcdddcba6926 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 27 Aug 2025 19:36:00 +0200 Subject: [PATCH 02/13] wip --- sentry-core/src/futures.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry-core/src/futures.rs b/sentry-core/src/futures.rs index 519714c3e..5d8e7f05e 100644 --- a/sentry-core/src/futures.rs +++ b/sentry-core/src/futures.rs @@ -3,8 +3,6 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use futures_core::stream::Stream; - use crate::Hub; /// A future that binds a `Hub` to its execution. @@ -68,8 +66,6 @@ pub trait SentryFutureExt: Sized { impl SentryFutureExt for F where F: Future {} -impl SentryFutureExt for F where F: Stream {} - #[cfg(all(test, feature = "test"))] mod tests { use crate::test::with_captured_events; From 04e614e54df4df62973dbc023bfdabbaa0651401 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 27 Aug 2025 21:40:59 +0200 Subject: [PATCH 03/13] wip --- CHANGELOG.md | 42 ++++++++++++--------- sentry-tracing/src/layer.rs | 29 ++++++++------- sentry-tracing/src/lib.rs | 11 ++++-- sentry-tracing/tests/dynamic_updates.rs | 49 ------------------------- sentry-tracing/tests/name_op_updates.rs | 48 ++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 85 deletions(-) delete mode 100644 sentry-tracing/tests/dynamic_updates.rs create mode 100644 sentry-tracing/tests/name_op_updates.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f976151e..55bedefa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ ## Unreleased +### Breaking changes + +- ref(tracing): rework tracing to Sentry span name/op conversion () by @lcian + - The `tracing` integration now uses the tracing span name as the Sentry span name by default. + - Before this change, the span name would be set based on the `tracing` span target (:: when using the `tracing::instrument` macro). + - The `tracing` integration now uses `default` as the default Sentry span op. + - Before this change, the span op would be set based on the `tracing` span name. + - When upgrading, please ensure to adapt any queries, metrics or dashboards to use the new span names/ops. + - Additional special fields have been added that allow overriding certain data on the Sentry span: + - `sentry.op`: override the Sentry span op + - `sentry.name`: override the Sentry span name + - `sentry.trace`: given a + - Example usage: + ```rust + #[tracing::instrument(skip_all, fields( + sentry.op = "http.server", + sentry.name = "GET /payments", + sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()), + ))] + async fn handle_request(headers: std::collections::HashMap) { + // ... + } + ``` + ### Features - feat(core): add Response context ([#874](https://github.com/getsentry/sentry-rust/pull/874)) by @lcian @@ -20,24 +44,6 @@ .contexts .insert("response".to_owned(), response.into()); ``` -- feat(tracing): enhance span control with special attributes and distributed tracing support - - The tracing integration now uses the tracing span name as the Sentry span name by default - - Span operation defaults to "default" instead of using the span name - - Added support for special span attributes: - - `sentry.name`: Override the span name in Sentry - - `sentry.op`: Override the span operation in Sentry - - `sentry.trace`: Enable distributed tracing by continuing from upstream trace headers - - Example: - ```rust - #[tracing::instrument(fields( - sentry.name = "payment_processing", - sentry.op = "payment.process", - sentry.trace = %incoming_trace_header - ))] - async fn process_payment(amount: u64) { - // Custom span name, operation, and distributed tracing - } - ``` ### Fixes diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs index 4ace8367e..4c23b8e0c 100644 --- a/sentry-tracing/src/layer.rs +++ b/sentry-tracing/src/layer.rs @@ -12,12 +12,11 @@ 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; -const SENTRY_NAME_ATTR: &str = "sentry.name"; -const SENTRY_OP_ATTR: &str = "sentry.op"; -const SENTRY_TRACE_ATTR: &str = "sentry.trace"; - bitflags! { /// The action that Sentry should perform for a given [`Event`] #[derive(Debug, Clone, Copy)] @@ -310,7 +309,7 @@ where sentry_core::TransactionContext::continue_from_headers( sentry_name, sentry_op, - [("sentry-trace", trace_header.as_str())].into_iter(), + [("sentry-trace", trace_header.as_str())], ) } else { sentry_core::TransactionContext::new(sentry_name, sentry_op) @@ -402,7 +401,7 @@ where let sentry_name = data .json_values - .remove(SENTRY_NAME_ATTR) + .remove(SENTRY_NAME_FIELD) .and_then(|v| match v { Value::String(s) => Some(s), _ => None, @@ -410,14 +409,14 @@ where let sentry_op = data .json_values - .remove(SENTRY_OP_ATTR) + .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_ATTR); + data.json_values.remove(SENTRY_TRACE_FIELD); if let Some(name) = sentry_name { span.set_name(&name); @@ -456,20 +455,22 @@ fn extract_span_data( visitor.json_values }); - let name = json_values.remove(SENTRY_NAME_ATTR).and_then(|v| match v { + 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_ATTR).and_then(|v| match v { + 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_ATTR).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) } diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index 0915daac6..7a06b625d 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -221,19 +221,19 @@ //! requires the span it's applied to to be a root span, i.e. no span should active upon its //! creation. //! +//! //! Example: //! -//! ```no_run +//! ``` //! #[tracing::instrument(skip_all, fields( //! sentry.name = "GET /payments", //! sentry.op = "http.server", -//! sentry.trace = req.headers.get("sentry-trace").unwrap_or(""), +//! sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()), //! ))] -//! async fn handle_request(amount: u64) { +//! async fn handle_request(headers: std::collections::HashMap) { //! // ... //! } //! ``` -//! #![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")] @@ -246,3 +246,6 @@ pub use converters::*; pub use layer::*; const TAGS_PREFIX: &str = "tags."; +const SENTRY_OP_FIELD: &str = "sentry.op"; +const SENTRY_NAME_FIELD: &str = "sentry.name"; +const SENTRY_TRACE_FIELD: &str = "sentry.trace"; diff --git a/sentry-tracing/tests/dynamic_updates.rs b/sentry-tracing/tests/dynamic_updates.rs deleted file mode 100644 index 7a4b9b94d..000000000 --- a/sentry-tracing/tests/dynamic_updates.rs +++ /dev/null @@ -1,49 +0,0 @@ -mod shared; - -#[tracing::instrument(fields(initial_field = "value", sentry.name = tracing::field::Empty, sentry.op = tracing::field::Empty))] -fn function_with_dynamic_updates() { - // Record new sentry attributes dynamically - tracing::Span::current().record("sentry.name", "updated_span_name"); - tracing::Span::current().record("sentry.op", "updated_operation"); - tracing::error!("event in updated span"); -} - -#[test] -fn should_update_span_name_and_op_dynamically() { - let transport = shared::init_sentry(1.0); // Sample all spans. - - function_with_dynamic_updates(); - - 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:#?}"), - }; - - // Validate transaction trace context shows updated operation - let trace = match event.contexts.get("trace").expect("to get 'trace' context") { - sentry::protocol::Context::Trace(trace) => trace, - unexpected => panic!("Expected trace context but got {unexpected:?}"), - }; - assert_eq!(trace.op.as_deref().unwrap(), "updated_operation"); - - // Confirm transaction has updated 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:#?}"), - }; - - // Check that the transaction name was updated - assert_eq!(transaction.name.as_deref().unwrap(), "updated_span_name"); - - // Verify the initial field is still present - let initial_field = trace - .data - .get("initial_field") - .expect("to have data attribute with name 'initial_field'"); - assert_eq!(initial_field, "value"); -} diff --git a/sentry-tracing/tests/name_op_updates.rs b/sentry-tracing/tests/name_op_updates.rs new file mode 100644 index 000000000..5c244c7dc --- /dev/null +++ b/sentry-tracing/tests/name_op_updates.rs @@ -0,0 +1,48 @@ +mod shared; + +#[tracing::instrument(fields( + some = "value", + sentry.name = tracing::field::Empty, + sentry.op = tracing::field::Empty, +))] +fn test_fun_record_on_creation() { + tracing::Span::current().record("sentry.name", "updated name"); + tracing::Span::current().record("sentry.op", "updated op"); +} + +#[tracing::instrument(fields( + some = "value", + sentry.name = tracing::field::Empty, + sentry.op = tracing::field::Empty, +))] +fn test_fun_record_later() { + tracing::Span::current().record("sentry.name", "updated name"); + tracing::Span::current().record("sentry.op", "updated op"); +} + +#[test] +fn should_update_sentry_op_and_name_based_on_fields() { + let transport = shared::init_sentry(1.0); + + for f in [test_fun_record_on_creation, test_fun_record_later] { + f(); + + let data = transport.fetch_and_clear_envelopes(); + assert_eq!(data.len(), 1); + // Confirm transaction has updated values + let transaction = data.first().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:#?}"), + }; + + assert_eq!(transaction.name.as_deref().unwrap(), "updated name"); + let ctx = transaction.contexts.get("trace"); + match ctx { + Some(sentry::protocol::Context::Trace(trace_ctx)) => { + assert_eq!(trace_ctx.op, Some("updated op".to_owned())) + } + _ => panic!("expected trace context"), + } + } +} From 7312bc251d8b558c6a30128c1ab395541a364ed9 Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Thu, 28 Aug 2025 15:25:30 +0200 Subject: [PATCH 04/13] update changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0842d97ee..15474f70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Breaking changes -- ref(tracing): rework tracing to Sentry span name/op conversion () by @lcian +- ref(tracing): rework tracing to Sentry span name/op conversion ([#887](https://github.com/getsentry/sentry-rust/pull/887)) by @lcian - The `tracing` integration now uses the tracing span name as the Sentry span name by default. - Before this change, the span name would be set based on the `tracing` span target (:: when using the `tracing::instrument` macro). - The `tracing` integration now uses `default` as the default Sentry span op. @@ -13,7 +13,8 @@ - Additional special fields have been added that allow overriding certain data on the Sentry span: - `sentry.op`: override the Sentry span op - `sentry.name`: override the Sentry span name - - `sentry.trace`: given a + - `sentry.trace`: given a string matching a valid `sentry-trace` header (sent automatically by client SDKs), continues the distributed trace instead of starting a new one. If the value is not a valid `sentry-trace` header or a trace is already started, this value is ignored. + - `sentry.op` and `sentry.name` can also be applied retroactively by declaring fields with value `tracing::field::Empty` and then recorded using `tracing::Span::record`. - Example usage: ```rust #[tracing::instrument(skip_all, fields( From ef97dc7f17f80d75d59ac92776ac227f78e59bd5 Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Thu, 28 Aug 2025 15:25:53 +0200 Subject: [PATCH 05/13] Fix punctuation in CHANGELOG for Sentry span ops Corrected punctuation in the CHANGELOG regarding Sentry span overrides. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15474f70f..ee8618bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ - Before this change, the span op would be set based on the `tracing` span name. - When upgrading, please ensure to adapt any queries, metrics or dashboards to use the new span names/ops. - Additional special fields have been added that allow overriding certain data on the Sentry span: - - `sentry.op`: override the Sentry span op - - `sentry.name`: override the Sentry span name + - `sentry.op`: override the Sentry span op. + - `sentry.name`: override the Sentry span name. - `sentry.trace`: given a string matching a valid `sentry-trace` header (sent automatically by client SDKs), continues the distributed trace instead of starting a new one. If the value is not a valid `sentry-trace` header or a trace is already started, this value is ignored. - `sentry.op` and `sentry.name` can also be applied retroactively by declaring fields with value `tracing::field::Empty` and then recorded using `tracing::Span::record`. - Example usage: From 93a8c04f821abe427a0bab026cb8196c72e6c5a4 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 9 Sep 2025 12:45:22 +0200 Subject: [PATCH 06/13] update test --- sentry-tracing/tests/name_op_updates.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sentry-tracing/tests/name_op_updates.rs b/sentry-tracing/tests/name_op_updates.rs index 5c244c7dc..f4b1aaa2d 100644 --- a/sentry-tracing/tests/name_op_updates.rs +++ b/sentry-tracing/tests/name_op_updates.rs @@ -2,13 +2,10 @@ mod shared; #[tracing::instrument(fields( some = "value", - sentry.name = tracing::field::Empty, - sentry.op = tracing::field::Empty, + sentry.name = "updated name", + sentry.op = "updated op", ))] -fn test_fun_record_on_creation() { - tracing::Span::current().record("sentry.name", "updated name"); - tracing::Span::current().record("sentry.op", "updated op"); -} +fn test_fun_record_on_creation() {} #[tracing::instrument(fields( some = "value", From 6f1a6457656a2895d56fb8eefc405e07f94e38a6 Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 11 Sep 2025 16:29:18 +0200 Subject: [PATCH 07/13] assert name --- sentry-tracing/tests/smoke.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-tracing/tests/smoke.rs b/sentry-tracing/tests/smoke.rs index f7c158924..9daf85fc6 100644 --- a/sentry-tracing/tests/smoke.rs +++ b/sentry-tracing/tests/smoke.rs @@ -32,6 +32,7 @@ fn should_instrument_function_with_event() { sentry::protocol::EnvelopeItem::Transaction(transaction) => transaction, unexpected => panic!("Expected transaction, but got {unexpected:#?}"), }; + assert_eq!(transaction.name, Some("function_with_tags".into())); assert_eq!(transaction.tags.len(), 1); assert_eq!(trace.data.len(), 3); From 9117788355d31ee1737f3c5cf5702d0fb9e68cbf Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 11 Sep 2025 16:30:15 +0200 Subject: [PATCH 08/13] improve --- sentry-tracing/tests/name_op_updates.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-tracing/tests/name_op_updates.rs b/sentry-tracing/tests/name_op_updates.rs index f4b1aaa2d..a6f3d15a9 100644 --- a/sentry-tracing/tests/name_op_updates.rs +++ b/sentry-tracing/tests/name_op_updates.rs @@ -26,7 +26,7 @@ fn should_update_sentry_op_and_name_based_on_fields() { let data = transport.fetch_and_clear_envelopes(); assert_eq!(data.len(), 1); - // Confirm transaction has updated values + let transaction = data.first().expect("should have 1 transaction"); let transaction = match transaction.items().next().unwrap() { sentry::protocol::EnvelopeItem::Transaction(transaction) => transaction, From 29a80779a899be933bad7af62c7971e20620fa0c Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 12 Sep 2025 12:16:07 +0200 Subject: [PATCH 09/13] address comments --- CHANGELOG.md | 16 +++++++++------- sentry-tracing/src/lib.rs | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee8618bd8..5c80058b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ - The `tracing` integration now uses `default` as the default Sentry span op. - Before this change, the span op would be set based on the `tracing` span name. - When upgrading, please ensure to adapt any queries, metrics or dashboards to use the new span names/ops. +- fix(actix): capture only server errors ([#877](https://github.com/getsentry/sentry-rust/pull/877)) by @lcian + - The Actix integration now properly honors the `capture_server_errors` option (enabled by default), capturing errors returned by middleware only if they are server errors (HTTP status code 5xx). + - Previously, if a middleware were to process the request after the Sentry middleware and return an error, our middleware would always capture it and send it to Sentry, regardless if it was a client, server or some other kind of error. + - With this change, we capture errors returned by middleware only if those errors can be classified as server errors. + - There is no change in behavior when it comes to errors returned by services, in which case the Sentry middleware only captures server errors exclusively. + +### Features + +- ref(tracing): rework tracing to Sentry span name/op conversion ([#887](https://github.com/getsentry/sentry-rust/pull/887)) by @lcian - Additional special fields have been added that allow overriding certain data on the Sentry span: - `sentry.op`: override the Sentry span op. - `sentry.name`: override the Sentry span name. @@ -26,13 +35,6 @@ // ... } ``` -- fix(actix): capture only server errors ([#877](https://github.com/getsentry/sentry-rust/pull/877)) - - The Actix integration now properly honors the `capture_server_errors` option (enabled by default), capturing errors returned by middleware only if they are server errors (HTTP status code 5xx). - - Previously, if a middleware were to process the request after the Sentry middleware and return an error, our middleware would always capture it and send it to Sentry, regardless if it was a client, server or some other kind of error. - - With this change, we capture errors returned by middleware only if those errors can be classified as server errors. - - There is no change in behavior when it comes to errors returned by services, in which case the Sentry middleware only captures server errors exclusively. - -### Features - feat(core): add Response context ([#874](https://github.com/getsentry/sentry-rust/pull/874)) by @lcian - The `Response` context can now be attached to events, to include information about HTTP responses such as headers, cookies and status code. diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index 7a06b625d..100b8efd4 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -210,15 +210,15 @@ //! 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, as `tracing` doesn't allow doing it. +//! 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 -//! frontend SDKs send automatically with outgoing requests, then the SDK will continue the trace using the given distributed tracing information. +//! 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 this will only be effective on span creation (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 +//! 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. //! //! From e808a35dd6ac5c785dc60c34c8f2e7109994f99d Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 12 Sep 2025 12:23:07 +0200 Subject: [PATCH 10/13] address comments --- sentry-tracing/src/layer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs index cc8bb2d1e..cc7073f88 100644 --- a/sentry-tracing/src/layer.rs +++ b/sentry-tracing/src/layer.rs @@ -443,7 +443,8 @@ where Default::default() } -/// Extracts the message and attributes from a span +/// 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, ) -> ( From 36589451be18a3302054f65867ca4e9826621270 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 12 Sep 2025 14:09:09 +0200 Subject: [PATCH 11/13] add default attributes --- sentry-tracing/src/layer.rs | 20 +++++++++++++++++++- sentry-tracing/tests/smoke.rs | 13 ++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs index cc7073f88..432d3bcca 100644 --- a/sentry-tracing/src/layer.rs +++ b/sentry-tracing/src/layer.rs @@ -308,7 +308,7 @@ where let hub = sentry_core::Hub::current(); let parent_sentry_span = hub.configure_scope(|scope| scope.get_span()); - let sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_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 { @@ -330,6 +330,8 @@ where // 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, @@ -435,6 +437,22 @@ where } } +fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &'static Metadata<'static>) { + 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()); + } +} + /// Creates a default Sentry layer pub fn layer() -> SentryLayer where diff --git a/sentry-tracing/tests/smoke.rs b/sentry-tracing/tests/smoke.rs index 9daf85fc6..cca7b1f4c 100644 --- a/sentry-tracing/tests/smoke.rs +++ b/sentry-tracing/tests/smoke.rs @@ -34,7 +34,7 @@ fn should_instrument_function_with_event() { }; assert_eq!(transaction.name, Some("function_with_tags".into())); assert_eq!(transaction.tags.len(), 1); - assert_eq!(trace.data.len(), 3); + assert_eq!(trace.data.len(), 7); let tag = transaction .tags @@ -51,4 +51,15 @@ fn should_instrument_function_with_event() { .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() + ); + assert_eq!( + trace.data.get("code.module.name"), + Some("smoke".into()).as_ref() + ); + assert!(trace.data.contains_key("code.file.path")); + assert!(trace.data.contains_key("code.line.number")); } From 1d68f983bb56530c8297cb86e35c3bd12035a268 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 12 Sep 2025 14:37:21 +0200 Subject: [PATCH 12/13] lifetime --- sentry-tracing/src/layer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs index 432d3bcca..202f7cf38 100644 --- a/sentry-tracing/src/layer.rs +++ b/sentry-tracing/src/layer.rs @@ -437,7 +437,7 @@ where } } -fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &'static Metadata<'static>) { +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() { From 5fdd55f2f3b0bcac9bfb1d5cafbbbe9aad9b2d67 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 12 Sep 2025 14:58:27 +0200 Subject: [PATCH 13/13] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd2dfa99..c89740cfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ // ... } ``` + - Additional attributes are sent along with each span by default: + - `sentry.tracing.target`: corresponds to the `tracing` span's `metadata.target()` + - `code.module.name`, `code.file.path`, `code.line.number` - feat(core): add Response context ([#874](https://github.com/getsentry/sentry-rust/pull/874)) by @lcian - The `Response` context can now be attached to events, to include information about HTTP responses such as headers, cookies and status code.