From 43e110b3c334f42e18953c0d1aa6832d927a69c0 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Fri, 23 Jan 2026 14:47:38 -0500 Subject: [PATCH] feat(profiling)!: take mime types in profile exporter --- examples/cxx/profiling.cpp | 3 +- libdd-profiling-ffi/cbindgen.toml | 1 + libdd-profiling-ffi/src/exporter.rs | 6 ++- libdd-profiling/src/cxx.rs | 54 ++++++++++++++++--- .../src/exporter/profile_exporter.rs | 23 ++++++++ libdd-profiling/tests/exporter_e2e.rs | 4 +- 6 files changed, 80 insertions(+), 11 deletions(-) diff --git a/examples/cxx/profiling.cpp b/examples/cxx/profiling.cpp index 29acbf73fe..07579e6a1a 100644 --- a/examples/cxx/profiling.cpp +++ b/examples/cxx/profiling.cpp @@ -253,7 +253,8 @@ int main() { // Files to compress and attach {AttachmentFile{ .name = "app_metadata.json", - .data = {metadata_bytes.data(), metadata_bytes.size()} + .data = {metadata_bytes.data(), metadata_bytes.size()}, + .mime = MimeType::ApplicationJson }}, // Additional per-profile tags { diff --git a/libdd-profiling-ffi/cbindgen.toml b/libdd-profiling-ffi/cbindgen.toml index faba62ec7a..c364b220aa 100644 --- a/libdd-profiling-ffi/cbindgen.toml +++ b/libdd-profiling-ffi/cbindgen.toml @@ -59,6 +59,7 @@ renaming_overrides_prefixing = true "VoidResult" = "ddog_VoidResult" "CbindgenIsDumbStringId" = "ddog_prof_StringId" +"MimeType" = "ddog_prof_MimeType" "Slice_GenerationalIdLabelId" = "ddog_prof_Slice_LabelId" "Slice_GenerationalIdLocationId" = "ddog_prof_Slice_LocationId" diff --git a/libdd-profiling-ffi/src/exporter.rs b/libdd-profiling-ffi/src/exporter.rs index 9a5661374b..65911448be 100644 --- a/libdd-profiling-ffi/src/exporter.rs +++ b/libdd-profiling-ffi/src/exporter.rs @@ -9,7 +9,7 @@ use libdd_common::tag::Tag; use libdd_common_ffi::slice::{AsBytes, ByteSlice, CharSlice, Slice}; use libdd_common_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; use libdd_profiling::exporter; -use libdd_profiling::exporter::ProfileExporter; +use libdd_profiling::exporter::{MimeType, ProfileExporter}; use libdd_profiling::internal::EncodedProfile; use std::borrow::Cow; use std::str::FromStr; @@ -29,6 +29,7 @@ pub enum ProfilingEndpoint<'a> { pub struct File<'a> { name: CharSlice<'a>, file: ByteSlice<'a>, + mime: MimeType, } #[must_use] @@ -184,7 +185,8 @@ unsafe fn into_vec_files<'a>(slice: Slice<'a, File>) -> Vec> .map(|file| { let name = file.name.try_to_utf8().unwrap_or("{invalid utf-8}"); let bytes = file.file.as_slice(); - exporter::File { name, bytes } + let mime = file.mime; + exporter::File { name, bytes, mime } }) .collect() } diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index cca67196d5..aa49cf5435 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -13,8 +13,20 @@ use crate::internal; // CXX Bridge - C++ Bindings // ============================================================================ +/// cbindgen:ignore #[cxx::bridge(namespace = "datadog::profiling")] pub mod ffi { + // Shared enums + #[derive(Debug)] + #[repr(u8)] + enum MimeType { + ApplicationJson, + ApplicationOctetStream, + TextCsv, + TextPlain, + TextXml, + } + // Shared structs - CXX-friendly types struct ValueType<'a> { type_: &'a str, @@ -68,6 +80,7 @@ pub mod ffi { struct AttachmentFile<'a> { name: &'a str, data: &'a [u8], + mime: MimeType, } // Opaque Rust types @@ -274,12 +287,30 @@ impl<'a> From<&ffi::Label<'a>> for api::Label<'a> { } } -impl<'a> From<&ffi::AttachmentFile<'a>> for exporter::File<'a> { - fn from(file: &ffi::AttachmentFile<'a>) -> Self { - exporter::File { +impl TryFrom for exporter::MimeType { + type Error = anyhow::Error; + + fn try_from(mime: ffi::MimeType) -> Result { + match mime { + ffi::MimeType::ApplicationJson => Ok(exporter::MimeType::ApplicationJson), + ffi::MimeType::ApplicationOctetStream => Ok(exporter::MimeType::ApplicationOctetStream), + ffi::MimeType::TextCsv => Ok(exporter::MimeType::TextCsv), + ffi::MimeType::TextPlain => Ok(exporter::MimeType::TextPlain), + ffi::MimeType::TextXml => Ok(exporter::MimeType::TextXml), + _ => anyhow::bail!("Unknown MimeType variant: {:?}", mime), + } + } +} + +impl<'a> TryFrom<&ffi::AttachmentFile<'a>> for exporter::File<'a> { + type Error = anyhow::Error; + + fn try_from(file: &ffi::AttachmentFile<'a>) -> Result { + Ok(exporter::File { name: file.name, bytes: file.data, - } + mime: file.mime.try_into()?, + }) } } @@ -620,8 +651,10 @@ impl ProfileExporter { let end_time = Some(std::time::SystemTime::now()); let encoded = old_profile.serialize_into_compressed_pprof(end_time, None)?; - let files_to_compress_vec: Vec = - files_to_compress.iter().map(Into::into).collect(); + let files_to_compress_vec: Vec = files_to_compress + .iter() + .map(TryInto::try_into) + .collect::, _>>()?; let additional_tags_vec: Vec = additional_tags .iter() @@ -901,10 +934,16 @@ mod tests { let file: exporter::File = (&ffi::AttachmentFile { name: "test.bin", data: &data, + mime: ffi::MimeType::ApplicationOctetStream, }) - .into(); + .try_into() + .expect("Failed to convert AttachmentFile"); assert_eq!(file.name, "test.bin"); assert_eq!(file.bytes, data.as_slice()); + assert!(matches!( + file.mime, + exporter::MimeType::ApplicationOctetStream + )); // Tag conversion with special characters let tag: libdd_common::tag::Tag = (&ffi::Tag { @@ -996,6 +1035,7 @@ mod tests { vec![ffi::AttachmentFile { name: "metadata.json", data: &attachment_data, + mime: ffi::MimeType::ApplicationJson, }], vec![ ffi::Tag { diff --git a/libdd-profiling/src/exporter/profile_exporter.rs b/libdd-profiling/src/exporter/profile_exporter.rs index f96c3c2a09..1a64c29132 100644 --- a/libdd-profiling/src/exporter/profile_exporter.rs +++ b/libdd-profiling/src/exporter/profile_exporter.rs @@ -45,9 +45,32 @@ pub struct ProfileExporter { runtime: Option, } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub enum MimeType { + ApplicationJson, + ApplicationOctetStream, + TextCsv, + TextPlain, + TextXml, +} + +impl MimeType { + pub fn as_str(&self) -> &'static str { + match self { + MimeType::ApplicationJson => mime::APPLICATION_JSON.as_ref(), + MimeType::ApplicationOctetStream => mime::APPLICATION_OCTET_STREAM.as_ref(), + MimeType::TextCsv => mime::TEXT_CSV.as_ref(), + MimeType::TextPlain => mime::TEXT_PLAIN.as_ref(), + MimeType::TextXml => mime::TEXT_XML.as_ref(), + } + } +} + pub struct File<'a> { pub name: &'a str, pub bytes: &'a [u8], + pub mime: MimeType, } impl ProfileExporter { diff --git a/libdd-profiling/tests/exporter_e2e.rs b/libdd-profiling/tests/exporter_e2e.rs index 27f9173a3c..3bca79c48e 100644 --- a/libdd-profiling/tests/exporter_e2e.rs +++ b/libdd-profiling/tests/exporter_e2e.rs @@ -7,7 +7,7 @@ use libdd_profiling::exporter::config; use libdd_profiling::exporter::utils::parse_http_request; -use libdd_profiling::exporter::{File, ProfileExporter}; +use libdd_profiling::exporter::{File, MimeType, ProfileExporter}; use libdd_profiling::internal::EncodedProfile; use std::collections::HashMap; use std::path::PathBuf; @@ -226,10 +226,12 @@ async fn export_full_profile( File { name: "jit.pprof", bytes: b"fake-jit-data", + mime: MimeType::ApplicationOctetStream, }, File { name: "metadata.json", bytes: b"{\"test\": true}", + mime: MimeType::ApplicationJson, }, ];