Skip to content

Commit d9da953

Browse files
committed
feat(sdk): add SpanBatch for borrowed span export
Change SpanExporter::export to take SpanBatch<'_> instead of Vec<SpanData>, matching the LogBatch pattern already used in the logs pipeline. The key win is in BatchSpanProcessor: instead of calling batch.split_off(0) on every export (which allocates a new Vec and moves all elements), the processor now wraps the existing buffer in a SpanBatch reference and clears it after export returns. This eliminates one Vec allocation per export cycle. Changes across 6 crates: - opentelemetry-sdk: new SpanBatch<'a> type, updated processors - opentelemetry-proto: added From<&SpanData> and From<&Link>, group_spans_by_resource_and_scope now takes &[SpanData] - opentelemetry-otlp: updated tonic and http trace exporters - opentelemetry-stdout: updated trace exporter - opentelemetry-zipkin: updated trace exporter - benchmarks: updated no-op exporters Breaking change: SpanExporter::export signature changed. Implementors need to update their export method to accept SpanBatch<'_> and use batch.iter() to access spans.
1 parent c52f4a3 commit d9da953

23 files changed

Lines changed: 176 additions & 71 deletions

File tree

opentelemetry-otlp/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## vNext
44

5+
- Updated trace exporters to use borrowed `SpanBatch` interface, reducing allocations in the export path.
56
- Add `build()` directly on `SpanExporterBuilder`, `MetricExporterBuilder`, and `LogExporterBuilder`
67
(before selecting a transport), which auto-selects the transport based on the
78
`OTEL_EXPORTER_OTLP_PROTOCOL` environment variable or enabled features.

opentelemetry-otlp/src/exporter/http/mod.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_sc
1616
#[cfg(feature = "logs")]
1717
use opentelemetry_sdk::logs::LogBatch;
1818
#[cfg(feature = "trace")]
19-
use opentelemetry_sdk::trace::SpanData;
19+
use opentelemetry_sdk::trace::SpanBatch;
2020
#[cfg(feature = "http-proto")]
2121
use prost::Message;
2222
use std::collections::HashMap;
@@ -584,10 +584,11 @@ impl OtlpHttpClient {
584584
#[cfg(feature = "trace")]
585585
fn build_trace_export_body(
586586
&self,
587-
spans: Vec<SpanData>,
587+
spans: SpanBatch<'_>,
588588
) -> Result<(Vec<u8>, &'static str, Option<&'static str>), String> {
589589
use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
590-
let resource_spans = group_spans_by_resource_and_scope(spans, &self.resource);
590+
let span_data: Vec<_> = spans.iter().cloned().collect();
591+
let resource_spans = group_spans_by_resource_and_scope(&span_data, &self.resource);
591592

592593
let req = ExportTraceServiceRequest { resource_spans };
593594
let (body, content_type) = match self.protocol {
@@ -1324,10 +1325,14 @@ mod tests {
13241325
#[cfg(feature = "trace")]
13251326
#[test]
13261327
fn test_build_trace_export_body_binary_protocol() {
1328+
use opentelemetry_sdk::trace::SpanBatch;
1329+
13271330
let client = create_test_client(crate::Protocol::HttpBinary, None);
13281331
let span_data = create_test_span_data();
13291332

1330-
let result = client.build_trace_export_body(vec![span_data]).unwrap();
1333+
let result = client
1334+
.build_trace_export_body(SpanBatch::new(&[span_data]))
1335+
.unwrap();
13311336
let (_body, content_type, content_encoding) = result;
13321337

13331338
assert_eq!(content_type, "application/x-protobuf");
@@ -1337,10 +1342,14 @@ mod tests {
13371342
#[cfg(all(feature = "trace", feature = "http-json"))]
13381343
#[test]
13391344
fn test_build_trace_export_body_json_protocol() {
1345+
use opentelemetry_sdk::trace::SpanBatch;
1346+
13401347
let client = create_test_client(crate::Protocol::HttpJson, None);
13411348
let span_data = create_test_span_data();
13421349

1343-
let result = client.build_trace_export_body(vec![span_data]).unwrap();
1350+
let result = client
1351+
.build_trace_export_body(SpanBatch::new(&[span_data]))
1352+
.unwrap();
13441353
let (_body, content_type, content_encoding) = result;
13451354

13461355
assert_eq!(content_type, "application/json");
@@ -1350,11 +1359,15 @@ mod tests {
13501359
#[cfg(all(feature = "trace", feature = "gzip-http"))]
13511360
#[test]
13521361
fn test_build_trace_export_body_with_compression() {
1362+
use opentelemetry_sdk::trace::SpanBatch;
1363+
13531364
let client =
13541365
create_test_client(crate::Protocol::HttpBinary, Some(crate::Compression::Gzip));
13551366
let span_data = create_test_span_data();
13561367

1357-
let result = client.build_trace_export_body(vec![span_data]).unwrap();
1368+
let result = client
1369+
.build_trace_export_body(SpanBatch::new(&[span_data]))
1370+
.unwrap();
13581371
let (_body, content_type, content_encoding) = result;
13591372

13601373
assert_eq!(content_type, "application/x-protobuf");

opentelemetry-otlp/src/exporter/http/trace.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ use crate::Protocol;
33
use opentelemetry::{otel_debug, otel_warn};
44
use opentelemetry_sdk::{
55
error::{OTelSdkError, OTelSdkResult},
6-
trace::{SpanData, SpanExporter},
6+
trace::{SpanBatch, SpanExporter},
77
};
88
#[cfg(feature = "http-proto")]
99
use prost::Message;
1010

1111
impl SpanExporter for OtlpHttpClient {
12-
async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
12+
async fn export(&self, batch: SpanBatch<'_>) -> OTelSdkResult {
1313
let response_body = self
1414
.export_http_with_retry(
1515
batch,

opentelemetry-otlp/src/exporter/tonic/trace.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_sc
99
use opentelemetry_sdk::error::OTelSdkError;
1010
use opentelemetry_sdk::{
1111
error::OTelSdkResult,
12-
trace::{SpanData, SpanExporter},
12+
trace::{SpanBatch, SpanExporter},
1313
};
1414
use tonic::{codegen::CompressionEncoding, service::Interceptor, transport::Channel, Request};
1515

@@ -71,8 +71,8 @@ impl TonicTracesClient {
7171
}
7272

7373
impl SpanExporter for TonicTracesClient {
74-
async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
75-
let batch = Arc::new(batch);
74+
async fn export(&self, batch: SpanBatch<'_>) -> OTelSdkResult {
75+
let batch: Arc<Vec<_>> = Arc::new(batch.iter().cloned().collect());
7676

7777
match super::tonic_retry_with_backoff(
7878
#[cfg(feature = "experimental-grpc-retry")]
@@ -108,7 +108,7 @@ impl SpanExporter for TonicTracesClient {
108108
})?;
109109

110110
let resource_spans =
111-
group_spans_by_resource_and_scope((*batch_clone).clone(), &self.resource);
111+
group_spans_by_resource_and_scope(&batch_clone, &self.resource);
112112

113113
otel_debug!(name: "TonicTracesClient.ExportStarted");
114114

opentelemetry-otlp/src/span.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use std::fmt::Debug;
66

77
use opentelemetry_sdk::error::OTelSdkResult;
8-
use opentelemetry_sdk::trace::SpanData;
8+
use opentelemetry_sdk::trace::SpanBatch;
99

1010
use crate::ExporterBuildError;
1111
#[cfg(feature = "grpc-tonic")]
@@ -170,7 +170,7 @@ impl SpanExporter {
170170
}
171171

172172
impl opentelemetry_sdk::trace::SpanExporter for SpanExporter {
173-
async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
173+
async fn export(&self, batch: SpanBatch<'_>) -> OTelSdkResult {
174174
match &self.client {
175175
#[cfg(feature = "grpc-tonic")]
176176
SupportedTransportClient::Tonic(client) => client.export(batch).await,

opentelemetry-proto/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## vNext
44

5+
- Added borrowing conversions (`From<&SpanData>`, `From<&Link>`) for trace proto types to support zero-copy span export. Changed `group_spans_by_resource_and_scope` to accept `&[SpanData]` instead of `Vec<SpanData>`.
56
- Updated `schemars` dependency to version 1.0.0.
67

78
## 0.31.0

opentelemetry-proto/src/transform/trace.rs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,71 @@ pub mod tonic {
6161
}
6262
}
6363
}
64+
65+
impl From<&Link> for span::Link {
66+
fn from(link: &Link) -> Self {
67+
span::Link {
68+
trace_id: link.span_context.trace_id().to_bytes().to_vec(),
69+
span_id: link.span_context.span_id().to_bytes().to_vec(),
70+
trace_state: link.span_context.trace_state().header(),
71+
attributes: Attributes::from(link.attributes.iter().cloned()).0,
72+
dropped_attributes_count: link.dropped_attributes_count,
73+
flags: super::build_span_flags(
74+
link.span_context.is_remote(),
75+
link.span_context.trace_flags().to_u8() as u32,
76+
),
77+
}
78+
}
79+
}
80+
81+
impl From<&opentelemetry_sdk::trace::SpanData> for Span {
82+
fn from(source_span: &opentelemetry_sdk::trace::SpanData) -> Self {
83+
let span_kind: span::SpanKind = source_span.span_kind.clone().into();
84+
Span {
85+
trace_id: source_span.span_context.trace_id().to_bytes().to_vec(),
86+
span_id: source_span.span_context.span_id().to_bytes().to_vec(),
87+
trace_state: source_span.span_context.trace_state().header(),
88+
parent_span_id: {
89+
if source_span.parent_span_id != SpanId::INVALID {
90+
source_span.parent_span_id.to_bytes().to_vec()
91+
} else {
92+
vec![]
93+
}
94+
},
95+
flags: super::build_span_flags(
96+
source_span.parent_span_is_remote,
97+
source_span.span_context.trace_flags().to_u8() as u32,
98+
),
99+
name: source_span.name.to_string(),
100+
kind: span_kind as i32,
101+
start_time_unix_nano: to_nanos(source_span.start_time),
102+
end_time_unix_nano: to_nanos(source_span.end_time),
103+
dropped_attributes_count: source_span.dropped_attributes_count,
104+
attributes: Attributes::from(source_span.attributes.iter().cloned()).0,
105+
dropped_events_count: source_span.events.dropped_count,
106+
events: source_span
107+
.events
108+
.iter()
109+
.map(|event| span::Event {
110+
time_unix_nano: to_nanos(event.timestamp),
111+
name: event.name.to_string(),
112+
attributes: Attributes::from(event.attributes.iter().cloned()).0,
113+
dropped_attributes_count: event.dropped_attributes_count,
114+
})
115+
.collect(),
116+
dropped_links_count: source_span.links.dropped_count,
117+
links: source_span.links.iter().map(Into::into).collect(),
118+
status: Some(Status {
119+
code: status::StatusCode::from(&source_span.status).into(),
120+
message: match &source_span.status {
121+
trace::Status::Error { description } => description.to_string(),
122+
_ => Default::default(),
123+
},
124+
}),
125+
}
126+
}
127+
}
128+
64129
impl From<opentelemetry_sdk::trace::SpanData> for Span {
65130
fn from(source_span: opentelemetry_sdk::trace::SpanData) -> Self {
66131
let span_kind: span::SpanKind = source_span.span_kind.into();
@@ -174,7 +239,7 @@ pub mod tonic {
174239
}
175240

176241
pub fn group_spans_by_resource_and_scope(
177-
spans: Vec<SpanData>,
242+
spans: &[SpanData],
178243
resource: &ResourceAttributesWithSchema,
179244
) -> Vec<ResourceSpans> {
180245
// Group spans by their instrumentation scope
@@ -198,7 +263,7 @@ pub mod tonic {
198263
.unwrap_or_default(),
199264
spans: span_records
200265
.into_iter()
201-
.map(|span_data| span_data.clone().into())
266+
.map(|span_data| span_data.into())
202267
.collect(),
203268
})
204269
.collect();
@@ -364,7 +429,7 @@ mod tests {
364429
let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema
365430

366431
let grouped_spans =
367-
crate::transform::trace::tonic::group_spans_by_resource_and_scope(spans, &resource);
432+
crate::transform::trace::tonic::group_spans_by_resource_and_scope(&spans, &resource);
368433

369434
assert_eq!(grouped_spans.len(), 1);
370435

@@ -413,7 +478,7 @@ mod tests {
413478
let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema
414479

415480
let grouped_spans =
416-
crate::transform::trace::tonic::group_spans_by_resource_and_scope(spans, &resource);
481+
crate::transform::trace::tonic::group_spans_by_resource_and_scope(&spans, &resource);
417482

418483
assert_eq!(grouped_spans.len(), 1);
419484

@@ -507,10 +572,9 @@ mod tests {
507572
};
508573

509574
let resource: ResourceAttributesWithSchema = (&resource).into();
510-
let grouped_spans = crate::transform::trace::tonic::group_spans_by_resource_and_scope(
511-
vec![span_data],
512-
&resource,
513-
);
575+
let spans = vec![span_data];
576+
let grouped_spans =
577+
crate::transform::trace::tonic::group_spans_by_resource_and_scope(&spans, &resource);
514578

515579
assert_eq!(grouped_spans.len(), 1);
516580
let resource_spans = &grouped_spans[0];

opentelemetry-sdk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## vNext
44

5+
- **Breaking** `SpanExporter::export` now takes `SpanBatch<'_>` instead of `Vec<SpanData>`, matching the borrowed `LogBatch` pattern used in the logs pipeline. This eliminates per-export `Vec` allocations in `BatchSpanProcessor`. Implementors should update their `export` signatures and use `batch.iter()` to access spans.
56
- 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`).
67
- `Aggregation` enum and `StreamBuilder::with_aggregation()` are now stable and no longer require the `spec_unstable_metrics_views` feature flag.
78
- Fix `service.name` Resource attribute fallback to follow OpenTelemetry

opentelemetry-sdk/benches/context.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use opentelemetry::{
99
};
1010
use opentelemetry_sdk::{
1111
error::OTelSdkResult,
12-
trace::{Sampler, SdkTracerProvider, SpanData, SpanExporter},
12+
trace::{Sampler, SdkTracerProvider, SpanBatch, SpanExporter},
1313
};
1414
#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))]
1515
use pprof::criterion::{Output, PProfProfiler};
@@ -166,7 +166,7 @@ fn parent_sampled_tracer(inner_sampler: Sampler) -> (SdkTracerProvider, BoxedTra
166166
struct NoopExporter;
167167

168168
impl SpanExporter for NoopExporter {
169-
async fn export(&self, _spans: Vec<SpanData>) -> OTelSdkResult {
169+
async fn export(&self, _spans: SpanBatch<'_>) -> OTelSdkResult {
170170
Ok(())
171171
}
172172
}

opentelemetry-sdk/benches/span.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use opentelemetry::{
2727
};
2828
use opentelemetry_sdk::{
2929
error::OTelSdkResult,
30-
trace::{self as sdktrace, SpanData, SpanExporter},
30+
trace::{self as sdktrace, SpanBatch, SpanExporter},
3131
};
3232
#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))]
3333
use pprof::criterion::{Output, PProfProfiler};
@@ -155,7 +155,7 @@ fn criterion_benchmark(c: &mut Criterion) {
155155
struct VoidExporter;
156156

157157
impl SpanExporter for VoidExporter {
158-
async fn export(&self, _spans: Vec<SpanData>) -> OTelSdkResult {
158+
async fn export(&self, _spans: SpanBatch<'_>) -> OTelSdkResult {
159159
Ok(())
160160
}
161161
}

0 commit comments

Comments
 (0)