Skip to content

Commit 1603dce

Browse files
khanayan123claudedmehala
authored
feat(telemetry): implement app-extended-heartbeat event (#301)
* 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) <noreply@anthropic.com> Co-authored-by: Damien Mehala <damien.mehala@datadoghq.com>
1 parent 910e3d5 commit 1603dce

File tree

8 files changed

+221
-9
lines changed

8 files changed

+221
-9
lines changed

include/datadog/environment.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ namespace environment {
6969
MACRO(DD_VERSION, STRING, "") \
7070
MACRO(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, BOOLEAN, true) \
7171
MACRO(DD_TELEMETRY_HEARTBEAT_INTERVAL, DECIMAL, 10) \
72+
MACRO(DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL, DECIMAL, 86400.0) \
7273
MACRO(DD_TELEMETRY_METRICS_ENABLED, BOOLEAN, true) \
7374
MACRO(DD_TELEMETRY_METRICS_INTERVAL_SECONDS, DECIMAL, 60) \
7475
MACRO(DD_TELEMETRY_DEBUG, BOOLEAN, false) \
@@ -95,7 +96,7 @@ enum Variable { DD_LIST_ENVIRONMENT_VARIABLES(WITH_COMMA) };
9596
#define QUOTED_WITH_COMMA(ARG, TYPE, DEFAULT_VALUE) \
9697
WITH_COMMA(QUOTED(ARG), TYPE, DEFAULT_VALUE)
9798

98-
inline const char *const variable_names[] = {
99+
inline const char* const variable_names[] = {
99100
DD_LIST_ENVIRONMENT_VARIABLES(QUOTED_WITH_COMMA)};
100101

101102
#undef QUOTED

include/datadog/telemetry/configuration.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ struct Configuration {
2929
// Interval at which the heartbeat payload will be sent.
3030
// Can be overriden by `DD_TELEMETRY_HEARTBEAT_INTERVAL` environment variable.
3131
tracing::Optional<double> heartbeat_interval_seconds;
32+
// Interval at which the extended heartbeat payload will be sent.
33+
// Can be overriden by `DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL` environment
34+
// variable. Default: 86400 seconds (24 hours).
35+
tracing::Optional<double> extended_heartbeat_interval_seconds;
3236
// `integration_name` is the name of the product integrating this library.
3337
// Example: "nginx", "envoy" or "istio".
3438
tracing::Optional<std::string> integration_name;
@@ -52,6 +56,7 @@ struct FinalizedConfiguration {
5256
bool report_logs;
5357
std::chrono::steady_clock::duration metrics_interval;
5458
std::chrono::steady_clock::duration heartbeat_interval;
59+
std::chrono::steady_clock::duration extended_heartbeat_interval;
5560
std::string integration_name;
5661
std::string integration_version;
5762
std::vector<Product> products;

src/datadog/telemetry/configuration.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ tracing::Expected<Configuration> load_telemetry_env_config() {
4848
env_cfg.heartbeat_interval_seconds = *maybe_value;
4949
}
5050

51+
if (auto extended_heartbeat_interval_seconds =
52+
lookup(environment::DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL)) {
53+
auto maybe_value = parse_double(*extended_heartbeat_interval_seconds);
54+
if (auto error = maybe_value.if_error()) {
55+
return *error;
56+
}
57+
env_cfg.extended_heartbeat_interval_seconds = *maybe_value;
58+
}
59+
5160
return env_cfg;
5261
}
5362

@@ -112,6 +121,19 @@ tracing::Expected<FinalizedConfiguration> finalize_config(
112121
std::chrono::duration_cast<std::chrono::milliseconds>(
113122
std::chrono::duration<double>(heartbeat_interval.second));
114123

124+
// extended_heartbeat_interval_seconds
125+
auto extended_heartbeat_interval =
126+
pick(env_config->extended_heartbeat_interval_seconds,
127+
user_config.extended_heartbeat_interval_seconds, 86400.);
128+
if (extended_heartbeat_interval.second <= 0) {
129+
return Error{
130+
Error::Code::OUT_OF_RANGE_INTEGER,
131+
"Telemetry extended heartbeat interval must be a positive value"};
132+
}
133+
result.extended_heartbeat_interval =
134+
std::chrono::duration_cast<std::chrono::milliseconds>(
135+
std::chrono::duration<double>(extended_heartbeat_interval.second));
136+
115137
// integration_name
116138
std::tie(origin, result.integration_name) =
117139
pick(env_config->integration_name, user_config.integration_name,

src/datadog/telemetry/telemetry_impl.cpp

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ void Telemetry::schedule_tasks() {
227227
tasks_.emplace_back(scheduler_->schedule_recurring_event(
228228
config_.metrics_interval, [this]() mutable { capture_metrics(); }));
229229
}
230+
231+
tasks_.emplace_back(scheduler_->schedule_recurring_event(
232+
config_.extended_heartbeat_interval, [this]() {
233+
send_payload("app-extended-heartbeat", extended_heartbeat_payload());
234+
}));
230235
}
231236

232237
Telemetry::~Telemetry() {
@@ -251,6 +256,7 @@ Telemetry::Telemetry(Telemetry&& rhs)
251256
distributions_(std::move(rhs.distributions_)),
252257
seq_id_(rhs.seq_id_),
253258
config_seq_ids_(rhs.config_seq_ids_),
259+
all_configurations_(rhs.all_configurations_),
254260
host_info_(rhs.host_info_) {
255261
cancel_tasks(rhs.tasks_);
256262
schedule_tasks();
@@ -274,6 +280,7 @@ Telemetry& Telemetry::operator=(Telemetry&& rhs) {
274280
std::swap(distributions_, rhs.distributions_);
275281
std::swap(seq_id_, rhs.seq_id_);
276282
std::swap(config_seq_ids_, rhs.config_seq_ids_);
283+
std::swap(all_configurations_, rhs.all_configurations_);
277284
std::swap(host_info_, rhs.host_info_);
278285
schedule_tasks();
279286
}
@@ -678,6 +685,24 @@ std::string Telemetry::app_started_payload() {
678685
return batch.dump();
679686
}
680687

688+
std::string Telemetry::extended_heartbeat_payload() {
689+
auto configuration_json = nlohmann::json::array();
690+
691+
for (const auto& [name, config_metadata] : all_configurations_) {
692+
configuration_json.emplace_back(
693+
serialize_configuration_field(config_metadata, config_seq_ids_[name]));
694+
}
695+
696+
auto extended_hb_msg = nlohmann::json{
697+
{"request_type", "app-extended-heartbeat"},
698+
{"payload", nlohmann::json{{"configuration", configuration_json}}},
699+
};
700+
701+
auto batch = generate_telemetry_body("message-batch");
702+
batch["payload"] = nlohmann::json::array({std::move(extended_hb_msg)});
703+
return batch.dump();
704+
}
705+
681706
nlohmann::json Telemetry::generate_telemetry_body(std::string request_type) {
682707
std::time_t tracer_time = std::chrono::duration_cast<std::chrono::seconds>(
683708
clock_().wall.time_since_epoch())
@@ -711,13 +736,8 @@ nlohmann::json Telemetry::generate_telemetry_body(std::string request_type) {
711736
});
712737
}
713738

714-
nlohmann::json Telemetry::generate_configuration_field(
715-
const ConfigMetadata& config_metadata) {
716-
// NOTE(@dmehala): `seq_id` should start at 1 so that the go backend can
717-
// detect between non set fields.
718-
config_seq_ids_[config_metadata.name] += 1;
719-
auto seq_id = config_seq_ids_[config_metadata.name];
720-
739+
nlohmann::json Telemetry::serialize_configuration_field(
740+
const ConfigMetadata& config_metadata, std::size_t seq_id) {
721741
auto j = nlohmann::json{{"name", to_string(config_metadata.name)},
722742
{"value", config_metadata.value},
723743
{"seq_id", seq_id}};
@@ -749,6 +769,16 @@ nlohmann::json Telemetry::generate_configuration_field(
749769
return j;
750770
}
751771

772+
nlohmann::json Telemetry::generate_configuration_field(
773+
const ConfigMetadata& config_metadata) {
774+
// NOTE(@dmehala): `seq_id` should start at 1 so that the go backend can
775+
// detect between non set fields.
776+
config_seq_ids_[config_metadata.name] += 1;
777+
all_configurations_[config_metadata.name] = config_metadata;
778+
return serialize_configuration_field(config_metadata,
779+
config_seq_ids_[config_metadata.name]);
780+
}
781+
752782
void Telemetry::capture_configuration_change(
753783
const std::vector<tracing::ConfigMetadata>& new_configuration) {
754784
configuration_snapshot_.insert(configuration_snapshot_.begin(),

src/datadog/telemetry/telemetry_impl.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ class Telemetry final {
6666
uint64_t seq_id_ = 0;
6767
// Track sequence id per configuration field
6868
std::unordered_map<tracing::ConfigName, std::size_t> config_seq_ids_;
69+
// Track the latest reported value for each configuration field
70+
std::unordered_map<tracing::ConfigName, tracing::ConfigMetadata>
71+
all_configurations_;
6972

7073
tracing::HostInfo host_info_;
7174

@@ -143,6 +146,8 @@ class Telemetry final {
143146
tracing::Optional<std::string> stacktrace = tracing::nullopt);
144147

145148
nlohmann::json generate_telemetry_body(std::string request_type);
149+
nlohmann::json serialize_configuration_field(
150+
const tracing::ConfigMetadata& config_metadata, std::size_t seq_id);
146151
nlohmann::json generate_configuration_field(
147152
const tracing::ConfigMetadata& config_metadata);
148153

@@ -152,6 +157,9 @@ class Telemetry final {
152157
// Constructs a messsage-batch containing `app-heartbeat`, and if metrics
153158
// have been modified, a `generate-metrics` message.
154159
std::string heartbeat_and_telemetry();
160+
// Constructs a message-batch containing `app-extended-heartbeat` with the
161+
// full configuration payload.
162+
std::string extended_heartbeat_payload();
155163
// Constructs a message-batch containing `app-closing`, and if metrics have
156164
// been modified, a `generate-metrics` message.
157165
std::string app_closing_payload();

supported-configurations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@
119119
"type": "BOOLEAN"
120120
}
121121
],
122+
"DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL": [
123+
{
124+
"default": "86400",
125+
"implementation": "A",
126+
"type": "DECIMAL"
127+
}
128+
],
122129
"DD_TELEMETRY_HEARTBEAT_INTERVAL": [
123130
{
124131
"default": "10",

test/telemetry/test_configuration.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ TELEMETRY_CONFIGURATION_TEST("defaults") {
2121
CHECK(cfg->report_metrics == true);
2222
CHECK(cfg->metrics_interval == 60s);
2323
CHECK(cfg->heartbeat_interval == 10s);
24+
CHECK(cfg->extended_heartbeat_interval == 86400s);
2425
CHECK(cfg->install_id.has_value() == false);
2526
CHECK(cfg->install_type.has_value() == false);
2627
CHECK(cfg->install_time.has_value() == false);
@@ -33,6 +34,7 @@ TELEMETRY_CONFIGURATION_TEST("code override") {
3334
cfg.report_metrics = false;
3435
cfg.metrics_interval_seconds = 1;
3536
cfg.heartbeat_interval_seconds = 2;
37+
cfg.extended_heartbeat_interval_seconds = 3600;
3638
cfg.integration_name = "test";
3739
cfg.integration_version = "2024.10.28";
3840

@@ -44,6 +46,7 @@ TELEMETRY_CONFIGURATION_TEST("code override") {
4446
CHECK(final_cfg->report_metrics == false);
4547
CHECK(final_cfg->metrics_interval == 1s);
4648
CHECK(final_cfg->heartbeat_interval == 2s);
49+
CHECK(final_cfg->extended_heartbeat_interval == 3600s);
4750
CHECK(final_cfg->integration_name == "test");
4851
CHECK(final_cfg->integration_version == "2024.10.28");
4952
}
@@ -113,6 +116,14 @@ TELEMETRY_CONFIGURATION_TEST("environment environment override") {
113116
REQUIRE(final_cfg);
114117
CHECK(final_cfg->heartbeat_interval == 42s);
115118
}
119+
120+
SECTION("Override extended heartbeat interval") {
121+
cfg.extended_heartbeat_interval_seconds = 99999;
122+
ddtest::EnvGuard env("DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL", "120");
123+
auto final_cfg = telemetry::finalize_config(cfg);
124+
REQUIRE(final_cfg);
125+
CHECK(final_cfg->extended_heartbeat_interval == 120s);
126+
}
116127
}
117128

118129
TELEMETRY_CONFIGURATION_TEST("validation") {
@@ -147,6 +158,22 @@ TELEMETRY_CONFIGURATION_TEST("validation") {
147158
REQUIRE(!final_cfg);
148159
}
149160
}
161+
162+
SECTION("extended heartbeat interval validation") {
163+
SECTION("code override") {
164+
telemetry::Configuration cfg;
165+
cfg.extended_heartbeat_interval_seconds = -100;
166+
167+
auto final_cfg = telemetry::finalize_config(cfg);
168+
REQUIRE(!final_cfg);
169+
}
170+
171+
SECTION("environment variable override") {
172+
ddtest::EnvGuard env("DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL", "-1");
173+
auto final_cfg = telemetry::finalize_config();
174+
REQUIRE(!final_cfg);
175+
}
176+
}
150177
}
151178

152179
TELEMETRY_CONFIGURATION_TEST("installation infos are used when available") {

0 commit comments

Comments
 (0)