From e677049bd43763ee5e0c7a127f53a4c2f847db97 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Thu, 29 Jan 2026 17:25:32 -0500 Subject: [PATCH 1/4] fix(profiling): missing headers in exporter --- libdd-common/src/entity_id/mod.rs | 12 ++++ libdd-common/src/lib.rs | 49 +++++++--------- .../src/exporter/profile_exporter.rs | 19 +++---- libdd-profiling/tests/common.rs | 57 +++++++++++++++++++ libdd-profiling/tests/exporter_e2e.rs | 5 ++ libdd-profiling/tests/file_exporter_test.rs | 5 ++ 6 files changed, 106 insertions(+), 41 deletions(-) create mode 100644 libdd-profiling/tests/common.rs diff --git a/libdd-common/src/entity_id/mod.rs b/libdd-common/src/entity_id/mod.rs index bc8a9bbf6c..bd88a0c4f0 100644 --- a/libdd-common/src/entity_id/mod.rs +++ b/libdd-common/src/entity_id/mod.rs @@ -99,3 +99,15 @@ pub static DD_EXTERNAL_ENV: LazyLock> = LazyLock::new(|| { leaked }); + +/// Returns an iterator of entity-related headers (container-id, entity-id, external-env) +/// as (header_name, header_value) string tuples for any that are available. +pub fn get_entity_headers() -> impl Iterator { + [ + get_container_id().map(|v| ("datadog-container-id", v)), + get_entity_id().map(|v| ("datadog-entity-id", v)), + (*DD_EXTERNAL_ENV).map(|v| ("datadog-external-env", v)), + ] + .into_iter() + .flatten() +} diff --git a/libdd-common/src/lib.rs b/libdd-common/src/lib.rs index 6ec3934a9d..45b194f92f 100644 --- a/libdd-common/src/lib.rs +++ b/libdd-common/src/lib.rs @@ -6,10 +6,7 @@ #![cfg_attr(not(test), deny(clippy::todo))] #![cfg_attr(not(test), deny(clippy::unimplemented))] -use hyper::{ - header::HeaderValue, - http::uri::{self}, -}; +use hyper::http::uri::{self}; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::sync::{Mutex, MutexGuard}; @@ -122,7 +119,6 @@ impl impl Iterator { + [ + self.api_key.as_ref().map(|v| ("dd-api-key", v.as_ref())), + self.test_token + .as_ref() + .map(|v| ("x-datadog-test-session-token", v.as_ref())), + ] + .into_iter() + .flatten() + } + /// Return a request builder with the following headers: /// - User agent /// - Api key @@ -252,32 +261,14 @@ impl Endpoint { .uri(self.url.clone()) .header(hyper::header::USER_AGENT, user_agent); - // Add the Api key header if available - if let Some(api_key) = &self.api_key { - builder = builder.header(header::DATADOG_API_KEY, HeaderValue::from_str(api_key)?); - } - - // Add the test session token if available - if let Some(token) = &self.test_token { - builder = builder.header( - header::X_DATADOG_TEST_SESSION_TOKEN, - HeaderValue::from_str(token)?, - ); - } - - // Add the Container Id header if available - if let Some(container_id) = entity_id::get_container_id() { - builder = builder.header(header::DATADOG_CONTAINER_ID, container_id); - } - - // Add the Entity Id header if available - if let Some(entity_id) = entity_id::get_entity_id() { - builder = builder.header(header::DATADOG_ENTITY_ID, entity_id); + // Add optional endpoint headers (api-key, test-token) + for (name, value) in self.get_optional_headers() { + builder = builder.header(name, value); } - // Add the External Env header if available - if let Some(external_env) = *DD_EXTERNAL_ENV { - builder = builder.header(header::DATADOG_EXTERNAL_ENV, external_env); + // Add entity-related headers (container-id, entity-id, external-env) + for (name, value) in entity_id::get_entity_headers() { + builder = builder.header(name, value); } Ok(builder) diff --git a/libdd-profiling/src/exporter/profile_exporter.rs b/libdd-profiling/src/exporter/profile_exporter.rs index efc9a54907..2f0389d200 100644 --- a/libdd-profiling/src/exporter/profile_exporter.rs +++ b/libdd-profiling/src/exporter/profile_exporter.rs @@ -150,7 +150,6 @@ impl ProfileExporter { // Pre-build all static headers let mut headers = reqwest::header::HeaderMap::new(); - // Always-present headers headers.insert( "Connection", reqwest::header::HeaderValue::from_static("close"), @@ -171,18 +170,14 @@ impl ProfileExporter { ))?, ); - // Optional headers (API key, test token) - if let Some(api_key) = &endpoint.api_key { - headers.insert( - "DD-API-KEY", - reqwest::header::HeaderValue::from_str(api_key)?, - ); + // Add optional endpoint headers (api-key, test-token) + for (name, value) in endpoint.get_optional_headers() { + headers.insert(name, reqwest::header::HeaderValue::from_str(value)?); } - if let Some(test_token) = &endpoint.test_token { - headers.insert( - "X-Datadog-Test-Session-Token", - reqwest::header::HeaderValue::from_str(test_token)?, - ); + + // Add entity-related headers (container-id, entity-id, external-env) + for (name, value) in libdd_common::entity_id::get_entity_headers() { + headers.insert(name, reqwest::header::HeaderValue::from_static(value)); } // Add Azure App Services tags if available diff --git a/libdd-profiling/tests/common.rs b/libdd-profiling/tests/common.rs new file mode 100644 index 0000000000..36fa39675b --- /dev/null +++ b/libdd-profiling/tests/common.rs @@ -0,0 +1,57 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Common test utilities + +use std::collections::HashMap; + +/// Validates that entity headers (container-id, entity-id, external-env) match +/// the values provided by libdd_common::entity_id +pub fn assert_entity_headers_match(headers: &HashMap) { + // Check for entity headers and validate their values match what libdd_common provides + let expected_container_id = libdd_common::entity_id::get_container_id(); + let expected_entity_id = libdd_common::entity_id::get_entity_id(); + let expected_external_env = *libdd_common::entity_id::DD_EXTERNAL_ENV; + + // Validate container ID + if let Some(expected) = expected_container_id { + assert_eq!( + headers.get("datadog-container-id"), + Some(&expected.to_string()), + "datadog-container-id header should match the value from entity_id::get_container_id()" + ); + } else { + assert!( + !headers.contains_key("datadog-container-id"), + "datadog-container-id header should not be present when entity_id::get_container_id() is None" + ); + } + + // Validate entity ID + if let Some(expected) = expected_entity_id { + assert_eq!( + headers.get("datadog-entity-id"), + Some(&expected.to_string()), + "datadog-entity-id header should match the value from entity_id::get_entity_id()" + ); + } else { + assert!( + !headers.contains_key("datadog-entity-id"), + "datadog-entity-id header should not be present when entity_id::get_entity_id() is None" + ); + } + + // Validate external env + if let Some(expected) = expected_external_env { + assert_eq!( + headers.get("datadog-external-env"), + Some(&expected.to_string()), + "datadog-external-env header should match the value from entity_id::DD_EXTERNAL_ENV" + ); + } else { + assert!( + !headers.contains_key("datadog-external-env"), + "datadog-external-env header should not be present when entity_id::DD_EXTERNAL_ENV is None" + ); + } +} diff --git a/libdd-profiling/tests/exporter_e2e.rs b/libdd-profiling/tests/exporter_e2e.rs index 3bca79c48e..0c76cbffa7 100644 --- a/libdd-profiling/tests/exporter_e2e.rs +++ b/libdd-profiling/tests/exporter_e2e.rs @@ -5,6 +5,8 @@ //! //! These tests validate the full export flow across different endpoint types. +mod common; + use libdd_profiling::exporter::config; use libdd_profiling::exporter::utils::parse_http_request; use libdd_profiling::exporter::{File, MimeType, ProfileExporter}; @@ -303,6 +305,9 @@ fn validate_full_export(req: &ReceivedRequest, expected_path: &str) -> anyhow::R assert_eq!(req.method, "POST"); assert_eq!(req.path, expected_path); + // Check for entity headers and validate their values match what libdd_common provides + common::assert_entity_headers_match(&req.headers); + // Parse the request to get multipart parts // We need to reconstruct a minimal HTTP request to parse let mut http_request_bytes = Vec::new(); diff --git a/libdd-profiling/tests/file_exporter_test.rs b/libdd-profiling/tests/file_exporter_test.rs index e6912ecda5..bc266c82e6 100644 --- a/libdd-profiling/tests/file_exporter_test.rs +++ b/libdd-profiling/tests/file_exporter_test.rs @@ -1,6 +1,8 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +mod common; + use libdd_profiling::exporter::utils::parse_http_request; use libdd_profiling::exporter::ProfileExporter; use libdd_profiling::internal::EncodedProfile; @@ -376,5 +378,8 @@ mod tests { request.headers.get("dd-evp-origin-version").unwrap(), profiling_library_version ); + + // Check for entity headers and validate their values match what libdd_common provides + common::assert_entity_headers_match(&request.headers); } } From 164f00280ee2ad84159e31762717a2b8efa09a6a Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbonne Date: Tue, 3 Feb 2026 16:35:35 -0500 Subject: [PATCH 2/4] Update libdd-common/src/lib.rs Co-authored-by: Levi Morrison --- libdd-common/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-common/src/lib.rs b/libdd-common/src/lib.rs index 45b194f92f..626dce4e3a 100644 --- a/libdd-common/src/lib.rs +++ b/libdd-common/src/lib.rs @@ -6,7 +6,7 @@ #![cfg_attr(not(test), deny(clippy::todo))] #![cfg_attr(not(test), deny(clippy::unimplemented))] -use hyper::http::uri::{self}; +use hyper::http::uri; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::sync::{Mutex, MutexGuard}; From 8f06e97056170f1feca4cac0a56cf9f2a5baf5b6 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Tue, 3 Feb 2026 17:03:47 -0500 Subject: [PATCH 3/4] fix c example --- examples/ffi/exporter_manager.c | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/ffi/exporter_manager.c b/examples/ffi/exporter_manager.c index e44551d8ec..eee8e7d6f8 100644 --- a/examples/ffi/exporter_manager.c +++ b/examples/ffi/exporter_manager.c @@ -9,6 +9,7 @@ #include #include #include +#include #include static ddog_CharSlice to_slice_c_char(const char *s) { return (ddog_CharSlice){.ptr = s, .len = strlen(s)}; } From 7ac277e32720c3a8d6b7c949d56bb7fcb7a8b6af Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Wed, 4 Feb 2026 13:32:57 -0500 Subject: [PATCH 4/4] comment test --- libdd-profiling/tests/common.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/libdd-profiling/tests/common.rs b/libdd-profiling/tests/common.rs index 36fa39675b..0b2f70ceb5 100644 --- a/libdd-profiling/tests/common.rs +++ b/libdd-profiling/tests/common.rs @@ -7,6 +7,30 @@ use std::collections::HashMap; /// Validates that entity headers (container-id, entity-id, external-env) match /// the values provided by libdd_common::entity_id +/// +/// # Current Limitations +/// +/// **NOTE:** This test helper has known limitations that should be addressed in a follow-up PR: +/// +/// 1. **Environment-dependent behavior**: The test changes its behavior dynamically based on the +/// exact execution environment of the test runner (e.g., whether running in a container, whether +/// certain environment variables are set). +/// +/// 2. **Non-deterministic across environments**: What passes on a local machine may fail in CI (or +/// vice versa) because the underlying entity detection functions return different values in +/// different environments. +/// +/// 3. **Incomplete test coverage**: We only exercise the codepaths that happen to be triggered in +/// the current test environment, not all possible combinations of entity headers being +/// present/absent. +/// +/// **Future improvement**: The ideal approach would be to refactor the underlying code +/// (`libdd_common::entity_id::get_container_id()`, `get_entity_id()`, etc.) to be more testable, +/// perhaps by making them accept injectable dependencies or configuration. Then we could test all +/// combinations: container-id [Some/None] × entity-id [Some/None] × external-env [Some/None] to +/// verify correct header inclusion/exclusion in all 8 cases. +/// +/// See discussion: https://github.com/DataDog/libdatadog/pull/1493#discussion_r2745712029 pub fn assert_entity_headers_match(headers: &HashMap) { // Check for entity headers and validate their values match what libdd_common provides let expected_container_id = libdd_common::entity_id::get_container_id();