From 031b40e773e53ac65599fec4e5ec0503c4b00d63 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 6 May 2026 17:13:44 +0200 Subject: [PATCH 01/15] Add `TracerSpan` FFI for field-by-field span construction Introduce an opaque `TracerSpan` handle wrapping `Span` and expose it to C callers via four FFI functions: - `ddog_tracer_span_new`: create a span with all scalar fields - `ddog_tracer_span_free`: release the span - `ddog_tracer_span_set_meta`: add a string tag - `ddog_tracer_span_set_metric`: add a numeric tag This enables language tracers (e.g. Ruby) to build spans field-by-field through the C API, bypassing msgpack serialization on the caller side. --- libdd-data-pipeline-ffi/Cargo.toml | 1 + libdd-data-pipeline-ffi/cbindgen.toml | 6 +- libdd-data-pipeline-ffi/src/lib.rs | 1 + libdd-data-pipeline-ffi/src/tracer.rs | 359 ++++++++++++++++++++++++++ 4 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 libdd-data-pipeline-ffi/src/tracer.rs diff --git a/libdd-data-pipeline-ffi/Cargo.toml b/libdd-data-pipeline-ffi/Cargo.toml index 662374a73e..758788ae90 100644 --- a/libdd-data-pipeline-ffi/Cargo.toml +++ b/libdd-data-pipeline-ffi/Cargo.toml @@ -35,4 +35,5 @@ libdd-data-pipeline = { path = "../libdd-data-pipeline" } libdd-shared-runtime = { version = "1.0.0", path = "../libdd-shared-runtime" } libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } libdd-tinybytes = { path = "../libdd-tinybytes" } +libdd-trace-utils = { path = "../libdd-trace-utils" } tracing = { version = "0.1", default-features = false } diff --git a/libdd-data-pipeline-ffi/cbindgen.toml b/libdd-data-pipeline-ffi/cbindgen.toml index 63c23705ca..cfe5d691d3 100644 --- a/libdd-data-pipeline-ffi/cbindgen.toml +++ b/libdd-data-pipeline-ffi/cbindgen.toml @@ -11,12 +11,13 @@ include_guard = "DDOG_DATA_PIPELINE_H" includes = ["common.h"] after_includes = """ typedef struct ddog_TraceExporter ddog_TraceExporter; +typedef struct ddog_TracerSpan ddog_TracerSpan; """ [export] prefix = "ddog_" renaming_overrides_prefixing = true -exclude = ["TraceExporter"] +exclude = ["TraceExporter", "TracerSpan"] [export.rename] "ByteSlice" = "ddog_ByteSlice" @@ -27,6 +28,7 @@ exclude = ["TraceExporter"] "ExporterResponse" = "ddog_TraceExporterResponse" "ExporterErrorCode" = "ddog_TraceExporterErrorCode" "ExporterError" = "ddog_TraceExporterError" +"TracerSpan" = "ddog_TracerSpan" [export.mangle] rename_types = "PascalCase" @@ -40,4 +42,4 @@ must_use = "DDOG_CHECK_RETURN" [parse] parse_deps = true -include = ["libdd-common", "libdd-common-ffi", "libdd-shared-runtime", "libdd-data-pipeline"] +include = ["libdd-common", "libdd-common-ffi", "libdd-shared-runtime", "libdd-data-pipeline", "libdd-trace-utils"] diff --git a/libdd-data-pipeline-ffi/src/lib.rs b/libdd-data-pipeline-ffi/src/lib.rs index f060476120..c8f594f391 100644 --- a/libdd-data-pipeline-ffi/src/lib.rs +++ b/libdd-data-pipeline-ffi/src/lib.rs @@ -9,6 +9,7 @@ mod error; mod response; mod trace_exporter; +mod tracer; #[cfg(all(feature = "catch_panic", panic = "unwind"))] macro_rules! catch_panic { diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs new file mode 100644 index 0000000000..bc7855198d --- /dev/null +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -0,0 +1,359 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! FFI functions for creating and manipulating individual tracer spans. +//! +//! Provides an opaque [`TracerSpan`] handle wrapping a `Span`, +//! allowing callers to construct spans field-by-field from C. + +use crate::error::{ExporterError, ExporterErrorCode as ErrorCode}; +use crate::{catch_panic, gen_error}; +use libdd_common_ffi::slice::AsBytes; +use libdd_common_ffi::CharSlice; +use libdd_tinybytes::BytesString; +use libdd_trace_utils::span::v04::SpanBytes; +use std::ptr::NonNull; + +// --------------------------------------------------------------------------- +// Helper +// --------------------------------------------------------------------------- + +/// Convert a [`CharSlice`] to a [`BytesString`], copying the bytes. +/// +/// Returns an error if the slice is not valid UTF-8. +#[inline] +fn charslice_to_bytesstring(s: CharSlice) -> Result> { + match BytesString::from_slice(s.as_bytes()) { + Ok(bs) => Ok(bs), + Err(_) => Err(Box::new(ExporterError::new( + ErrorCode::InvalidInput, + &ErrorCode::InvalidInput.to_string(), + ))), + } +} + +// --------------------------------------------------------------------------- +// TracerSpan +// --------------------------------------------------------------------------- + +/// Opaque handle wrapping a single `Span`. +pub struct TracerSpan(pub(crate) SpanBytes); + +/// Create a new span with all scalar fields set. +/// +/// String fields are copied from the provided slices. The `meta` and +/// `metrics` maps start empty; use [`ddog_tracer_span_set_meta`] and +/// [`ddog_tracer_span_set_metric`] to populate them. +/// +/// # Arguments +/// +/// * `out_handle` – Receives the new `TracerSpan` handle on success. +/// * `service`, `name`, `resource`, `span_type` – UTF-8 string fields. +/// * `trace_id_low`, `trace_id_high` – 128-bit trace ID split into two 64-bit halves (low = bits +/// 0‥63, high = bits 64‥127). +/// * `span_id` – Span identifier. +/// * `parent_id` – Parent span identifier (0 for root spans). +/// * `start` – Start time in nanoseconds since Unix epoch. +/// * `duration` – Duration in nanoseconds. +/// * `error` – Error status (0 = no error). +/// +/// # Safety +/// +/// `out_handle` must point to valid, writable memory for a `Box`. +/// All `CharSlice` arguments must point to valid memory for their stated +/// length. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_new( + out_handle: NonNull>, + service: CharSlice, + name: CharSlice, + resource: CharSlice, + span_type: CharSlice, + trace_id_low: u64, + trace_id_high: u64, + span_id: u64, + parent_id: u64, + start: i64, + duration: i64, + error: i32, +) -> Option> { + catch_panic!( + { + let service = match charslice_to_bytesstring(service) { + Ok(s) => s, + Err(e) => return Some(e), + }; + let name = match charslice_to_bytesstring(name) { + Ok(s) => s, + Err(e) => return Some(e), + }; + let resource = match charslice_to_bytesstring(resource) { + Ok(s) => s, + Err(e) => return Some(e), + }; + let span_type = match charslice_to_bytesstring(span_type) { + Ok(s) => s, + Err(e) => return Some(e), + }; + + let trace_id: u128 = ((trace_id_high as u128) << 64) | (trace_id_low as u128); + + let span = SpanBytes { + service, + name, + resource, + r#type: span_type, + trace_id, + span_id, + parent_id, + start, + duration, + error, + ..Default::default() + }; + + out_handle.as_ptr().write(Box::new(TracerSpan(span))); + None + }, + gen_error!(ErrorCode::Panic) + ) +} + +/// Free a `TracerSpan` and all its contents. +/// +/// After this call the handle is invalid and must not be reused. +/// +/// # Safety +/// +/// `handle` must have been created by [`ddog_tracer_span_new`] and must not +/// be used after this call. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_free(handle: Box) { + drop(handle); +} + +/// Add or overwrite a string tag (`meta`) on the span. +/// +/// Both `key` and `value` are copied into the span. +/// +/// # Safety +/// +/// `handle` must be a valid pointer to a `TracerSpan`. +/// `key` and `value` must point to valid UTF-8 memory. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_set_meta( + handle: Option<&mut TracerSpan>, + key: CharSlice, + value: CharSlice, +) -> Option> { + catch_panic!( + if let Some(span) = handle { + let key = match charslice_to_bytesstring(key) { + Ok(s) => s, + Err(e) => return Some(e), + }; + let value = match charslice_to_bytesstring(value) { + Ok(s) => s, + Err(e) => return Some(e), + }; + span.0.meta.insert(key, value); + None + } else { + gen_error!(ErrorCode::InvalidArgument) + }, + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a numeric tag (`metric`) on the span. +/// +/// The `key` is copied into the span. +/// +/// # Safety +/// +/// `handle` must be a valid pointer to a `TracerSpan`. +/// `key` must point to valid UTF-8 memory. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_set_metric( + handle: Option<&mut TracerSpan>, + key: CharSlice, + value: f64, +) -> Option> { + catch_panic!( + if let Some(span) = handle { + let key = match charslice_to_bytesstring(key) { + Ok(s) => s, + Err(e) => return Some(e), + }; + span.0.metrics.insert(key, value); + None + } else { + gen_error!(ErrorCode::InvalidArgument) + }, + gen_error!(ErrorCode::Panic) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::ddog_trace_exporter_error_free; + use std::mem::MaybeUninit; + + fn cs(s: &str) -> CharSlice<'_> { + CharSlice::from_bytes(s.as_bytes()) + } + + unsafe fn make_minimal_span() -> Box { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = ddog_tracer_span_new( + out, + cs("svc"), + cs("op"), + cs("res"), + cs(""), + 1, + 0, + 1, + 0, + 0, + 0, + 0, + ); + assert!(err.is_none()); + handle.assume_init() + } + + #[test] + fn new_sets_all_scalar_fields() { + unsafe { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + + let err = ddog_tracer_span_new( + out, + cs("my-service"), + cs("web.request"), + cs("GET /users"), + cs("web"), + 0xdeadbeef, // trace_id_low + 0x00000001, // trace_id_high + 12345, // span_id + 67890, // parent_id + 1_700_000_000_000_000_000i64, // start (ns) + 25_000_000, // duration (25 ms) + 0, // error + ); + assert!(err.is_none()); + + let span = handle.assume_init(); + assert_eq!(span.0.service.as_ref(), "my-service"); + assert_eq!(span.0.name.as_ref(), "web.request"); + assert_eq!(span.0.resource.as_ref(), "GET /users"); + assert_eq!(span.0.r#type.as_ref(), "web"); + assert_eq!(span.0.trace_id, (1u128 << 64) | 0xdeadbeef); + assert_eq!(span.0.span_id, 12345); + assert_eq!(span.0.parent_id, 67890); + assert_eq!(span.0.start, 1_700_000_000_000_000_000); + assert_eq!(span.0.duration, 25_000_000); + assert_eq!(span.0.error, 0); + assert!(span.0.meta.is_empty()); + assert!(span.0.metrics.is_empty()); + assert!(span.0.span_links.is_empty()); + assert!(span.0.span_events.is_empty()); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_inserts_entries() { + unsafe { + let mut span = make_minimal_span(); + + let err = ddog_tracer_span_set_meta(Some(&mut *span), cs("http.method"), cs("GET")); + assert!(err.is_none()); + + let err = ddog_tracer_span_set_meta(Some(&mut *span), cs("http.url"), cs("/users")); + assert!(err.is_none()); + + assert_eq!(span.0.meta.len(), 2); + assert_eq!(span.0.meta.get("http.method").unwrap().as_ref(), "GET"); + assert_eq!(span.0.meta.get("http.url").unwrap().as_ref(), "/users"); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_overwrites_existing_key() { + unsafe { + let mut span = make_minimal_span(); + + ddog_tracer_span_set_meta(Some(&mut *span), cs("k"), cs("v1")); + ddog_tracer_span_set_meta(Some(&mut *span), cs("k"), cs("v2")); + + assert_eq!(span.0.meta.len(), 1); + assert_eq!(span.0.meta.get("k").unwrap().as_ref(), "v2"); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_metric_inserts_entries() { + unsafe { + let mut span = make_minimal_span(); + + let err = ddog_tracer_span_set_metric(Some(&mut *span), cs("_dd.measured"), 1.0); + assert!(err.is_none()); + + let err = + ddog_tracer_span_set_metric(Some(&mut *span), cs("_sampling_priority_v1"), 2.0); + assert!(err.is_none()); + + assert_eq!(span.0.metrics.len(), 2); + assert_eq!(*span.0.metrics.get("_dd.measured").unwrap(), 1.0); + assert_eq!(*span.0.metrics.get("_sampling_priority_v1").unwrap(), 2.0); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_null_handle_returns_error() { + unsafe { + let err = ddog_tracer_span_set_meta(None, cs("k"), cs("v")); + assert!(err.is_some()); + ddog_trace_exporter_error_free(err); + } + } + + #[test] + fn set_metric_null_handle_returns_error() { + unsafe { + let err = ddog_tracer_span_set_metric(None, cs("k"), 1.0); + assert!(err.is_some()); + ddog_trace_exporter_error_free(err); + } + } + + #[test] + fn new_with_empty_strings_succeeds() { + unsafe { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + + let err = + ddog_tracer_span_new(out, cs(""), cs(""), cs(""), cs(""), 0, 0, 0, 0, 0, 0, 0); + assert!(err.is_none()); + + let span = handle.assume_init(); + assert_eq!(span.0.name.as_ref(), ""); + assert_eq!(span.0.service.as_ref(), ""); + + ddog_tracer_span_free(span); + } + } +} From 1d7879d0676c3c5e07e8f4af470630245a6e894d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 6 May 2026 17:17:04 +0200 Subject: [PATCH 02/15] Add `TracerTraceChunks` FFI for grouping spans into traces Introduce an opaque `TracerTraceChunks` handle wrapping `Vec>` and expose it to C callers via four FFI functions: - `ddog_tracer_trace_chunks_new`: create a container with optional capacity hint - `ddog_tracer_trace_chunks_free`: release the container - `ddog_tracer_trace_chunks_begin_chunk`: start a new trace chunk - `ddog_tracer_trace_chunks_push_span`: move a `TracerSpan` into the current chunk, consuming it Spans are consumed on push to enforce single-ownership and prevent double-use from C callers. --- libdd-data-pipeline-ffi/cbindgen.toml | 4 +- libdd-data-pipeline-ffi/src/tracer.rs | 182 +++++++++++++++++++++++++- 2 files changed, 182 insertions(+), 4 deletions(-) diff --git a/libdd-data-pipeline-ffi/cbindgen.toml b/libdd-data-pipeline-ffi/cbindgen.toml index cfe5d691d3..cf72c6e758 100644 --- a/libdd-data-pipeline-ffi/cbindgen.toml +++ b/libdd-data-pipeline-ffi/cbindgen.toml @@ -12,12 +12,13 @@ includes = ["common.h"] after_includes = """ typedef struct ddog_TraceExporter ddog_TraceExporter; typedef struct ddog_TracerSpan ddog_TracerSpan; +typedef struct ddog_TracerTraceChunks ddog_TracerTraceChunks; """ [export] prefix = "ddog_" renaming_overrides_prefixing = true -exclude = ["TraceExporter", "TracerSpan"] +exclude = ["TraceExporter", "TracerSpan", "TracerTraceChunks"] [export.rename] "ByteSlice" = "ddog_ByteSlice" @@ -29,6 +30,7 @@ exclude = ["TraceExporter", "TracerSpan"] "ExporterErrorCode" = "ddog_TraceExporterErrorCode" "ExporterError" = "ddog_TraceExporterError" "TracerSpan" = "ddog_TracerSpan" +"TracerTraceChunks" = "ddog_TracerTraceChunks" [export.mangle] rename_types = "PascalCase" diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index bc7855198d..7fbbcb1a86 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -1,10 +1,13 @@ // Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! FFI functions for creating and manipulating individual tracer spans. +//! FFI functions for creating and manipulating tracer spans and trace chunks. //! -//! Provides an opaque [`TracerSpan`] handle wrapping a `Span`, -//! allowing callers to construct spans field-by-field from C. +//! Provides opaque handles for building trace data from C: +//! +//! - [`TracerSpan`] wraps a single `Span`, constructed field-by-field. +//! - [`TracerTraceChunks`] wraps `Vec>`, grouping spans into trace chunks ready for +//! export. use crate::error::{ExporterError, ExporterErrorCode as ErrorCode}; use crate::{catch_panic, gen_error}; @@ -194,6 +197,102 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( ) } +// --------------------------------------------------------------------------- +// TracerTraceChunks +// --------------------------------------------------------------------------- + +/// Opaque handle wrapping `Vec>` — a list of trace chunks, +/// each containing a list of spans. +pub struct TracerTraceChunks(pub(crate) Vec>); + +/// Create a new empty trace chunks container. +/// +/// `capacity` is a hint for the expected number of chunks; pass 0 if +/// unknown. +/// +/// # Safety +/// +/// `out_handle` must point to valid, writable memory for a +/// `Box`. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_trace_chunks_new( + capacity: usize, + out_handle: NonNull>, +) -> Option> { + catch_panic!( + { + let chunks = if capacity > 0 { + Vec::with_capacity(capacity) + } else { + Vec::new() + }; + out_handle + .as_ptr() + .write(Box::new(TracerTraceChunks(chunks))); + None + }, + gen_error!(ErrorCode::Panic) + ) +} + +/// Free a trace chunks container and all its contents. +/// +/// After this call the handle is invalid and must not be reused. +/// +/// # Safety +/// +/// `handle` must have been created by [`ddog_tracer_trace_chunks_new`]. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_trace_chunks_free(handle: Box) { + drop(handle); +} + +/// Start a new chunk (trace) inside the container. +/// +/// Subsequent [`ddog_tracer_trace_chunks_push_span`] calls will append +/// spans to this chunk until the next `begin_chunk` call. +/// +/// # Safety +/// +/// `handle` must be a valid pointer to a `TracerTraceChunks`. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_trace_chunks_begin_chunk( + handle: Option<&mut TracerTraceChunks>, +) { + if let Some(chunks) = handle { + chunks.0.push(Vec::new()); + } +} + +/// Move a span into the current (last) chunk, consuming the span handle. +/// +/// A chunk must have been started with +/// [`ddog_tracer_trace_chunks_begin_chunk`] before calling this function. +/// +/// # Safety +/// +/// * `handle` must be a valid pointer to a `TracerTraceChunks`. +/// * `span` is consumed and must not be used after this call. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_trace_chunks_push_span( + handle: Option<&mut TracerTraceChunks>, + span: Box, +) -> Option> { + catch_panic!( + if let Some(chunks) = handle { + if let Some(chunk) = chunks.0.last_mut() { + chunk.push(span.0); + None + } else { + gen_error!(ErrorCode::InvalidArgument) + } + } else { + gen_error!(ErrorCode::InvalidArgument) + }, + gen_error!(ErrorCode::Panic) + ) +} + #[cfg(test)] mod tests { use super::*; @@ -356,4 +455,81 @@ mod tests { ddog_tracer_span_free(span); } } + + // -- TracerTraceChunks tests -------------------------------------------- + + unsafe fn make_chunks(capacity: usize) -> Box { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = ddog_tracer_trace_chunks_new(capacity, out); + assert!(err.is_none()); + handle.assume_init() + } + + #[test] + fn trace_chunks_build_and_push() { + unsafe { + let mut chunks = make_chunks(2); + + // Chunk 1: two spans + ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks)); + + let s1 = make_minimal_span(); + let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s1); + assert!(err.is_none()); + + let s2 = make_minimal_span(); + let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s2); + assert!(err.is_none()); + + // Chunk 2: one span + ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks)); + let s3 = make_minimal_span(); + let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s3); + assert!(err.is_none()); + + assert_eq!(chunks.0.len(), 2); + assert_eq!(chunks.0[0].len(), 2); + assert_eq!(chunks.0[1].len(), 1); + + ddog_tracer_trace_chunks_free(chunks); + } + } + + #[test] + fn push_span_without_begin_chunk_returns_error() { + unsafe { + let mut chunks = make_chunks(0); + + // No begin_chunk — push should fail + let s = make_minimal_span(); + let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s); + assert!(err.is_some()); + ddog_trace_exporter_error_free(err); + + ddog_tracer_trace_chunks_free(chunks); + } + } + + #[test] + fn trace_chunks_empty_is_valid() { + unsafe { + let chunks = make_chunks(0); + assert_eq!(chunks.0.len(), 0); + ddog_tracer_trace_chunks_free(chunks); + } + } + + #[test] + fn trace_chunks_empty_chunk_is_valid() { + unsafe { + let mut chunks = make_chunks(1); + ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks)); + + assert_eq!(chunks.0.len(), 1); + assert_eq!(chunks.0[0].len(), 0); + + ddog_tracer_trace_chunks_free(chunks); + } + } } From 977c75f37186989b37032c8264dab998edc3f7b6 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 6 May 2026 17:18:51 +0200 Subject: [PATCH 03/15] Add `ddog_trace_exporter_send_trace_chunks` FFI function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire `TracerTraceChunks` to the existing `TraceExporter::send_trace_chunks` method through a new C-callable function. The chunks are consumed on call and the agent response is optionally written to an out-parameter. This completes the span-building FFI surface: callers can now construct spans via `ddog_tracer_span_*`, group them via `ddog_tracer_trace_chunks_*`, and send them via `ddog_trace_exporter_send_trace_chunks` — all without serializing to msgpack on the caller side. The `TraceExporter` type alias in `trace_exporter.rs` is widened to `pub(crate)` to allow cross-module access. --- libdd-data-pipeline-ffi/src/trace_exporter.rs | 2 +- libdd-data-pipeline-ffi/src/tracer.rs | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/libdd-data-pipeline-ffi/src/trace_exporter.rs b/libdd-data-pipeline-ffi/src/trace_exporter.rs index 29795ca144..5271c86e63 100644 --- a/libdd-data-pipeline-ffi/src/trace_exporter.rs +++ b/libdd-data-pipeline-ffi/src/trace_exporter.rs @@ -14,7 +14,7 @@ use libdd_data_pipeline::trace_exporter::{ TraceExporterInputFormat, TraceExporterOutputFormat, }; -type TraceExporter = GenericTraceExporter; +pub(crate) type TraceExporter = GenericTraceExporter; use libdd_shared_runtime::SharedRuntime; use std::{ptr::NonNull, sync::Arc, time::Duration}; diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 7fbbcb1a86..b3805aa694 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -10,6 +10,8 @@ //! export. use crate::error::{ExporterError, ExporterErrorCode as ErrorCode}; +use crate::response::ExporterResponse; +use crate::trace_exporter::TraceExporter; use crate::{catch_panic, gen_error}; use libdd_common_ffi::slice::AsBytes; use libdd_common_ffi::CharSlice; @@ -293,6 +295,51 @@ pub unsafe extern "C" fn ddog_tracer_trace_chunks_push_span( ) } +// --------------------------------------------------------------------------- +// Send trace chunks +// --------------------------------------------------------------------------- + +/// Send trace chunks through a [`TraceExporter`], consuming the chunks. +/// +/// This calls `TraceExporter::send_trace_chunks` which processes stats, +/// serializes in the configured output format, and sends to the agent +/// with retry logic. +/// +/// On success, if `response_out` is non-null, a heap-allocated +/// [`ExporterResponse`] is written there. The caller owns it and must +/// free it with `ddog_trace_exporter_response_free`. +/// +/// # Safety +/// +/// * `exporter` must be a valid `TraceExporter` pointer. +/// * `chunks` is consumed and must not be used after this call. +/// * If `response_out` is non-null it must point to valid writable memory for a +/// `Box`. +#[no_mangle] +pub unsafe extern "C" fn ddog_trace_exporter_send_trace_chunks( + exporter: Option<&TraceExporter>, + chunks: Box, + response_out: Option>>, +) -> Option> { + let exporter = match exporter { + Some(e) => e, + None => return gen_error!(ErrorCode::InvalidArgument), + }; + + catch_panic!( + match exporter.send_trace_chunks(chunks.0) { + Ok(resp) => { + if let Some(out) = response_out { + out.as_ptr().write(Box::new(ExporterResponse::from(resp))); + } + None + } + Err(e) => Some(Box::new(ExporterError::from(e))), + }, + gen_error!(ErrorCode::Panic) + ) +} + #[cfg(test)] mod tests { use super::*; From ced27fb63c568df412b3521319a6c559174e6d42 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 7 May 2026 14:02:33 +0200 Subject: [PATCH 04/15] Use `map_err` in `charslice_to_bytesstring` Replace the `match` with identity `Ok` arm with `map_err`, which is the idiomatic Rust combinator for transforming only the error side of a `Result`. --- libdd-data-pipeline-ffi/src/tracer.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index b3805aa694..be26a7ddf6 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -28,13 +28,12 @@ use std::ptr::NonNull; /// Returns an error if the slice is not valid UTF-8. #[inline] fn charslice_to_bytesstring(s: CharSlice) -> Result> { - match BytesString::from_slice(s.as_bytes()) { - Ok(bs) => Ok(bs), - Err(_) => Err(Box::new(ExporterError::new( + BytesString::from_slice(s.as_bytes()).map_err(|_| { + Box::new(ExporterError::new( ErrorCode::InvalidInput, &ErrorCode::InvalidInput.to_string(), - ))), - } + )) + }) } // --------------------------------------------------------------------------- From 45900d31382b3cc2cd7a0fa667fa694b2cbd2115 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 7 May 2026 14:03:18 +0200 Subject: [PATCH 05/15] Make `TracerSpan` and `TracerTraceChunks` inner fields private The inner fields are only accessed within the `tracer` module itself, so `pub(crate)` is unnecessarily broad. Default (private) visibility is sufficient since the module owns all access. --- libdd-data-pipeline-ffi/src/tracer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index be26a7ddf6..413d820a09 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -41,7 +41,7 @@ fn charslice_to_bytesstring(s: CharSlice) -> Result`. -pub struct TracerSpan(pub(crate) SpanBytes); +pub struct TracerSpan(SpanBytes); /// Create a new span with all scalar fields set. /// @@ -204,7 +204,7 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( /// Opaque handle wrapping `Vec>` — a list of trace chunks, /// each containing a list of spans. -pub struct TracerTraceChunks(pub(crate) Vec>); +pub struct TracerTraceChunks(Vec>); /// Create a new empty trace chunks container. /// From e728408b91631cd5d374111fe5815eb700293514 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 7 May 2026 14:03:39 +0200 Subject: [PATCH 06/15] Simplify `ddog_tracer_trace_chunks_new` capacity handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Vec::with_capacity(0)` and `Vec::new()` are identical — both create an empty vec with no heap allocation — so the branch is unnecessary. --- libdd-data-pipeline-ffi/src/tracer.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 413d820a09..5f9c0aa67d 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -222,11 +222,7 @@ pub unsafe extern "C" fn ddog_tracer_trace_chunks_new( ) -> Option> { catch_panic!( { - let chunks = if capacity > 0 { - Vec::with_capacity(capacity) - } else { - Vec::new() - }; + let chunks = Vec::with_capacity(capacity); out_handle .as_ptr() .write(Box::new(TracerTraceChunks(chunks))); From 19bf03e51346fed72570de456fb343720c417c7a Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 7 May 2026 14:04:00 +0200 Subject: [PATCH 07/15] Use `let-else` for null exporter check in `ddog_trace_exporter_send_trace_chunks` Replace the `match` with `let Some(...) = ... else { return ... }`, which is the idiomatic Rust pattern for early-return on a refutable binding (stabilized in Rust 1.65). --- libdd-data-pipeline-ffi/src/tracer.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 5f9c0aa67d..798478f0ec 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -316,9 +316,8 @@ pub unsafe extern "C" fn ddog_trace_exporter_send_trace_chunks( chunks: Box, response_out: Option>>, ) -> Option> { - let exporter = match exporter { - Some(e) => e, - None => return gen_error!(ErrorCode::InvalidArgument), + let Some(exporter) = exporter else { + return gen_error!(ErrorCode::InvalidArgument); }; catch_panic!( From b2fa5a6ec29da814c4d4d638f677c282d5c9fc24 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 7 May 2026 14:04:43 +0200 Subject: [PATCH 08/15] Make test helpers safe functions with internal `unsafe` blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make_minimal_span` and `make_chunks` have no safety preconditions for their callers — all unsafety is self-contained. Move the `unsafe` from the function signature into internal blocks so the functions present a safe interface. --- libdd-data-pipeline-ffi/src/tracer.rs | 54 ++++++++++++++------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 798478f0ec..2ce42829fb 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -344,25 +344,27 @@ mod tests { CharSlice::from_bytes(s.as_bytes()) } - unsafe fn make_minimal_span() -> Box { - let mut handle = MaybeUninit::>::uninit(); - let out = NonNull::new(handle.as_mut_ptr()).unwrap(); - let err = ddog_tracer_span_new( - out, - cs("svc"), - cs("op"), - cs("res"), - cs(""), - 1, - 0, - 1, - 0, - 0, - 0, - 0, - ); - assert!(err.is_none()); - handle.assume_init() + fn make_minimal_span() -> Box { + unsafe { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = ddog_tracer_span_new( + out, + cs("svc"), + cs("op"), + cs("res"), + cs(""), + 1, + 0, + 1, + 0, + 0, + 0, + 0, + ); + assert!(err.is_none()); + handle.assume_init() + } } #[test] @@ -499,12 +501,14 @@ mod tests { // -- TracerTraceChunks tests -------------------------------------------- - unsafe fn make_chunks(capacity: usize) -> Box { - let mut handle = MaybeUninit::>::uninit(); - let out = NonNull::new(handle.as_mut_ptr()).unwrap(); - let err = ddog_tracer_trace_chunks_new(capacity, out); - assert!(err.is_none()); - handle.assume_init() + fn make_chunks(capacity: usize) -> Box { + unsafe { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = ddog_tracer_trace_chunks_new(capacity, out); + assert!(err.is_none()); + handle.assume_init() + } } #[test] From 61a13fe0beae1f019bf95f92e822e6d2cea77d4d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 7 May 2026 14:08:13 +0200 Subject: [PATCH 09/15] Use inner closure with `?` operator in `ddog_tracer_span_new` Extract the span-building logic into an inner closure that returns `Result`, allowing the `?` operator to propagate conversion errors. The outer body calls `inner().err()` to map `Ok(()) -> None` and `Err(e) -> Some(e)`, matching the FFI return type. --- libdd-data-pipeline-ffi/src/tracer.rs | 59 ++++++++++++--------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 2ce42829fb..7182a1130b 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -83,41 +83,32 @@ pub unsafe extern "C" fn ddog_tracer_span_new( ) -> Option> { catch_panic!( { - let service = match charslice_to_bytesstring(service) { - Ok(s) => s, - Err(e) => return Some(e), - }; - let name = match charslice_to_bytesstring(name) { - Ok(s) => s, - Err(e) => return Some(e), - }; - let resource = match charslice_to_bytesstring(resource) { - Ok(s) => s, - Err(e) => return Some(e), + let inner = || -> Result<(), Box> { + let service = charslice_to_bytesstring(service)?; + let name = charslice_to_bytesstring(name)?; + let resource = charslice_to_bytesstring(resource)?; + let span_type = charslice_to_bytesstring(span_type)?; + + let trace_id: u128 = ((trace_id_high as u128) << 64) | (trace_id_low as u128); + + let span = SpanBytes { + service, + name, + resource, + r#type: span_type, + trace_id, + span_id, + parent_id, + start, + duration, + error, + ..Default::default() + }; + + out_handle.as_ptr().write(Box::new(TracerSpan(span))); + Ok(()) }; - let span_type = match charslice_to_bytesstring(span_type) { - Ok(s) => s, - Err(e) => return Some(e), - }; - - let trace_id: u128 = ((trace_id_high as u128) << 64) | (trace_id_low as u128); - - let span = SpanBytes { - service, - name, - resource, - r#type: span_type, - trace_id, - span_id, - parent_id, - start, - duration, - error, - ..Default::default() - }; - - out_handle.as_ptr().write(Box::new(TracerSpan(span))); - None + inner().err() }, gen_error!(ErrorCode::Panic) ) From e709e7aa4b6a02ce626513e3611e05a4d661e000 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 7 May 2026 14:33:17 +0200 Subject: [PATCH 10/15] Use `&mut MaybeUninit>` for out-parameters Replace `NonNull>` with `&mut MaybeUninit>` for out-parameters in `ddog_tracer_span_new` and `ddog_tracer_trace_chunks_new`. This makes the uninitialized nature of the slot explicit in the type system and allows using `MaybeUninit::write` instead of raw `ptr::write`. Note that `&mut Box` cannot be used here: assignment through `&mut Box` drops the old value first, which is undefined behavior when the slot is uninitialized. `MaybeUninit::write` overwrites without dropping, which is the correct semantics for out-parameters. cbindgen produces identical C signatures (`T **out_handle`) for all three representations. --- libdd-data-pipeline-ffi/src/tracer.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 7182a1130b..088fa2e5bd 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -17,6 +17,7 @@ use libdd_common_ffi::slice::AsBytes; use libdd_common_ffi::CharSlice; use libdd_tinybytes::BytesString; use libdd_trace_utils::span::v04::SpanBytes; +use std::mem::MaybeUninit; use std::ptr::NonNull; // --------------------------------------------------------------------------- @@ -68,7 +69,7 @@ pub struct TracerSpan(SpanBytes); /// length. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_new( - out_handle: NonNull>, + out_handle: &mut MaybeUninit>, service: CharSlice, name: CharSlice, resource: CharSlice, @@ -83,7 +84,7 @@ pub unsafe extern "C" fn ddog_tracer_span_new( ) -> Option> { catch_panic!( { - let inner = || -> Result<(), Box> { + let mut inner = || -> Result<(), Box> { let service = charslice_to_bytesstring(service)?; let name = charslice_to_bytesstring(name)?; let resource = charslice_to_bytesstring(resource)?; @@ -105,7 +106,7 @@ pub unsafe extern "C" fn ddog_tracer_span_new( ..Default::default() }; - out_handle.as_ptr().write(Box::new(TracerSpan(span))); + out_handle.write(Box::new(TracerSpan(span))); Ok(()) }; inner().err() @@ -209,14 +210,12 @@ pub struct TracerTraceChunks(Vec>); #[no_mangle] pub unsafe extern "C" fn ddog_tracer_trace_chunks_new( capacity: usize, - out_handle: NonNull>, + out_handle: &mut MaybeUninit>, ) -> Option> { catch_panic!( { let chunks = Vec::with_capacity(capacity); - out_handle - .as_ptr() - .write(Box::new(TracerTraceChunks(chunks))); + out_handle.write(Box::new(TracerTraceChunks(chunks))); None }, gen_error!(ErrorCode::Panic) @@ -338,9 +337,8 @@ mod tests { fn make_minimal_span() -> Box { unsafe { let mut handle = MaybeUninit::>::uninit(); - let out = NonNull::new(handle.as_mut_ptr()).unwrap(); let err = ddog_tracer_span_new( - out, + &mut handle, cs("svc"), cs("op"), cs("res"), @@ -362,10 +360,9 @@ mod tests { fn new_sets_all_scalar_fields() { unsafe { let mut handle = MaybeUninit::>::uninit(); - let out = NonNull::new(handle.as_mut_ptr()).unwrap(); let err = ddog_tracer_span_new( - out, + &mut handle, cs("my-service"), cs("web.request"), cs("GET /users"), @@ -476,10 +473,9 @@ mod tests { fn new_with_empty_strings_succeeds() { unsafe { let mut handle = MaybeUninit::>::uninit(); - let out = NonNull::new(handle.as_mut_ptr()).unwrap(); let err = - ddog_tracer_span_new(out, cs(""), cs(""), cs(""), cs(""), 0, 0, 0, 0, 0, 0, 0); + ddog_tracer_span_new(&mut handle, cs(""), cs(""), cs(""), cs(""), 0, 0, 0, 0, 0, 0, 0); assert!(err.is_none()); let span = handle.assume_init(); @@ -495,8 +491,7 @@ mod tests { fn make_chunks(capacity: usize) -> Box { unsafe { let mut handle = MaybeUninit::>::uninit(); - let out = NonNull::new(handle.as_mut_ptr()).unwrap(); - let err = ddog_tracer_trace_chunks_new(capacity, out); + let err = ddog_tracer_trace_chunks_new(capacity, &mut handle); assert!(err.is_none()); handle.assume_init() } From 9b72f688524233154d5846381839ce82b189dbc2 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 13 May 2026 13:11:30 +0200 Subject: [PATCH 11/15] Revert to `NonNull>` for out-parameters Restore the established `NonNull>` convention used throughout `trace_exporter.rs` for FFI out-parameters. While `&mut MaybeUninit` is technically more precise, it is less idiomatic for FFI and inconsistent with the rest of the module. cbindgen produces identical C headers for both representations. --- libdd-data-pipeline-ffi/src/tracer.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 088fa2e5bd..7182a1130b 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -17,7 +17,6 @@ use libdd_common_ffi::slice::AsBytes; use libdd_common_ffi::CharSlice; use libdd_tinybytes::BytesString; use libdd_trace_utils::span::v04::SpanBytes; -use std::mem::MaybeUninit; use std::ptr::NonNull; // --------------------------------------------------------------------------- @@ -69,7 +68,7 @@ pub struct TracerSpan(SpanBytes); /// length. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_new( - out_handle: &mut MaybeUninit>, + out_handle: NonNull>, service: CharSlice, name: CharSlice, resource: CharSlice, @@ -84,7 +83,7 @@ pub unsafe extern "C" fn ddog_tracer_span_new( ) -> Option> { catch_panic!( { - let mut inner = || -> Result<(), Box> { + let inner = || -> Result<(), Box> { let service = charslice_to_bytesstring(service)?; let name = charslice_to_bytesstring(name)?; let resource = charslice_to_bytesstring(resource)?; @@ -106,7 +105,7 @@ pub unsafe extern "C" fn ddog_tracer_span_new( ..Default::default() }; - out_handle.write(Box::new(TracerSpan(span))); + out_handle.as_ptr().write(Box::new(TracerSpan(span))); Ok(()) }; inner().err() @@ -210,12 +209,14 @@ pub struct TracerTraceChunks(Vec>); #[no_mangle] pub unsafe extern "C" fn ddog_tracer_trace_chunks_new( capacity: usize, - out_handle: &mut MaybeUninit>, + out_handle: NonNull>, ) -> Option> { catch_panic!( { let chunks = Vec::with_capacity(capacity); - out_handle.write(Box::new(TracerTraceChunks(chunks))); + out_handle + .as_ptr() + .write(Box::new(TracerTraceChunks(chunks))); None }, gen_error!(ErrorCode::Panic) @@ -337,8 +338,9 @@ mod tests { fn make_minimal_span() -> Box { unsafe { let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); let err = ddog_tracer_span_new( - &mut handle, + out, cs("svc"), cs("op"), cs("res"), @@ -360,9 +362,10 @@ mod tests { fn new_sets_all_scalar_fields() { unsafe { let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); let err = ddog_tracer_span_new( - &mut handle, + out, cs("my-service"), cs("web.request"), cs("GET /users"), @@ -473,9 +476,10 @@ mod tests { fn new_with_empty_strings_succeeds() { unsafe { let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); let err = - ddog_tracer_span_new(&mut handle, cs(""), cs(""), cs(""), cs(""), 0, 0, 0, 0, 0, 0, 0); + ddog_tracer_span_new(out, cs(""), cs(""), cs(""), cs(""), 0, 0, 0, 0, 0, 0, 0); assert!(err.is_none()); let span = handle.assume_init(); @@ -491,7 +495,8 @@ mod tests { fn make_chunks(capacity: usize) -> Box { unsafe { let mut handle = MaybeUninit::>::uninit(); - let err = ddog_tracer_trace_chunks_new(capacity, &mut handle); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = ddog_tracer_trace_chunks_new(capacity, out); assert!(err.is_none()); handle.assume_init() } From 9967542936693c82be9e97dd0a91b2f809d59c75 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 13 May 2026 13:15:55 +0200 Subject: [PATCH 12/15] Introduce `TracerSpanFields` struct for `ddog_tracer_span_new` Replace 11 positional parameters with a `#[repr(C)]` struct passed by reference. This avoids holding all fields on the stack per call and allows adding or changing fields without breaking the function signature. Follows the same pattern as `TelemetryClientConfig` elsewhere in the codebase. --- libdd-data-pipeline-ffi/cbindgen.toml | 1 + libdd-data-pipeline-ffi/src/tracer.rs | 141 ++++++++++++++------------ 2 files changed, 77 insertions(+), 65 deletions(-) diff --git a/libdd-data-pipeline-ffi/cbindgen.toml b/libdd-data-pipeline-ffi/cbindgen.toml index cf72c6e758..a0fe97f467 100644 --- a/libdd-data-pipeline-ffi/cbindgen.toml +++ b/libdd-data-pipeline-ffi/cbindgen.toml @@ -30,6 +30,7 @@ exclude = ["TraceExporter", "TracerSpan", "TracerTraceChunks"] "ExporterErrorCode" = "ddog_TraceExporterErrorCode" "ExporterError" = "ddog_TraceExporterError" "TracerSpan" = "ddog_TracerSpan" +"TracerSpanFields" = "ddog_TracerSpanFields" "TracerTraceChunks" = "ddog_TracerTraceChunks" [export.mangle] diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 7182a1130b..e60ed5c57a 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -43,53 +43,52 @@ fn charslice_to_bytesstring(s: CharSlice) -> Result`. pub struct TracerSpan(SpanBytes); +/// FFI-safe bundle of scalar fields for creating a [`TracerSpan`]. +/// +/// Passed by reference to [`ddog_tracer_span_new`] so that adding or +/// changing fields does not break the function signature. +#[derive(Debug)] +#[repr(C)] +pub struct TracerSpanFields<'a> { + pub service: CharSlice<'a>, + pub name: CharSlice<'a>, + pub resource: CharSlice<'a>, + pub span_type: CharSlice<'a>, + pub trace_id_low: u64, + pub trace_id_high: u64, + pub span_id: u64, + pub parent_id: u64, + pub start: i64, + pub duration: i64, + pub error: i32, +} + /// Create a new span with all scalar fields set. /// /// String fields are copied from the provided slices. The `meta` and /// `metrics` maps start empty; use [`ddog_tracer_span_set_meta`] and /// [`ddog_tracer_span_set_metric`] to populate them. /// -/// # Arguments -/// -/// * `out_handle` – Receives the new `TracerSpan` handle on success. -/// * `service`, `name`, `resource`, `span_type` – UTF-8 string fields. -/// * `trace_id_low`, `trace_id_high` – 128-bit trace ID split into two 64-bit halves (low = bits -/// 0‥63, high = bits 64‥127). -/// * `span_id` – Span identifier. -/// * `parent_id` – Parent span identifier (0 for root spans). -/// * `start` – Start time in nanoseconds since Unix epoch. -/// * `duration` – Duration in nanoseconds. -/// * `error` – Error status (0 = no error). -/// /// # Safety /// /// `out_handle` must point to valid, writable memory for a `Box`. -/// All `CharSlice` arguments must point to valid memory for their stated -/// length. +/// All `CharSlice` fields in `fields` must point to valid memory for their +/// stated length. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_new( out_handle: NonNull>, - service: CharSlice, - name: CharSlice, - resource: CharSlice, - span_type: CharSlice, - trace_id_low: u64, - trace_id_high: u64, - span_id: u64, - parent_id: u64, - start: i64, - duration: i64, - error: i32, + fields: &TracerSpanFields, ) -> Option> { catch_panic!( { let inner = || -> Result<(), Box> { - let service = charslice_to_bytesstring(service)?; - let name = charslice_to_bytesstring(name)?; - let resource = charslice_to_bytesstring(resource)?; - let span_type = charslice_to_bytesstring(span_type)?; + let service = charslice_to_bytesstring(fields.service)?; + let name = charslice_to_bytesstring(fields.name)?; + let resource = charslice_to_bytesstring(fields.resource)?; + let span_type = charslice_to_bytesstring(fields.span_type)?; - let trace_id: u128 = ((trace_id_high as u128) << 64) | (trace_id_low as u128); + let trace_id: u128 = + ((fields.trace_id_high as u128) << 64) | (fields.trace_id_low as u128); let span = SpanBytes { service, @@ -97,11 +96,11 @@ pub unsafe extern "C" fn ddog_tracer_span_new( resource, r#type: span_type, trace_id, - span_id, - parent_id, - start, - duration, - error, + span_id: fields.span_id, + parent_id: fields.parent_id, + start: fields.start, + duration: fields.duration, + error: fields.error, ..Default::default() }; @@ -339,20 +338,20 @@ mod tests { unsafe { let mut handle = MaybeUninit::>::uninit(); let out = NonNull::new(handle.as_mut_ptr()).unwrap(); - let err = ddog_tracer_span_new( - out, - cs("svc"), - cs("op"), - cs("res"), - cs(""), - 1, - 0, - 1, - 0, - 0, - 0, - 0, - ); + let fields = TracerSpanFields { + service: cs("svc"), + name: cs("op"), + resource: cs("res"), + span_type: cs(""), + trace_id_low: 1, + trace_id_high: 0, + span_id: 1, + parent_id: 0, + start: 0, + duration: 0, + error: 0, + }; + let err = ddog_tracer_span_new(out, &fields); assert!(err.is_none()); handle.assume_init() } @@ -364,20 +363,20 @@ mod tests { let mut handle = MaybeUninit::>::uninit(); let out = NonNull::new(handle.as_mut_ptr()).unwrap(); - let err = ddog_tracer_span_new( - out, - cs("my-service"), - cs("web.request"), - cs("GET /users"), - cs("web"), - 0xdeadbeef, // trace_id_low - 0x00000001, // trace_id_high - 12345, // span_id - 67890, // parent_id - 1_700_000_000_000_000_000i64, // start (ns) - 25_000_000, // duration (25 ms) - 0, // error - ); + let fields = TracerSpanFields { + service: cs("my-service"), + name: cs("web.request"), + resource: cs("GET /users"), + span_type: cs("web"), + trace_id_low: 0xdeadbeef, + trace_id_high: 0x00000001, + span_id: 12345, + parent_id: 67890, + start: 1_700_000_000_000_000_000i64, + duration: 25_000_000, + error: 0, + }; + let err = ddog_tracer_span_new(out, &fields); assert!(err.is_none()); let span = handle.assume_init(); @@ -478,8 +477,20 @@ mod tests { let mut handle = MaybeUninit::>::uninit(); let out = NonNull::new(handle.as_mut_ptr()).unwrap(); - let err = - ddog_tracer_span_new(out, cs(""), cs(""), cs(""), cs(""), 0, 0, 0, 0, 0, 0, 0); + let fields = TracerSpanFields { + service: cs(""), + name: cs(""), + resource: cs(""), + span_type: cs(""), + trace_id_low: 0, + trace_id_high: 0, + span_id: 0, + parent_id: 0, + start: 0, + duration: 0, + error: 0, + }; + let err = ddog_tracer_span_new(out, &fields); assert!(err.is_none()); let span = handle.assume_init(); From 1e8bc642a5ff90ab5984f6bd2deb8a9f447ec844 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 13 May 2026 16:06:05 +0200 Subject: [PATCH 13/15] Add `capacity` parameter to `ddog_tracer_trace_chunks_begin_chunk` Accept a capacity hint for pre-allocating the inner span vector, avoiding reallocations when the number of spans is known beforehand. --- libdd-data-pipeline-ffi/src/tracer.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index e60ed5c57a..8b89d05db5 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -239,15 +239,19 @@ pub unsafe extern "C" fn ddog_tracer_trace_chunks_free(handle: Box, + capacity: usize, ) { if let Some(chunks) = handle { - chunks.0.push(Vec::new()); + chunks.0.push(Vec::with_capacity(capacity)); } } @@ -519,7 +523,7 @@ mod tests { let mut chunks = make_chunks(2); // Chunk 1: two spans - ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks)); + ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 2); let s1 = make_minimal_span(); let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s1); @@ -530,7 +534,7 @@ mod tests { assert!(err.is_none()); // Chunk 2: one span - ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks)); + ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 1); let s3 = make_minimal_span(); let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s3); assert!(err.is_none()); @@ -571,7 +575,7 @@ mod tests { fn trace_chunks_empty_chunk_is_valid() { unsafe { let mut chunks = make_chunks(1); - ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks)); + ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 0); assert_eq!(chunks.0.len(), 1); assert_eq!(chunks.0[0].len(), 0); From b9ea7272dbea65f361a7a2ec666838fd149f7347 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 13 May 2026 16:06:49 +0200 Subject: [PATCH 14/15] Add error return and null handle check to `ddog_tracer_trace_chunks_begin_chunk` Change the return type from void to `Option>` and wrap the body in `catch_panic!` with a null handle check, making the function consistent with every other mutating function in the tracer FFI API. --- libdd-data-pipeline-ffi/src/tracer.rs | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 8b89d05db5..b3764946fb 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -249,10 +249,16 @@ pub unsafe extern "C" fn ddog_tracer_trace_chunks_free(handle: Box, capacity: usize, -) { - if let Some(chunks) = handle { - chunks.0.push(Vec::with_capacity(capacity)); - } +) -> Option> { + catch_panic!( + if let Some(chunks) = handle { + chunks.0.push(Vec::with_capacity(capacity)); + None + } else { + gen_error!(ErrorCode::InvalidArgument) + }, + gen_error!(ErrorCode::Panic) + ) } /// Move a span into the current (last) chunk, consuming the span handle. @@ -523,7 +529,8 @@ mod tests { let mut chunks = make_chunks(2); // Chunk 1: two spans - ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 2); + let err = ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 2); + assert!(err.is_none()); let s1 = make_minimal_span(); let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s1); @@ -534,7 +541,8 @@ mod tests { assert!(err.is_none()); // Chunk 2: one span - ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 1); + let err = ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 1); + assert!(err.is_none()); let s3 = make_minimal_span(); let err = ddog_tracer_trace_chunks_push_span(Some(&mut *chunks), s3); assert!(err.is_none()); @@ -547,6 +555,15 @@ mod tests { } } + #[test] + fn begin_chunk_null_handle_returns_error() { + unsafe { + let err = ddog_tracer_trace_chunks_begin_chunk(None, 0); + assert!(err.is_some()); + ddog_trace_exporter_error_free(err); + } + } + #[test] fn push_span_without_begin_chunk_returns_error() { unsafe { @@ -575,7 +592,8 @@ mod tests { fn trace_chunks_empty_chunk_is_valid() { unsafe { let mut chunks = make_chunks(1); - ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 0); + let err = ddog_tracer_trace_chunks_begin_chunk(Some(&mut *chunks), 0); + assert!(err.is_none()); assert_eq!(chunks.0.len(), 1); assert_eq!(chunks.0[0].len(), 0); From 693a1caf7b7e6d9a41c0ea1b8323fd1c4e6ed304 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 26 May 2026 16:40:59 +0200 Subject: [PATCH 15/15] Empty (CI trigger)