From e40dd5d8f73fe888f48467bf1a23b7dc56db0163 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 25 Mar 2026 10:35:27 -0400 Subject: [PATCH 01/11] Implement stable session identifier headers for telemetry Adds DD-Session-ID and DD-Root-Session-ID HTTP headers to all telemetry requests per the Stable Service Instance Identifier RFC. - DD-Session-ID is always set to the tracer's runtime_id - DD-Root-Session-ID is only set when it differs from the session ID (i.e. when running as a child process) - Root session ID is propagated to exec'd children via _DD_ROOT_CPP_SESSION_ID env var, read in finalize_config() - _DD_ROOT_CPP_SESSION_ID registered in the environment variable registry and supported-configurations.json Co-Authored-By: Claude Sonnet 4.6 --- include/datadog/environment.h | 9 ++- include/datadog/tracer_config.h | 1 + include/datadog/tracer_signature.h | 5 +- src/datadog/environment.cpp | 17 ++++-- src/datadog/telemetry/telemetry.cpp | 11 ++-- src/datadog/telemetry/telemetry_impl.cpp | 15 ++++- src/datadog/tracer.cpp | 8 ++- src/datadog/tracer_config.cpp | 4 ++ supported-configurations.json | 7 +++ test/remote_config/test_remote_config.cpp | 8 ++- test/telemetry/test_telemetry.cpp | 71 ++++++++++++++++++++++- test/test_datadog_agent.cpp | 3 +- 12 files changed, 137 insertions(+), 22 deletions(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index f2846b37..c467dad3 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -81,7 +81,8 @@ namespace environment { MACRO(DD_APM_TRACING_ENABLED, BOOLEAN, true) \ MACRO(DD_TRACE_RESOURCE_RENAMING_ENABLED, BOOLEAN, false) \ MACRO(DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, BOOLEAN, false) \ - MACRO(DD_EXTERNAL_ENV, STRING, "") + MACRO(DD_EXTERNAL_ENV, STRING, "") \ + MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, "") #define ENV_DEFAULT_RESOLVED_IN_CODE(X) X #define WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) ARG, @@ -95,7 +96,7 @@ enum Variable { DD_LIST_ENVIRONMENT_VARIABLES(WITH_COMMA) }; #define QUOTED_WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) \ WITH_COMMA(QUOTED(ARG), TYPE, DEFAULT_VALUE) -inline const char *const variable_names[] = { +inline const char* const variable_names[] = { DD_LIST_ENVIRONMENT_VARIABLES(QUOTED_WITH_COMMA)}; #undef QUOTED @@ -110,6 +111,10 @@ StringView name(Variable variable); // `nullopt` if that variable is not set in the environment. Optional lookup(Variable variable); +// Set the specified environment `variable` to `value`. Does not overwrite if +// already set (equivalent to setenv(..., 0) on POSIX). +void set(Variable variable, StringView value); + std::string to_json(); } // namespace environment diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index 2ee4cd02..f342a61b 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -225,6 +225,7 @@ class FinalizedTracerConfig final { bool log_on_startup; bool generate_128bit_trace_ids; Optional runtime_id; + Optional root_session_id; Clock clock; std::string integration_name; std::string integration_version; diff --git a/include/datadog/tracer_signature.h b/include/datadog/tracer_signature.h index 042b4d37..73decae7 100644 --- a/include/datadog/tracer_signature.h +++ b/include/datadog/tracer_signature.h @@ -33,6 +33,7 @@ namespace tracing { struct TracerSignature { RuntimeID runtime_id; + std::string root_session_id; std::string default_service; std::string default_environment; std::string library_version; @@ -40,8 +41,10 @@ struct TracerSignature { StringView library_language_version; TracerSignature() = delete; - TracerSignature(RuntimeID id, std::string service, std::string environment) + TracerSignature(RuntimeID id, std::string root_session, std::string service, + std::string environment) : runtime_id(id), + root_session_id(std::move(root_session)), default_service(std::move(service)), default_environment(std::move(environment)), library_version(tracer_version), diff --git a/src/datadog/environment.cpp b/src/datadog/environment.cpp index 4dc3eb3f..4ad7b547 100644 --- a/src/datadog/environment.cpp +++ b/src/datadog/environment.cpp @@ -11,19 +11,28 @@ namespace environment { StringView name(Variable variable) { return variable_names[variable]; } Optional lookup(Variable variable) { - const char *name = variable_names[variable]; - const char *value = std::getenv(name); + const char* name = variable_names[variable]; + const char* value = std::getenv(name); if (!value) { return nullopt; } return StringView{value}; } +void set(Variable variable, StringView value) { + const char* name = variable_names[variable]; +#ifdef _WIN32 + _putenv_s(name, value.data()); +#else + setenv(name, value.data(), /*overwrite=*/0); +#endif +} + std::string to_json() { auto result = nlohmann::json::object({}); - for (const char *name : variable_names) { - if (const char *value = std::getenv(name)) { + for (const char* name : variable_names) { + if (const char* value = std::getenv(name)) { result[name] = value; } } diff --git a/src/datadog/telemetry/telemetry.cpp b/src/datadog/telemetry/telemetry.cpp index 3cd51545..0cac32ce 100644 --- a/src/datadog/telemetry/telemetry.cpp +++ b/src/datadog/telemetry/telemetry.cpp @@ -53,10 +53,13 @@ void init(FinalizedConfiguration configuration, std::shared_ptr client, std::shared_ptr event_scheduler, tracing::HTTPClient::URL agent_url, tracing::Clock clock) { - instance(Ctor_param{configuration, - tracing::TracerSignature(tracing::RuntimeID::generate(), - tracing::get_process_name(), ""), - logger, client, event_scheduler, agent_url, clock}); + auto runtime_id = tracing::RuntimeID::generate(); + auto root_session_id = runtime_id.string(); + instance(Ctor_param{ + configuration, + tracing::TracerSignature(runtime_id, std::move(root_session_id), + tracing::get_process_name(), ""), + logger, client, event_scheduler, agent_url, clock}); } void init(FinalizedConfiguration configuration, diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index d9464dd7..9072c23f 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -302,13 +302,18 @@ void Telemetry::app_started() { auto payload = app_started_payload(); auto on_headers = [payload_size = payload.size(), - debug_enabled = config_.debug](DictWriter& headers) { + debug_enabled = config_.debug, + &sig = tracer_signature_](DictWriter& headers) { headers.set("Content-Type", "application/json"); headers.set("Content-Length", std::to_string(payload_size)); headers.set("DD-Telemetry-API-Version", "v2"); headers.set("DD-Client-Library-Language", "cpp"); headers.set("DD-Client-Library-Version", tracer_version); headers.set("DD-Telemetry-Request-Type", "app-started"); + headers.set("DD-Session-ID", sig.runtime_id.string()); + if (sig.root_session_id != sig.runtime_id.string()) { + headers.set("DD-Root-Session-ID", sig.root_session_id); + } if (debug_enabled) { headers.set("DD-Telemetry-Debug-Enabled", "true"); } @@ -356,14 +361,18 @@ void Telemetry::app_closing() { void Telemetry::send_payload(StringView request_type, std::string payload) { auto set_telemetry_headers = [request_type, payload_size = payload.size(), - debug_enabled = - config_.debug](DictWriter& headers) { + debug_enabled = config_.debug, + &sig = tracer_signature_](DictWriter& headers) { headers.set("Content-Type", "application/json"); headers.set("Content-Length", std::to_string(payload_size)); headers.set("DD-Telemetry-API-Version", "v2"); headers.set("DD-Client-Library-Language", "cpp"); headers.set("DD-Client-Library-Version", tracer_version); headers.set("DD-Telemetry-Request-Type", request_type); + headers.set("DD-Session-ID", sig.runtime_id.string()); + if (sig.root_session_id != sig.runtime_id.string()) { + headers.set("DD-Root-Session-ID", sig.root_session_id); + } if (debug_enabled) { headers.set("DD-Telemetry-Debug-Enabled", "true"); } diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 7603b92b..2437db43 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -48,8 +48,9 @@ Tracer::Tracer(const FinalizedTracerConfig& config, : logger_(config.logger), runtime_id_(config.runtime_id ? *config.runtime_id : RuntimeID::generate()), - signature_{runtime_id_, config.defaults.service, - config.defaults.environment}, + signature_{runtime_id_, + config.root_session_id.value_or(runtime_id_.string()), + config.defaults.service, config.defaults.environment}, config_manager_(std::make_shared(config)), collector_(/* see constructor body */), span_sampler_( @@ -64,6 +65,9 @@ Tracer::Tracer(const FinalizedTracerConfig& config, baggage_extraction_enabled_(false), tracing_enabled_(config.tracing_enabled), resource_renaming_mode_(config.resource_renaming_mode) { + environment::set(environment::_DD_ROOT_CPP_SESSION_ID, + signature_.root_session_id); + telemetry::init(config.telemetry, signature_, logger_, config.http_client, config.event_scheduler, config.agent_url); if (config.report_hostname) { diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index a10b17a8..adf8cf79 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -407,6 +407,10 @@ Expected finalize_config(const TracerConfig &user_config, final_config.runtime_id = user_config.runtime_id; } + if (auto val = lookup(environment::_DD_ROOT_CPP_SESSION_ID)) { + final_config.root_session_id = std::string{*val}; + } + final_config.process_tags = user_config.process_tags; auto agent_finalized = diff --git a/supported-configurations.json b/supported-configurations.json index 4ee3558a..4b7747dd 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -28,6 +28,13 @@ "type": "STRING" } ], + "_DD_ROOT_CPP_SESSION_ID": [ + { + "default": null, + "implementation": "A", + "type": "STRING" + } + ], "DD_INSTRUMENTATION_INSTALL_ID": [ { "default": "", diff --git a/test/remote_config/test_remote_config.cpp b/test/remote_config/test_remote_config.cpp index 232de5af..9a22bd48 100644 --- a/test/remote_config/test_remote_config.cpp +++ b/test/remote_config/test_remote_config.cpp @@ -47,8 +47,10 @@ auto logger = std::make_shared(); REMOTE_CONFIG_TEST("initial state payload") { // Verify the initial payload structure for a remote configuration instance. + auto runtime_id = RuntimeID::generate(); const TracerSignature tracer_signature{ - /* runtime_id = */ RuntimeID::generate(), + /* runtime_id = */ runtime_id, + /* root_session_id = */ runtime_id.string(), /* service = */ "testsvc", /* environment = */ "test"}; @@ -92,8 +94,10 @@ REMOTE_CONFIG_TEST("initial state payload") { // TODO: test all combination of product and capabilities generation REMOTE_CONFIG_TEST("response processing") { + auto runtime_id = RuntimeID::generate(); const TracerSignature tracer_signature{ - /* runtime_id = */ RuntimeID::generate(), + /* runtime_id = */ runtime_id, + /* root_session_id = */ runtime_id.string(), /* service = */ "testsvc", /* environment = */ "test"}; diff --git a/test/telemetry/test_telemetry.cpp b/test/telemetry/test_telemetry.cpp index 83590773..35731e63 100644 --- a/test/telemetry/test_telemetry.cpp +++ b/test/telemetry/test_telemetry.cpp @@ -89,8 +89,10 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry lifecycle") { auto client = std::make_shared(); auto scheduler = std::make_shared(); + auto runtime_id = RuntimeID::generate(); const TracerSignature tracer_signature{ - /* runtime_id = */ RuntimeID::generate(), + /* runtime_id = */ runtime_id, + /* root_session_id = */ runtime_id.string(), /* service = */ "testsvc", /* environment = */ "test"}; @@ -353,6 +355,65 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry lifecycle") { } } +TELEMETRY_IMPLEMENTATION_TEST("session ID headers") { + auto logger = std::make_shared(); + auto client = std::make_shared(); + auto scheduler = std::make_shared(); + auto url = HTTPClient::URL::parse("http://localhost:8000"); + + SECTION("root process: DD-Session-ID present, DD-Root-Session-ID absent") { + auto rid = RuntimeID::generate(); + const TracerSignature sig{rid, rid.string(), "testsvc", "test"}; + + Telemetry telemetry{*finalize_config(), sig, logger, client, + scheduler, *url}; + + auto it = client->request_headers.items.find("DD-Session-ID"); + REQUIRE(it != client->request_headers.items.end()); + CHECK(it->second == rid.string()); + + CHECK(client->request_headers.items.find("DD-Root-Session-ID") == + client->request_headers.items.end()); + } + + SECTION("child process: DD-Root-Session-ID present when different") { + auto rid = RuntimeID::generate(); + auto root_rid = RuntimeID::generate(); + const TracerSignature sig{rid, root_rid.string(), "testsvc", "test"}; + + Telemetry telemetry{*finalize_config(), sig, logger, client, + scheduler, *url}; + + auto session_it = client->request_headers.items.find("DD-Session-ID"); + REQUIRE(session_it != client->request_headers.items.end()); + CHECK(session_it->second == rid.string()); + + auto root_it = client->request_headers.items.find("DD-Root-Session-ID"); + REQUIRE(root_it != client->request_headers.items.end()); + CHECK(root_it->second == root_rid.string()); + } + + SECTION("heartbeat includes session headers") { + auto rid = RuntimeID::generate(); + auto root_rid = RuntimeID::generate(); + const TracerSignature sig{rid, root_rid.string(), "testsvc", "test"}; + + Telemetry telemetry{*finalize_config(), sig, logger, client, + scheduler, *url}; + + client->request_headers.items.clear(); + scheduler->trigger_heartbeat(); + + auto session_it = client->request_headers.items.find("DD-Session-ID"); + REQUIRE(session_it != client->request_headers.items.end()); + CHECK(session_it->second == rid.string()); + + auto root_it = client->request_headers.items.find("DD-Root-Session-ID"); + REQUIRE(root_it != client->request_headers.items.end()); + CHECK(root_it->second == root_rid.string()); + } +} + TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry API") { const Clock clock = [] { TimePoint result; @@ -364,8 +425,10 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry API") { auto client = std::make_shared(); auto scheduler = std::make_shared(); + auto runtime_id = RuntimeID::generate(); const TracerSignature tracer_signature{ - /* runtime_id = */ RuntimeID::generate(), + /* runtime_id = */ runtime_id, + /* root_session_id = */ runtime_id.string(), /* service = */ "testsvc", /* environment = */ "test"}; @@ -869,8 +932,10 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry configuration") { auto client = std::make_shared(); auto scheduler = std::make_shared(); + auto runtime_id = RuntimeID::generate(); const TracerSignature tracer_signature{ - /* runtime_id = */ RuntimeID::generate(), + /* runtime_id = */ runtime_id, + /* root_session_id = */ runtime_id.string(), /* service = */ "testsvc", /* environment = */ "test"}; diff --git a/test/test_datadog_agent.cpp b/test/test_datadog_agent.cpp index 92ee9a06..ac7c4a3e 100644 --- a/test/test_datadog_agent.cpp +++ b/test/test_datadog_agent.cpp @@ -199,7 +199,8 @@ DATADOG_AGENT_TEST("Remote Configuration") { auto finalized = finalize_config(config); REQUIRE(finalized); - const TracerSignature signature(RuntimeID::generate(), "testsvc", "test"); + auto rid = RuntimeID::generate(); + const TracerSignature signature(rid, rid.string(), "testsvc", "test"); // TODO: set telemetry mock auto config_manager = std::make_shared(*finalized); From 3fab2f500116e9330b76794993a42d55ed0780f9 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 25 Mar 2026 10:40:47 -0400 Subject: [PATCH 02/11] Resync supported-configurations.json via config-inversion tool Co-Authored-By: Claude Sonnet 4.6 --- supported-configurations.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/supported-configurations.json b/supported-configurations.json index 4b7747dd..a742b937 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -28,13 +28,6 @@ "type": "STRING" } ], - "_DD_ROOT_CPP_SESSION_ID": [ - { - "default": null, - "implementation": "A", - "type": "STRING" - } - ], "DD_INSTRUMENTATION_INSTALL_ID": [ { "default": "", @@ -286,6 +279,13 @@ "implementation": "A", "type": "STRING" } + ], + "_DD_ROOT_CPP_SESSION_ID": [ + { + "default": "", + "implementation": "A", + "type": "STRING" + } ] }, "version": "2" From d8053511b53d8613db3878d3a02f4bee6bae70cd Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 25 Mar 2026 10:50:13 -0400 Subject: [PATCH 03/11] Fix config-inversion: output null default for _DD_ROOT_CPP_SESSION_ID The central registry expects null as the default value. Change the macro default from "" to nullptr and teach the config-inversion tool to emit JSON null for nullptr defaults. Co-Authored-By: Claude Sonnet 4.6 --- include/datadog/environment.h | 2 +- supported-configurations.json | 2 +- tools/config-inversion/main.cpp | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index c467dad3..478d6ac3 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -82,7 +82,7 @@ namespace environment { MACRO(DD_TRACE_RESOURCE_RENAMING_ENABLED, BOOLEAN, false) \ MACRO(DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, BOOLEAN, false) \ MACRO(DD_EXTERNAL_ENV, STRING, "") \ - MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, "") + MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, nullptr) #define ENV_DEFAULT_RESOLVED_IN_CODE(X) X #define WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) ARG, diff --git a/supported-configurations.json b/supported-configurations.json index a742b937..85aa9f79 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -282,7 +282,7 @@ ], "_DD_ROOT_CPP_SESSION_ID": [ { - "default": "", + "default": null, "implementation": "A", "type": "STRING" } diff --git a/tools/config-inversion/main.cpp b/tools/config-inversion/main.cpp index 1bef85dd..7efcca24 100644 --- a/tools/config-inversion/main.cpp +++ b/tools/config-inversion/main.cpp @@ -11,12 +11,17 @@ namespace fs = std::filesystem; namespace env = datadog::tracing::environment; template -std::string to_string_any(const T& value) { +nlohmann::json to_json_default(const T& value) { std::ostringstream oss; oss << value; return oss.str(); } +template <> +nlohmann::json to_json_default(std::nullptr_t const&) { + return nullptr; +} + nlohmann::json build_configuration() { nlohmann::json j; j["version"] = "2"; @@ -31,7 +36,7 @@ nlohmann::json build_configuration() { #define X(NAME, TYPE, DEFAULT_VALUE) \ do { \ auto obj = nlohmann::json::object(); \ - obj["default"] = to_string_any(DEFAULT_VALUE); \ + obj["default"] = to_json_default(DEFAULT_VALUE); \ obj["implementation"] = "A"; \ obj["type"] = QUOTED(TYPE); \ supported_configurations[QUOTED(NAME)] = nlohmann::json::array({obj}); \ From 1f8a254dc06de937023a4b9e062d47923719d1ae Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 25 Mar 2026 11:09:22 -0400 Subject: [PATCH 04/11] Match registry: _DD_ROOT_CPP_SESSION_ID implementation B, default "" The central registry has this config as implementation B with an empty string default. Update environment.h to use "" and teach config-inversion to emit "B" for this var via an override map. Co-Authored-By: Claude Sonnet 4.6 --- include/datadog/environment.h | 2 +- supported-configurations.json | 4 ++-- tools/config-inversion/main.cpp | 26 ++++++++++++++------------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 478d6ac3..c467dad3 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -82,7 +82,7 @@ namespace environment { MACRO(DD_TRACE_RESOURCE_RENAMING_ENABLED, BOOLEAN, false) \ MACRO(DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, BOOLEAN, false) \ MACRO(DD_EXTERNAL_ENV, STRING, "") \ - MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, nullptr) + MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, "") #define ENV_DEFAULT_RESOLVED_IN_CODE(X) X #define WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) ARG, diff --git a/supported-configurations.json b/supported-configurations.json index 85aa9f79..86df6967 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -282,8 +282,8 @@ ], "_DD_ROOT_CPP_SESSION_ID": [ { - "default": null, - "implementation": "A", + "default": "", + "implementation": "B", "type": "STRING" } ] diff --git a/tools/config-inversion/main.cpp b/tools/config-inversion/main.cpp index 7efcca24..2da23a35 100644 --- a/tools/config-inversion/main.cpp +++ b/tools/config-inversion/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "datadog/environment.h" @@ -11,16 +12,16 @@ namespace fs = std::filesystem; namespace env = datadog::tracing::environment; template -nlohmann::json to_json_default(const T& value) { +std::string to_string_any(const T& value) { std::ostringstream oss; oss << value; return oss.str(); } -template <> -nlohmann::json to_json_default(std::nullptr_t const&) { - return nullptr; -} +// Variables whose registry implementation letter differs from the default "A". +static const std::unordered_map implementation_overrides = { + {"_DD_ROOT_CPP_SESSION_ID", "B"}, +}; nlohmann::json build_configuration() { nlohmann::json j; @@ -33,13 +34,14 @@ nlohmann::json build_configuration() { #define ENV_DEFAULT_RESOLVED_IN_CODE(X) "" -#define X(NAME, TYPE, DEFAULT_VALUE) \ - do { \ - auto obj = nlohmann::json::object(); \ - obj["default"] = to_json_default(DEFAULT_VALUE); \ - obj["implementation"] = "A"; \ - obj["type"] = QUOTED(TYPE); \ - supported_configurations[QUOTED(NAME)] = nlohmann::json::array({obj}); \ +#define X(NAME, TYPE, DEFAULT_VALUE) \ + do { \ + auto obj = nlohmann::json::object(); \ + obj["default"] = to_string_any(DEFAULT_VALUE); \ + auto it = implementation_overrides.find(QUOTED(NAME)); \ + obj["implementation"] = (it != implementation_overrides.end()) ? it->second : "A"; \ + obj["type"] = QUOTED(TYPE); \ + supported_configurations[QUOTED(NAME)] = nlohmann::json::array({obj}); \ } while (0); DD_LIST_ENVIRONMENT_VARIABLES(X) From 468dd13f35d085c834e2d7af95c7dba14e85d648 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 25 Mar 2026 11:22:55 -0400 Subject: [PATCH 05/11] Revert to implementation A with null default for _DD_ROOT_CPP_SESSION_ID Registry entry had trailing space in name (now fixed by registry team). Use implementation A and null default to match registry. Co-Authored-By: Claude Sonnet 4.6 --- include/datadog/environment.h | 2 +- supported-configurations.json | 4 ++-- tools/config-inversion/main.cpp | 26 ++++++++++++-------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index c467dad3..478d6ac3 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -82,7 +82,7 @@ namespace environment { MACRO(DD_TRACE_RESOURCE_RENAMING_ENABLED, BOOLEAN, false) \ MACRO(DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, BOOLEAN, false) \ MACRO(DD_EXTERNAL_ENV, STRING, "") \ - MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, "") + MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, nullptr) #define ENV_DEFAULT_RESOLVED_IN_CODE(X) X #define WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) ARG, diff --git a/supported-configurations.json b/supported-configurations.json index 86df6967..85aa9f79 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -282,8 +282,8 @@ ], "_DD_ROOT_CPP_SESSION_ID": [ { - "default": "", - "implementation": "B", + "default": null, + "implementation": "A", "type": "STRING" } ] diff --git a/tools/config-inversion/main.cpp b/tools/config-inversion/main.cpp index 2da23a35..c162651b 100644 --- a/tools/config-inversion/main.cpp +++ b/tools/config-inversion/main.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "datadog/environment.h" @@ -12,16 +11,16 @@ namespace fs = std::filesystem; namespace env = datadog::tracing::environment; template -std::string to_string_any(const T& value) { +nlohmann::json to_json_default(const T& value) { std::ostringstream oss; oss << value; return oss.str(); } -// Variables whose registry implementation letter differs from the default "A". -static const std::unordered_map implementation_overrides = { - {"_DD_ROOT_CPP_SESSION_ID", "B"}, -}; +template <> +nlohmann::json to_json_default(std::nullptr_t const&) { + return nullptr; +} nlohmann::json build_configuration() { nlohmann::json j; @@ -34,14 +33,13 @@ nlohmann::json build_configuration() { #define ENV_DEFAULT_RESOLVED_IN_CODE(X) "" -#define X(NAME, TYPE, DEFAULT_VALUE) \ - do { \ - auto obj = nlohmann::json::object(); \ - obj["default"] = to_string_any(DEFAULT_VALUE); \ - auto it = implementation_overrides.find(QUOTED(NAME)); \ - obj["implementation"] = (it != implementation_overrides.end()) ? it->second : "A"; \ - obj["type"] = QUOTED(TYPE); \ - supported_configurations[QUOTED(NAME)] = nlohmann::json::array({obj}); \ +#define X(NAME, TYPE, DEFAULT_VALUE) \ + do { \ + auto obj = nlohmann::json::object(); \ + obj["default"] = to_json_default(DEFAULT_VALUE); \ + obj["implementation"] = "A"; \ + obj["type"] = QUOTED(TYPE); \ + supported_configurations[QUOTED(NAME)] = nlohmann::json::array({obj}); \ } while (0); DD_LIST_ENVIRONMENT_VARIABLES(X) From c106643b54ee35f374654e34c275f0414cba83e5 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 14:14:11 -0400 Subject: [PATCH 06/11] Update include/datadog/environment.h Co-authored-by: Zach Montoya --- include/datadog/environment.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 478d6ac3..c467dad3 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -82,7 +82,7 @@ namespace environment { MACRO(DD_TRACE_RESOURCE_RENAMING_ENABLED, BOOLEAN, false) \ MACRO(DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, BOOLEAN, false) \ MACRO(DD_EXTERNAL_ENV, STRING, "") \ - MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, nullptr) + MACRO(_DD_ROOT_CPP_SESSION_ID, STRING, "") #define ENV_DEFAULT_RESOLVED_IN_CODE(X) X #define WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) ARG, From 25fb7ea3b9da7c4aa1980642ed4111951c9a667a Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 14:45:37 -0400 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20use=20empty=20string=20default,=20move=20env=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change _DD_ROOT_CPP_SESSION_ID default from nullptr to "" in environment.h - Move _DD_ROOT_CPP_SESSION_ID lookup into load_tracer_env_config() so it follows the same pattern as other env var loading - Add root_session_id field to TracerConfig struct - Update supported-configurations.json to implementation B with default "" - No changes to config-inversion tool Co-Authored-By: Claude Opus 4.6 (1M context) --- include/datadog/tracer_config.h | 5 +++++ src/datadog/tracer_config.cpp | 8 ++++++-- supported-configurations.json | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index f342a61b..0021a5e2 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -189,6 +189,11 @@ struct TracerConfig { // This option is ignored if `resource_renaming_enabled` is not `true`. Optional resource_renaming_always_simplified_endpoint; + // Root session ID inherited from the parent process via + // `_DD_ROOT_CPP_SESSION_ID`. Tracks the runtime ID of the first C++ process + // in a fork/spawn chain. + Optional root_session_id; + /// A mapping of process-specific tags used to uniquely identify processes. /// /// The `process_tags` map allows associating arbitrary string-based keys and diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index adf8cf79..39ed3986 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -261,6 +261,10 @@ Expected load_tracer_env_config(Logger &logger) { return std::move(error); } + if (auto val = lookup(environment::_DD_ROOT_CPP_SESSION_ID)) { + env_cfg.root_session_id = std::string{*val}; + } + return env_cfg; } @@ -407,8 +411,8 @@ Expected finalize_config(const TracerConfig &user_config, final_config.runtime_id = user_config.runtime_id; } - if (auto val = lookup(environment::_DD_ROOT_CPP_SESSION_ID)) { - final_config.root_session_id = std::string{*val}; + if (env_config->root_session_id) { + final_config.root_session_id = env_config->root_session_id; } final_config.process_tags = user_config.process_tags; diff --git a/supported-configurations.json b/supported-configurations.json index 85aa9f79..86df6967 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -282,8 +282,8 @@ ], "_DD_ROOT_CPP_SESSION_ID": [ { - "default": null, - "implementation": "A", + "default": "", + "implementation": "B", "type": "STRING" } ] From d323be44462b40db224283aa40ecb57ed19eddac Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 14:53:04 -0400 Subject: [PATCH 08/11] fix: revert config-inversion changes, no longer needed with "" default Since _DD_ROOT_CPP_SESSION_ID now defaults to "" (not nullptr), the config-inversion tool doesn't need the nullptr specialization. Revert to original to_string_any and fix implementation marker to "A". Co-Authored-By: Claude Opus 4.6 (1M context) --- supported-configurations.json | 2 +- tools/config-inversion/main.cpp | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/supported-configurations.json b/supported-configurations.json index 86df6967..a742b937 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -283,7 +283,7 @@ "_DD_ROOT_CPP_SESSION_ID": [ { "default": "", - "implementation": "B", + "implementation": "A", "type": "STRING" } ] diff --git a/tools/config-inversion/main.cpp b/tools/config-inversion/main.cpp index c162651b..1bef85dd 100644 --- a/tools/config-inversion/main.cpp +++ b/tools/config-inversion/main.cpp @@ -11,17 +11,12 @@ namespace fs = std::filesystem; namespace env = datadog::tracing::environment; template -nlohmann::json to_json_default(const T& value) { +std::string to_string_any(const T& value) { std::ostringstream oss; oss << value; return oss.str(); } -template <> -nlohmann::json to_json_default(std::nullptr_t const&) { - return nullptr; -} - nlohmann::json build_configuration() { nlohmann::json j; j["version"] = "2"; @@ -36,7 +31,7 @@ nlohmann::json build_configuration() { #define X(NAME, TYPE, DEFAULT_VALUE) \ do { \ auto obj = nlohmann::json::object(); \ - obj["default"] = to_json_default(DEFAULT_VALUE); \ + obj["default"] = to_string_any(DEFAULT_VALUE); \ obj["implementation"] = "A"; \ obj["type"] = QUOTED(TYPE); \ supported_configurations[QUOTED(NAME)] = nlohmann::json::array({obj}); \ From 3d9252b266b807e75af545a952b9edeeb30c4321 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 15:00:21 -0400 Subject: [PATCH 09/11] fix: revert unrelated formatting, deduplicate session headers, fix Windows setenv - Revert whitespace-only changes (const char * style) in environment.cpp and environment.h to keep the diff focused on the feature. - Extract set_session_headers() helper in telemetry_impl.cpp to avoid duplicating session header logic in app_started() and send_payload(). - Fix Windows _putenv_s path to check getenv() first, matching the POSIX setenv(..., 0) "don't overwrite" semantics. Co-Authored-By: Claude Opus 4.6 (1M context) --- include/datadog/environment.h | 2 +- src/datadog/environment.cpp | 14 ++++++++------ src/datadog/telemetry/telemetry_impl.cpp | 18 ++++++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index c467dad3..143f06b0 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -96,7 +96,7 @@ enum Variable { DD_LIST_ENVIRONMENT_VARIABLES(WITH_COMMA) }; #define QUOTED_WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) \ WITH_COMMA(QUOTED(ARG), TYPE, DEFAULT_VALUE) -inline const char* const variable_names[] = { +inline const char *const variable_names[] = { DD_LIST_ENVIRONMENT_VARIABLES(QUOTED_WITH_COMMA)}; #undef QUOTED diff --git a/src/datadog/environment.cpp b/src/datadog/environment.cpp index 4ad7b547..6306a5be 100644 --- a/src/datadog/environment.cpp +++ b/src/datadog/environment.cpp @@ -11,8 +11,8 @@ namespace environment { StringView name(Variable variable) { return variable_names[variable]; } Optional lookup(Variable variable) { - const char* name = variable_names[variable]; - const char* value = std::getenv(name); + const char *name = variable_names[variable]; + const char *value = std::getenv(name); if (!value) { return nullopt; } @@ -20,9 +20,11 @@ Optional lookup(Variable variable) { } void set(Variable variable, StringView value) { - const char* name = variable_names[variable]; + const char *name = variable_names[variable]; #ifdef _WIN32 - _putenv_s(name, value.data()); + if (!std::getenv(name)) { + _putenv_s(name, value.data()); + } #else setenv(name, value.data(), /*overwrite=*/0); #endif @@ -31,8 +33,8 @@ void set(Variable variable, StringView value) { std::string to_json() { auto result = nlohmann::json::object({}); - for (const char* name : variable_names) { - if (const char* value = std::getenv(name)) { + for (const char *name : variable_names) { + if (const char *value = std::getenv(name)) { result[name] = value; } } diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 9072c23f..20039def 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -60,6 +60,14 @@ HTTPClient::URL make_telemetry_endpoint(HTTPClient::URL url) { return url; } +void set_session_headers(DictWriter& headers, + const tracing::TracerSignature& sig) { + headers.set("DD-Session-ID", sig.runtime_id.string()); + if (sig.root_session_id != sig.runtime_id.string()) { + headers.set("DD-Root-Session-ID", sig.root_session_id); + } +} + void cancel_tasks(std::vector& tasks) { for (auto& cancel_task : tasks) { cancel_task(); @@ -310,10 +318,7 @@ void Telemetry::app_started() { headers.set("DD-Client-Library-Language", "cpp"); headers.set("DD-Client-Library-Version", tracer_version); headers.set("DD-Telemetry-Request-Type", "app-started"); - headers.set("DD-Session-ID", sig.runtime_id.string()); - if (sig.root_session_id != sig.runtime_id.string()) { - headers.set("DD-Root-Session-ID", sig.root_session_id); - } + set_session_headers(headers, sig); if (debug_enabled) { headers.set("DD-Telemetry-Debug-Enabled", "true"); } @@ -369,10 +374,7 @@ void Telemetry::send_payload(StringView request_type, std::string payload) { headers.set("DD-Client-Library-Language", "cpp"); headers.set("DD-Client-Library-Version", tracer_version); headers.set("DD-Telemetry-Request-Type", request_type); - headers.set("DD-Session-ID", sig.runtime_id.string()); - if (sig.root_session_id != sig.runtime_id.string()) { - headers.set("DD-Root-Session-ID", sig.root_session_id); - } + set_session_headers(headers, sig); if (debug_enabled) { headers.set("DD-Telemetry-Debug-Enabled", "true"); } From b3c0f73ce199f0abb0b8bd227805908886597c25 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 15:08:24 -0400 Subject: [PATCH 10/11] Update test/telemetry/test_telemetry.cpp Co-authored-by: Zach Montoya --- test/telemetry/test_telemetry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/telemetry/test_telemetry.cpp b/test/telemetry/test_telemetry.cpp index 35731e63..3d2424a6 100644 --- a/test/telemetry/test_telemetry.cpp +++ b/test/telemetry/test_telemetry.cpp @@ -401,7 +401,7 @@ TELEMETRY_IMPLEMENTATION_TEST("session ID headers") { Telemetry telemetry{*finalize_config(), sig, logger, client, scheduler, *url}; - client->request_headers.items.clear(); + client->clear(); scheduler->trigger_heartbeat(); auto session_it = client->request_headers.items.find("DD-Session-ID"); From b673a7e81d51884e7ba36c1b6c1a57d165161ad0 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 15:30:52 -0400 Subject: [PATCH 11/11] fix: include request_headers in MockHTTPClient::clear() The suggestion to use client->clear() instead of client->request_headers.items.clear() exposed that clear() only reset request_body. Update it to also clear request_headers so the heartbeat session-header test validates fresh headers. Co-Authored-By: Claude Opus 4.6 (1M context) --- test/mocks/http_clients.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/mocks/http_clients.h b/test/mocks/http_clients.h index e369b299..a742bb56 100644 --- a/test/mocks/http_clients.h +++ b/test/mocks/http_clients.h @@ -41,7 +41,10 @@ struct MockHTTPClient : public HTTPClient { ErrorHandler on_error_; std::string request_body; - void clear() { request_body = ""; } + void clear() { + request_body = ""; + request_headers.items.clear(); + } Expected post( const URL&, HeadersSetter set_headers, std::string body,