Repository navigation
feat: Split tracing integration into separate layers - #1342
Open
szokeasaurusrex wants to merge 15 commits into
Open
szokeasaurusrex wants to merge 15 commits into
szokeasaurusrex wants to merge 15 commits into
Conversation
|
szokeasaurusrex
force-pushed
the
szokeasaurusrex/tracing-integration-split
branch
from
October 6, 2026 13:02
9fc1c7e to
46a1c63
Compare
szokeasaurusrex
commented
Oct 7, 2026
Comment on lines
+54
to
+355
| #[inline(always)] | ||
| fn record_fields<'a, K: AsRef<str> + Into<Cow<'a, str>>>( | ||
| span: &TransactionOrSpan, | ||
| data: BTreeMap<K, Value>, | ||
| ) { | ||
| 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<Hub>, | ||
| } | ||
|
|
||
| impl<S> Layer<S> 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::<SentrySpanData>() { | ||
| 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::<SentrySpanData>().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::<SentrySpanData>() { | ||
| 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::<SentrySpanData>() { | ||
| 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<String>, | ||
| Option<String>, | ||
| Option<String>, | ||
| ) { | ||
| 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<String> = 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<SpanGuardStack> = 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<T: Into<Value>>(&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(); | ||
| } | ||
| } |
Member
Author
There was a problem hiding this comment.
This has all been moved over from the legacy layer (defined in mod.rs).
szokeasaurusrex
marked this pull request as ready for review
October 7, 2026 14:05
alexander-alderman-webb
previously approved these changes
Oct 9, 2026
szokeasaurusrex
dismissed
alexander-alderman-webb’s stale review
October 9, 2026 08:51
more thorough review needed
giortzisg
reviewed
Oct 12, 2026
szokeasaurusrex
added this pull request to stack #1349
October 13, 2026 09:14
szokeasaurusrex
added a commit
that referenced
this pull request
Oct 13, 2026
Deprecate tracing event to Sentry telemetry conversion helper functions from the public API, as these are only useful for building event mappers, themselves deprecated in #1342. As these functions are still needed internally within the crate, these changes add `pub(crate)` helpers to replace the deprecated `pub` functions; these `pub` functions just call the `pub(crate)` helpers.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split the tracing integration into four separate tracing layers, which each capture only one type of Sentry telemetry: Spans, Logs, Errors, and Breadcrumbs.
Previously, the tracing integration captured all four of these types of telemetry by default; now, users can select which ones they want. Our documentation recommends only enabling capture of Spans and Logs for most users, as these correspond most closely to the type of telemetry the tracing crate captures, while Errors and Breadcrumbs primarily remain for users who want the legacy data types. With this change, enabling the
LogLayeris considered a sufficient opt-in to have logs be sent to Sentry, as that layer is only used for sending Sentry Logs.While this change may increase complexity for users, it also makes customizing the tracing integration's behavior much easier, as we now rely primarily on filtering provided by the
tracing_subscribercrate rather than custom filtering that we ourselves had to define and maintain. It also makes it very explicit to users to know what kind of telemetry the integration is going to capture.This change is fully backwards-compatible. We keep the legacy combined layer, although it is deprecated and has been reimplemented as a composition of the separate layers.
Closes #1339
Closes RUST-285