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..a0fe97f467 100644 --- a/libdd-data-pipeline-ffi/cbindgen.toml +++ b/libdd-data-pipeline-ffi/cbindgen.toml @@ -11,12 +11,14 @@ include_guard = "DDOG_DATA_PIPELINE_H" 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"] +exclude = ["TraceExporter", "TracerSpan", "TracerTraceChunks"] [export.rename] "ByteSlice" = "ddog_ByteSlice" @@ -27,6 +29,9 @@ exclude = ["TraceExporter"] "ExporterResponse" = "ddog_TraceExporterResponse" "ExporterErrorCode" = "ddog_TraceExporterErrorCode" "ExporterError" = "ddog_TraceExporterError" +"TracerSpan" = "ddog_TracerSpan" +"TracerSpanFields" = "ddog_TracerSpanFields" +"TracerTraceChunks" = "ddog_TracerTraceChunks" [export.mangle] rename_types = "PascalCase" @@ -40,4 +45,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/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 new file mode 100644 index 0000000000..b3764946fb --- /dev/null +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -0,0 +1,604 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! FFI functions for creating and manipulating tracer spans and trace chunks. +//! +//! 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::response::ExporterResponse; +use crate::trace_exporter::TraceExporter; +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> { + BytesString::from_slice(s.as_bytes()).map_err(|_| { + Box::new(ExporterError::new( + ErrorCode::InvalidInput, + &ErrorCode::InvalidInput.to_string(), + )) + }) +} + +// --------------------------------------------------------------------------- +// TracerSpan +// --------------------------------------------------------------------------- + +/// Opaque handle wrapping a single `Span`. +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. +/// +/// # Safety +/// +/// `out_handle` must point to valid, writable memory for a `Box`. +/// 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>, + fields: &TracerSpanFields, +) -> Option> { + catch_panic!( + { + let inner = || -> Result<(), Box> { + 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 = + ((fields.trace_id_high as u128) << 64) | (fields.trace_id_low as u128); + + let span = SpanBytes { + service, + name, + resource, + r#type: span_type, + trace_id, + span_id: fields.span_id, + parent_id: fields.parent_id, + start: fields.start, + duration: fields.duration, + error: fields.error, + ..Default::default() + }; + + out_handle.as_ptr().write(Box::new(TracerSpan(span))); + Ok(()) + }; + inner().err() + }, + 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) + ) +} + +// --------------------------------------------------------------------------- +// TracerTraceChunks +// --------------------------------------------------------------------------- + +/// Opaque handle wrapping `Vec>` — a list of trace chunks, +/// each containing a list of spans. +pub struct TracerTraceChunks(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 = Vec::with_capacity(capacity); + 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. +/// +/// `capacity` is a hint for the expected number of spans in this chunk; +/// pass 0 if unknown. +/// +/// # 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>, + capacity: usize, +) -> 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. +/// +/// 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) + ) +} + +// --------------------------------------------------------------------------- +// 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 Some(exporter) = exporter else { + 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::*; + use crate::error::ddog_trace_exporter_error_free; + use std::mem::MaybeUninit; + + fn cs(s: &str) -> CharSlice<'_> { + CharSlice::from_bytes(s.as_bytes()) + } + + fn make_minimal_span() -> Box { + unsafe { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + 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() + } + } + + #[test] + fn new_sets_all_scalar_fields() { + unsafe { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + + 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(); + 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 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(); + assert_eq!(span.0.name.as_ref(), ""); + assert_eq!(span.0.service.as_ref(), ""); + + ddog_tracer_span_free(span); + } + } + + // -- TracerTraceChunks 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); + 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 + 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); + 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 + 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()); + + 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 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 { + 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); + 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); + + ddog_tracer_trace_chunks_free(chunks); + } + } +}