From 445f045618decb88912f8d302576ba9457a1da74 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 31 Mar 2026 00:14:39 -0400 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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();