diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 9489527ad6..57f00cdfd3 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; @@ -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(), @@ -67,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 /// @@ -134,10 +144,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>, @@ -167,10 +180,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>, @@ -192,6 +208,45 @@ 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. +/// +/// 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` 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>, + 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), + }; + 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)); + None + } else { + gen_error!(ErrorCode::InvalidArgument) + }, + gen_error!(ErrorCode::Panic) + ) +} + // --------------------------------------------------------------------------- // TracerTraceChunks // --------------------------------------------------------------------------- @@ -312,17 +367,14 @@ pub extern "C" fn ddog_trace_exporter_cancel_token_new() -> Box) { if let Some(token) = token { @@ -346,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. +/// Computes stats, serializes in the configured output format, and sends to the agent with +/// retries. /// -/// 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. +/// 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 @@ -407,6 +451,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 +513,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 +576,39 @@ 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); + } + } + + // 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_last_write_wins() { + 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"); + assert_eq!(span.0.meta_struct.len(), 2); + + ddog_tracer_span_free(span); + } + } + #[test] fn set_meta_null_handle_returns_error() { unsafe { @@ -545,6 +627,102 @@ 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 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); + } + } + + // 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 { + 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 {