From 445f045618decb88912f8d302576ba9457a1da74 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 00:14:39 -0400 Subject: [PATCH 01/17] feat(telemetry): implement app-extended-heartbeat event Add support for the app-extended-heartbeat telemetry event per the telemetry v2 API spec. The event fires periodically (default 24h) and includes the full configuration payload, matching app-started. The interval is configurable via DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL (integer seconds) to enable system testing with shorter intervals. Co-Authored-By: Claude Opus 4.6 (1M context) --- include/datadog/environment.h | 1 + include/datadog/telemetry/configuration.h | 5 +++++ src/datadog/telemetry/configuration.cpp | 22 +++++++++++++++++++ src/datadog/telemetry/telemetry_impl.cpp | 26 +++++++++++++++++++++++ src/datadog/telemetry/telemetry_impl.h | 3 +++ 5 files changed, 57 insertions(+) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index f2846b37..b19349db 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -69,6 +69,7 @@ namespace environment { MACRO(DD_VERSION, STRING, "") \ MACRO(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, BOOLEAN, true) \ MACRO(DD_TELEMETRY_HEARTBEAT_INTERVAL, DECIMAL, 10) \ + MACRO(DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL, INT, 86400) \ MACRO(DD_TELEMETRY_METRICS_ENABLED, BOOLEAN, true) \ MACRO(DD_TELEMETRY_METRICS_INTERVAL_SECONDS, DECIMAL, 60) \ MACRO(DD_TELEMETRY_DEBUG, BOOLEAN, false) \ diff --git a/include/datadog/telemetry/configuration.h b/include/datadog/telemetry/configuration.h index 51693f26..3689cfaa 100644 --- a/include/datadog/telemetry/configuration.h +++ b/include/datadog/telemetry/configuration.h @@ -29,6 +29,10 @@ struct Configuration { // Interval at which the heartbeat payload will be sent. // Can be overriden by `DD_TELEMETRY_HEARTBEAT_INTERVAL` environment variable. tracing::Optional heartbeat_interval_seconds; + // Interval at which the extended heartbeat payload will be sent. + // Can be overriden by `DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL` environment + // variable. Default: 86400 seconds (24 hours). + tracing::Optional extended_heartbeat_interval_seconds; // `integration_name` is the name of the product integrating this library. // Example: "nginx", "envoy" or "istio". tracing::Optional integration_name; @@ -52,6 +56,7 @@ struct FinalizedConfiguration { bool report_logs; std::chrono::steady_clock::duration metrics_interval; std::chrono::steady_clock::duration heartbeat_interval; + std::chrono::steady_clock::duration extended_heartbeat_interval; std::string integration_name; std::string integration_version; std::vector products; diff --git a/src/datadog/telemetry/configuration.cpp b/src/datadog/telemetry/configuration.cpp index cc8d2e85..cd6868a6 100644 --- a/src/datadog/telemetry/configuration.cpp +++ b/src/datadog/telemetry/configuration.cpp @@ -48,6 +48,15 @@ tracing::Expected load_telemetry_env_config() { env_cfg.heartbeat_interval_seconds = *maybe_value; } + if (auto extended_heartbeat_interval_seconds = + lookup(environment::DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL)) { + auto maybe_value = parse_double(*extended_heartbeat_interval_seconds); + if (auto error = maybe_value.if_error()) { + return *error; + } + env_cfg.extended_heartbeat_interval_seconds = static_cast(*maybe_value); + } + return env_cfg; } @@ -112,6 +121,19 @@ tracing::Expected finalize_config( std::chrono::duration_cast( std::chrono::duration(heartbeat_interval.second)); + // extended_heartbeat_interval_seconds + auto extended_heartbeat_interval = + pick(env_config->extended_heartbeat_interval_seconds, + user_config.extended_heartbeat_interval_seconds, 86400); + if (extended_heartbeat_interval.second <= 0) { + return Error{ + Error::Code::OUT_OF_RANGE_INTEGER, + "Telemetry extended heartbeat interval must be a positive value"}; + } + result.extended_heartbeat_interval = + std::chrono::duration_cast( + std::chrono::seconds(extended_heartbeat_interval.second)); + // integration_name std::tie(origin, result.integration_name) = pick(env_config->integration_name, user_config.integration_name, diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index d9464dd7..373e1602 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -223,6 +223,10 @@ void Telemetry::schedule_tasks() { config_.heartbeat_interval, [this]() { send_payload("app-heartbeat", heartbeat_and_telemetry()); })); + tasks_.emplace_back(scheduler_->schedule_recurring_event( + config_.extended_heartbeat_interval, + [this]() { send_payload("app-extended-heartbeat", extended_heartbeat_payload()); })); + if (config_.report_metrics) { tasks_.emplace_back(scheduler_->schedule_recurring_event( config_.metrics_interval, [this]() mutable { capture_metrics(); })); @@ -678,6 +682,28 @@ std::string Telemetry::app_started_payload() { return batch.dump(); } +std::string Telemetry::extended_heartbeat_payload() { + auto configuration_json = nlohmann::json::array(); + + for (const auto& product : config_.products) { + for (const auto& [_, config_metadatas] : product.configurations) { + for (const auto& config_metadata : config_metadatas) { + configuration_json.emplace_back( + generate_configuration_field(config_metadata)); + } + } + } + + auto extended_hb_msg = nlohmann::json{ + {"request_type", "app-extended-heartbeat"}, + {"payload", nlohmann::json{{"configuration", configuration_json}}}, + }; + + auto batch = generate_telemetry_body("message-batch"); + batch["payload"] = nlohmann::json::array({std::move(extended_hb_msg)}); + return batch.dump(); +} + nlohmann::json Telemetry::generate_telemetry_body(std::string request_type) { std::time_t tracer_time = std::chrono::duration_cast( clock_().wall.time_since_epoch()) diff --git a/src/datadog/telemetry/telemetry_impl.h b/src/datadog/telemetry/telemetry_impl.h index 7c92db3b..1b9e21c7 100644 --- a/src/datadog/telemetry/telemetry_impl.h +++ b/src/datadog/telemetry/telemetry_impl.h @@ -152,6 +152,9 @@ class Telemetry final { // Constructs a messsage-batch containing `app-heartbeat`, and if metrics // have been modified, a `generate-metrics` message. std::string heartbeat_and_telemetry(); + // Constructs a message-batch containing `app-extended-heartbeat` with the + // full configuration payload. + std::string extended_heartbeat_payload(); // Constructs a message-batch containing `app-closing`, and if metrics have // been modified, a `generate-metrics` message. std::string app_closing_payload(); From 33bede866acdfd8cd5db04fcdd250df028f0f866 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 00:19:36 -0400 Subject: [PATCH 02/17] style: fix clang-format violations Co-Authored-By: Claude Opus 4.6 (1M context) --- src/datadog/telemetry/configuration.cpp | 3 ++- src/datadog/telemetry/telemetry_impl.cpp | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/datadog/telemetry/configuration.cpp b/src/datadog/telemetry/configuration.cpp index cd6868a6..829fdb59 100644 --- a/src/datadog/telemetry/configuration.cpp +++ b/src/datadog/telemetry/configuration.cpp @@ -54,7 +54,8 @@ tracing::Expected load_telemetry_env_config() { if (auto error = maybe_value.if_error()) { return *error; } - env_cfg.extended_heartbeat_interval_seconds = static_cast(*maybe_value); + env_cfg.extended_heartbeat_interval_seconds = + static_cast(*maybe_value); } return env_cfg; diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 373e1602..e066228e 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -224,8 +224,9 @@ void Telemetry::schedule_tasks() { [this]() { send_payload("app-heartbeat", heartbeat_and_telemetry()); })); tasks_.emplace_back(scheduler_->schedule_recurring_event( - config_.extended_heartbeat_interval, - [this]() { send_payload("app-extended-heartbeat", extended_heartbeat_payload()); })); + config_.extended_heartbeat_interval, [this]() { + send_payload("app-extended-heartbeat", extended_heartbeat_payload()); + })); if (config_.report_metrics) { tasks_.emplace_back(scheduler_->schedule_recurring_event( From d29b06587cb8cb30fe7f38788b0d0361f4b71dba Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 00:26:36 -0400 Subject: [PATCH 03/17] chore: regenerate supported-configurations.json Co-Authored-By: Claude Opus 4.6 (1M context) --- supported-configurations.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/supported-configurations.json b/supported-configurations.json index 4ee3558a..ed938703 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -119,6 +119,13 @@ "type": "BOOLEAN" } ], + "DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL": [ + { + "default": "86400", + "implementation": "A", + "type": "INT" + } + ], "DD_TELEMETRY_HEARTBEAT_INTERVAL": [ { "default": "10", From f42f6d8393b53797cb23da80cdb096edb66c21da Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 00:40:19 -0400 Subject: [PATCH 04/17] fix(telemetry): fix task scheduling order and add test for extended heartbeat default Move extended heartbeat scheduling after metrics to preserve the positional task order expected by FakeEventScheduler in tests (heartbeat=0, metrics=1). Add default value check for extended_heartbeat_interval in test_configuration. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/datadog/telemetry/telemetry_impl.cpp | 10 +++++----- test/telemetry/test_configuration.cpp | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index e066228e..4e45edff 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -223,15 +223,15 @@ void Telemetry::schedule_tasks() { config_.heartbeat_interval, [this]() { send_payload("app-heartbeat", heartbeat_and_telemetry()); })); - tasks_.emplace_back(scheduler_->schedule_recurring_event( - config_.extended_heartbeat_interval, [this]() { - send_payload("app-extended-heartbeat", extended_heartbeat_payload()); - })); - if (config_.report_metrics) { tasks_.emplace_back(scheduler_->schedule_recurring_event( config_.metrics_interval, [this]() mutable { capture_metrics(); })); } + + tasks_.emplace_back(scheduler_->schedule_recurring_event( + config_.extended_heartbeat_interval, [this]() { + send_payload("app-extended-heartbeat", extended_heartbeat_payload()); + })); } Telemetry::~Telemetry() { diff --git a/test/telemetry/test_configuration.cpp b/test/telemetry/test_configuration.cpp index 24373e38..b5fcabe5 100644 --- a/test/telemetry/test_configuration.cpp +++ b/test/telemetry/test_configuration.cpp @@ -21,6 +21,7 @@ TELEMETRY_CONFIGURATION_TEST("defaults") { CHECK(cfg->report_metrics == true); CHECK(cfg->metrics_interval == 60s); CHECK(cfg->heartbeat_interval == 10s); + CHECK(cfg->extended_heartbeat_interval == 86400s); CHECK(cfg->install_id.has_value() == false); CHECK(cfg->install_type.has_value() == false); CHECK(cfg->install_time.has_value() == false); From c0cab718f36bff78125918e439b294a1929176db Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 00:49:41 -0400 Subject: [PATCH 05/17] fix(test): use interval-based task identification in FakeEventScheduler The FakeEventScheduler used positional indexing to identify callbacks, which broke when the extended heartbeat task was added. Use interval duration to distinguish metrics (<=60s) from extended heartbeat (>60s) callbacks instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- test/telemetry/test_telemetry.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/telemetry/test_telemetry.cpp b/test/telemetry/test_telemetry.cpp index 83590773..7aad5a34 100644 --- a/test/telemetry/test_telemetry.cpp +++ b/test/telemetry/test_telemetry.cpp @@ -46,19 +46,27 @@ struct FakeEventScheduler : public EventScheduler { size_t count_tasks = 0; std::function heartbeat_callback = nullptr; std::function metrics_callback = nullptr; + std::function extended_heartbeat_callback = nullptr; Optional heartbeat_interval; Optional metrics_interval; + Optional extended_heartbeat_interval; bool cancelled = false; // NOTE: White box testing. This is a limitation of the event scheduler API. + // Tasks are registered in order: heartbeat (0), metrics (1, if enabled), + // extended heartbeat (last). Cancel schedule_recurring_event(std::chrono::steady_clock::duration interval, std::function callback) override { if (count_tasks == 0) { heartbeat_callback = callback; heartbeat_interval = interval; - } else if (count_tasks == 1) { + } else if (interval <= std::chrono::minutes(1)) { + // Metrics interval is <= 60s; extended heartbeat is much larger. metrics_callback = callback; metrics_interval = interval; + } else { + extended_heartbeat_callback = callback; + extended_heartbeat_interval = interval; } count_tasks++; return [this]() { cancelled = true; }; From 6766649ef591ca8cdd0de5861f894c9181627424 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 01:00:42 -0400 Subject: [PATCH 06/17] test(telemetry): verify extended heartbeat includes configuration payload Add test that creates a telemetry instance with configuration, triggers the extended heartbeat, and verifies the payload contains the expected configuration entries. Co-Authored-By: Claude Opus 4.6 (1M context) --- test/telemetry/test_telemetry.cpp | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/telemetry/test_telemetry.cpp b/test/telemetry/test_telemetry.cpp index 7aad5a34..3754043c 100644 --- a/test/telemetry/test_telemetry.cpp +++ b/test/telemetry/test_telemetry.cpp @@ -82,6 +82,11 @@ struct FakeEventScheduler : public EventScheduler { metrics_callback(); } + void trigger_extended_heartbeat() { + assert(extended_heartbeat_callback != nullptr); + extended_heartbeat_callback(); + } + std::string config() const override { return nlohmann::json::object({{"type", "FakeEventScheduler"}}).dump(); } @@ -399,6 +404,49 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry API") { REQUIRE(find_payload(message_batch["payload"], "app-heartbeat")); } + SECTION("generates an extended heartbeat with configuration") { + client->clear(); + + Product product; + product.name = Product::Name::tracing; + product.enabled = true; + product.version = tracer_version; + product.configurations = + std::unordered_map>{ + {ConfigName::SERVICE_NAME, + {ConfigMetadata(ConfigName::SERVICE_NAME, "my-service", + ConfigMetadata::Origin::CODE)}}, + }; + + Configuration cfg; + cfg.products.emplace_back(std::move(product)); + + auto scheduler2 = std::make_shared(); + Telemetry telemetry2{*finalize_config(cfg), + tracer_signature, + logger, + client, + scheduler2, + *url}; + + client->clear(); + scheduler2->trigger_extended_heartbeat(); + + auto message_batch = nlohmann::json::parse(client->request_body); + REQUIRE(is_valid_telemetry_payload(message_batch)); + + auto ext_hb = + find_payload(message_batch["payload"], "app-extended-heartbeat"); + REQUIRE(ext_hb.has_value()); + + auto& config_payload = (*ext_hb)["payload"]["configuration"]; + REQUIRE(config_payload.is_array()); + REQUIRE(config_payload.size() == 1); + CHECK(config_payload[0]["name"] == "service"); + CHECK(config_payload[0]["value"] == "my-service"); + CHECK(config_payload[0]["origin"] == "code"); + } + SECTION("metrics reporting") { SECTION("counters are correctly serialized in generate-metrics payload") { client->clear(); From efe9d01a8e70b250475dec05301eba2c803c6632 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 10:13:07 -0400 Subject: [PATCH 07/17] Update include/datadog/environment.h Co-authored-by: Damien Mehala --- 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 b19349db..67d09d3b 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -69,7 +69,7 @@ namespace environment { MACRO(DD_VERSION, STRING, "") \ MACRO(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, BOOLEAN, true) \ MACRO(DD_TELEMETRY_HEARTBEAT_INTERVAL, DECIMAL, 10) \ - MACRO(DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL, INT, 86400) \ + MACRO(DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL, DECIMAL, 86400.0) \ MACRO(DD_TELEMETRY_METRICS_ENABLED, BOOLEAN, true) \ MACRO(DD_TELEMETRY_METRICS_INTERVAL_SECONDS, DECIMAL, 60) \ MACRO(DD_TELEMETRY_DEBUG, BOOLEAN, false) \ From ffd6ec720049c29c809d05e61ea45fbd7019048f Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 10:13:19 -0400 Subject: [PATCH 08/17] Update src/datadog/telemetry/configuration.cpp Co-authored-by: Damien Mehala --- src/datadog/telemetry/configuration.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datadog/telemetry/configuration.cpp b/src/datadog/telemetry/configuration.cpp index 829fdb59..f004029a 100644 --- a/src/datadog/telemetry/configuration.cpp +++ b/src/datadog/telemetry/configuration.cpp @@ -125,7 +125,7 @@ tracing::Expected finalize_config( // extended_heartbeat_interval_seconds auto extended_heartbeat_interval = pick(env_config->extended_heartbeat_interval_seconds, - user_config.extended_heartbeat_interval_seconds, 86400); + user_config.extended_heartbeat_interval_seconds, 86400.); if (extended_heartbeat_interval.second <= 0) { return Error{ Error::Code::OUT_OF_RANGE_INTEGER, From c83bee7ed6e8cdc061e73f714be40569bd71d1b1 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 10:13:32 -0400 Subject: [PATCH 09/17] Update include/datadog/telemetry/configuration.h Co-authored-by: Damien Mehala --- include/datadog/telemetry/configuration.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/datadog/telemetry/configuration.h b/include/datadog/telemetry/configuration.h index 3689cfaa..7ab83e84 100644 --- a/include/datadog/telemetry/configuration.h +++ b/include/datadog/telemetry/configuration.h @@ -32,7 +32,7 @@ struct Configuration { // Interval at which the extended heartbeat payload will be sent. // Can be overriden by `DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL` environment // variable. Default: 86400 seconds (24 hours). - tracing::Optional extended_heartbeat_interval_seconds; + tracing::Optional extended_heartbeat_interval_seconds; // `integration_name` is the name of the product integrating this library. // Example: "nginx", "envoy" or "istio". tracing::Optional integration_name; From 4c74565fd722bbcfa26cca990bd78fd0f81166fa Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 10:13:44 -0400 Subject: [PATCH 10/17] Update src/datadog/telemetry/configuration.cpp Co-authored-by: Damien Mehala --- src/datadog/telemetry/configuration.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datadog/telemetry/configuration.cpp b/src/datadog/telemetry/configuration.cpp index f004029a..53f4e75b 100644 --- a/src/datadog/telemetry/configuration.cpp +++ b/src/datadog/telemetry/configuration.cpp @@ -55,7 +55,7 @@ tracing::Expected load_telemetry_env_config() { return *error; } env_cfg.extended_heartbeat_interval_seconds = - static_cast(*maybe_value); + *maybe_value; } return env_cfg; From 02c72060d598bbb01f2f3e4b6932866cf3a2bc0a Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 10:36:25 -0400 Subject: [PATCH 11/17] fix(telemetry): use all_configurations_ for extended heartbeat payload Track all reported configurations in all_configurations_ (updated on every generate_configuration_field call) so that extended_heartbeat_payload reflects runtime config changes, not just the static startup state from config_.products. Serialize with current seq_ids without incrementing. Co-Authored-By: Claude Sonnet 4.6 --- src/datadog/telemetry/telemetry_impl.cpp | 35 ++++++++++++++++++++---- src/datadog/telemetry/telemetry_impl.h | 3 ++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 4e45edff..8fba8707 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -256,6 +256,7 @@ Telemetry::Telemetry(Telemetry&& rhs) distributions_(std::move(rhs.distributions_)), seq_id_(rhs.seq_id_), config_seq_ids_(rhs.config_seq_ids_), + all_configurations_(rhs.all_configurations_), host_info_(rhs.host_info_) { cancel_tasks(rhs.tasks_); schedule_tasks(); @@ -279,6 +280,7 @@ Telemetry& Telemetry::operator=(Telemetry&& rhs) { std::swap(distributions_, rhs.distributions_); std::swap(seq_id_, rhs.seq_id_); std::swap(config_seq_ids_, rhs.config_seq_ids_); + std::swap(all_configurations_, rhs.all_configurations_); std::swap(host_info_, rhs.host_info_); schedule_tasks(); } @@ -686,13 +688,33 @@ std::string Telemetry::app_started_payload() { std::string Telemetry::extended_heartbeat_payload() { auto configuration_json = nlohmann::json::array(); - for (const auto& product : config_.products) { - for (const auto& [_, config_metadatas] : product.configurations) { - for (const auto& config_metadata : config_metadatas) { - configuration_json.emplace_back( - generate_configuration_field(config_metadata)); - } + for (const auto& [name, config_metadata] : all_configurations_) { + auto seq_id = config_seq_ids_[name]; + auto j = nlohmann::json{{"name", to_string(config_metadata.name)}, + {"value", config_metadata.value}, + {"seq_id", seq_id}}; + + switch (config_metadata.origin) { + case ConfigMetadata::Origin::ENVIRONMENT_VARIABLE: + j["origin"] = "env_var"; + break; + case ConfigMetadata::Origin::CODE: + j["origin"] = "code"; + break; + case ConfigMetadata::Origin::REMOTE_CONFIG: + j["origin"] = "remote_config"; + break; + case ConfigMetadata::Origin::DEFAULT: + j["origin"] = "default"; + break; + } + + if (config_metadata.error) { + j["error"] = {{"code", config_metadata.error->code}, + {"message", config_metadata.error->message}}; } + + configuration_json.emplace_back(std::move(j)); } auto extended_hb_msg = nlohmann::json{ @@ -744,6 +766,7 @@ nlohmann::json Telemetry::generate_configuration_field( // detect between non set fields. config_seq_ids_[config_metadata.name] += 1; auto seq_id = config_seq_ids_[config_metadata.name]; + all_configurations_[config_metadata.name] = config_metadata; auto j = nlohmann::json{{"name", to_string(config_metadata.name)}, {"value", config_metadata.value}, diff --git a/src/datadog/telemetry/telemetry_impl.h b/src/datadog/telemetry/telemetry_impl.h index 1b9e21c7..75bd8718 100644 --- a/src/datadog/telemetry/telemetry_impl.h +++ b/src/datadog/telemetry/telemetry_impl.h @@ -66,6 +66,9 @@ class Telemetry final { uint64_t seq_id_ = 0; // Track sequence id per configuration field std::unordered_map config_seq_ids_; + // Track the latest reported value for each configuration field + std::unordered_map + all_configurations_; tracing::HostInfo host_info_; From f11010938e33021058ef380b9de1ac86017fac70 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 11:05:51 -0400 Subject: [PATCH 12/17] fix(telemetry): fix clang-format violations and chrono cast for double interval - Fix trailing spaces on DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL macro line - Inline extended_heartbeat_interval_seconds assignment per clang-format - Fix std::chrono::seconds cast to use duration since interval is now double Co-Authored-By: Claude Sonnet 4.6 --- include/datadog/environment.h | 4 ++-- src/datadog/telemetry/configuration.cpp | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 67d09d3b..06b416fc 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -69,7 +69,7 @@ namespace environment { MACRO(DD_VERSION, STRING, "") \ MACRO(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, BOOLEAN, true) \ MACRO(DD_TELEMETRY_HEARTBEAT_INTERVAL, DECIMAL, 10) \ - MACRO(DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL, DECIMAL, 86400.0) \ + MACRO(DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL, DECIMAL, 86400.0) \ MACRO(DD_TELEMETRY_METRICS_ENABLED, BOOLEAN, true) \ MACRO(DD_TELEMETRY_METRICS_INTERVAL_SECONDS, DECIMAL, 60) \ MACRO(DD_TELEMETRY_DEBUG, BOOLEAN, false) \ @@ -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/telemetry/configuration.cpp b/src/datadog/telemetry/configuration.cpp index 53f4e75b..0ad2dcef 100644 --- a/src/datadog/telemetry/configuration.cpp +++ b/src/datadog/telemetry/configuration.cpp @@ -54,8 +54,7 @@ tracing::Expected load_telemetry_env_config() { if (auto error = maybe_value.if_error()) { return *error; } - env_cfg.extended_heartbeat_interval_seconds = - *maybe_value; + env_cfg.extended_heartbeat_interval_seconds = *maybe_value; } return env_cfg; @@ -133,7 +132,7 @@ tracing::Expected finalize_config( } result.extended_heartbeat_interval = std::chrono::duration_cast( - std::chrono::seconds(extended_heartbeat_interval.second)); + std::chrono::duration(extended_heartbeat_interval.second)); // integration_name std::tie(origin, result.integration_name) = From 9ff67215a82f23ace86173d64fc0274d774e1b26 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 11:08:50 -0400 Subject: [PATCH 13/17] test(telemetry): verify extended heartbeat reflects runtime config changes Add a test that simulates a remote config override after startup and confirms the extended heartbeat reports the updated value and origin. Co-Authored-By: Claude Sonnet 4.6 --- test/telemetry/test_telemetry.cpp | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/telemetry/test_telemetry.cpp b/test/telemetry/test_telemetry.cpp index 3754043c..a2715f69 100644 --- a/test/telemetry/test_telemetry.cpp +++ b/test/telemetry/test_telemetry.cpp @@ -447,6 +447,58 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry API") { CHECK(config_payload[0]["origin"] == "code"); } + SECTION( + "extended heartbeat reflects runtime configuration changes (remote " + "config)") { + client->clear(); + + Product product; + product.name = Product::Name::tracing; + product.enabled = true; + product.version = tracer_version; + product.configurations = + std::unordered_map>{ + {ConfigName::SERVICE_NAME, + {ConfigMetadata(ConfigName::SERVICE_NAME, "my-service", + ConfigMetadata::Origin::CODE)}}, + }; + + Configuration cfg; + cfg.products.emplace_back(std::move(product)); + + auto scheduler2 = std::make_shared(); + Telemetry telemetry2{*finalize_config(cfg), tracer_signature, logger, + client, scheduler2, *url}; + + // Simulate a remote config update overriding SERVICE_NAME + telemetry2.capture_configuration_change( + {{ConfigName::SERVICE_NAME, "rc-service", + ConfigMetadata::Origin::REMOTE_CONFIG}}); + telemetry2.send_configuration_change(); + + client->clear(); + scheduler2->trigger_extended_heartbeat(); + + auto payload = nlohmann::json::parse(client->request_body); + REQUIRE(is_valid_telemetry_payload(payload)); + + auto ext_hb = find_payload(payload["payload"], "app-extended-heartbeat"); + REQUIRE(ext_hb.has_value()); + + auto& configuration = (*ext_hb)["payload"]["configuration"]; + REQUIRE(configuration.is_array()); + + bool found = false; + for (const auto& entry : configuration) { + if (entry["name"] == "service") { + found = true; + CHECK(entry["value"] == "rc-service"); + CHECK(entry["origin"] == "remote_config"); + } + } + CHECK(found); + } + SECTION("metrics reporting") { SECTION("counters are correctly serialized in generate-metrics payload") { client->clear(); From 63ee7b1658e3da11e9e48218afd654f9c925cb5b Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 11:12:58 -0400 Subject: [PATCH 14/17] refactor(telemetry): extract serialize_configuration_field helper Deduplicate the origin switch + error serialization logic shared by generate_configuration_field and extended_heartbeat_payload into a pure serialize_configuration_field(metadata, seq_id) helper. Co-Authored-By: Claude Sonnet 4.6 --- src/datadog/telemetry/telemetry_impl.cpp | 48 +++++++----------------- src/datadog/telemetry/telemetry_impl.h | 2 + test/telemetry/test_telemetry.cpp | 8 +++- 3 files changed, 22 insertions(+), 36 deletions(-) diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 8fba8707..4617fafd 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -689,32 +689,8 @@ std::string Telemetry::extended_heartbeat_payload() { auto configuration_json = nlohmann::json::array(); for (const auto& [name, config_metadata] : all_configurations_) { - auto seq_id = config_seq_ids_[name]; - auto j = nlohmann::json{{"name", to_string(config_metadata.name)}, - {"value", config_metadata.value}, - {"seq_id", seq_id}}; - - switch (config_metadata.origin) { - case ConfigMetadata::Origin::ENVIRONMENT_VARIABLE: - j["origin"] = "env_var"; - break; - case ConfigMetadata::Origin::CODE: - j["origin"] = "code"; - break; - case ConfigMetadata::Origin::REMOTE_CONFIG: - j["origin"] = "remote_config"; - break; - case ConfigMetadata::Origin::DEFAULT: - j["origin"] = "default"; - break; - } - - if (config_metadata.error) { - j["error"] = {{"code", config_metadata.error->code}, - {"message", config_metadata.error->message}}; - } - - configuration_json.emplace_back(std::move(j)); + configuration_json.emplace_back( + serialize_configuration_field(config_metadata, config_seq_ids_[name])); } auto extended_hb_msg = nlohmann::json{ @@ -760,14 +736,8 @@ nlohmann::json Telemetry::generate_telemetry_body(std::string request_type) { }); } -nlohmann::json Telemetry::generate_configuration_field( - const ConfigMetadata& config_metadata) { - // NOTE(@dmehala): `seq_id` should start at 1 so that the go backend can - // detect between non set fields. - config_seq_ids_[config_metadata.name] += 1; - auto seq_id = config_seq_ids_[config_metadata.name]; - all_configurations_[config_metadata.name] = config_metadata; - +nlohmann::json Telemetry::serialize_configuration_field( + const ConfigMetadata& config_metadata, std::size_t seq_id) { auto j = nlohmann::json{{"name", to_string(config_metadata.name)}, {"value", config_metadata.value}, {"seq_id", seq_id}}; @@ -799,6 +769,16 @@ nlohmann::json Telemetry::generate_configuration_field( return j; } +nlohmann::json Telemetry::generate_configuration_field( + const ConfigMetadata& config_metadata) { + // NOTE(@dmehala): `seq_id` should start at 1 so that the go backend can + // detect between non set fields. + config_seq_ids_[config_metadata.name] += 1; + all_configurations_[config_metadata.name] = config_metadata; + return serialize_configuration_field(config_metadata, + config_seq_ids_[config_metadata.name]); +} + void Telemetry::capture_configuration_change( const std::vector& new_configuration) { configuration_snapshot_.insert(configuration_snapshot_.begin(), diff --git a/src/datadog/telemetry/telemetry_impl.h b/src/datadog/telemetry/telemetry_impl.h index 75bd8718..d916846c 100644 --- a/src/datadog/telemetry/telemetry_impl.h +++ b/src/datadog/telemetry/telemetry_impl.h @@ -146,6 +146,8 @@ class Telemetry final { tracing::Optional stacktrace = tracing::nullopt); nlohmann::json generate_telemetry_body(std::string request_type); + nlohmann::json serialize_configuration_field( + const tracing::ConfigMetadata& config_metadata, std::size_t seq_id); nlohmann::json generate_configuration_field( const tracing::ConfigMetadata& config_metadata); diff --git a/test/telemetry/test_telemetry.cpp b/test/telemetry/test_telemetry.cpp index a2715f69..e05c867c 100644 --- a/test/telemetry/test_telemetry.cpp +++ b/test/telemetry/test_telemetry.cpp @@ -467,8 +467,12 @@ TELEMETRY_IMPLEMENTATION_TEST("Tracer telemetry API") { cfg.products.emplace_back(std::move(product)); auto scheduler2 = std::make_shared(); - Telemetry telemetry2{*finalize_config(cfg), tracer_signature, logger, - client, scheduler2, *url}; + Telemetry telemetry2{*finalize_config(cfg), + tracer_signature, + logger, + client, + scheduler2, + *url}; // Simulate a remote config update overriding SERVICE_NAME telemetry2.capture_configuration_change( From 67a420315804c152b49f1ce30de49905bd0b8fef Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 11:16:31 -0400 Subject: [PATCH 15/17] chore: regenerate supported-configurations.json Update DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL type from INT to DECIMAL. Co-Authored-By: Claude Sonnet 4.6 --- supported-configurations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supported-configurations.json b/supported-configurations.json index ed938703..6f5e3d79 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -123,7 +123,7 @@ { "default": "86400", "implementation": "A", - "type": "INT" + "type": "DECIMAL" } ], "DD_TELEMETRY_HEARTBEAT_INTERVAL": [ From c2c27a4d665c6e2ccf310cbc0d0831a4165fb165 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 14:02:01 -0400 Subject: [PATCH 16/17] test(telemetry): add env var override test for extended heartbeat interval Co-Authored-By: Claude Opus 4.6 (1M context) --- test/telemetry/test_configuration.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/telemetry/test_configuration.cpp b/test/telemetry/test_configuration.cpp index b5fcabe5..eaee6c75 100644 --- a/test/telemetry/test_configuration.cpp +++ b/test/telemetry/test_configuration.cpp @@ -114,6 +114,14 @@ TELEMETRY_CONFIGURATION_TEST("environment environment override") { REQUIRE(final_cfg); CHECK(final_cfg->heartbeat_interval == 42s); } + + SECTION("Override extended heartbeat interval") { + cfg.extended_heartbeat_interval_seconds = 99999; + ddtest::EnvGuard env("DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL", "120"); + auto final_cfg = telemetry::finalize_config(cfg); + REQUIRE(final_cfg); + CHECK(final_cfg->extended_heartbeat_interval == 120s); + } } TELEMETRY_CONFIGURATION_TEST("validation") { From c888b841974b67e85c42c02d1c31b7d7b31ae5e4 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 1 Apr 2026 14:04:20 -0400 Subject: [PATCH 17/17] test(telemetry): add code override and validation tests for extended heartbeat interval Co-Authored-By: Claude Opus 4.6 (1M context) --- test/telemetry/test_configuration.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/telemetry/test_configuration.cpp b/test/telemetry/test_configuration.cpp index eaee6c75..8b242e67 100644 --- a/test/telemetry/test_configuration.cpp +++ b/test/telemetry/test_configuration.cpp @@ -34,6 +34,7 @@ TELEMETRY_CONFIGURATION_TEST("code override") { cfg.report_metrics = false; cfg.metrics_interval_seconds = 1; cfg.heartbeat_interval_seconds = 2; + cfg.extended_heartbeat_interval_seconds = 3600; cfg.integration_name = "test"; cfg.integration_version = "2024.10.28"; @@ -45,6 +46,7 @@ TELEMETRY_CONFIGURATION_TEST("code override") { CHECK(final_cfg->report_metrics == false); CHECK(final_cfg->metrics_interval == 1s); CHECK(final_cfg->heartbeat_interval == 2s); + CHECK(final_cfg->extended_heartbeat_interval == 3600s); CHECK(final_cfg->integration_name == "test"); CHECK(final_cfg->integration_version == "2024.10.28"); } @@ -156,6 +158,22 @@ TELEMETRY_CONFIGURATION_TEST("validation") { REQUIRE(!final_cfg); } } + + SECTION("extended heartbeat interval validation") { + SECTION("code override") { + telemetry::Configuration cfg; + cfg.extended_heartbeat_interval_seconds = -100; + + auto final_cfg = telemetry::finalize_config(cfg); + REQUIRE(!final_cfg); + } + + SECTION("environment variable override") { + ddtest::EnvGuard env("DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL", "-1"); + auto final_cfg = telemetry::finalize_config(); + REQUIRE(!final_cfg); + } + } } TELEMETRY_CONFIGURATION_TEST("installation infos are used when available") {