From bad622f25bba5ec023d48bda994b04f0611dc8d0 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 29 Jul 2026 19:13:25 +0200 Subject: [PATCH] feat(data-pipeline): set structured span values Add a span setter that accepts structured-value tokens and encodes them directly into `meta_struct`. Native tracers can populate structured metadata without managing an intermediate encoded blob or paying an additional value copy. Validate the key and complete token stream before replacing an existing entry. Encoding failures and invalid handles therefore leave the span unchanged. --- .../src/structured_value.rs | 31 +++-- libdd-data-pipeline-ffi/src/tracer.rs | 130 +++++++++++++++++- 2 files changed, 148 insertions(+), 13 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/structured_value.rs b/libdd-data-pipeline-ffi/src/structured_value.rs index ae0e94c7cc..f33161777f 100644 --- a/libdd-data-pipeline-ffi/src/structured_value.rs +++ b/libdd-data-pipeline-ffi/src/structured_value.rs @@ -194,6 +194,24 @@ fn encode_one( Ok(()) } +pub(crate) unsafe fn encode_value( + tokens: Slice>, +) -> Result, Box> { + let tokens = tokens + .try_as_slice() + .map_err(|_| invalid_input("structured value token slice is invalid"))?; + if tokens.is_empty() { + return Err(invalid_input("structured value token slice is empty")); + } + let mut output = Vec::new(); + let mut index = 0; + encode_one(tokens, &mut index, 0, &mut output)?; + if index != tokens.len() { + return Err(invalid_input("structured value has trailing tokens")); + } + Ok(output) +} + /// Encode one flat preorder structured value as an owned MessagePack blob. /// /// On success, `out_handle` receives an owned blob that must be freed with @@ -214,18 +232,7 @@ pub unsafe extern "C" fn ddog_tracer_encode_value( catch_panic!( { let inner = || -> Result<(), Box> { - let tokens = tokens - .try_as_slice() - .map_err(|_| invalid_input("structured value token slice is invalid"))?; - if tokens.is_empty() { - return Err(invalid_input("structured value token slice is empty")); - } - let mut output = Vec::new(); - let mut index = 0; - encode_one(tokens, &mut index, 0, &mut output)?; - if index != tokens.len() { - return Err(invalid_input("structured value has trailing tokens")); - } + let output = encode_value(tokens)?; out_handle .as_ptr() .write(Box::new(TracerEncodedValue(output))); diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index d47ec37c70..8483329034 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -11,9 +11,10 @@ use crate::error::{ExporterError, ExporterErrorCode as ErrorCode}; use crate::response::ExporterResponse; +use crate::structured_value::{encode_value, TracerValueToken}; use crate::trace_exporter::TraceExporter; use crate::{catch_panic, gen_error}; -use libdd_common_ffi::slice::{AsBytes, ByteSlice}; +use libdd_common_ffi::slice::{AsBytes, ByteSlice, Slice}; use libdd_common_ffi::CharSlice; use libdd_tinybytes::{Bytes, BytesString}; use libdd_trace_utils::span::v04::SpanBytes; @@ -224,6 +225,40 @@ pub unsafe extern "C" fn ddog_tracer_span_set_meta_struct_blob( ) } +/// Encode and add or overwrite a structured metadata entry (`meta_struct`) on +/// the span. +/// +/// The `key` is copied into the span. The structured-value tokens are fully +/// validated and encoded directly into the span without exposing an +/// intermediate blob to the caller. The span is unchanged on failure. +/// +/// # Safety +/// +/// `handle` must be a valid pointer to a `TracerSpan`. `key` must point to +/// valid UTF-8 memory. `tokens` and every byte slice referenced by its tokens +/// must remain valid for this call. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_set_meta_struct( + handle: Option<&mut TracerSpan>, + key: CharSlice, + tokens: Slice>, +) -> Option> { + catch_panic!( + if let Some(span) = handle { + let mut inner = || -> Result<(), Box> { + let key = charslice_to_bytesstring(key)?; + let value = encode_value(tokens)?; + span.0.meta_struct.insert(key, Bytes::from(value)); + Ok(()) + }; + inner().err() + } else { + gen_error!(ErrorCode::InvalidArgument) + }, + gen_error!(ErrorCode::Panic) + ) +} + // --------------------------------------------------------------------------- // TracerTraceChunks // --------------------------------------------------------------------------- @@ -433,6 +468,10 @@ pub unsafe extern "C" fn ddog_trace_exporter_send_trace_chunks( mod tests { use super::*; use crate::error::ddog_trace_exporter_error_free; + use crate::structured_value::{ + ddog_tracer_encode_value, ddog_tracer_encoded_value_as_slice, + ddog_tracer_encoded_value_free, TracerEncodedValue, + }; use std::mem::MaybeUninit; fn cs(s: &str) -> CharSlice<'_> { @@ -443,6 +482,18 @@ mod tests { ByteSlice::from(bytes) } + fn nil_token() -> TracerValueToken<'static> { + TracerValueToken { + kind: 0, + bool_value: 0, + child_count: 0, + i64_value: 0, + u64_value: 0, + f64_value: 0.0, + bytes: ByteSlice::empty(), + } + } + fn make_minimal_span() -> Box { unsafe { let mut handle = MaybeUninit::>::uninit(); @@ -594,6 +645,83 @@ mod tests { } } + #[test] + fn set_meta_struct_matches_standalone_encoder_without_copying_value() { + unsafe { + let tokens = [nil_token()]; + let token_slice = Slice::from(tokens.as_slice()); + let expected = { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + assert!(ddog_tracer_encode_value(token_slice, out).is_none()); + let blob = handle.assume_init(); + let bytes = ddog_tracer_encoded_value_as_slice(Some(&blob)) + .as_bytes() + .to_vec(); + ddog_tracer_encoded_value_free(Some(blob)); + bytes + }; + + let mut span = make_minimal_span(); + let encoded = encode_value(token_slice).unwrap(); + let encoded_ptr = encoded.as_ptr(); + let bytes = Bytes::from(encoded); + assert_eq!(bytes.as_ptr(), encoded_ptr); + + assert!( + ddog_tracer_span_set_meta_struct(Some(&mut *span), cs("key"), token_slice,) + .is_none() + ); + assert_eq!(span.0.meta_struct.get("key").unwrap().as_ref(), expected); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_struct_failure_does_not_replace_existing_value() { + unsafe { + let mut span = make_minimal_span(); + assert!(ddog_tracer_span_set_meta_struct_blob( + Some(&mut *span), + cs("key"), + bs(b"existing"), + ) + .is_none()); + + let empty: &[TracerValueToken<'_>] = &[]; + let error = + ddog_tracer_span_set_meta_struct(Some(&mut *span), cs("key"), Slice::from(empty)); + assert!(error.is_some()); + assert_eq!(span.0.meta_struct.get("key").unwrap().as_ref(), b"existing"); + ddog_trace_exporter_error_free(error); + + let invalid_key = CharSlice::from_bytes(&[0xff]); + let tokens = [nil_token()]; + let error = ddog_tracer_span_set_meta_struct( + Some(&mut *span), + invalid_key, + Slice::from(tokens.as_slice()), + ); + assert!(error.is_some()); + assert_eq!(span.0.meta_struct.get("key").unwrap().as_ref(), b"existing"); + ddog_trace_exporter_error_free(error); + + ddog_tracer_span_free(span); + } + } + + #[test] + fn set_meta_struct_null_handle_returns_error() { + unsafe { + let tokens = [nil_token()]; + let error = + ddog_tracer_span_set_meta_struct(None, cs("key"), Slice::from(tokens.as_slice())); + assert!(error.is_some()); + ddog_trace_exporter_error_free(error); + } + } + #[test] fn set_meta_null_handle_returns_error() { unsafe {