diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ee60bdf4..e50999f52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -234,22 +234,35 @@ jobs: cmake -S .. -DDatadog_ROOT=$LIBDD_OUTPUT_FOLDER cmake --build . fi - - name: "Test building CXX bindings (Unix)" + - name: "Test building CXX bindings - Crashinfo (Unix)" shell: bash if: matrix.platform != 'windows-latest' run: | set -e cd examples/cxx ./build-and-run-crashinfo.sh + - name: "Test building CXX bindings - Profiling (Unix)" + shell: bash + if: matrix.platform != 'windows-latest' + run: | + set -e + cd examples/cxx + ./build-profiling.sh - name: "Setup MSVC (Windows)" if: matrix.platform == 'windows-latest' uses: ilammy/msvc-dev-cmd@v1 - - name: "Test building CXX bindings (Windows)" + - name: "Test building CXX bindings - Crashinfo (Windows)" shell: pwsh if: matrix.platform == 'windows-latest' run: | cd examples/cxx .\build-and-run-crashinfo.ps1 + - name: "Test building CXX bindings - Profiling (Windows)" + shell: pwsh + if: matrix.platform == 'windows-latest' + run: | + cd examples/cxx + .\build-profiling.ps1 cross-centos7: name: build and test using cross - on centos7 diff --git a/.gitignore b/.gitignore index 07fa526c8..3e0700058 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build/ cmake-build-debug deliverables docker-linux-target +release target tmp __fuzz__ @@ -25,3 +26,7 @@ docker-sync.yml libtest.so libtest_cpp.so examples/cxx/crashinfo +examples/cxx/crashinfo.exe +examples/cxx/profiling +examples/cxx/profiling.exe +profile.pprof diff --git a/Cargo.lock b/Cargo.lock index 2941dfb6f..0a9c1a4a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2850,6 +2850,8 @@ dependencies = [ "chrono", "criterion", "crossbeam-utils", + "cxx", + "cxx-build", "futures", "hashbrown 0.16.0", "http", diff --git a/examples/cxx/README.md b/examples/cxx/README.md index 68ea54112..6e0b81336 100644 --- a/examples/cxx/README.md +++ b/examples/cxx/README.md @@ -1,12 +1,35 @@ -# CXX Bindings Example for libdd-crashtracker +# CXX Bindings Examples -This example demonstrates how to use the CXX bindings for the libdd-crashtracker crate, providing a safer and more idiomatic C++ API compared to the traditional C FFI. +This directory contains C++ examples demonstrating the CXX bindings for libdatadog components. -## Features +CXX bindings provide a safer and more idiomatic C++ API compared to the traditional C FFI bindings, with automatic memory management and exception handling. -The CXX bindings provide access to: +## Examples -### Core Types +### Crashtracker (`crashinfo.cpp`) + +Demonstrates building crash reports using the CXX bindings for `libdd-crashtracker`. + +**Build and run:** + +Unix (Linux/macOS): +```bash +./build-and-run-crashinfo.sh +``` + +Windows: +```powershell +.\build-and-run-crashinfo.ps1 +``` + +**Key features:** +- Type-safe crash report builder API +- Support for stack traces, frames, and metadata +- Process and OS information +- Automatic memory management +- Exception-based error handling + +**Core Types:** - `CrashInfoBuilder` - Builder for constructing crash information - `StackFrame` - Individual stack frame with debug info and addresses - `StackTrace` - Collection of stack frames @@ -15,115 +38,85 @@ The CXX bindings provide access to: - `ProcInfo` - Process information - `OsInfo` - Operating system information -### Enums -- `ErrorKind` - Type of error (Panic, UnhandledException, UnixSignal) -- `BuildIdType` - Build ID format (GNU, GO, PDB, SHA1) -- `FileType` - Binary file format (APK, ELF, PE) +**Enums:** +- `CxxErrorKind` - Type of error (Panic, UnhandledException, UnixSignal) +- `CxxBuildIdType` - Build ID format (GNU, GO, PDB, SHA1) +- `CxxFileType` - Binary file format (APK, ELF, PE) -### Key API +### Profiling (`profiling.cpp`) -**Object Creation:** -```cpp -auto builder = CrashInfoBuilder::create(); -auto frame = StackFrame::create(); -auto stacktrace = StackTrace::create(); -``` +Demonstrates building profiling data and exporting to Datadog using the CXX bindings for `libdd-profiling`. -**CrashInfoBuilder Methods:** -- `set_kind(CxxErrorKind)` - Set error type (Panic, UnhandledException, UnixSignal) -- `with_message(String)` - Set error message -- `with_counter(String, i64)` - Add a named counter -- `with_log_message(String, bool)` - Add a log message -- `with_fingerprint(String)` - Set crash fingerprint -- `with_incomplete(bool)` - Mark as incomplete -- `set_metadata(Metadata)` - Set library metadata -- `set_proc_info(ProcInfo)` - Set process information -- `set_os_info(OsInfo)` - Set OS information -- `add_stack(Box)` - Add a stack trace -- `with_timestamp_now()` - Set current timestamp -- `with_file(String)` - Add a file to the report - -**StackFrame Methods:** -- `with_function(String)`, `with_file(String)`, `with_line(u32)`, `with_column(u32)` - Set debug info -- `with_ip(usize)`, `with_sp(usize)` - Set instruction/stack pointers -- `with_module_base_address(usize)`, `with_symbol_address(usize)` - Set base addresses -- `with_build_id(String)` - Set build ID -- `build_id_type(CxxBuildIdType)` - Set build ID format (GNU, GO, PDB, SHA1) -- `file_type(CxxFileType)` - Set binary format (APK, ELF, PE) -- `with_path(String)` - Set module path -- `with_relative_address(usize)` - Set relative address - -**StackTrace Methods:** -- `add_frame(Box, bool)` - Add a frame (bool = incomplete) -- `mark_complete()` - Mark trace as complete - -**Building & Output:** -```cpp -auto crash_info = crashinfo_build(std::move(builder)); -auto json = crash_info->to_json(); -``` - -## Building and Running - -### Unix (Linux/macOS) - -The `build-and-run-crashinfo.sh` script handles the entire build process: +**Build and run:** +Unix (Linux/macOS): ```bash -./examples/cxx/build-and-run-crashinfo.sh +./build-profiling.sh ``` -### Windows - -The `build-and-run-crashinfo.ps1` PowerShell script handles the build process on Windows: - +Windows: ```powershell -.\examples\cxx\build-and-run-crashinfo.ps1 +.\build-profiling.ps1 ``` -**Prerequisites for Windows:** -- Either MSVC (via Visual Studio) or MinGW/LLVM with C++ compiler -- PowerShell 5.0 or later (comes with Windows 10+) -- Rust toolchain +**Key features:** +- Type-safe API for building profiles +- Support for samples, locations, mappings, and labels +- String interning for efficient memory usage +- Upscaling rules (Poisson and Proportional) +- Endpoint tracking for web service profiling +- Pprof format serialization with zstd compression +- **Export to Datadog** via agent or agentless mode +- Support for attaching additional compressed files +- Per-profile tags and metadata +- Automatic memory management +- Exception-based error handling +- Modern C++20 syntax with designated initializers and `std::format` -The build script will: -1. Build libdd-crashtracker with the `cxx` feature enabled -2. Find the CXX bridge headers and libraries -3. Compile the C++ example (automatically detects MSVC or MinGW/Clang) -4. Run the example and display the output +**Core Types:** +- `Profile` - Profile builder for collecting samples +- `ProfileExporter` - Exporter for sending profiles to Datadog +- `Tag` - Key-value tags for profile metadata +- `AttachmentFile` - Additional file to attach to profile (name + data bytes) -## Example Output +**Export Modes:** -The example creates a crash report with: -- Error kind and message -- Library metadata with tags -- Process and OS information -- A stack trace with multiple frames (debug info + binary addresses) -- Counters and log messages -- Timestamp +By default, the example saves the profile to `profile.pprof`. To export to Datadog, set environment variables: + +1. **Agent mode**: Sends profiles to the local Datadog agent + ```bash + DD_AGENT_URL=http://localhost:8126 ./build-profiling.sh + ``` + +2. **Agentless mode**: Sends profiles directly to Datadog intake + ```bash + DD_API_KEY=your-api-key DD_SITE=datadoghq.com ./build-profiling.sh + ``` + +**API Example:** -The output is a JSON object that can be sent to Datadog's crash tracking service. +See [`profiling.cpp`](profiling.cpp) for a complete example showing profile creation, sample collection, and exporting to Datadog with optional attachments and metadata. -## Notes +**Requirements:** +- C++20 compiler +- For agent mode: Datadog agent running (default: localhost:8126) +- For agentless mode: Valid Datadog API key -- The CXX bindings use `rust::String` types which need to be converted to `std::string` for use with standard C++ streams -- All functions that can fail will use exceptions (standard C++ exception handling) -- The bindings are type-safe and prevent many common C FFI errors -- Memory is managed automatically through RAII and smart pointers +## Build Scripts -## Comparison to C FFI +The examples use a consolidated build system: -The CXX bindings provide several advantages over the traditional C FFI: +- **Unix (Linux/macOS)**: `build-and-run.sh ` +- **Windows**: `build-and-run.ps1 -CrateName -ExampleName ` -1. **Type Safety**: No void pointers, proper type checking at compile time -2. **Memory Safety**: Automatic memory management through smart pointers -3. **Ergonomics**: More natural C++ idioms, no need for manual handle management -4. **Error Handling**: Exceptions instead of error codes -5. **String Handling**: Seamless `rust::String` ↔ C++ string interop +Convenience wrappers are provided for each example: +- `build-and-run-crashinfo.sh` / `build-and-run-crashinfo.ps1` +- `build-profiling.sh` / `build-profiling.ps1` ## Requirements - C++20 or later - Rust toolchain +- C++ compiler (clang++ or g++) - Platform: macOS, Linux, or Windows - Windows: Requires MSVC (via Visual Studio) or MinGW/LLVM diff --git a/examples/cxx/build-and-run-crashinfo.ps1 b/examples/cxx/build-and-run-crashinfo.ps1 index f48ff6002..45277c027 100644 --- a/examples/cxx/build-and-run-crashinfo.ps1 +++ b/examples/cxx/build-and-run-crashinfo.ps1 @@ -1,163 +1,13 @@ # Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ # SPDX-License-Identifier: Apache-2.0 -# Build and run the CXX crashinfo example on Windows -$ErrorActionPreference = "Stop" - +# Build and run the CXX crashinfo example $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path -$PROJECT_ROOT = (Get-Item (Join-Path $SCRIPT_DIR ".." "..")).FullName -Set-Location $PROJECT_ROOT - -Write-Host "šŸ”Ø Building libdd-crashtracker with cxx feature..." -ForegroundColor Cyan -cargo build -p libdd-crashtracker --features cxx --release - -Write-Host "šŸ” Finding CXX bridge headers..." -ForegroundColor Cyan -$CXX_BRIDGE_INCLUDE = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out\cxxbridge\include" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName -$CXX_BRIDGE_CRATE = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out\cxxbridge\crate" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName -$RUST_CXX_INCLUDE = Get-ChildItem -Path "target\release\build\cxx-*\out" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - -if (-not $CXX_BRIDGE_INCLUDE -or -not $CXX_BRIDGE_CRATE -or -not $RUST_CXX_INCLUDE) { - Write-Host "āŒ Error: Could not find CXX bridge directories" -ForegroundColor Red - exit 1 -} - -Write-Host "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" -ForegroundColor Green -Write-Host "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" -ForegroundColor Green -Write-Host "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" -ForegroundColor Green - -# Check if we have MSVC (cl.exe) or MinGW (g++/clang++) -# Note: Prefer MSVC on Windows as it's the default Rust toolchain -$MSVC = Get-Command cl.exe -ErrorAction SilentlyContinue -$GPP = Get-Command g++.exe -ErrorAction SilentlyContinue -$CLANGPP = Get-Command clang++.exe -ErrorAction SilentlyContinue - -# Auto-detect which toolchain Rust used by checking which library exists -# Note: On Windows, Rust still uses 'lib' prefix even for MSVC .lib files -$HAS_MSVC_LIB = Test-Path (Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.lib") -$HAS_GNU_LIB = (Test-Path (Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.a")) -or ` - (Test-Path (Join-Path $PROJECT_ROOT "target\release\liblibdd_crashtracker.a")) - -if ($HAS_MSVC_LIB -and $MSVC) { - $USE_MSVC = $true - Write-Host "Detected MSVC Rust toolchain" -ForegroundColor Cyan -} elseif ($HAS_GNU_LIB -and ($GPP -or $CLANGPP)) { - $USE_MSVC = $false - Write-Host "Detected GNU Rust toolchain" -ForegroundColor Cyan -} elseif ($MSVC) { - $USE_MSVC = $true - Write-Host "Defaulting to MSVC (library not found yet, will check after)" -ForegroundColor Yellow -} elseif ($GPP -or $CLANGPP) { - $USE_MSVC = $false - Write-Host "Defaulting to GNU toolchain (library not found yet, will check after)" -ForegroundColor Yellow -} else { - Write-Host "āŒ Error: No C++ compiler found. Please install MSVC (via Visual Studio) or MinGW/LLVM" -ForegroundColor Red - exit 1 -} - -Write-Host "šŸ”Ø Finding libraries..." -ForegroundColor Cyan -# Note: Rust library naming varies by platform and toolchain -if ($USE_MSVC) { - # MSVC: libdd_crashtracker.lib (Rust keeps the lib prefix even on Windows) - $CRASHTRACKER_LIB = Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.lib" - $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out" -Filter "libdd-crashtracker-cxx.lib" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName -} else { - # MinGW: Try both possible naming patterns - $CRASHTRACKER_LIB_1 = Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.a" - $CRASHTRACKER_LIB_2 = Join-Path $PROJECT_ROOT "target\release\liblibdd_crashtracker.a" - - if (Test-Path $CRASHTRACKER_LIB_1) { - $CRASHTRACKER_LIB = $CRASHTRACKER_LIB_1 - } elseif (Test-Path $CRASHTRACKER_LIB_2) { - $CRASHTRACKER_LIB = $CRASHTRACKER_LIB_2 - } else { - $CRASHTRACKER_LIB = $CRASHTRACKER_LIB_1 # Use this for error message - } - - # Try both naming patterns for CXX bridge - $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out" -Filter "libdd-crashtracker-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - if (-not $CXX_BRIDGE_LIB) { - $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out" -Filter "liblibdd-crashtracker-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - } -} - -if (-not (Test-Path $CRASHTRACKER_LIB)) { - Write-Host "āŒ Error: Could not find libdd-crashtracker library at $CRASHTRACKER_LIB" -ForegroundColor Red - if (-not $MSVC) { - Write-Host "Searched for: libdd_crashtracker.a and liblibdd_crashtracker.a" -ForegroundColor Yellow - Write-Host "Files in target/release/:" -ForegroundColor Yellow - Get-ChildItem -Path "target\release" -Filter "*crashtracker*" | Select-Object -First 10 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } - exit 1 -} - -if (-not $CXX_BRIDGE_LIB) { - if ($USE_MSVC) { - Write-Host "āŒ Error: Could not find CXX bridge library (looking for libdd-crashtracker-cxx.lib)" -ForegroundColor Red - } else { - Write-Host "āŒ Error: Could not find CXX bridge library" -ForegroundColor Red - Write-Host "Searched for: libdd-crashtracker-cxx.a and liblibdd-crashtracker-cxx.a" -ForegroundColor Yellow - } - exit 1 -} - -Write-Host "šŸ“š Crashtracker library: $CRASHTRACKER_LIB" -ForegroundColor Green -Write-Host "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" -ForegroundColor Green - -Write-Host "šŸ”Ø Compiling C++ example..." -ForegroundColor Cyan - -if ($USE_MSVC) { - Write-Host "Using MSVC compiler" -ForegroundColor Yellow - - # MSVC compilation - # Use /MD (dynamic CRT) to match the default Rust build - cl.exe /std:c++20 /EHsc /MD ` - /I"$CXX_BRIDGE_INCLUDE" ` - /I"$CXX_BRIDGE_CRATE" ` - /I"$RUST_CXX_INCLUDE" ` - /I"$PROJECT_ROOT" ` - examples\cxx\crashinfo.cpp ` - "$CRASHTRACKER_LIB" ` - "$CXX_BRIDGE_LIB" ` - ws2_32.lib advapi32.lib userenv.lib ntdll.lib bcrypt.lib ` - dbghelp.lib psapi.lib ole32.lib powrprof.lib ` - /Fe:examples\cxx\crashinfo.exe - - if ($LASTEXITCODE -ne 0) { - Write-Host "āŒ Compilation failed" -ForegroundColor Red - exit 1 - } -} elseif ($GPP -or $CLANGPP) { - $COMPILER = if ($GPP) { "g++" } else { "clang++" } - Write-Host "Using $COMPILER compiler" -ForegroundColor Yellow - - # MinGW/Clang compilation - needs proper library ordering and Rust std lib - & $COMPILER -std=c++20 ` - -I"$CXX_BRIDGE_INCLUDE" ` - -I"$CXX_BRIDGE_CRATE" ` - -I"$RUST_CXX_INCLUDE" ` - -I"$PROJECT_ROOT" ` - examples/cxx/crashinfo.cpp ` - "$CXX_BRIDGE_LIB" ` - "$CRASHTRACKER_LIB" ` - -lws2_32 -ladvapi32 -luserenv -lntdll -lbcrypt ` - -ldbghelp -lpsapi -lole32 -lpowrprof ` - -lgcc_eh -lpthread ` - -o examples/cxx/crashinfo.exe - - if ($LASTEXITCODE -ne 0) { - Write-Host "āŒ Compilation failed" -ForegroundColor Red - exit 1 - } -} - -Write-Host "šŸš€ Running example..." -ForegroundColor Cyan -& ".\examples\cxx\crashinfo.exe" - -if ($LASTEXITCODE -ne 0) { - Write-Host "āŒ Example failed with exit code $LASTEXITCODE" -ForegroundColor Red - exit 1 -} -Write-Host "" -Write-Host "āœ… Success!" -ForegroundColor Green +& "$SCRIPT_DIR\build-and-run.ps1" ` + -CrateName "libdd-crashtracker" ` + -ExampleName "crashinfo" ` + -ExtraMsvcLibs "dbghelp.lib psapi.lib powrprof.lib" ` + -ExtraGnuLibs "-ldbghelp -lpsapi -lole32 -lpowrprof" +exit $LASTEXITCODE diff --git a/examples/cxx/build-and-run-crashinfo.sh b/examples/cxx/build-and-run-crashinfo.sh index acc154888..9770419c0 100755 --- a/examples/cxx/build-and-run-crashinfo.sh +++ b/examples/cxx/build-and-run-crashinfo.sh @@ -3,67 +3,8 @@ # SPDX-License-Identifier: Apache-2.0 # Build and run the CXX crashinfo example -set -e - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$PROJECT_ROOT" - -echo "šŸ”Ø Building libdd-crashtracker with cxx feature..." -cargo build -p libdd-crashtracker --features cxx --release - -echo "šŸ” Finding CXX bridge headers..." -CXX_BRIDGE_INCLUDE=$(find target/release/build/libdd-crashtracker-*/out/cxxbridge/include -type d 2>/dev/null | head -n 1) -CXX_BRIDGE_CRATE=$(find target/release/build/libdd-crashtracker-*/out/cxxbridge/crate -type d 2>/dev/null | head -n 1) -RUST_CXX_INCLUDE=$(find target/release/build/cxx-*/out -type d 2>/dev/null | head -n 1) - -if [ -z "$CXX_BRIDGE_INCLUDE" ] || [ -z "$CXX_BRIDGE_CRATE" ] || [ -z "$RUST_CXX_INCLUDE" ]; then - echo "āŒ Error: Could not find CXX bridge directories" - exit 1 -fi - -echo "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" -echo "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" -echo "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" - -echo "šŸ”Ø Finding libraries..." -CRASHTRACKER_LIB="$PROJECT_ROOT/target/release/liblibdd_crashtracker.a" -CXX_BRIDGE_LIB=$(find target/release/build/libdd-crashtracker-*/out -name "liblibdd-crashtracker-cxx.a" | head -n 1) - -if [ ! -f "$CRASHTRACKER_LIB" ]; then - echo "āŒ Error: Could not find libdd-crashtracker library at $CRASHTRACKER_LIB" - exit 1 -fi - -if [ ! -f "$CXX_BRIDGE_LIB" ]; then - echo "āŒ Error: Could not find CXX bridge library" - exit 1 -fi - -echo "šŸ“š Crashtracker library: $CRASHTRACKER_LIB" -echo "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" - -echo "šŸ”Ø Compiling C++ example..." -# Platform-specific linker flags -if [[ "$OSTYPE" == "darwin"* ]]; then - PLATFORM_LIBS="-framework Security -framework CoreFoundation" -else - PLATFORM_LIBS="" -fi - -c++ -std=c++20 \ - -I"$CXX_BRIDGE_INCLUDE" \ - -I"$CXX_BRIDGE_CRATE" \ - -I"$RUST_CXX_INCLUDE" \ - -I"$PROJECT_ROOT" \ - examples/cxx/crashinfo.cpp \ - "$CRASHTRACKER_LIB" \ - "$CXX_BRIDGE_LIB" \ - -lpthread -ldl $PLATFORM_LIBS \ - -o examples/cxx/crashinfo - -echo "šŸš€ Running example..." -./examples/cxx/crashinfo -echo "" -echo "āœ… Success!" +# Note: Extra libraries for crashtracker on Unix/Linux/macOS are typically not needed +# as the equivalent functionality is provided by the standard system libraries +exec "$SCRIPT_DIR/build-and-run.sh" libdd-crashtracker crashinfo diff --git a/examples/cxx/build-and-run.ps1 b/examples/cxx/build-and-run.ps1 new file mode 100644 index 000000000..70e7cdb3f --- /dev/null +++ b/examples/cxx/build-and-run.ps1 @@ -0,0 +1,206 @@ +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Generic script to build and run CXX examples +# Usage: .\build-and-run.ps1 -CrateName -ExampleName [-ExtraMsvcLibs ] [-ExtraGnuLibs ] [-ExtraMsvcFlags ] +# Example: .\build-and-run.ps1 -CrateName libdd-profiling -ExampleName profiling +# Example: .\build-and-run.ps1 -CrateName libdd-crashtracker -ExampleName crashinfo -ExtraMsvcLibs "dbghelp.lib psapi.lib" -ExtraGnuLibs "-ldbghelp -lpsapi" -ExtraMsvcFlags "/MD" + +param( + [Parameter(Mandatory=$true)] + [string]$CrateName, + + [Parameter(Mandatory=$true)] + [string]$ExampleName, + + [Parameter(Mandatory=$false)] + [string]$ExtraMsvcLibs = "", + + [Parameter(Mandatory=$false)] + [string]$ExtraGnuLibs = "", + + [Parameter(Mandatory=$false)] + [string]$ExtraMsvcFlags = "" +) + +$ErrorActionPreference = "Stop" + +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +$PROJECT_ROOT = (Get-Item (Join-Path $SCRIPT_DIR ".." "..")).FullName +Set-Location $PROJECT_ROOT + +Write-Host "šŸ”Ø Building $CrateName with cxx feature..." -ForegroundColor Cyan +cargo build -p $CrateName --features cxx --release + +Write-Host "šŸ” Finding CXX bridge headers..." -ForegroundColor Cyan +$CXX_BRIDGE_INCLUDE = Get-ChildItem -Path "target\release\build\$CrateName-*\out\cxxbridge\include" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName +$CXX_BRIDGE_CRATE = Get-ChildItem -Path "target\release\build\$CrateName-*\out\cxxbridge\crate" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName +$RUST_CXX_INCLUDE = Get-ChildItem -Path "target\release\build\cxx-*\out" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName + +if (-not $CXX_BRIDGE_INCLUDE -or -not $CXX_BRIDGE_CRATE -or -not $RUST_CXX_INCLUDE) { + Write-Host "āŒ Error: Could not find CXX bridge directories" -ForegroundColor Red + exit 1 +} + +Write-Host "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" -ForegroundColor Green +Write-Host "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" -ForegroundColor Green +Write-Host "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" -ForegroundColor Green + +# Check if we have MSVC (cl.exe) or MinGW (g++/clang++) +$MSVC = Get-Command cl.exe -ErrorAction SilentlyContinue +$GPP = Get-Command g++.exe -ErrorAction SilentlyContinue +$CLANGPP = Get-Command clang++.exe -ErrorAction SilentlyContinue + +# Convert crate name with dashes to underscores for library name +$LibName = $CrateName -replace '-', '_' + +# Auto-detect which toolchain Rust used by checking which library exists +$HAS_MSVC_LIB = Test-Path (Join-Path $PROJECT_ROOT "target\release\${LibName}.lib") +$HAS_GNU_LIB = (Test-Path (Join-Path $PROJECT_ROOT "target\release\${LibName}.a")) -or ` + (Test-Path (Join-Path $PROJECT_ROOT "target\release\lib${LibName}.a")) + +if ($HAS_MSVC_LIB -and $MSVC) { + $USE_MSVC = $true + Write-Host "Detected MSVC Rust toolchain" -ForegroundColor Cyan +} elseif ($HAS_GNU_LIB -and ($GPP -or $CLANGPP)) { + $USE_MSVC = $false + Write-Host "Detected GNU Rust toolchain" -ForegroundColor Cyan +} elseif ($MSVC) { + $USE_MSVC = $true + Write-Host "Defaulting to MSVC (library not found yet, will check after)" -ForegroundColor Yellow +} elseif ($GPP -or $CLANGPP) { + $USE_MSVC = $false + Write-Host "Defaulting to GNU toolchain (library not found yet, will check after)" -ForegroundColor Yellow +} else { + Write-Host "āŒ Error: No C++ compiler found. Please install MSVC (via Visual Studio) or MinGW/LLVM" -ForegroundColor Red + exit 1 +} + +Write-Host "šŸ”Ø Finding libraries..." -ForegroundColor Cyan +if ($USE_MSVC) { + # MSVC naming + $CRATE_LIB = Join-Path $PROJECT_ROOT "target\release\${LibName}.lib" + $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\$CrateName-*\out" -Filter "$CrateName-cxx.lib" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName +} else { + # MinGW naming - try both patterns + $CRATE_LIB_1 = Join-Path $PROJECT_ROOT "target\release\${LibName}.a" + $CRATE_LIB_2 = Join-Path $PROJECT_ROOT "target\release\lib${LibName}.a" + + if (Test-Path $CRATE_LIB_1) { + $CRATE_LIB = $CRATE_LIB_1 + } elseif (Test-Path $CRATE_LIB_2) { + $CRATE_LIB = $CRATE_LIB_2 + } else { + $CRATE_LIB = $CRATE_LIB_1 + } + + # Try both naming patterns for CXX bridge + $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\$CrateName-*\out" -Filter "$CrateName-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName + if (-not $CXX_BRIDGE_LIB) { + $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\$CrateName-*\out" -Filter "lib$CrateName-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName + } +} + +if (-not (Test-Path $CRATE_LIB)) { + Write-Host "āŒ Error: Could not find $CrateName library at $CRATE_LIB" -ForegroundColor Red + if (-not $USE_MSVC) { + Write-Host "Searched for: ${LibName}.a and lib${LibName}.a" -ForegroundColor Yellow + } + exit 1 +} + +if (-not $CXX_BRIDGE_LIB) { + Write-Host "āŒ Error: Could not find CXX bridge library for $CrateName" -ForegroundColor Red + exit 1 +} + +Write-Host "šŸ“š Crate library: $CRATE_LIB" -ForegroundColor Green +Write-Host "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" -ForegroundColor Green + +Write-Host "šŸ”Ø Compiling C++ example..." -ForegroundColor Cyan + +$ExampleCpp = "examples\cxx\$ExampleName.cpp" +$ExampleExe = "examples\cxx\$ExampleName.exe" + +if ($USE_MSVC) { + Write-Host "Using MSVC compiler" -ForegroundColor Yellow + + # Build the compile command + # Note: /MD is required to match Rust's dynamic CRT (default on Windows) + $MsvcArgs = @( + "/std:c++20", "/EHsc", "/MD" + ) + + # Add extra MSVC flags if specified + if ($ExtraMsvcFlags) { + $MsvcArgs += $ExtraMsvcFlags.Split(" ") + } + + $MsvcArgs += @( + "/I$CXX_BRIDGE_INCLUDE", + "/I$CXX_BRIDGE_CRATE", + "/I$RUST_CXX_INCLUDE", + "/I$PROJECT_ROOT", + $ExampleCpp, + $CRATE_LIB, + $CXX_BRIDGE_LIB, + "ws2_32.lib", "advapi32.lib", "userenv.lib", "ntdll.lib", "bcrypt.lib", "ole32.lib" + ) + + # Add extra MSVC libraries if specified + if ($ExtraMsvcLibs) { + $MsvcArgs += $ExtraMsvcLibs.Split(" ") + } + + $MsvcArgs += "/Fe$ExampleExe" + + & cl.exe $MsvcArgs + + if ($LASTEXITCODE -ne 0) { + Write-Host "āŒ Compilation failed" -ForegroundColor Red + exit 1 + } +} elseif ($GPP -or $CLANGPP) { + $COMPILER = if ($GPP) { "g++" } else { "clang++" } + Write-Host "Using $COMPILER compiler" -ForegroundColor Yellow + + # Build the compile command + $GnuArgs = @( + "-std=c++20", + "-I`"$CXX_BRIDGE_INCLUDE`"", + "-I`"$CXX_BRIDGE_CRATE`"", + "-I`"$RUST_CXX_INCLUDE`"", + "-I`"$PROJECT_ROOT`"", + $ExampleCpp, + "`"$CXX_BRIDGE_LIB`"", + "`"$CRATE_LIB`"", + "-lws2_32", "-ladvapi32", "-luserenv", "-lntdll", "-lbcrypt" + ) + + # Add extra GNU libraries if specified + if ($ExtraGnuLibs) { + $GnuArgs += $ExtraGnuLibs.Split(" ") + } + + $GnuArgs += @("-lgcc_eh", "-lpthread", "-o", $ExampleExe) + + & $COMPILER $GnuArgs + + if ($LASTEXITCODE -ne 0) { + Write-Host "āŒ Compilation failed" -ForegroundColor Red + exit 1 + } +} + +Write-Host "šŸš€ Running example..." -ForegroundColor Cyan +& ".\$ExampleExe" + +if ($LASTEXITCODE -ne 0) { + Write-Host "āŒ Example failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "āœ… Success!" -ForegroundColor Green + + diff --git a/examples/cxx/build-and-run.sh b/examples/cxx/build-and-run.sh new file mode 100755 index 000000000..914f79abd --- /dev/null +++ b/examples/cxx/build-and-run.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Generic script to build and run CXX examples +# Usage: ./build-and-run.sh [extra-libs] [extra-cxx-flags] +# Example: ./build-and-run.sh libdd-profiling profiling +# Example: ./build-and-run.sh libdd-crashtracker crashinfo "-ldbghelp -lpsapi -lole32 -lpowrprof" "" + +set -e + +if [ $# -lt 2 ]; then + echo "Usage: $0 [extra-libs] [extra-cxx-flags]" + echo "Example: $0 libdd-profiling profiling" + echo "Example: $0 libdd-crashtracker crashinfo '-ldbghelp -lpsapi -lole32 -lpowrprof'" + exit 1 +fi + +CRATE_NAME="$1" +EXAMPLE_NAME="$2" +EXTRA_LIBS="${3:-}" +EXTRA_CXX_FLAGS="${4:-}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$PROJECT_ROOT" + +echo "šŸ”Ø Building $CRATE_NAME with cxx feature..." +cargo build -p "$CRATE_NAME" --features cxx --release + +echo "šŸ” Finding CXX bridge headers..." +CXX_BRIDGE_INCLUDE=$(find target/release/build/${CRATE_NAME}-*/out/cxxbridge/include -type d 2>/dev/null | head -n 1) +CXX_BRIDGE_CRATE=$(find target/release/build/${CRATE_NAME}-*/out/cxxbridge/crate -type d 2>/dev/null | head -n 1) +RUST_CXX_INCLUDE=$(find target/release/build/cxx-*/out -type d 2>/dev/null | head -n 1) + +if [ -z "$CXX_BRIDGE_INCLUDE" ] || [ -z "$CXX_BRIDGE_CRATE" ] || [ -z "$RUST_CXX_INCLUDE" ]; then + echo "āŒ Error: Could not find CXX bridge directories" + exit 1 +fi + +echo "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" +echo "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" +echo "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" + +echo "šŸ”Ø Finding libraries..." +# Convert crate name with dashes to underscores for library name +LIB_NAME=$(echo "$CRATE_NAME" | tr '-' '_') +CRATE_LIB="$PROJECT_ROOT/target/release/lib${LIB_NAME}.a" +CXX_BRIDGE_LIB=$(find target/release/build/${CRATE_NAME}-*/out -name "lib${CRATE_NAME}-cxx.a" | head -n 1) + +if [ ! -f "$CRATE_LIB" ]; then + echo "āŒ Error: Could not find $CRATE_NAME library at $CRATE_LIB" + exit 1 +fi + +if [ ! -f "$CXX_BRIDGE_LIB" ]; then + echo "āŒ Error: Could not find CXX bridge library" + exit 1 +fi + +echo "šŸ“š Crate library: $CRATE_LIB" +echo "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" + +echo "šŸ”Ø Compiling C++ example..." +# Platform-specific linker flags +if [[ "$OSTYPE" == "darwin"* ]]; then + PLATFORM_LIBS="-framework Security -framework CoreFoundation" +else + PLATFORM_LIBS="" +fi + +c++ -std=c++20 $EXTRA_CXX_FLAGS \ + -I"$CXX_BRIDGE_INCLUDE" \ + -I"$CXX_BRIDGE_CRATE" \ + -I"$RUST_CXX_INCLUDE" \ + -I"$PROJECT_ROOT" \ + "examples/cxx/${EXAMPLE_NAME}.cpp" \ + "$CRATE_LIB" \ + "$CXX_BRIDGE_LIB" \ + -lpthread -ldl $PLATFORM_LIBS $EXTRA_LIBS \ + -o "examples/cxx/${EXAMPLE_NAME}" + +echo "šŸš€ Running example..." +"./examples/cxx/${EXAMPLE_NAME}" + +echo "" +echo "āœ… Success!" + + diff --git a/examples/cxx/build-profiling.ps1 b/examples/cxx/build-profiling.ps1 new file mode 100644 index 000000000..b58d23858 --- /dev/null +++ b/examples/cxx/build-profiling.ps1 @@ -0,0 +1,7 @@ +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Build and run the CXX profiling example +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +& "$SCRIPT_DIR\build-and-run.ps1" -CrateName "libdd-profiling" -ExampleName "profiling" +exit $LASTEXITCODE diff --git a/examples/cxx/build-profiling.sh b/examples/cxx/build-profiling.sh new file mode 100755 index 000000000..defd8a08c --- /dev/null +++ b/examples/cxx/build-profiling.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Build and run the CXX profiling example +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/build-and-run.sh" libdd-profiling profiling diff --git a/examples/cxx/profiling.cpp b/examples/cxx/profiling.cpp new file mode 100644 index 000000000..32ece1610 --- /dev/null +++ b/examples/cxx/profiling.cpp @@ -0,0 +1,316 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include "libdd-profiling/src/cxx.rs.h" + +using namespace datadog::profiling; + +int main() { + try { + std::cout << "=== Datadog Profiling CXX Bindings Example ===" << std::endl; + std::cout << "\nCreating Profile..." << std::endl; + + ValueType wall_time{ + .type_ = "wall-time", + .unit = "nanoseconds" + }; + + Period period{ + .value_type = wall_time, + .value = 60 + }; + + auto profile = Profile::create({wall_time}, period); + std::cout << "āœ… Profile created" << std::endl; + + std::cout << "Adding upscaling rules..." << std::endl; + + // Poisson upscaling for sampled data + std::vector value_offsets = {0}; + profile->add_upscaling_rule_poisson( + {value_offsets.data(), value_offsets.size()}, + "thread_id", + "0", + 0, + 0, + 1000000 + ); + + // Proportional upscaling (scale by factor) + profile->add_upscaling_rule_proportional( + {value_offsets.data(), value_offsets.size()}, + "thread_id", + "1", + 100.0 + ); + + std::cout << "āœ… Added upscaling rules" << std::endl; + + std::cout << "Adding samples..." << std::endl; + for (int i = 0; i < 100; i++) { + // String storage must outlive add_sample() call for the profile to intern them + std::vector string_storage; + string_storage.push_back(std::format("hot_function_{}", i % 3)); + string_storage.push_back(std::format("_Z12hot_function{}v", i % 3)); + string_storage.push_back(std::format("process_request_{}", i % 5)); + string_storage.push_back(std::format("_Z15process_request{}v", i % 5)); + + Mapping mapping{ + .memory_start = 0x10000000, + .memory_limit = 0x20000000, + .file_offset = 0, + .filename = "/usr/lib/libexample.so", + .build_id = "abc123" + }; + + auto wall_time_value = 1000000 + (i % 1000) * 1000; + + if (i % 7 == 0) { + profile->add_sample(Sample{ + .locations = { + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[0], + .system_name = string_storage[1], + .filename = "/src/hot_path.cpp" + }, + .address = uint64_t(0x10003000 + (i % 3) * 0x100), + .line = 100 + (i % 3) * 10 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[2], + .system_name = string_storage[3], + .filename = "/src/handler.cpp" + }, + .address = uint64_t(0x10002000 + (i % 5) * 0x80), + .line = 50 + (i % 5) * 5 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = "main", + .system_name = "main", + .filename = "/src/main.cpp" + }, + .address = 0x10001000, + .line = 42 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = "worker_loop", + .system_name = "_Z11worker_loopv", + .filename = "/src/worker.cpp" + }, + .address = 0x10000500, + .line = 25 + } + }, + .values = {wall_time_value}, + .labels = { + Label{.key = "thread_id", .str = "", .num = int64_t(i % 4), .num_unit = ""}, + Label{.key = "sample_id", .str = "", .num = int64_t(i), .num_unit = ""} + } + }); + } else { + profile->add_sample(Sample{ + .locations = { + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[0], + .system_name = string_storage[1], + .filename = "/src/hot_path.cpp" + }, + .address = uint64_t(0x10003000 + (i % 3) * 0x100), + .line = 100 + (i % 3) * 10 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[2], + .system_name = string_storage[3], + .filename = "/src/handler.cpp" + }, + .address = uint64_t(0x10002000 + (i % 5) * 0x80), + .line = 50 + (i % 5) * 5 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = "main", + .system_name = "main", + .filename = "/src/main.cpp" + }, + .address = 0x10001000, + .line = 42 + } + }, + .values = {wall_time_value}, + .labels = { + Label{.key = "thread_id", .str = "", .num = int64_t(i % 4), .num_unit = ""}, + Label{.key = "sample_id", .str = "", .num = int64_t(i), .num_unit = ""} + } + }); + } + } + + std::cout << "āœ… Added 100 samples" << std::endl; + + std::cout << "Adding endpoint mappings..." << std::endl; + profile->add_endpoint(12345, "/api/users"); + profile->add_endpoint(67890, "/api/orders"); + profile->add_endpoint(11111, "/api/products"); + + profile->add_endpoint_count("/api/users", 150); + profile->add_endpoint_count("/api/orders", 75); + profile->add_endpoint_count("/api/products", 200); + std::cout << "āœ… Added endpoint mappings and counts" << std::endl; + + // Check if we should export to Datadog or save to file + const char* agent_url = std::getenv("DD_AGENT_URL"); + const char* api_key = std::getenv("DD_API_KEY"); + + if (agent_url || api_key) { + // Export to Datadog + std::cout << "\n=== Exporting to Datadog ===" << std::endl; + + try { + // Example: Create an additional file to attach (e.g., application metadata) + std::string app_metadata = R"({ + "app_version": "1.2.3", + "build_id": "abc123", + "profiling_mode": "continuous", + "sample_count": 100 +})"; + std::vector metadata_bytes(app_metadata.begin(), app_metadata.end()); + + if (api_key) { + // Agentless mode - send directly to Datadog intake + const char* site = std::getenv("DD_SITE"); + std::string dd_site = site ? site : "datadoghq.com"; + + std::cout << "Creating agentless exporter (site: " << dd_site << ")..." << std::endl; + auto exporter = ProfileExporter::create_agentless_exporter( + "dd-trace-cpp", + "1.0.0", + "native", + { + Tag{.key = "service", .value = "profiling-example"}, + Tag{.key = "env", .value = "dev"}, + Tag{.key = "example", .value = "cxx"} + }, + dd_site.c_str(), + api_key, + 10000 // 10 second timeout (0 = use default) + ); + std::cout << "āœ… Exporter created" << std::endl; + + std::cout << "Exporting profile to Datadog with additional metadata..." << std::endl; + + exporter->send_profile( + *profile, + // Files to compress and attach + {AttachmentFile{ + .name = "app_metadata.json", + .data = {metadata_bytes.data(), metadata_bytes.size()} + }}, + // Additional per-profile tags + { + Tag{.key = "export_id", .value = "12345"}, + Tag{.key = "host", .value = "example-host"} + }, + // Process-level tags (comma-separated) + "language:cpp,profiler_version:1.0,runtime:native", + // Internal metadata (JSON string) + R"({"profiler_version": "1.0", "custom_field": "demo"})", + // System info (JSON string) + R"({"os": "macos", "arch": "arm64", "cores": 8})" + ); + std::cout << "āœ… Profile exported successfully!" << std::endl; + } else { + // Agent mode - send to local Datadog agent + std::cout << "Creating agent exporter (url: " << agent_url << ")..." << std::endl; + auto exporter = ProfileExporter::create_agent_exporter( + "dd-trace-cpp", + "1.0.0", + "native", + { + Tag{.key = "service", .value = "profiling-example"}, + Tag{.key = "env", .value = "dev"}, + Tag{.key = "example", .value = "cxx"} + }, + agent_url, + 10000 // 10 second timeout (0 = use default) + ); + std::cout << "āœ… Exporter created" << std::endl; + + std::cout << "Exporting profile to Datadog with additional metadata..." << std::endl; + + exporter->send_profile( + *profile, + // Files to compress and attach + {AttachmentFile{ + .name = "app_metadata.json", + .data = {metadata_bytes.data(), metadata_bytes.size()} + }}, + // Additional per-profile tags + { + Tag{.key = "export_id", .value = "12345"}, + Tag{.key = "host", .value = "example-host"} + }, + // Process-level tags (comma-separated) + "language:cpp,profiler_version:1.0,runtime:native", + // Internal metadata (JSON string) + R"({"profiler_version": "1.0", "custom_field": "demo"})", + // System info (JSON string) + R"({"os": "macos", "arch": "arm64", "cores": 8})" + ); + std::cout << "āœ… Profile exported successfully!" << std::endl; + } + + } catch (const std::exception& e) { + std::cerr << "āš ļø Failed to export profile: " << e.what() << std::endl; + std::cerr << " Falling back to file export..." << std::endl; + + // Fall back to file export on error + auto serialized = profile->serialize_to_vec(); + std::ofstream out("profile.pprof", std::ios::binary); + out.write(reinterpret_cast(serialized.data()), serialized.size()); + out.close(); + std::cout << "āœ… Profile written to profile.pprof" << std::endl; + } + } else { + // Save to file + std::cout << "\n=== Saving to File ===" << std::endl; + std::cout << "Serializing profile..." << std::endl; + auto serialized = profile->serialize_to_vec(); + std::cout << "āœ… Profile serialized to " << serialized.size() << " bytes" << std::endl; + + std::ofstream out("profile.pprof", std::ios::binary); + out.write(reinterpret_cast(serialized.data()), serialized.size()); + out.close(); + std::cout << "āœ… Profile written to profile.pprof" << std::endl; + + std::cout << "\nā„¹ļø To export to Datadog instead, set environment variables:" << std::endl; + std::cout << " Agent mode: DD_AGENT_URL=http://localhost:8126" << std::endl; + std::cout << " Agentless mode: DD_API_KEY= [DD_SITE=datadoghq.com]" << std::endl; + } + + std::cout << "\nāœ… Success!" << std::endl; + return 0; + + } catch (const std::exception& e) { + std::cerr << "āŒ Exception: " << e.what() << std::endl; + return 1; + } +} diff --git a/libdd-profiling/Cargo.toml b/libdd-profiling/Cargo.toml index bc8eb7092..5c8b9c113 100644 --- a/libdd-profiling/Cargo.toml +++ b/libdd-profiling/Cargo.toml @@ -13,9 +13,13 @@ license.workspace = true autobenches = false [lib] -crate-type = ["lib"] +crate-type = ["lib", "staticlib"] bench = false +[features] +default = [] +cxx = ["dep:cxx", "dep:cxx-build"] + [[bench]] name = "main" harness = false @@ -50,9 +54,13 @@ thiserror = "2" tokio = {version = "1.23", features = ["rt", "macros"]} tokio-util = "0.7.1" zstd = { version = "0.13", default-features = false } +cxx = { version = "1.0", optional = true } [dev-dependencies] bolero = "0.13" criterion = "0.5.1" lz4_flex = { version = "0.9", default-features = false, features = ["std", "frame"] } proptest = "1" + +[build-dependencies] +cxx-build = { version = "1.0", optional = true } diff --git a/libdd-profiling/build.rs b/libdd-profiling/build.rs new file mode 100644 index 000000000..b32dff0f9 --- /dev/null +++ b/libdd-profiling/build.rs @@ -0,0 +1,14 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +fn main() { + // Build CXX bridge if feature is enabled + #[cfg(feature = "cxx")] + { + cxx_build::bridge("src/cxx.rs") + .flag_if_supported("-std=c++20") + .compile("libdd-profiling-cxx"); + + println!("cargo:rerun-if-changed=src/cxx.rs"); + } +} diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs new file mode 100644 index 000000000..9c332f042 --- /dev/null +++ b/libdd-profiling/src/cxx.rs @@ -0,0 +1,882 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! CXX bindings for profiling module - provides a safe and idiomatic C++ API + +#![allow(clippy::needless_lifetimes)] + +use crate::api; +use crate::exporter; +use crate::internal; + +// ============================================================================ +// CXX Bridge - C++ Bindings +// ============================================================================ + +#[cxx::bridge(namespace = "datadog::profiling")] +pub mod ffi { + // Shared structs - CXX-friendly types + struct ValueType<'a> { + type_: &'a str, + unit: &'a str, + } + + struct Period<'a> { + value_type: ValueType<'a>, + value: i64, + } + + struct Mapping<'a> { + memory_start: u64, + memory_limit: u64, + file_offset: u64, + filename: &'a str, + build_id: &'a str, + } + + struct Function<'a> { + name: &'a str, + system_name: &'a str, + filename: &'a str, + } + + struct Location<'a> { + mapping: Mapping<'a>, + function: Function<'a>, + address: u64, + line: i64, + } + + struct Label<'a> { + key: &'a str, + str: &'a str, + num: i64, + num_unit: &'a str, + } + + struct Sample<'a> { + locations: Vec>, + values: Vec, + labels: Vec>, + } + + struct Tag<'a> { + key: &'a str, + value: &'a str, + } + + struct AttachmentFile<'a> { + name: &'a str, + data: &'a [u8], + } + + // Opaque Rust types + extern "Rust" { + type Profile; + type ProfileExporter; + + // Static factory methods for Profile + #[Self = "Profile"] + fn create(sample_types: Vec, period: &Period) -> Result>; + + // Profile methods + fn add_sample(self: &mut Profile, sample: &Sample) -> Result<()>; + fn add_endpoint(self: &mut Profile, local_root_span_id: u64, endpoint: &str) -> Result<()>; + fn add_endpoint_count(self: &mut Profile, endpoint: &str, value: i64) -> Result<()>; + + // Upscaling rule methods (one for each variant) + fn add_upscaling_rule_poisson( + self: &mut Profile, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value_offset: usize, + sampling_distance: u64, + ) -> Result<()>; + + fn add_upscaling_rule_poisson_non_sample_type_count( + self: &mut Profile, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value: u64, + sampling_distance: u64, + ) -> Result<()>; + + fn add_upscaling_rule_proportional( + self: &mut Profile, + offset_values: &[usize], + label_name: &str, + label_value: &str, + scale: f64, + ) -> Result<()>; + + fn reset(self: &mut Profile) -> Result<()>; + fn serialize_to_vec(self: &mut Profile) -> Result>; + + // Static factory methods for ProfileExporter + #[Self = "ProfileExporter"] + fn create_agent_exporter( + profiling_library_name: &str, + profiling_library_version: &str, + family: &str, + tags: Vec, + agent_url: &str, + timeout_ms: u64, + ) -> Result>; + + #[Self = "ProfileExporter"] + fn create_agentless_exporter( + profiling_library_name: &str, + profiling_library_version: &str, + family: &str, + tags: Vec, + site: &str, + api_key: &str, + timeout_ms: u64, + ) -> Result>; + + // ProfileExporter methods + /// Sends a profile to Datadog. + /// + /// # Arguments + /// * `profile` - Profile to send (will be reset after sending) + /// * `files_to_compress` - Additional files to compress and attach (e.g., heap dumps) + /// * `additional_tags` - Per-profile tags (in addition to exporter-level tags) + /// * `internal_metadata` - Internal metadata as JSON string (e.g., `{"key": "value"}`) See + /// Datadog-internal "RFC: Attaching internal metadata to pprof profiles" Pass empty + /// string "" if not needed + /// * `process_tags` - Process-level tags as comma-separated string (e.g., + /// "runtime:native,profiler_version:1.0") Pass empty string "" if not needed + /// * `info` - System/environment info as JSON string (e.g., `{"os": "linux", "arch": + /// "x86_64"}`) See Datadog-internal "RFC: Pprof System Info Support" Pass empty string "" + /// if not needed + fn send_profile( + self: &ProfileExporter, + profile: &mut Profile, + files_to_compress: Vec, + additional_tags: Vec, + process_tags: &str, + internal_metadata: &str, + info: &str, + ) -> Result<()>; + } +} + +// ============================================================================ +// From Implementations - Convert CXX types to API types +// ============================================================================ + +impl<'a> From<&ffi::ValueType<'a>> for api::ValueType<'a> { + fn from(vt: &ffi::ValueType<'a>) -> Self { + api::ValueType::new(vt.type_, vt.unit) + } +} + +impl<'a> From<&ffi::Period<'a>> for api::Period<'a> { + fn from(period: &ffi::Period<'a>) -> Self { + api::Period { + r#type: (&period.value_type).into(), + value: period.value, + } + } +} + +impl<'a> From<&ffi::Mapping<'a>> for api::Mapping<'a> { + fn from(mapping: &ffi::Mapping<'a>) -> Self { + api::Mapping { + memory_start: mapping.memory_start, + memory_limit: mapping.memory_limit, + file_offset: mapping.file_offset, + filename: mapping.filename, + build_id: mapping.build_id, + } + } +} + +impl<'a> From<&ffi::Function<'a>> for api::Function<'a> { + fn from(func: &ffi::Function<'a>) -> Self { + api::Function { + name: func.name, + system_name: func.system_name, + filename: func.filename, + } + } +} + +impl<'a> From<&ffi::Location<'a>> for api::Location<'a> { + fn from(loc: &ffi::Location<'a>) -> Self { + api::Location { + mapping: (&loc.mapping).into(), + function: (&loc.function).into(), + address: loc.address, + line: loc.line, + } + } +} + +impl<'a> From<&ffi::Label<'a>> for api::Label<'a> { + fn from(label: &ffi::Label<'a>) -> Self { + api::Label { + key: label.key, + str: label.str, + num: label.num, + num_unit: label.num_unit, + } + } +} + +impl<'a> From<&ffi::AttachmentFile<'a>> for exporter::File<'a> { + fn from(file: &ffi::AttachmentFile<'a>) -> Self { + exporter::File { + name: file.name, + bytes: file.data, + } + } +} + +impl<'a> TryFrom<&ffi::Tag<'a>> for exporter::Tag { + type Error = anyhow::Error; + + fn try_from(tag: &ffi::Tag<'a>) -> Result { + exporter::Tag::new(tag.key, tag.value) + } +} + +// ============================================================================ +// Profile - Wrapper around internal::Profile +// ============================================================================ + +pub struct Profile { + inner: internal::Profile, +} + +impl Profile { + pub fn create( + sample_types: Vec, + period: &ffi::Period, + ) -> anyhow::Result> { + // Convert using From trait + let types: Vec = sample_types.iter().map(Into::into).collect(); + let period_value: api::Period = period.into(); + + // Profile::try_new interns the strings + let inner = internal::Profile::try_new(&types, Some(period_value))?; + + Ok(Box::new(Profile { inner })) + } + + pub fn add_sample(&mut self, sample: &ffi::Sample) -> anyhow::Result<()> { + let api_sample = api::Sample { + locations: sample.locations.iter().map(Into::into).collect(), + values: &sample.values, + labels: sample.labels.iter().map(Into::into).collect(), + }; + + // Profile interns the strings + self.inner.try_add_sample(api_sample, None)?; + Ok(()) + } + + pub fn add_endpoint(&mut self, local_root_span_id: u64, endpoint: &str) -> anyhow::Result<()> { + self.inner + .add_endpoint(local_root_span_id, std::borrow::Cow::Borrowed(endpoint)) + } + + pub fn add_endpoint_count(&mut self, endpoint: &str, value: i64) -> anyhow::Result<()> { + self.inner + .add_endpoint_count(std::borrow::Cow::Borrowed(endpoint), value) + } + + pub fn add_upscaling_rule_poisson( + &mut self, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value_offset: usize, + sampling_distance: u64, + ) -> anyhow::Result<()> { + let upscaling_info = api::UpscalingInfo::Poisson { + sum_value_offset, + count_value_offset, + sampling_distance, + }; + self.inner + .add_upscaling_rule(offset_values, label_name, label_value, upscaling_info) + } + + pub fn add_upscaling_rule_poisson_non_sample_type_count( + &mut self, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value: u64, + sampling_distance: u64, + ) -> anyhow::Result<()> { + let upscaling_info = api::UpscalingInfo::PoissonNonSampleTypeCount { + sum_value_offset, + count_value, + sampling_distance, + }; + self.inner + .add_upscaling_rule(offset_values, label_name, label_value, upscaling_info) + } + + pub fn add_upscaling_rule_proportional( + &mut self, + offset_values: &[usize], + label_name: &str, + label_value: &str, + scale: f64, + ) -> anyhow::Result<()> { + let upscaling_info = api::UpscalingInfo::Proportional { scale }; + self.inner + .add_upscaling_rule(offset_values, label_name, label_value, upscaling_info) + } + + pub fn reset(&mut self) -> anyhow::Result<()> { + // Reset and discard the old profile + self.inner.reset_and_return_previous()?; + Ok(()) + } + + pub fn serialize_to_vec(&mut self) -> anyhow::Result> { + // Reset the profile and get the old one to serialize + let old_profile = self.inner.reset_and_return_previous()?; + let end_time = Some(std::time::SystemTime::now()); + let encoded = old_profile.serialize_into_compressed_pprof(end_time, None)?; + Ok(encoded.buffer) + } +} + +// ============================================================================ +// ProfileExporter - Wrapper around exporter::ProfileExporter +// ============================================================================ + +pub struct ProfileExporter { + inner: exporter::ProfileExporter, +} + +impl ProfileExporter { + pub fn create_agent_exporter( + profiling_library_name: &str, + profiling_library_version: &str, + family: &str, + tags: Vec, + agent_url: &str, + timeout_ms: u64, + ) -> anyhow::Result> { + let mut endpoint = exporter::config::agent(agent_url.parse()?)?; + + // Set timeout if non-zero (0 means use default) + if timeout_ms > 0 { + endpoint.timeout_ms = timeout_ms; + } + + let tags_vec: Vec = tags + .iter() + .map(TryInto::try_into) + .collect::, _>>()?; + + let tags_option = if tags_vec.is_empty() { + None + } else { + Some(tags_vec) + }; + + let inner = exporter::ProfileExporter::new( + profiling_library_name.to_string(), + profiling_library_version.to_string(), + family.to_string(), + tags_option, + endpoint, + )?; + + Ok(Box::new(ProfileExporter { inner })) + } + + pub fn create_agentless_exporter( + profiling_library_name: &str, + profiling_library_version: &str, + family: &str, + tags: Vec, + site: &str, + api_key: &str, + timeout_ms: u64, + ) -> anyhow::Result> { + let mut endpoint = exporter::config::agentless(site, api_key.to_string())?; + + // Set timeout if non-zero (0 means use default) + if timeout_ms > 0 { + endpoint.timeout_ms = timeout_ms; + } + + let tags_vec: Vec = tags + .iter() + .map(TryInto::try_into) + .collect::, _>>()?; + + let tags_option = if tags_vec.is_empty() { + None + } else { + Some(tags_vec) + }; + + let inner = exporter::ProfileExporter::new( + profiling_library_name.to_string(), + profiling_library_version.to_string(), + family.to_string(), + tags_option, + endpoint, + )?; + + Ok(Box::new(ProfileExporter { inner })) + } + + /// Sends a profile to Datadog. + /// + /// # Arguments + /// * `profile` - Profile to send (will be reset after sending) + /// * `files_to_compress` - Additional files to compress and attach + /// * `additional_tags` - Per-profile tags (in addition to exporter-level tags) + /// * `internal_metadata` - Internal metadata as JSON string. Empty string if not needed. + /// Example: `{"custom_field": "value", "version": "1.0"}` + /// * `info` - System/environment info as JSON string. Empty string if not needed. Example: + /// `{"os": "linux", "arch": "x86_64", "kernel": "5.15.0"}` + pub fn send_profile( + &self, + profile: &mut Profile, + files_to_compress: Vec, + additional_tags: Vec, + process_tags: &str, + internal_metadata: &str, + info: &str, + ) -> anyhow::Result<()> { + // Reset the profile and get the old one to export + let old_profile = profile.inner.reset_and_return_previous()?; + let end_time = Some(std::time::SystemTime::now()); + let encoded = old_profile.serialize_into_compressed_pprof(end_time, None)?; + + // Convert attachment files to exporter::File + let files_to_compress_vec: Vec = + files_to_compress.iter().map(Into::into).collect(); + + // Convert additional tags + let additional_tags_vec: Option> = if additional_tags.is_empty() { + None + } else { + Some( + additional_tags + .iter() + .map(TryInto::try_into) + .collect::, _>>()?, + ) + }; + + // Parse JSON strings if provided + let internal_metadata_json = if internal_metadata.is_empty() { + None + } else { + Some(serde_json::from_str(internal_metadata)?) + }; + + let info_json = if info.is_empty() { + None + } else { + Some(serde_json::from_str(info)?) + }; + + // Build and send the request + let process_tags_opt = if process_tags.is_empty() { + None + } else { + Some(process_tags) + }; + + let request = self.inner.build( + encoded, + &files_to_compress_vec, + &[], // files_to_export_unmodified - empty + additional_tags_vec.as_ref(), + process_tags_opt, + internal_metadata_json, + info_json, + )?; + let response = self.inner.send(request, None)?; + + // Check response status + if !response.status().is_success() { + anyhow::bail!("Failed to export profile: HTTP {}", response.status()); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_LIB_NAME: &str = "dd-trace-test"; + const TEST_LIB_VERSION: &str = "1.0.0"; + const TEST_FAMILY: &str = "test"; + + fn create_test_value_type() -> ffi::ValueType<'static> { + ffi::ValueType { + type_: "wall-time", + unit: "nanoseconds", + } + } + + fn create_test_profile() -> Box { + let wall_time = create_test_value_type(); + let period = ffi::Period { + value_type: wall_time, + value: 60, + }; + Profile::create(vec![create_test_value_type()], &period).unwrap() + } + + fn create_test_location(address: u64, line: i64) -> ffi::Location<'static> { + ffi::Location { + mapping: ffi::Mapping { + memory_start: address & 0xFFFF0000, + memory_limit: (address & 0xFFFF0000) + 0x10000000, + file_offset: 0, + filename: "/usr/lib/libtest.so", + build_id: "abc123", + }, + function: ffi::Function { + name: "test_function", + system_name: "_Z13test_functionv", + filename: "/src/test.cpp", + }, + address, + line, + } + } + + fn create_test_sample() -> ffi::Sample<'static> { + ffi::Sample { + locations: vec![create_test_location(0x10003000, 100)], + values: vec![1000000], + labels: vec![], + } + } + + fn create_test_exporter() -> Box { + ProfileExporter::create_agent_exporter( + TEST_LIB_NAME, + TEST_LIB_VERSION, + TEST_FAMILY, + vec![ffi::Tag { + key: "env", + value: "test", + }], + "http://localhost:1", // Port 1 unlikely to have server + 100, + ) + .unwrap() + } + + #[test] + fn test_profile_operations() { + let mut profile = create_test_profile(); + + // Verify profile starts empty + assert_eq!( + profile.inner.only_for_testing_num_aggregated_samples(), + 0, + "Profile should start with no samples" + ); + + // Add samples and verify they're tracked + let sample = create_test_sample(); + profile.add_sample(&sample).unwrap(); + assert_eq!( + profile.inner.only_for_testing_num_aggregated_samples(), + 1, + "Profile should have 1 sample after adding" + ); + + // Add another sample with different address + let sample2 = ffi::Sample { + locations: vec![create_test_location(0x20003000, 200)], + values: vec![2000000], + labels: vec![], + }; + profile.add_sample(&sample2).unwrap(); + assert_eq!( + profile.inner.only_for_testing_num_aggregated_samples(), + 2, + "Profile should have 2 samples" + ); + + // Test endpoints + profile.add_endpoint(12345, "/api/test").unwrap(); + profile.add_endpoint(67890, "/api/other").unwrap(); + profile.add_endpoint_count("/api/test", 100).unwrap(); + + // Test upscaling rules (verify they don't error) + profile + .add_upscaling_rule_poisson(&[0], "thread_id", "0", 0, 0, 1000000) + .unwrap(); + profile + .add_upscaling_rule_proportional(&[0], "thread_id", "1", 100.0) + .unwrap(); + profile + .add_upscaling_rule_poisson_non_sample_type_count( + &[0], + "thread_id", + "2", + 0, + 50, + 1000000, + ) + .unwrap(); + + // Serialize and verify output + let serialized = profile.serialize_to_vec().unwrap(); + assert!( + serialized.len() > 100, + "Serialized profile should be non-trivial" + ); + + // Verify it's a valid pprof by checking for gzip/zstd magic bytes + assert!( + serialized.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) || // zstd magic + serialized.starts_with(&[0x1f, 0x8b]), // gzip magic + "Serialized profile should be compressed" + ); + + // After serialization (which resets), profile should be empty + assert_eq!( + profile.inner.only_for_testing_num_aggregated_samples(), + 0, + "Profile should be empty after serialize_to_vec" + ); + + // Add sample and test explicit reset + profile.add_sample(&sample).unwrap(); + assert_eq!(profile.inner.only_for_testing_num_aggregated_samples(), 1); + profile.reset().unwrap(); + assert_eq!( + profile.inner.only_for_testing_num_aggregated_samples(), + 0, + "Profile should be empty after reset" + ); + } + + #[test] + fn test_exporter_create() { + // Test agent exporter with default timeout + assert!(ProfileExporter::create_agent_exporter( + TEST_LIB_NAME, + TEST_LIB_VERSION, + TEST_FAMILY, + vec![ffi::Tag { + key: "service", + value: "test" + }], + "http://localhost:8126", + 0, + ) + .is_ok()); + + // Test with multiple tags and custom timeout + assert!(ProfileExporter::create_agent_exporter( + TEST_LIB_NAME, + TEST_LIB_VERSION, + TEST_FAMILY, + vec![ + ffi::Tag { + key: "service", + value: "my-service" + }, + ffi::Tag { + key: "env", + value: "prod" + }, + ffi::Tag { + key: "version", + value: "2.0" + }, + ], + "http://localhost:8126", + 10000, + ) + .is_ok()); + + // Test agentless exporters with different sites + assert!(ProfileExporter::create_agentless_exporter( + TEST_LIB_NAME, + TEST_LIB_VERSION, + TEST_FAMILY, + vec![], + "datadoghq.com", + "fake-api-key", + 5000, + ) + .is_ok()); + + assert!(ProfileExporter::create_agentless_exporter( + TEST_LIB_NAME, + TEST_LIB_VERSION, + TEST_FAMILY, + vec![], + "datadoghq.eu", + "fake-api-key", + 0, + ) + .is_ok()); + + // Test with no tags + assert!(ProfileExporter::create_agent_exporter( + TEST_LIB_NAME, + TEST_LIB_VERSION, + TEST_FAMILY, + vec![], + "http://localhost:8126", + 0, + ) + .is_ok()); + } + + #[test] + fn test_type_conversions() { + // AttachmentFile conversion + let data = vec![1u8, 2, 3, 4, 5, 255, 128, 0]; + let file: exporter::File = (&ffi::AttachmentFile { + name: "test.bin", + data: &data, + }) + .into(); + assert_eq!(file.name, "test.bin"); + assert_eq!(file.bytes, data.as_slice()); + + // Tag conversion with special characters + let tag: exporter::Tag = (&ffi::Tag { + key: "test-key.with_special:chars", + value: "test_value/with@special#chars", + }) + .try_into() + .unwrap(); + assert_eq!( + tag.as_ref(), + "test-key.with_special:chars:test_value/with@special#chars" + ); + + // Tag validation - empty key should fail + assert!(TryInto::::try_into(&ffi::Tag { + key: "", + value: "value" + }) + .is_err()); + + // ValueType conversion + let vt: api::ValueType = (&ffi::ValueType { + type_: "cpu-samples", + unit: "count", + }) + .into(); + assert_eq!(vt.r#type, "cpu-samples"); + assert_eq!(vt.unit, "count"); + + // Mapping conversion + let mapping: api::Mapping = (&ffi::Mapping { + memory_start: 0x1000, + memory_limit: 0x2000, + file_offset: 0x100, + filename: "/lib/test.so", + build_id: "build123", + }) + .into(); + assert_eq!( + ( + mapping.memory_start, + mapping.memory_limit, + mapping.file_offset + ), + (0x1000, 0x2000, 0x100) + ); + assert_eq!( + (mapping.filename, mapping.build_id), + ("/lib/test.so", "build123") + ); + + // Function conversion + let function: api::Function = (&ffi::Function { + name: "my_func", + system_name: "_Z7my_funcv", + filename: "/src/file.cpp", + }) + .into(); + assert_eq!( + (function.name, function.system_name, function.filename), + ("my_func", "_Z7my_funcv", "/src/file.cpp") + ); + + // Label conversion + let label: api::Label = (&ffi::Label { + key: "thread_id", + str: "", + num: 42, + num_unit: "thread", + }) + .into(); + assert_eq!( + (label.key, label.num, label.num_unit), + ("thread_id", 42, "thread") + ); + } + + #[test] + fn test_send_profile_with_attachments() { + let mut profile = create_test_profile(); + profile.add_sample(&create_test_sample()).unwrap(); + + let exporter = create_test_exporter(); + let attachment_data = br#"{"test": "data", "number": 123}"#.to_vec(); + + // Send with full parameters - should fail with connection error but build request correctly + let result = exporter.send_profile( + &mut profile, + vec![ffi::AttachmentFile { + name: "metadata.json", + data: &attachment_data, + }], + vec![ + ffi::Tag { + key: "profile_type", + value: "cpu", + }, + ffi::Tag { + key: "runtime", + value: "native", + }, + ], + "language:rust,profiler_version:1.0", + r#"{"version": "1.0", "profiler": "test"}"#, + r#"{"os": "linux", "arch": "x86_64", "cores": 8}"#, + ); + + assert!(result.is_err(), "Should fail when no server available"); + assert_eq!( + profile.inner.only_for_testing_num_aggregated_samples(), + 0, + "Profile should be reset after send attempt" + ); + + // Test with empty optional parameters + profile.add_sample(&create_test_sample()).unwrap(); + let result2 = exporter.send_profile(&mut profile, vec![], vec![], "", "", ""); + assert!( + result2.is_err(), + "Should fail with empty optional params too" + ); + } +} diff --git a/libdd-profiling/src/lib.rs b/libdd-profiling/src/lib.rs index 01a99d6c4..b9d7c1df1 100644 --- a/libdd-profiling/src/lib.rs +++ b/libdd-profiling/src/lib.rs @@ -9,6 +9,8 @@ pub mod api; pub mod api2; pub mod collections; +#[cfg(feature = "cxx")] +pub mod cxx; pub mod exporter; pub mod internal; pub mod iter;