Skip to content

Commit 92cdfe6

Browse files
khanayan123claudezacharycmontoya
authored
Implement stable session identifier headers for telemetry (#295)
* 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 <noreply@anthropic.com> * Resync supported-configurations.json via config-inversion tool Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * Update include/datadog/environment.h Co-authored-by: Zach Montoya <zach.montoya@datadoghq.com> * fix: address PR review — use empty string default, move env lookup - 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * Update test/telemetry/test_telemetry.cpp Co-authored-by: Zach Montoya <zach.montoya@datadoghq.com> * 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) <noreply@anthropic.com> * fix: address review feedback from xlamorlette - Rename environment::set to set_if_not_set for clarity - Add config registry link and comment for _DD_ROOT_CPP_SESSION_ID - Add backward-compatible 3-param TracerSignature constructor Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move config registry link to top of file, remove inline link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove inline comment on _DD_ROOT_CPP_SESSION_ID Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: simplify telemetry init() using 3-param TracerSignature constructor Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix clang-format violation in telemetry.cpp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback from xlamorlette - Rename constructor params to match member names - Rename rid to session_rid in session header tests - Use 3-param TracerSignature constructor where root_session == runtime_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: replace env var with singleton for root session ID Replace _DD_ROOT_CPP_SESSION_ID environment variable propagation with a thread-safe singleton (std::call_once). The singleton is immutable after first init, making it safe across forks. - Add set_root_session_id/get_root_session_id in tracer_config - Remove _DD_ROOT_CPP_SESSION_ID from environment registry - Remove environment::set_if_not_set (no longer needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: store session_id in local var, simplify app_started() via send_payload() Address reviewer nits: - Cache sig.runtime_id.string() in set_session_headers to avoid double call - Replace app_started() body with send_payload("app-started", ...) since send_payload is a superset with additional response metrics Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: move root session ID singleton to internal header Move set/get into datadog::tracing::root_session_id namespace in src/datadog/root_session_id.h (internal header), matching the codebase pattern of keeping implementation details out of public headers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add root_session_id.h to Bazel BUILD file Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: move root session ID singleton internals to .cpp file Hide instance() and flag() in an anonymous namespace in root_session_id.cpp, matching the telemetry Meyer's singleton pattern. Only set() and get() are exposed in the header. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: revert formatting-only changes in environment.cpp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: revert formatting-only changes in tracer_config.cpp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: revert unnecessary changes in test_remote_config.cpp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert "chore: revert unnecessary changes in test_remote_config.cpp" This reverts commit 212fc67. * chore: revert test_remote_config.cpp to main (no functional changes needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: restore original TracerSignature style in test_telemetry.cpp Keep inline comment style matching main for unchanged test sections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: revert unnecessary changes in test_datadog_agent.cpp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace std::call_once with mutex+bool guard in root session singleton std::call_once can throw std::system_error on some glibc versions due to pthread_once issues. Use a simple mutex+bool guard instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * simplify: use static local singleton pattern for root session ID Match the codebase's existing singleton pattern (static local variable). No mutex or call_once needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add mutex to root session ID singleton for thread safety Protect concurrent set() calls with std::mutex, matching the codebase's standard pattern for shared state protection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use std::atomic for root session ID singleton guard Replace std::mutex with std::atomic<bool> compare_exchange_strong to avoid potential pthread issues in Docker/CI environments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * revert: restore original app_started() implementation Revert the app_started() → send_payload() simplification as it was out of scope for this PR. Keep only the session headers addition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: simplify root session ID to single get_or_init() singleton Replace separate set()/get() with a single get_or_init(runtime_id) that initializes on first call and returns the stored value on subsequent calls. Remove root_session_id from TracerConfig/FinalizedTracerConfig — the singleton is now the sole source of truth, read directly in Tracer construction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add root_session_id singleton test Verify get_or_init returns the first runtime ID on subsequent calls with different values (first-writer-wins semantics). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: move get_or_init into TracerSignature 3-param constructor The 3-param constructor now calls root_session_id::get_or_init internally, so every TracerSignature construction via the short form goes through the singleton. Tracer no longer needs to call get_or_init directly. Session header tests use the 4-param constructor to control values directly without depending on singleton state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fix fork test to compare against actual singleton value The singleton may already be set from a prior test case. Compare the child's result against what get_or_init actually returned, not the runtime ID passed in this test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: acquire root_session_id from singleton in session header tests Use root_session_id::get_or_init to acquire the root session ID and use it throughout the session header test cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * revert: move get_or_init back to tracer.cpp, remove tracer_signature.cpp Keep the 3-param TracerSignature constructor as a simple inline delegating constructor. The get_or_init call stays in tracer.cpp where the Tracer is constructed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix clang-format violations in test files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove test_root_session_id.cpp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove unnecessary comments Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback - Simplify singleton to static const Meyer's pattern, remove .cpp file - Use C++17 compound namespaces in root_session_id.h - Rename sig -> signature in telemetry_impl.cpp - Remove singleton usage from tests, use 3-param constructor for match - Add blank lines between constructors in tracer_signature.h - Fix extra spaces in test Telemetry init alignment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove extra spaces in test Telemetry init alignment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: restore clang-format alignment in test_telemetry.cpp clang-format requires the extra spaces for argument alignment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: rename sig to tracer_signature to fix clang-format alignment Using the longer variable name avoids clang-format's column alignment padding while matching the naming convention in the rest of the file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: re-add root_session_id to TracerConfig for integration seeding Integrations (nginx, httpd, kong) need to set root_session_id in the master process before workers fork. The Tracer passes it to get_or_init, falling back to runtime_id if not provided. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: match field order in TracerConfig/FinalizedTracerConfig, remove redundant if checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Zach Montoya <zach.montoya@datadoghq.com>
1 parent 967246f commit 92cdfe6

File tree

10 files changed

+123
-14
lines changed

10 files changed

+123
-14
lines changed

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ cc_library(
4545
"src/datadog/propagation_style.cpp",
4646
"src/datadog/random.cpp",
4747
"src/datadog/random.h",
48+
"src/datadog/root_session_id.h",
4849
"src/datadog/rate.cpp",
4950
"src/datadog/remote_config/product.cpp",
5051
"src/datadog/remote_config/remote_config.cpp",

include/datadog/environment.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ namespace environment {
2020

2121
// Central registry for supported environment variables.
2222
// All configurations must be registered here.
23+
// See also:
24+
// https://feature-parity.us1.prod.dog/configurations?viewType=configurations
2325
//
2426
// This registry is the single source of truth for:
2527
// - env variable name allowlist (`include/datadog/environment.h`)

include/datadog/tracer_config.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ struct TracerConfig {
189189
// This option is ignored if `resource_renaming_enabled` is not `true`.
190190
Optional<bool> resource_renaming_always_simplified_endpoint;
191191

192+
// Root session ID for stable telemetry correlation across forked workers.
193+
// Integrations (nginx, httpd, kong) should set this in the master process
194+
// before workers fork so all Tracers share the same root.
195+
Optional<std::string> root_session_id;
196+
192197
/// A mapping of process-specific tags used to uniquely identify processes.
193198
///
194199
/// The `process_tags` map allows associating arbitrary string-based keys and
@@ -225,6 +230,7 @@ class FinalizedTracerConfig final {
225230
bool log_on_startup;
226231
bool generate_128bit_trace_ids;
227232
Optional<RuntimeID> runtime_id;
233+
Optional<std::string> root_session_id;
228234
Clock clock;
229235
std::string integration_name;
230236
std::string integration_version;

include/datadog/tracer_signature.h

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,27 @@ namespace tracing {
3333

3434
struct TracerSignature {
3535
RuntimeID runtime_id;
36+
std::string root_session_id;
3637
std::string default_service;
3738
std::string default_environment;
3839
std::string library_version;
3940
StringView library_language;
4041
StringView library_language_version;
4142

4243
TracerSignature() = delete;
43-
TracerSignature(RuntimeID id, std::string service, std::string environment)
44-
: runtime_id(id),
45-
default_service(std::move(service)),
46-
default_environment(std::move(environment)),
44+
45+
TracerSignature(RuntimeID runtime_id, std::string default_service,
46+
std::string default_environment)
47+
: TracerSignature(runtime_id, runtime_id.string(),
48+
std::move(default_service),
49+
std::move(default_environment)) {}
50+
51+
TracerSignature(RuntimeID runtime_id, std::string root_session_id,
52+
std::string default_service, std::string default_environment)
53+
: runtime_id(runtime_id),
54+
root_session_id(std::move(root_session_id)),
55+
default_service(std::move(default_service)),
56+
default_environment(std::move(default_environment)),
4757
library_version(tracer_version),
4858
library_language("cpp"),
4959
library_language_version(DD_TRACE_STRINGIFY(__cplusplus), 6) {}

src/datadog/root_session_id.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
namespace datadog::tracing::root_session_id {
6+
7+
inline const std::string& get_or_init(const std::string& runtime_id) {
8+
static const std::string id = runtime_id;
9+
return id;
10+
}
11+
12+
} // namespace datadog::tracing::root_session_id

src/datadog/telemetry/telemetry_impl.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ HTTPClient::URL make_telemetry_endpoint(HTTPClient::URL url) {
6060
return url;
6161
}
6262

63+
void set_session_headers(DictWriter& headers,
64+
const tracing::TracerSignature& signature) {
65+
const auto& session_id = signature.runtime_id.string();
66+
headers.set("DD-Session-ID", session_id);
67+
if (signature.root_session_id != session_id) {
68+
headers.set("DD-Root-Session-ID", signature.root_session_id);
69+
}
70+
}
71+
6372
void cancel_tasks(std::vector<tracing::EventScheduler::Cancel>& tasks) {
6473
for (auto& cancel_task : tasks) {
6574
cancel_task();
@@ -309,13 +318,15 @@ void Telemetry::app_started() {
309318
auto payload = app_started_payload();
310319

311320
auto on_headers = [payload_size = payload.size(),
312-
debug_enabled = config_.debug](DictWriter& headers) {
321+
debug_enabled = config_.debug,
322+
&signature = tracer_signature_](DictWriter& headers) {
313323
headers.set("Content-Type", "application/json");
314324
headers.set("Content-Length", std::to_string(payload_size));
315325
headers.set("DD-Telemetry-API-Version", "v2");
316326
headers.set("DD-Client-Library-Language", "cpp");
317327
headers.set("DD-Client-Library-Version", tracer_version);
318328
headers.set("DD-Telemetry-Request-Type", "app-started");
329+
set_session_headers(headers, signature);
319330
if (debug_enabled) {
320331
headers.set("DD-Telemetry-Debug-Enabled", "true");
321332
}
@@ -363,14 +374,16 @@ void Telemetry::app_closing() {
363374

364375
void Telemetry::send_payload(StringView request_type, std::string payload) {
365376
auto set_telemetry_headers = [request_type, payload_size = payload.size(),
366-
debug_enabled =
367-
config_.debug](DictWriter& headers) {
377+
debug_enabled = config_.debug,
378+
&signature =
379+
tracer_signature_](DictWriter& headers) {
368380
headers.set("Content-Type", "application/json");
369381
headers.set("Content-Length", std::to_string(payload_size));
370382
headers.set("DD-Telemetry-API-Version", "v2");
371383
headers.set("DD-Client-Library-Language", "cpp");
372384
headers.set("DD-Client-Library-Version", tracer_version);
373385
headers.set("DD-Telemetry-Request-Type", request_type);
386+
set_session_headers(headers, signature);
374387
if (debug_enabled) {
375388
headers.set("DD-Telemetry-Debug-Enabled", "true");
376389
}

src/datadog/tracer.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "msgpack.h"
2525
#include "platform_util.h"
2626
#include "random.h"
27+
#include "root_session_id.h"
2728
#include "span_data.h"
2829
#include "span_sampler.h"
2930
#include "tags.h"
@@ -48,8 +49,10 @@ Tracer::Tracer(const FinalizedTracerConfig& config,
4849
: logger_(config.logger),
4950
runtime_id_(config.runtime_id ? *config.runtime_id
5051
: RuntimeID::generate()),
51-
signature_{runtime_id_, config.defaults.service,
52-
config.defaults.environment},
52+
signature_{runtime_id_,
53+
root_session_id::get_or_init(
54+
config.root_session_id.value_or(runtime_id_.string())),
55+
config.defaults.service, config.defaults.environment},
5356
config_manager_(std::make_shared<ConfigManager>(config)),
5457
collector_(/* see constructor body */),
5558
span_sampler_(

src/datadog/tracer_config.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,8 @@ Expected<FinalizedTracerConfig> finalize_config(const TracerConfig &user_config,
403403
final_config.injection_styles.erase(it);
404404
}
405405

406-
if (user_config.runtime_id) {
407-
final_config.runtime_id = user_config.runtime_id;
408-
}
409-
406+
final_config.runtime_id = user_config.runtime_id;
407+
final_config.root_session_id = user_config.root_session_id;
410408
final_config.process_tags = user_config.process_tags;
411409

412410
auto agent_finalized =

test/mocks/http_clients.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ struct MockHTTPClient : public HTTPClient {
4141
ErrorHandler on_error_;
4242
std::string request_body;
4343

44-
void clear() { request_body = ""; }
44+
void clear() {
45+
request_body = "";
46+
request_headers.items.clear();
47+
}
4548

4649
Expected<void> post(
4750
const URL&, HeadersSetter set_headers, std::string body,

test/telemetry/test_telemetry.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,67 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry lifecycle") {
366366
}
367367
}
368368

369+
TELEMETRY_IMPLEMENTATION_TEST("session ID headers") {
370+
auto logger = std::make_shared<MockLogger>();
371+
auto client = std::make_shared<MockHTTPClient>();
372+
auto scheduler = std::make_shared<FakeEventScheduler>();
373+
auto url = HTTPClient::URL::parse("http://localhost:8000");
374+
375+
SECTION("root process: DD-Session-ID present, DD-Root-Session-ID absent") {
376+
auto session_rid = RuntimeID::generate();
377+
const TracerSignature tracer_signature(session_rid, "testsvc", "test");
378+
379+
Telemetry telemetry{
380+
*finalize_config(), tracer_signature, logger, client, scheduler, *url};
381+
382+
auto it = client->request_headers.items.find("DD-Session-ID");
383+
REQUIRE(it != client->request_headers.items.end());
384+
CHECK(it->second == session_rid.string());
385+
386+
CHECK(client->request_headers.items.find("DD-Root-Session-ID") ==
387+
client->request_headers.items.end());
388+
}
389+
390+
SECTION("child process: DD-Root-Session-ID present when different") {
391+
auto session_rid = RuntimeID::generate();
392+
auto root_rid = RuntimeID::generate();
393+
const TracerSignature tracer_signature(session_rid, root_rid.string(),
394+
"testsvc", "test");
395+
396+
Telemetry telemetry{
397+
*finalize_config(), tracer_signature, logger, client, scheduler, *url};
398+
399+
auto session_it = client->request_headers.items.find("DD-Session-ID");
400+
REQUIRE(session_it != client->request_headers.items.end());
401+
CHECK(session_it->second == session_rid.string());
402+
403+
auto root_it = client->request_headers.items.find("DD-Root-Session-ID");
404+
REQUIRE(root_it != client->request_headers.items.end());
405+
CHECK(root_it->second == root_rid.string());
406+
}
407+
408+
SECTION("heartbeat includes session headers") {
409+
auto session_rid = RuntimeID::generate();
410+
auto root_rid = RuntimeID::generate();
411+
const TracerSignature tracer_signature(session_rid, root_rid.string(),
412+
"testsvc", "test");
413+
414+
Telemetry telemetry{
415+
*finalize_config(), tracer_signature, logger, client, scheduler, *url};
416+
417+
client->clear();
418+
scheduler->trigger_heartbeat();
419+
420+
auto session_it = client->request_headers.items.find("DD-Session-ID");
421+
REQUIRE(session_it != client->request_headers.items.end());
422+
CHECK(session_it->second == session_rid.string());
423+
424+
auto root_it = client->request_headers.items.find("DD-Root-Session-ID");
425+
REQUIRE(root_it != client->request_headers.items.end());
426+
CHECK(root_it->second == root_rid.string());
427+
}
428+
}
429+
369430
TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry API") {
370431
const Clock clock = [] {
371432
TimePoint result;

0 commit comments

Comments
 (0)