From 27ad1afaada18959c8c8ffd5de16bb1490cd1d5f Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 29 Jul 2026 17:27:10 +0200 Subject: [PATCH 1/6] feat(data-pipeline): add meta struct blob setter Expose a C setter that copies an opaque binary value into a span's `meta_struct` map. Native tracers can now supply pre-encoded MessagePack without transferring ownership or requiring Rust to interpret it. Reject null span handles and non-UTF-8 keys. Reusing a key replaces its value, and the copied bytes remain owned by the span after the call. --- libdd-data-pipeline-ffi/src/tracer.rs | 95 ++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 9489527ad6..d47ec37c70 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -13,9 +13,9 @@ 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::slice::{AsBytes, ByteSlice}; use libdd_common_ffi::CharSlice; -use libdd_tinybytes::BytesString; +use libdd_tinybytes::{Bytes, BytesString}; use libdd_trace_utils::span::v04::SpanBytes; use std::ptr::NonNull; @@ -192,6 +192,38 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( ) } +/// Add or overwrite a structured metadata entry (`meta_struct`) on the span. +/// +/// The `key` and opaque binary `value` are copied into the span. The value is +/// not interpreted or validated as MessagePack. +/// +/// # Safety +/// +/// `handle` must be a valid pointer to a `TracerSpan`. `key` must point to +/// valid UTF-8 memory, and `value` must point to valid memory for its length. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_set_meta_struct_blob( + handle: Option<&mut TracerSpan>, + key: CharSlice, + value: ByteSlice, +) -> 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 + .meta_struct + .insert(key, Bytes::copy_from_slice(value.as_bytes())); + None + } else { + gen_error!(ErrorCode::InvalidArgument) + }, + gen_error!(ErrorCode::Panic) + ) +} + // --------------------------------------------------------------------------- // TracerTraceChunks // --------------------------------------------------------------------------- @@ -407,6 +439,10 @@ mod tests { CharSlice::from_bytes(s.as_bytes()) } + fn bs(bytes: &[u8]) -> ByteSlice<'_> { + ByteSlice::from(bytes) + } + fn make_minimal_span() -> Box { unsafe { let mut handle = MaybeUninit::>::uninit(); @@ -465,6 +501,7 @@ mod tests { assert_eq!(span.0.error, 0); assert!(span.0.meta.is_empty()); assert!(span.0.metrics.is_empty()); + assert!(span.0.meta_struct.is_empty()); assert!(span.0.span_links.is_empty()); assert!(span.0.span_events.is_empty()); @@ -527,6 +564,36 @@ mod tests { } } + #[test] + fn set_meta_struct_blob_inserts_binary_entries() { + unsafe { + let mut span = make_minimal_span(); + let value = b"\x82\xa6nested\x92\xc3\xc0\xa3raw\xc4\x03\x00\xff\x80"; + + let err = + ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), cs("_dd.stack"), bs(value)); + assert!(err.is_none()); + + assert_eq!(span.0.meta_struct.get("_dd.stack").unwrap().as_ref(), value); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_struct_blob_overwrites_existing_key() { + unsafe { + let mut span = make_minimal_span(); + + ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), cs("k"), bs(b"first")); + ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), cs("k"), bs(b"second")); + + assert_eq!(span.0.meta_struct.get("k").unwrap().as_ref(), b"second"); + + ddog_tracer_span_free(span); + } + } + #[test] fn set_meta_null_handle_returns_error() { unsafe { @@ -545,6 +612,30 @@ mod tests { } } + #[test] + fn set_meta_struct_blob_null_handle_returns_error() { + unsafe { + let err = ddog_tracer_span_set_meta_struct_blob(None, cs("k"), bs(b"value")); + assert!(err.is_some()); + ddog_trace_exporter_error_free(err); + } + } + + #[test] + fn set_meta_struct_blob_invalid_key_returns_error() { + unsafe { + let mut span = make_minimal_span(); + let key = CharSlice::from_bytes(&[0xff]); + + let err = ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), key, bs(b"value")); + assert!(err.is_some()); + assert!(span.0.meta_struct.is_empty()); + ddog_trace_exporter_error_free(err); + + ddog_tracer_span_free(span); + } + } + #[test] fn new_with_empty_strings_succeeds() { unsafe { From 10bfcb88c9816a9c17963502ff94866fe40026f1 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sun, 2 Aug 2026 15:54:54 -0400 Subject: [PATCH 2/6] use try_as_bytes instead of as_bytes to return meaningful error instead of panic error --- libdd-data-pipeline-ffi/src/tracer.rs | 82 +++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index d47ec37c70..47b5b54db8 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -27,10 +27,17 @@ type TokioCancellationToken = tokio_util::sync::CancellationToken; /// Convert a [`CharSlice`] to a [`BytesString`], copying the bytes. /// -/// Returns an error if the slice is not valid UTF-8. +/// Returns [`ErrorCode::InvalidArgument`] if the slice is malformed and [`ErrorCode::InvalidInput`] +/// if the bytes are not valid UTF-8. #[inline] fn charslice_to_bytesstring(s: CharSlice) -> Result> { - BytesString::from_slice(s.as_bytes()).map_err(|_| { + let bytes = s.try_as_bytes().map_err(|_| { + Box::new(ExporterError::new( + ErrorCode::InvalidArgument, + &ErrorCode::InvalidArgument.to_string(), + )) + })?; + BytesString::from_slice(bytes).map_err(|_| { Box::new(ExporterError::new( ErrorCode::InvalidInput, &ErrorCode::InvalidInput.to_string(), @@ -197,10 +204,13 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( /// The `key` and opaque binary `value` are copied into the span. The value is /// not interpreted or validated as MessagePack. /// +/// Returns an error if `handle` is null, if `key` is not valid UTF-8, or if either slice is +/// malformed. +/// /// # Safety /// -/// `handle` must be a valid pointer to a `TracerSpan`. `key` must point to -/// valid UTF-8 memory, and `value` must point to valid memory for its length. +/// `handle` must be a valid pointer to a `TracerSpan`. `key` and `value` must point to valid +/// memory for their stated lengths. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_set_meta_struct_blob( handle: Option<&mut TracerSpan>, @@ -213,9 +223,13 @@ pub unsafe extern "C" fn ddog_tracer_span_set_meta_struct_blob( Ok(s) => s, Err(e) => return Some(e), }; + let value = match value.try_as_bytes() { + Ok(v) => v, + Err(_) => return gen_error!(ErrorCode::InvalidArgument), + }; span.0 .meta_struct - .insert(key, Bytes::copy_from_slice(value.as_bytes())); + .insert(key, Bytes::copy_from_slice(value)); None } else { gen_error!(ErrorCode::InvalidArgument) @@ -636,6 +650,64 @@ mod tests { } } + #[test] + fn set_meta_struct_blob_null_value_returns_error() { + unsafe { + let mut span = make_minimal_span(); + let value = ByteSlice::from_raw_parts(std::ptr::null(), 5); + + let err = ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), cs("k"), value); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + assert!(span.0.meta_struct.is_empty()); + ddog_trace_exporter_error_free(err); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_struct_blob_null_key_returns_error() { + unsafe { + let mut span = make_minimal_span(); + let key = CharSlice::from_raw_parts(std::ptr::null(), 5); + + let err = ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), key, bs(b"value")); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + assert!(span.0.meta_struct.is_empty()); + ddog_trace_exporter_error_free(err); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_struct_blob_accepts_empty_value() { + unsafe { + let mut span = make_minimal_span(); + + let err = ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), cs("k"), bs(b"")); + assert!(err.is_none()); + assert_eq!(span.0.meta_struct.get("k").unwrap().as_ref(), b""); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_null_value_returns_error() { + unsafe { + let mut span = make_minimal_span(); + let value = CharSlice::from_raw_parts(std::ptr::null(), 5); + + let err = ddog_tracer_span_set_meta(Some(&mut *span), cs("k"), value); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + assert!(span.0.meta.is_empty()); + ddog_trace_exporter_error_free(err); + + ddog_tracer_span_free(span); + } + } + #[test] fn new_with_empty_strings_succeeds() { unsafe { From 14e9972d83f505b9f864933008aead671b7f26c7 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sun, 2 Aug 2026 16:33:29 -0400 Subject: [PATCH 3/6] update rustdoc comments to make clear vecmap defers dedup to serialization --- libdd-data-pipeline-ffi/src/tracer.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 47b5b54db8..74b805b959 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -199,11 +199,14 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( ) } -/// Add or overwrite a structured metadata entry (`meta_struct`) on the span. +/// Add a structured metadata entry (`meta_struct`) to the span. /// /// The `key` and opaque binary `value` are copied into the span. The value is /// not interpreted or validated as MessagePack. /// +/// Repeating a `key` appends a new entry rather than replacing the previous one. The last value +/// written for a key is the one read back and the one serialized +/// /// Returns an error if `handle` is null, if `key` is not valid UTF-8, or if either slice is /// malformed. /// @@ -594,8 +597,10 @@ mod tests { } } + // Repeated keys are appended, not replaced: `VecMap` defers deduplication to encode time, + // so both entries are retained and the last one wins on read. #[test] - fn set_meta_struct_blob_overwrites_existing_key() { + fn set_meta_struct_blob_last_write_wins() { unsafe { let mut span = make_minimal_span(); @@ -603,6 +608,7 @@ mod tests { ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), cs("k"), bs(b"second")); assert_eq!(span.0.meta_struct.get("k").unwrap().as_ref(), b"second"); + assert_eq!(span.0.meta_struct.len(), 2); ddog_tracer_span_free(span); } From 9cdfa1694e8f8f744d350c175c8a517b5ecc4584 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sun, 2 Aug 2026 16:47:08 -0400 Subject: [PATCH 4/6] fix safety comments --- libdd-data-pipeline-ffi/src/tracer.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 74b805b959..39959c2c87 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -141,10 +141,13 @@ pub unsafe extern "C" fn ddog_tracer_span_free(handle: Box) { /// /// Both `key` and `value` are copied into the span. /// +/// Returns an error if `handle` is null, if `key` or `value` is not valid UTF-8, or if either +/// slice is malformed. +/// /// # Safety /// -/// `handle` must be a valid pointer to a `TracerSpan`. -/// `key` and `value` must point to valid UTF-8 memory. +/// `handle` must be a valid pointer to a `TracerSpan`. `key` and `value` must point to valid +/// memory for their stated lengths. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_set_meta( handle: Option<&mut TracerSpan>, @@ -174,10 +177,13 @@ pub unsafe extern "C" fn ddog_tracer_span_set_meta( /// /// The `key` is copied into the span. /// +/// Returns an error if `handle` is null, if `key` is not valid UTF-8, or if the slice is +/// malformed. +/// /// # Safety /// -/// `handle` must be a valid pointer to a `TracerSpan`. -/// `key` must point to valid UTF-8 memory. +/// `handle` must be a valid pointer to a `TracerSpan`. `key` must point to valid memory for its +/// stated length. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_set_metric( handle: Option<&mut TracerSpan>, @@ -205,7 +211,7 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( /// not interpreted or validated as MessagePack. /// /// Repeating a `key` appends a new entry rather than replacing the previous one. The last value -/// written for a key is the one read back and the one serialized +/// written for a key is the one read back and the one serialized. /// /// Returns an error if `handle` is null, if `key` is not valid UTF-8, or if either slice is /// malformed. From 6926f58dc617f7d1f89c6772127c4011803e7b5b Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sun, 2 Aug 2026 17:19:56 -0400 Subject: [PATCH 5/6] add test for empty string key on meta_struct --- libdd-data-pipeline-ffi/src/tracer.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 39959c2c87..57c3c40dbf 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -705,6 +705,20 @@ mod tests { } } + // An empty key is valid UTF-8 and is technically accepted. + #[test] + fn set_meta_struct_blob_accepts_empty_key() { + unsafe { + let mut span = make_minimal_span(); + + let err = ddog_tracer_span_set_meta_struct_blob(Some(&mut *span), cs(""), bs(b"value")); + assert!(err.is_none()); + assert_eq!(span.0.meta_struct.get("").unwrap().as_ref(), b"value"); + + ddog_tracer_span_free(span); + } + } + #[test] fn set_meta_null_value_returns_error() { unsafe { From 220d6f8f0b9e45f766fef17b76046553d8760e79 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Mon, 3 Aug 2026 18:36:28 -0400 Subject: [PATCH 6/6] cleanup rustdocs --- libdd-data-pipeline-ffi/src/tracer.rs | 51 +++++++++++---------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 57c3c40dbf..57f00cdfd3 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -74,9 +74,12 @@ pub struct TracerSpanFields<'a> { /// 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. +/// String fields are copied from the provided slices. The `meta`, `metrics` and `meta_struct` +/// maps start empty; use [`ddog_tracer_span_set_meta`], [`ddog_tracer_span_set_metric`] and +/// [`ddog_tracer_span_set_meta_struct_blob`] to populate them. +/// +/// Returns an error if `fields` is null, if any string field is not valid UTF-8, or if any of +/// its slices is malformed. /// /// # Safety /// @@ -205,14 +208,11 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( ) } -/// Add a structured metadata entry (`meta_struct`) to the span. +/// Add or overwrite a structured metadata entry (`meta_struct`) on the span. /// /// The `key` and opaque binary `value` are copied into the span. The value is /// not interpreted or validated as MessagePack. /// -/// Repeating a `key` appends a new entry rather than replacing the previous one. The last value -/// written for a key is the one read back and the one serialized. -/// /// Returns an error if `handle` is null, if `key` is not valid UTF-8, or if either slice is /// malformed. /// @@ -367,17 +367,14 @@ pub extern "C" fn ddog_trace_exporter_cancel_token_new() -> Box) { if let Some(token) = token { @@ -401,19 +398,11 @@ pub extern "C" fn ddog_trace_exporter_cancel_token_drop( /// 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. -/// -/// When `cancel` is non-null, cancelling that token (via -/// [`ddog_trace_exporter_cancel_token_cancel`]) while the send is in progress -/// aborts the in-flight request and returns an error with code -/// [`ExporterErrorCode::IoError`]. Cancellation is cooperative: it only takes -/// effect while a request is actually in flight. A token that is already -/// cancelled when the send starts makes this function return that error -/// immediately, and cancelling after the send has finished has no effect. -/// Cancelling an in-flight send may cause the trace chunks being sent to be -/// lost. +/// Computes stats, serializes in the configured output format, and sends to the agent with +/// retries. +/// +/// When `cancel` is non-null, cancelling that token aborts the in-flight request; see +/// [`ddog_trace_exporter_cancel_token_cancel`]. /// /// On success, if `response_out` is non-null, a heap-allocated /// [`ExporterResponse`] is written there. The caller owns it and must