From 70bfa0c65ec659340e06dec25d874f0591fa9efc Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 20 Feb 2026 12:06:57 -0600 Subject: [PATCH 1/6] perf(sdk): store InstrumentationScope in Arc to avoid clone per processor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SdkTracer now wraps InstrumentationScope in Arc internally, which makes tracer cloning significantly cheaper (pointer bump instead of deep clone with attribute Vec allocation). The multi-processor span export path is also optimized: instead of calling build_export_data() per processor (which clones the scope from the tracer each time), SpanData is now built once and cloned for each additional processor. The last processor receives the original without a clone. This is a non-breaking change — SpanData's public instrumentation_scope field remains InstrumentationScope (not Arc). Supersedes #3267, thanks to @taisho6339 for the original approach. --- opentelemetry-sdk/CHANGELOG.md | 1 + opentelemetry-sdk/src/trace/span.rs | 16 ++++++++++------ opentelemetry-sdk/src/trace/tracer.rs | 8 ++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 6be068e8cd..627d9ab2ac 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -13,6 +13,7 @@ - **Breaking** The SDK `testing` feature is now runtime agnostic. [#3407][3407] - `TokioSpanExporter` and `new_tokio_test_exporter` have been renamed to `TestSpanExporter` and `new_test_exporter`. - The following transitive dependencies and features have been removed: `tokio/rt`, `tokio/time`, `tokio/macros`, `tokio/rt-multi-thread`, `tokio-stream`, `experimental_async_runtime` +- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer cloning cheaper and avoiding redundant scope clones in the multi-processor span export path. - Add 32-bit platform support by using `portable-atomic` for `AtomicI64` and `AtomicU64` in the metrics module. This enables compilation on 32-bit ARM targets (e.g., `armv5te-unknown-linux-gnueabi`, `armv7-unknown-linux-gnueabihf`). - `Aggregation` enum and `StreamBuilder::with_aggregation()` are now stable and no longer require the `spec_unstable_metrics_views` feature flag. - Fix `service.name` Resource attribute fallback to follow OpenTelemetry diff --git a/opentelemetry-sdk/src/trace/span.rs b/opentelemetry-sdk/src/trace/span.rs index 4753650d53..eaa745a93c 100644 --- a/opentelemetry-sdk/src/trace/span.rs +++ b/opentelemetry-sdk/src/trace/span.rs @@ -229,12 +229,16 @@ impl Span { )); } processors => { - for processor in processors { - processor.on_end(build_export_data( - data.clone(), - self.span_context.clone(), - &self.tracer, - )); + let export_data = + build_export_data(data, self.span_context.clone(), &self.tracer); + let last_idx = processors.len() - 1; + for (i, processor) in processors.iter().enumerate() { + if i < last_idx { + processor.on_end(export_data.clone()); + } else { + processor.on_end(export_data); + break; + } } } } diff --git a/opentelemetry-sdk/src/trace/tracer.rs b/opentelemetry-sdk/src/trace/tracer.rs index 279992b56b..bb454defb9 100644 --- a/opentelemetry-sdk/src/trace/tracer.rs +++ b/opentelemetry-sdk/src/trace/tracer.rs @@ -17,11 +17,12 @@ use opentelemetry::{ Context, InstrumentationScope, KeyValue, }; use std::fmt; +use std::sync::Arc; /// `Tracer` implementation to create and manage spans #[derive(Clone)] pub struct SdkTracer { - scope: InstrumentationScope, + scope: Arc, provider: SdkTracerProvider, } @@ -39,7 +40,10 @@ impl fmt::Debug for SdkTracer { impl SdkTracer { /// Create a new tracer (used internally by `TracerProvider`s). pub(crate) fn new(scope: InstrumentationScope, provider: SdkTracerProvider) -> Self { - SdkTracer { scope, provider } + SdkTracer { + scope: Arc::new(scope), + provider, + } } /// TracerProvider associated with this tracer. From 4456f451cc73593cb887c0520fe092c1b403f58c Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 20 Feb 2026 12:11:54 -0600 Subject: [PATCH 2/6] test(sdk): add multi-processor span delivery test Verifies that when multiple span processors are configured, all processors receive the span data with correct name and attributes. --- opentelemetry-sdk/src/trace/span.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/opentelemetry-sdk/src/trace/span.rs b/opentelemetry-sdk/src/trace/span.rs index eaa745a93c..06baca0c35 100644 --- a/opentelemetry-sdk/src/trace/span.rs +++ b/opentelemetry-sdk/src/trace/span.rs @@ -713,6 +713,35 @@ mod tests { assert_eq!(event_vec.len(), DEFAULT_MAX_EVENT_PER_SPAN as usize); } + #[test] + fn multiple_processors_receive_span_data() { + use crate::trace::InMemorySpanExporterBuilder; + + let exporter1 = InMemorySpanExporterBuilder::new().build(); + let exporter2 = InMemorySpanExporterBuilder::new().build(); + + let provider = crate::trace::SdkTracerProvider::builder() + .with_simple_exporter(exporter1.clone()) + .with_simple_exporter(exporter2.clone()) + .build(); + + let tracer = provider.tracer("test"); + let mut span = tracer.start("multi_processor_span"); + span.set_attribute(KeyValue::new("key", "value")); + span.end(); + + let spans1 = exporter1.get_finished_spans().unwrap(); + let spans2 = exporter2.get_finished_spans().unwrap(); + + assert_eq!(spans1.len(), 1); + assert_eq!(spans2.len(), 1); + assert_eq!(spans1[0].name, "multi_processor_span"); + assert_eq!(spans2[0].name, "multi_processor_span"); + assert_eq!(spans1[0].attributes, spans2[0].attributes); + + let _ = provider.shutdown(); + } + #[test] fn test_span_exported_data() { let provider = crate::trace::SdkTracerProvider::builder() From 80fc0f372a0ae8d9c4e42c7a9dad6c6d6be6790e Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 20 Feb 2026 12:16:15 -0600 Subject: [PATCH 3/6] refactor: use split_last for multi-processor path, fix changelog Use split_last() instead of index tracking for the multi-processor on_end loop. Removes unnecessary break statement and makes the clone-for-rest, move-for-last intent explicit. Fix changelog entry to describe what actually changed without overstating the scope clone savings. --- opentelemetry-sdk/CHANGELOG.md | 2 +- opentelemetry-sdk/src/trace/span.rs | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 627d9ab2ac..2a96e77b2b 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -13,7 +13,7 @@ - **Breaking** The SDK `testing` feature is now runtime agnostic. [#3407][3407] - `TokioSpanExporter` and `new_tokio_test_exporter` have been renamed to `TestSpanExporter` and `new_test_exporter`. - The following transitive dependencies and features have been removed: `tokio/rt`, `tokio/time`, `tokio/macros`, `tokio/rt-multi-thread`, `tokio-stream`, `experimental_async_runtime` -- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer cloning cheaper and avoiding redundant scope clones in the multi-processor span export path. +- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer clones cheaper (Arc refcount increment instead of deep copy). The multi-processor span export path now builds export data once instead of per processor. - Add 32-bit platform support by using `portable-atomic` for `AtomicI64` and `AtomicU64` in the metrics module. This enables compilation on 32-bit ARM targets (e.g., `armv5te-unknown-linux-gnueabi`, `armv7-unknown-linux-gnueabihf`). - `Aggregation` enum and `StreamBuilder::with_aggregation()` are now stable and no longer require the `spec_unstable_metrics_views` feature flag. - Fix `service.name` Resource attribute fallback to follow OpenTelemetry diff --git a/opentelemetry-sdk/src/trace/span.rs b/opentelemetry-sdk/src/trace/span.rs index 06baca0c35..01f8a6ad28 100644 --- a/opentelemetry-sdk/src/trace/span.rs +++ b/opentelemetry-sdk/src/trace/span.rs @@ -231,15 +231,11 @@ impl Span { processors => { let export_data = build_export_data(data, self.span_context.clone(), &self.tracer); - let last_idx = processors.len() - 1; - for (i, processor) in processors.iter().enumerate() { - if i < last_idx { - processor.on_end(export_data.clone()); - } else { - processor.on_end(export_data); - break; - } + let (last, rest) = processors.split_last().unwrap(); + for processor in rest { + processor.on_end(export_data.clone()); } + last.on_end(export_data); } } } From 89f9cb4a544f4f408796973fb3bffa9179b2f2d7 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 20 Feb 2026 12:25:10 -0600 Subject: [PATCH 4/6] style: fix rustfmt formatting --- opentelemetry-sdk/src/trace/span.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/trace/span.rs b/opentelemetry-sdk/src/trace/span.rs index 01f8a6ad28..b8e0519de2 100644 --- a/opentelemetry-sdk/src/trace/span.rs +++ b/opentelemetry-sdk/src/trace/span.rs @@ -229,8 +229,7 @@ impl Span { )); } processors => { - let export_data = - build_export_data(data, self.span_context.clone(), &self.tracer); + let export_data = build_export_data(data, self.span_context.clone(), &self.tracer); let (last, rest) = processors.split_last().unwrap(); for processor in rest { processor.on_end(export_data.clone()); From f82222800f32c8c6378d6fc47543ced1b6f6c1e2 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 5 Mar 2026 20:05:49 -0600 Subject: [PATCH 5/6] test: strengthen multi-processor test with instrumentation_scope assertions Verify that instrumentation_scope is correctly propagated to all processors in both the clone (rest) and move (last) paths. Also clarify changelog wording. --- opentelemetry-sdk/CHANGELOG.md | 2 +- opentelemetry-sdk/src/trace/span.rs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 2a96e77b2b..39a739db44 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -13,7 +13,7 @@ - **Breaking** The SDK `testing` feature is now runtime agnostic. [#3407][3407] - `TokioSpanExporter` and `new_tokio_test_exporter` have been renamed to `TestSpanExporter` and `new_test_exporter`. - The following transitive dependencies and features have been removed: `tokio/rt`, `tokio/time`, `tokio/macros`, `tokio/rt-multi-thread`, `tokio-stream`, `experimental_async_runtime` -- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer clones cheaper (Arc refcount increment instead of deep copy). The multi-processor span export path now builds export data once instead of per processor. +- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer clones cheaper (Arc refcount increment instead of deep copy). The multi-processor span export path now builds export data once and clones for additional processors, instead of rebuilding per processor. - Add 32-bit platform support by using `portable-atomic` for `AtomicI64` and `AtomicU64` in the metrics module. This enables compilation on 32-bit ARM targets (e.g., `armv5te-unknown-linux-gnueabi`, `armv7-unknown-linux-gnueabihf`). - `Aggregation` enum and `StreamBuilder::with_aggregation()` are now stable and no longer require the `spec_unstable_metrics_views` feature flag. - Fix `service.name` Resource attribute fallback to follow OpenTelemetry diff --git a/opentelemetry-sdk/src/trace/span.rs b/opentelemetry-sdk/src/trace/span.rs index b8e0519de2..882e296902 100644 --- a/opentelemetry-sdk/src/trace/span.rs +++ b/opentelemetry-sdk/src/trace/span.rs @@ -733,6 +733,13 @@ mod tests { assert_eq!(spans1[0].name, "multi_processor_span"); assert_eq!(spans2[0].name, "multi_processor_span"); assert_eq!(spans1[0].attributes, spans2[0].attributes); + // Verify instrumentation scope is correctly propagated to both processors + assert_eq!(spans1[0].instrumentation_scope.name(), "test"); + assert_eq!(spans2[0].instrumentation_scope.name(), "test"); + assert_eq!( + spans1[0].instrumentation_scope, + spans2[0].instrumentation_scope + ); let _ = provider.shutdown(); } From e7bb9af4298f4e7197c6535aa43764022f4f3bf5 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 6 Mar 2026 13:36:37 -0600 Subject: [PATCH 6/6] refactor: remove multi-processor on_end optimization (superseded by #3410) --- opentelemetry-sdk/CHANGELOG.md | 2 +- opentelemetry-sdk/src/trace/span.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 39a739db44..c8f1d0622c 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -13,7 +13,7 @@ - **Breaking** The SDK `testing` feature is now runtime agnostic. [#3407][3407] - `TokioSpanExporter` and `new_tokio_test_exporter` have been renamed to `TestSpanExporter` and `new_test_exporter`. - The following transitive dependencies and features have been removed: `tokio/rt`, `tokio/time`, `tokio/macros`, `tokio/rt-multi-thread`, `tokio-stream`, `experimental_async_runtime` -- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer clones cheaper (Arc refcount increment instead of deep copy). The multi-processor span export path now builds export data once and clones for additional processors, instead of rebuilding per processor. +- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer clones cheaper (Arc refcount increment instead of deep copy). - Add 32-bit platform support by using `portable-atomic` for `AtomicI64` and `AtomicU64` in the metrics module. This enables compilation on 32-bit ARM targets (e.g., `armv5te-unknown-linux-gnueabi`, `armv7-unknown-linux-gnueabihf`). - `Aggregation` enum and `StreamBuilder::with_aggregation()` are now stable and no longer require the `spec_unstable_metrics_views` feature flag. - Fix `service.name` Resource attribute fallback to follow OpenTelemetry diff --git a/opentelemetry-sdk/src/trace/span.rs b/opentelemetry-sdk/src/trace/span.rs index 882e296902..09cc7474c2 100644 --- a/opentelemetry-sdk/src/trace/span.rs +++ b/opentelemetry-sdk/src/trace/span.rs @@ -229,12 +229,13 @@ impl Span { )); } processors => { - let export_data = build_export_data(data, self.span_context.clone(), &self.tracer); - let (last, rest) = processors.split_last().unwrap(); - for processor in rest { - processor.on_end(export_data.clone()); + for processor in processors { + processor.on_end(build_export_data( + data.clone(), + self.span_context.clone(), + &self.tracer, + )); } - last.on_end(export_data); } } }