Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions LICENSE-3rdparty.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32028,9 +32028,9 @@ third_party_libraries:
- package_name: stringmetrics
package_version: 2.2.2
repository: https://github.com/pluots/stringmetrics
license: License specified in file ($CARGO_HOME/registry/src/index.crates.io-6f17d22bba15001f/stringmetrics-2.2.2/LICENSE)
license: License specified in file ($CARGO_HOME/registry/src/github.com-25cdd57fae9f0462/stringmetrics-2.2.2/LICENSE)
licenses:
- license: License specified in file ($CARGO_HOME/registry/src/index.crates.io-6f17d22bba15001f/stringmetrics-2.2.2/LICENSE)
- license: License specified in file ($CARGO_HOME/registry/src/github.com-25cdd57fae9f0462/stringmetrics-2.2.2/LICENSE)
text: |
Copyright 2022 Trevor Gross

Expand Down
7 changes: 7 additions & 0 deletions libdd-profiling/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ rand = "0.8"
# The default resolver can hold locks or other global state that can cause deadlocks
# or corruption when the process forks (e.g., in PHP-FPM or other forking environments).
reqwest = { version = "0.13", features = ["multipart", "rustls", "hickory-dns"], default-features = false}
rustls-platform-verifier = "0.6"
rustc-hash = { version = "1.1", default-features = false }
serde = {version = "1.0", features = ["derive"]}
serde_json = {version = "1.0"}
Expand All @@ -63,6 +64,12 @@ tokio = {version = "1.23", features = ["rt", "macros", "net", "io-util", "fs"]}
tokio-util = { version = "0.7.1", default-features = false }
zstd = { version = "0.13", default-features = false }

# aws-lc-rs is preferred on Unix; ring is used on Windows where aws-lc-rs has build issues.
[target.'cfg(unix)'.dependencies]
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] }

[target.'cfg(not(unix))'.dependencies]
rustls = { version = "0.23", default-features = false, features = ["ring"] }

[dev-dependencies]
bolero = "0.13"
Expand Down
1 change: 1 addition & 0 deletions libdd-profiling/src/exporter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod config;
mod errors;
pub mod exporter_manager;
mod profile_exporter;
mod tls;

pub use errors::SendError;
pub use exporter_manager::ExporterManager;
Expand Down
11 changes: 9 additions & 2 deletions libdd-profiling/src/exporter/profile_exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ impl ProfileExporter {
/// The exporter can be used from any thread, but if using `send_blocking()`, the exporter
/// should remain on the same thread for all blocking calls. See [`send_blocking`] for details.
///
/// # Performance
///
/// TLS configuration is cached globally and reused across exporter
/// instances, avoiding repeated root store loading on Linux.
///
/// [`send_blocking`]: ProfileExporter::send_blocking
pub fn new(
profiling_library_name: &str,
Expand All @@ -73,8 +78,7 @@ impl ProfileExporter {
mut tags: Vec<Tag>,
endpoint: Endpoint,
) -> anyhow::Result<Self> {
let (builder, request_url) = endpoint.to_reqwest_client_builder()?;

let tls_config = super::tls::cached_tls_config()?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tls should not be needed for http / uds

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design of the reqwest library is that a client can connect to multiple URLs. It doesn't know if it will use HTTP/HTTPS until later, but the config is done here.. We can possibly-rewrite the necessary components because we should know if we'll be using HTTPS or not, but that's not how the prior nor current code was designed (probably because we inherit such design unintentionally from tokio/reqwest).

// Pre-build all static headers
let mut headers = reqwest::header::HeaderMap::new();

Expand Down Expand Up @@ -118,6 +122,9 @@ impl ProfileExporter {
// Precompute the base tags string (includes configured tags + Azure App Services tags)
let base_tags_string: String = tags.iter().flat_map(|tag| [tag.as_ref(), ","]).collect();

let (builder, request_url) = endpoint.to_reqwest_client_builder()?;
let builder = builder.tls_backend_preconfigured(tls_config.0);

Ok(Self {
client: builder.build()?,
family: family.to_string(),
Expand Down
82 changes: 82 additions & 0 deletions libdd-profiling/src/exporter/tls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! Pre-initialized TLS configuration for high-performance profile export.
//!
//! On Linux, [`TlsConfig::new`] eagerly loads native root certificates via the
//! platform verifier, avoiding repeated expensive disk I/O on every
//! [`ProfileExporter`] creation.
//!
//! On macOS, the platform verifier's `Verifier::new()` is cheap (no cert
//! loading), but the actual Security.framework work happens lazily during each
//! TLS handshake. Creating a [`TlsConfig`] still avoids redundant `reqwest`
//! client setup on every exporter creation.
//!
//! # Fork Safety
//!
//! `TlsConfig` does **not** call Security.framework APIs directly, so it is
//! safe to create before `fork()`. Security.framework work is deferred to
//! each child's first TLS handshake.
//!
//! [`ProfileExporter`]: super::ProfileExporter

/// Wraps a [`rustls::ClientConfig`] that has been pre-configured with the
/// platform certificate verifier. Clone is cheap (inner `Arc`).
#[derive(Clone)]
pub(crate) struct TlsConfig(pub(crate) rustls::ClientConfig);

impl TlsConfig {
/// Create a new TLS configuration using the platform certificate verifier.
///
/// On Linux, this eagerly loads the native root certificate store, which is
/// the expensive operation that was previously repeated on every
/// `ProfileExporter::new` call.
///
/// On macOS, this is lightweight; the platform verifier defers
/// Security.framework calls to the first TLS handshake.
pub fn new() -> Result<Self, rustls::Error> {
use rustls_platform_verifier::BuilderVerifierExt;

// Use an explicit CryptoProvider rather than relying on
// `CryptoProvider::get_default_or_install_from_crate_features()`.
// Feature unification can enable both `aws-lc-rs` and `ring` in the
// same build (reqwest enables aws-lc-rs while libdd-common enables
// ring on Windows), which causes the automatic detection to panic.
let provider = rustls::crypto::CryptoProvider::get_default()
.cloned()
.unwrap_or_else(|| std::sync::Arc::new(Self::default_crypto_provider()));

let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.with_platform_verifier()?
.with_no_client_auth();
Ok(Self(config))
}

/// Returns the platform-appropriate default crypto provider.
///
/// Matches the convention used by `libdd-common`: `aws-lc-rs` on Unix,
/// `ring` on Windows (where `aws-lc-rs` has issues).
fn default_crypto_provider() -> rustls::crypto::CryptoProvider {
#[cfg(unix)]
{
rustls::crypto::aws_lc_rs::default_provider()
}
#[cfg(not(unix))]
{
rustls::crypto::ring::default_provider()
}
}
}

static TLS_CONFIG: std::sync::LazyLock<Result<TlsConfig, String>> =
std::sync::LazyLock::new(|| {
TlsConfig::new().map_err(|err| format!("failed to initialize TLS configuration: {err}"))
});

pub(crate) fn cached_tls_config() -> anyhow::Result<TlsConfig> {
TLS_CONFIG
.as_ref()
.map(Clone::clone)
.map_err(|err| anyhow::anyhow!("{err}"))
}
Loading