diff --git a/BUILD.bazel b/BUILD.bazel index 1a0523a4..ae4a9631 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -35,6 +35,7 @@ cc_library( "src/datadog/span_matcher.cpp", "src/datadog/span_sampler_config.cpp", "src/datadog/span_sampler.cpp", + "src/datadog/string_util.cpp", "src/datadog/tag_propagation.cpp", "src/datadog/tags.cpp", "src/datadog/threaded_event_scheduler.cpp", @@ -51,6 +52,7 @@ cc_library( hdrs = [ "src/datadog/base64.h", "src/datadog/cerr_logger.h", + "src/datadog/config.h", "src/datadog/clock.h", "src/datadog/config_manager.h", "src/datadog/config_update.h", @@ -99,6 +101,7 @@ cc_library( "src/datadog/span_matcher.h", "src/datadog/span_sampler_config.h", "src/datadog/span_sampler.h", + "src/datadog/string_util.h", "src/datadog/string_view.h", "src/datadog/tag_propagation.h", "src/datadog/tags.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index b36d3399..c1838193 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ target_sources(dd_trace_cpp-objects PRIVATE src/datadog/span_matcher.cpp src/datadog/span_sampler_config.cpp src/datadog/span_sampler.cpp + src/datadog/string_util.cpp src/datadog/tags.cpp src/datadog/tag_propagation.cpp src/datadog/threaded_event_scheduler.cpp @@ -142,6 +143,7 @@ target_sources(dd_trace_cpp-objects PUBLIC BASE_DIRS src/ FILES src/datadog/base64.h + src/datadog/config.h src/datadog/cerr_logger.h src/datadog/clock.h src/datadog/config_manager.h @@ -191,6 +193,7 @@ target_sources(dd_trace_cpp-objects PUBLIC src/datadog/span_matcher.h src/datadog/span_sampler_config.h src/datadog/span_sampler.h + src/datadog/string_util.h src/datadog/string_view.h src/datadog/tag_propagation.h src/datadog/tags.h diff --git a/src/datadog/config.h b/src/datadog/config.h new file mode 100644 index 00000000..aa987865 --- /dev/null +++ b/src/datadog/config.h @@ -0,0 +1,72 @@ +#pragma once + +#include "error.h" +#include "optional.h" + +namespace datadog { +namespace tracing { + +// Enumerates available configuration names for the tracing library +enum class ConfigName : char { + SERVICE_NAME, + SERVICE_ENV, + SERVICE_VERSION, + REPORT_TRACES, + TAGS, + EXTRACTION_STYLES, + INJECTION_STYLES, + STARTUP_LOGS, + REPORT_TELEMETRY, + DELEGATE_SAMPLING, + GENEREATE_128BIT_TRACE_IDS, + AGENT_URL, + RC_POLL_INTERVAL, + TRACE_SAMPLING_RATE, + TRACE_SAMPLING_LIMIT, + TRACE_SAMPLING_RULES, + SPAN_SAMPLING_RULES, +}; + +// Represents metadata for configuration parameters +struct ConfigMetadata { + enum class Origin : char { + ENVIRONMENT_VARIABLE, // Originating from environment variables + CODE, // Defined in code + REMOTE_CONFIG, // Retrieeved from remote configuration + DEFAULT // Default value + }; + + // Name of the configuration parameter + ConfigName name; + // Value of the configuration parameter + std::string value; + // Origin of the configuration parameter + Origin origin; + // Optional error associated with the configuration parameter + Optional error; + + ConfigMetadata() = default; + ConfigMetadata(ConfigName n, std::string v, Origin orig, + Optional err = nullopt) + : name(n), value(std::move(v)), origin(orig), error(err) {} +}; + +// Return a pair containing the configuration origin and value of a +// configuration value chosen from one of the specified `from_env`, +// `from_config`, and `fallback`. This function defines the relative precedence +// among configuration values originating from the environment, programmatic +// configuration, and default configuration. +template +std::pair pick(const Optional &from_env, + const Optional &from_user, + DefaultValue fallback) { + if (from_env) { + return {ConfigMetadata::Origin::ENVIRONMENT_VARIABLE, *from_env}; + } else if (from_user) { + return {ConfigMetadata::Origin::CODE, *from_user}; + } + return {ConfigMetadata::Origin::DEFAULT, fallback}; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/config_manager.cpp b/src/datadog/config_manager.cpp index 3789c34a..00b11b4f 100644 --- a/src/datadog/config_manager.cpp +++ b/src/datadog/config_manager.cpp @@ -1,5 +1,9 @@ #include "config_manager.h" +#include + +#include "parse_util.h" +#include "string_util.h" #include "trace_sampler.h" namespace datadog { @@ -7,74 +11,110 @@ namespace tracing { ConfigManager::ConfigManager(const FinalizedTracerConfig& config) : clock_(config.clock), - default_trace_sampler_( + default_metadata_(config.metadata), + trace_sampler_( std::make_shared(config.trace_sampler, clock_)), - current_trace_sampler_(default_trace_sampler_), - default_span_defaults_(std::make_shared(config.defaults)), - current_span_defaults_(default_span_defaults_), - default_report_traces_(config.report_traces), - current_report_traces_(default_report_traces_) {} + span_defaults_(std::make_shared(config.defaults)), + report_traces_(config.report_traces) {} std::shared_ptr ConfigManager::trace_sampler() { std::lock_guard lock(mutex_); - return current_trace_sampler_; + return trace_sampler_.value(); } std::shared_ptr ConfigManager::span_defaults() { std::lock_guard lock(mutex_); - return current_span_defaults_; + return span_defaults_.value(); } bool ConfigManager::report_traces() { std::lock_guard lock(mutex_); - return current_report_traces_; + return report_traces_.value(); } -void ConfigManager::update(const ConfigUpdate& conf) { +std::vector ConfigManager::update(const ConfigUpdate& conf) { + std::vector metadata; + std::lock_guard lock(mutex_); - if (conf.trace_sampler) { - if (auto finalized_trace_sampler_cfg = - finalize_config(*conf.trace_sampler)) { - current_trace_sampler_ = - std::make_shared(*finalized_trace_sampler_cfg, clock_); - } else { - // TODO: report error - } + if (!conf.trace_sampling_rate) { + reset_config(ConfigName::TRACE_SAMPLING_RATE, trace_sampler_, metadata); } else { - current_trace_sampler_ = default_trace_sampler_; - } + ConfigMetadata trace_sampling_metadata( + ConfigName::TRACE_SAMPLING_RATE, + to_string(*conf.trace_sampling_rate, 1), + ConfigMetadata::Origin::REMOTE_CONFIG); + + TraceSamplerConfig trace_sampler_cfg; + trace_sampler_cfg.sample_rate = *conf.trace_sampling_rate; - if (conf.tags) { - auto new_span_defaults = - std::make_shared(*current_span_defaults_); - new_span_defaults->tags = std::move(*conf.tags); + auto finalized_trace_sampler_cfg = finalize_config(trace_sampler_cfg); + if (auto error = finalized_trace_sampler_cfg.if_error()) { + trace_sampling_metadata.error = *error; + } + + auto trace_sampler = + std::make_shared(*finalized_trace_sampler_cfg, clock_); - current_span_defaults_ = new_span_defaults; + // This reset rate limiting and `TraceSampler` has no `operator==`. + // TODO: Instead of creating another `TraceSampler`, we should + // update the default sampling rate. + trace_sampler_ = std::move(trace_sampler); + metadata.emplace_back(std::move(trace_sampling_metadata)); + } + + if (!conf.tags) { + reset_config(ConfigName::TAGS, span_defaults_, metadata); } else { - current_span_defaults_ = default_span_defaults_; + ConfigMetadata tags_metadata(ConfigName::TAGS, join(*conf.tags, ","), + ConfigMetadata::Origin::REMOTE_CONFIG); + + auto parsed_tags = parse_tags(*conf.tags); + if (auto error = parsed_tags.if_error()) { + tags_metadata.error = *error; + } + + if (*parsed_tags != span_defaults_.value()->tags) { + auto new_span_defaults = + std::make_shared(*span_defaults_.value()); + new_span_defaults->tags = std::move(*parsed_tags); + + span_defaults_ = new_span_defaults; + metadata.emplace_back(std::move(tags_metadata)); + } } - if (conf.report_traces) { - current_report_traces_ = *conf.report_traces; + if (!conf.report_traces) { + reset_config(ConfigName::REPORT_TRACES, report_traces_, metadata); } else { - current_report_traces_ = default_report_traces_; + if (conf.report_traces != report_traces_.value()) { + report_traces_ = *conf.report_traces; + metadata.emplace_back(ConfigName::REPORT_TRACES, + to_string(*conf.report_traces), + ConfigMetadata::Origin::REMOTE_CONFIG); + } } + + return metadata; } -void ConfigManager::reset() { - std::lock_guard lock(mutex_); - current_trace_sampler_ = default_trace_sampler_; - current_span_defaults_ = default_span_defaults_; - current_report_traces_ = default_report_traces_; +template +void ConfigManager::reset_config(ConfigName name, T& conf, + std::vector& metadata) { + if (conf.is_original_value()) return; + + conf.reset(); + metadata.emplace_back(default_metadata_[name]); } +std::vector ConfigManager::reset() { return update({}); } + nlohmann::json ConfigManager::config_json() const { std::lock_guard lock(mutex_); return nlohmann::json{ - {"default", to_json(*current_span_defaults_)}, - {"trace_sampler", current_trace_sampler_->config_json()}, - {"report_traces", current_report_traces_}}; + {"default", to_json(*span_defaults_.value())}, + {"trace_sampler", trace_sampler_.value()->config_json()}, + {"report_traces", report_traces_.value()}}; } } // namespace tracing diff --git a/src/datadog/config_manager.h b/src/datadog/config_manager.h index 51b3c6b0..190cb9cb 100644 --- a/src/datadog/config_manager.h +++ b/src/datadog/config_manager.h @@ -10,6 +10,7 @@ #include "clock.h" #include "config_update.h" #include "json.hpp" +#include "optional.h" #include "span_defaults.h" #include "tracer_config.h" @@ -17,16 +18,50 @@ namespace datadog { namespace tracing { class ConfigManager { + // A class template for managing dynamic configuration values. + // + // This class allows storing and managing dynamic configuration values. It + // maintains an original value and a current value, allowing for updates and + // resets. + // + // Additionally, it provides methods for accessing the current value and + // checking whether it has been modified from its original state. + template + class DynamicConfig { + Value original_value_; + Optional current_value_; + + public: + // Constructs a DynamicConf object with the given initial value + explicit DynamicConfig(Value original_value) + : original_value_(original_value) {} + + // Resets the current value of the configuration to the original value + void reset() { current_value_ = nullopt; } + + // Returns whether the current value is the original value + bool is_original_value() const { return !current_value_.has_value(); } + + const Value& value() const { + return current_value_.has_value() ? *current_value_ : original_value_; + } + + // Updates the current value of the configuration + void operator=(const Value& rhs) { current_value_ = rhs; } + }; + mutable std::mutex mutex_; Clock clock_; - std::shared_ptr default_trace_sampler_; - std::shared_ptr current_trace_sampler_; + std::unordered_map default_metadata_; - std::shared_ptr default_span_defaults_; - std::shared_ptr current_span_defaults_; + DynamicConfig> trace_sampler_; + DynamicConfig> span_defaults_; + DynamicConfig report_traces_; - bool default_report_traces_; - bool current_report_traces_; + private: + template + void reset_config(ConfigName name, T& conf, + std::vector& metadata); public: ConfigManager(const FinalizedTracerConfig& config); @@ -41,11 +76,11 @@ class ConfigManager { bool report_traces(); // Apply the specified `conf` update. - void update(const ConfigUpdate& conf); + std::vector update(const ConfigUpdate& conf); // Restore the configuration that was passed to this object's constructor, // overriding any previous calls to `update`. - void reset(); + std::vector reset(); // Return a JSON representation of the current configuration managed by this // object. diff --git a/src/datadog/config_update.h b/src/datadog/config_update.h index 20001e22..cd7e07f4 100644 --- a/src/datadog/config_update.h +++ b/src/datadog/config_update.h @@ -15,8 +15,8 @@ namespace tracing { // remote configuration value. struct ConfigUpdate { Optional report_traces; - Optional trace_sampler; - Optional> tags; + Optional trace_sampling_rate; + Optional> tags; }; } // namespace tracing diff --git a/src/datadog/datadog_agent.cpp b/src/datadog/datadog_agent.cpp index 457101c2..484a2b81 100644 --- a/src/datadog/datadog_agent.cpp +++ b/src/datadog/datadog_agent.cpp @@ -360,40 +360,33 @@ void DatadogAgent::flush() { } } -void DatadogAgent::send_app_started() { - auto payload = tracer_telemetry_->app_started(); +void DatadogAgent::send_telemetry(std::string payload) { auto post_result = http_client_->post(telemetry_endpoint_, set_content_type_json, std::move(payload), telemetry_on_response_, telemetry_on_error_, clock_().tick + request_timeout_); if (auto* error = post_result.if_error()) { - logger_->log_error(error->with_prefix( - "Unexpected error submitting telemetry app-started event: ")); + logger_->log_error( + error->with_prefix("Unexpected error submitting telemetry event: ")); } } +void DatadogAgent::send_app_started( + const std::unordered_map& config_metadata) { + send_telemetry(tracer_telemetry_->app_started(config_metadata)); +} + void DatadogAgent::send_heartbeat_and_telemetry() { - auto payload = tracer_telemetry_->heartbeat_and_telemetry(); - auto post_result = - http_client_->post(telemetry_endpoint_, set_content_type_json, - std::move(payload), telemetry_on_response_, - telemetry_on_error_, clock_().tick + request_timeout_); - if (auto* error = post_result.if_error()) { - logger_->log_error(error->with_prefix( - "Unexpected error submitting telemetry app-heartbeat event: ")); - } + send_telemetry(tracer_telemetry_->heartbeat_and_telemetry()); } void DatadogAgent::send_app_closing() { - auto payload = tracer_telemetry_->app_closing(); - auto post_result = - http_client_->post(telemetry_endpoint_, set_content_type_json, - std::move(payload), telemetry_on_response_, - telemetry_on_error_, clock_().tick + request_timeout_); - if (auto* error = post_result.if_error()) { - logger_->log_error(error->with_prefix( - "Unexpected error submitting telemetry app-closing event: ")); - } + send_telemetry(tracer_telemetry_->app_closing()); +} + +void DatadogAgent::send_configuration_change( + const std::vector& config) { + send_telemetry(tracer_telemetry_->configuration_change(config)); } void DatadogAgent::get_and_apply_remote_configuration_updates() { @@ -433,10 +426,11 @@ void DatadogAgent::get_and_apply_remote_configuration_updates() { } if (!response_json.empty()) { - // TODO (during Active Configuration): `process_response` should - // return a list of configuration update and should be consumed by - // telemetry. - remote_config_.process_response(response_json); + auto updated_configuration = + remote_config_.process_response(response_json); + if (!updated_configuration.empty()) { + send_configuration_change(updated_configuration); + } } }; diff --git a/src/datadog/datadog_agent.h b/src/datadog/datadog_agent.h index 1b7f6787..2a23aaff 100644 --- a/src/datadog/datadog_agent.h +++ b/src/datadog/datadog_agent.h @@ -59,6 +59,7 @@ class DatadogAgent : public Collector { RemoteConfigurationManager remote_config_; void flush(); + void send_telemetry(std::string); void send_heartbeat_and_telemetry(); void send_app_closing(); @@ -73,7 +74,10 @@ class DatadogAgent : public Collector { std::vector>&& spans, const std::shared_ptr& response_handler) override; - void send_app_started(); + void send_app_started( + const std::unordered_map& config_metadata); + + void send_configuration_change(const std::vector& config); void get_and_apply_remote_configuration_updates(); diff --git a/src/datadog/datadog_agent_config.cpp b/src/datadog/datadog_agent_config.cpp index 593f8a80..c42f306d 100644 --- a/src/datadog/datadog_agent_config.cpp +++ b/src/datadog/datadog_agent_config.cpp @@ -46,7 +46,7 @@ Expected load_datadog_agent_env_config() { Expected finalize_config( const DatadogAgentConfig& user_config, const std::shared_ptr& logger, const Clock& clock) { - auto env_config = load_datadog_agent_env_config(); + Expected env_config = load_datadog_agent_env_config(); if (auto error = env_config.if_error()) { return *error; } @@ -76,7 +76,7 @@ Expected finalize_config( if (auto flush_interval_milliseconds = value_or(env_config->flush_interval_milliseconds, - user_config.flush_interval_milliseconds, 200); + user_config.flush_interval_milliseconds, 2000); flush_interval_milliseconds > 0) { result.flush_interval = std::chrono::milliseconds(flush_interval_milliseconds); @@ -88,7 +88,7 @@ Expected finalize_config( if (auto request_timeout_milliseconds = value_or(env_config->request_timeout_milliseconds, - user_config.request_timeout_milliseconds, 200); + user_config.request_timeout_milliseconds, 2000); request_timeout_milliseconds > 0) { result.request_timeout = std::chrono::milliseconds(request_timeout_milliseconds); @@ -100,7 +100,7 @@ Expected finalize_config( if (auto shutdown_timeout_milliseconds = value_or(env_config->shutdown_timeout_milliseconds, - user_config.shutdown_timeout_milliseconds, 200); + user_config.shutdown_timeout_milliseconds, 2000); shutdown_timeout_milliseconds > 0) { result.shutdown_timeout = std::chrono::milliseconds(shutdown_timeout_milliseconds); @@ -122,13 +122,15 @@ Expected finalize_config( "positive number of seconds."}; } - auto url = - value_or(env_config->url, user_config.url, "http://localhost:8126"); + const auto [origin, url] = + pick(env_config->url, user_config.url, "http://localhost:8126"); auto parsed_url = HTTPClient::URL::parse(url); if (auto* error = parsed_url.if_error()) { return std::move(*error); } result.url = *parsed_url; + result.metadata[ConfigName::AGENT_URL] = + ConfigMetadata(ConfigName::AGENT_URL, url, origin); return result; } diff --git a/src/datadog/datadog_agent_config.h b/src/datadog/datadog_agent_config.h index a95bd454..ef4e13f4 100644 --- a/src/datadog/datadog_agent_config.h +++ b/src/datadog/datadog_agent_config.h @@ -14,9 +14,11 @@ #include #include #include +#include #include #include "clock.h" +#include "config.h" #include "expected.h" #include "http_client.h" #include "string_view.h" @@ -76,6 +78,7 @@ class FinalizedDatadogAgentConfig { std::chrono::steady_clock::duration request_timeout; std::chrono::steady_clock::duration shutdown_timeout; std::chrono::steady_clock::duration remote_configuration_poll_interval; + std::unordered_map metadata; }; Expected finalize_config( diff --git a/src/datadog/parse_util.cpp b/src/datadog/parse_util.cpp index d7ecd1bc..7632419f 100644 --- a/src/datadog/parse_util.cpp +++ b/src/datadog/parse_util.cpp @@ -47,9 +47,9 @@ StringView strip(StringView input) { const auto not_whitespace = [](unsigned char ch) { return !std::isspace(ch); }; - const char* const begin = + const char *const begin = std::find_if(input.begin(), input.end(), not_whitespace); - const char* const end = + const char *const end = std::find_if(input.rbegin(), std::make_reverse_iterator(begin), not_whitespace) .base(); @@ -107,10 +107,75 @@ bool starts_with(StringView subject, StringView prefix) { prefix.end(); } -void to_lower(std::string& text) { +void to_lower(std::string &text) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { return std::tolower(ch); }); } +// List items are separated by an optional comma (",") and any amount of +// whitespace. +// Leading and trailing whitespace is ignored. +std::vector parse_list(StringView input) { + using uchar = unsigned char; + + input = strip(input); + std::vector items; + if (input.empty()) { + return items; + } + + const char *const end = input.end(); + + const char *current = input.begin(); + const char *begin_delim; + do { + const char *begin_item = + std::find_if(current, end, [](uchar ch) { return !std::isspace(ch); }); + begin_delim = std::find_if(begin_item, end, [](uchar ch) { + return std::isspace(ch) || ch == ','; + }); + + items.emplace_back(begin_item, std::size_t(begin_delim - begin_item)); + + const char *end_delim = std::find_if( + begin_delim, end, [](uchar ch) { return !std::isspace(ch); }); + + if (end_delim != end && *end_delim == ',') { + ++end_delim; + } + + current = end_delim; + } while (begin_delim != end); + + return items; +} + +Expected> parse_tags( + std::vector list) { + std::unordered_map tags; + + for (const StringView &token : list) { + const auto separator = std::find(token.begin(), token.end(), ':'); + if (separator == token.end()) { + std::string message; + message += "Unable to parse a key/value from the tag text \""; + append(message, token); + // message += + // "\" because it does not contain the separator character \":\". " + // "Error occurred in list of tags \""; + // append(message, input); + message += "."; + return Error{Error::TAG_MISSING_SEPARATOR, std::move(message)}; + } + + std::string key{token.begin(), separator}; + std::string value{separator + 1, token.end()}; + // If there are duplicate values, then the last one wins. + tags.insert_or_assign(std::move(key), std::move(value)); + } + + return tags; +} + } // namespace tracing } // namespace datadog diff --git a/src/datadog/parse_util.h b/src/datadog/parse_util.h index 65233e87..c2104447 100644 --- a/src/datadog/parse_util.h +++ b/src/datadog/parse_util.h @@ -4,6 +4,8 @@ #include #include +#include +#include #include "expected.h" #include "string_view.h" @@ -39,5 +41,19 @@ bool starts_with(StringView subject, StringView prefix); // Convert the specified `text` to lower case in-place. void to_lower(std::string& text); +// List items are separated by an optional comma (",") and any amount of +// whitespace. +// Leading and trailing whitespace are ignored. +std::vector parse_list(StringView input); + +Expected> parse_tags( + std::vector list); + +inline Expected> parse_tags( + StringView input) { + // Within a tag, the key and value are separated by a colon (":"). + return parse_tags(parse_list(input)); +} + } // namespace tracing } // namespace datadog diff --git a/src/datadog/remote_config.cpp b/src/datadog/remote_config.cpp index 4afc5031..08ce8c6a 100644 --- a/src/datadog/remote_config.cpp +++ b/src/datadog/remote_config.cpp @@ -51,57 +51,22 @@ constexpr std::array k_apm_capabilities = constexpr StringView k_apm_product = "APM_TRACING"; constexpr StringView k_apm_product_path_substring = "/APM_TRACING/"; -Expected> parse_tags( - const std::vector& list_of_tags) { - std::unordered_map tags; - - // Within a tag, the key and value are separated by a colon (":"). - for (const StringView& token : list_of_tags) { - const auto separator = std::find(token.begin(), token.end(), ':'); - if (separator == token.end()) { - std::string message; - message += "Unable to parse a key/value from the tag text \""; - append(message, token); - message += - "\" because it does not contain the separator character \":\"."; - return Error{Error::TAG_MISSING_SEPARATOR, std::move(message)}; - } - - std::string key{token.begin(), separator}; - std::string value{separator + 1, token.end()}; - // If there are duplicate values, then the last one wins. - tags.insert_or_assign(std::move(key), std::move(value)); - } - - return tags; -} - ConfigUpdate parse_dynamic_config(const nlohmann::json& j) { ConfigUpdate config_update; if (auto sampling_rate_it = j.find("tracing_sampling_rate"); sampling_rate_it != j.cend()) { - TraceSamplerConfig trace_sampler_cfg; - trace_sampler_cfg.sample_rate = *sampling_rate_it; - - config_update.trace_sampler = trace_sampler_cfg; + config_update.trace_sampling_rate = *sampling_rate_it; } if (auto tags_it = j.find("tracing_tags"); tags_it != j.cend()) { - auto parsed_tags = parse_tags(*tags_it); - if (parsed_tags.if_error()) { - // TODO: report to telemetry - } else { - config_update.tags = std::move(*parsed_tags); - } + config_update.tags = *tags_it; } if (auto tracing_enabled_it = j.find("tracing_enabled"); tracing_enabled_it != j.cend()) { if (tracing_enabled_it->is_boolean()) { config_update.report_traces = tracing_enabled_it->get(); - } else { - // TODO: report to telemetry } } @@ -171,7 +136,11 @@ nlohmann::json RemoteConfigurationManager::make_request_payload() { return j; } -void RemoteConfigurationManager::process_response(const nlohmann::json& json) { +std::vector RemoteConfigurationManager::process_response( + const nlohmann::json& json) { + std::vector config_update; + config_update.reserve(8); + state_.error_message = nullopt; try { @@ -189,10 +158,12 @@ void RemoteConfigurationManager::process_response(const nlohmann::json& json) { if (client_configs_it == json.cend()) { if (!applied_config_.empty()) { std::for_each(applied_config_.cbegin(), applied_config_.cend(), - [this](const auto it) { revert_config(it.second); }); + [this, &config_update](const auto it) { + config_update = revert_config(it.second); + }); applied_config_.clear(); } - return; + return config_update; } // Keep track of config path received to know which ones to revert. @@ -223,7 +194,7 @@ void RemoteConfigurationManager::process_response(const nlohmann::json& json) { "target file having path \""; append(*state_.error_message, config_path); *state_.error_message += '\"'; - return; + return config_update; } const auto config_json = nlohmann::json::parse( @@ -243,14 +214,14 @@ void RemoteConfigurationManager::process_response(const nlohmann::json& json) { new_config.version = config_json.at("revision"); new_config.content = parse_dynamic_config(config_json.at("lib_config")); - apply_config(new_config); + config_update = apply_config(new_config); applied_config_[std::string{config_path}] = new_config; } // Applied configuration not present must be reverted. for (auto it = applied_config_.cbegin(); it != applied_config_.cend();) { if (!visited_config.count(it->first)) { - revert_config(it->second); + config_update = revert_config(it->second); it = applied_config_.erase(it); } else { it++; @@ -261,15 +232,20 @@ void RemoteConfigurationManager::process_response(const nlohmann::json& json) { error_message += e.what(); state_.error_message = std::move(error_message); + return config_update; } + + return config_update; } -void RemoteConfigurationManager::apply_config(Configuration config) { - config_manager_->update(config.content); +std::vector RemoteConfigurationManager::apply_config( + Configuration config) { + return config_manager_->update(config.content); } -void RemoteConfigurationManager::revert_config(Configuration) { - config_manager_->reset(); +std::vector RemoteConfigurationManager::revert_config( + Configuration) { + return config_manager_->reset(); } } // namespace tracing diff --git a/src/datadog/remote_config.h b/src/datadog/remote_config.h index 86ccfc2d..6d8310e1 100644 --- a/src/datadog/remote_config.h +++ b/src/datadog/remote_config.h @@ -62,17 +62,17 @@ class RemoteConfigurationManager { // Handles the response received from a remote source and udates the internal // state accordingly. - void process_response(const nlohmann::json& json); + std::vector process_response(const nlohmann::json& json); private: // Tell if a `config_path` is a new configuration update. bool is_new_config(StringView config_path, const nlohmann::json& config_meta); // Apply a remote configuration. - void apply_config(Configuration config); + std::vector apply_config(Configuration config); // Revert a remote configuration. - void revert_config(Configuration config); + std::vector revert_config(Configuration config); }; } // namespace tracing diff --git a/src/datadog/span_sampler_config.cpp b/src/datadog/span_sampler_config.cpp index f0dc295c..bde23eb8 100644 --- a/src/datadog/span_sampler_config.cpp +++ b/src/datadog/span_sampler_config.cpp @@ -13,6 +13,15 @@ namespace datadog { namespace tracing { namespace { +std::string to_string(const std::vector &rules) { + nlohmann::json res; + for (const auto &r : rules) { + res.emplace_back(r.to_json()); + } + + return res.dump(); +} + // `env_var` is the name of the environment variable from which `rules_raw` was // obtained. It's used for error messages. Expected> parse_rules(StringView rules_raw, @@ -207,7 +216,7 @@ SpanSamplerConfig::Rule::Rule(const SpanMatcher &base) : SpanMatcher(base) {} Expected finalize_config( const SpanSamplerConfig &user_config, Logger &logger) { - auto env_config = load_span_sampler_env_config(logger); + Expected env_config = load_span_sampler_env_config(logger); if (auto error = env_config.if_error()) { return *error; } @@ -217,8 +226,14 @@ Expected finalize_config( std::vector rules; if (!env_config->rules.empty()) { rules = env_config->rules; + result.metadata[ConfigName::SPAN_SAMPLING_RULES] = + ConfigMetadata(ConfigName::SPAN_SAMPLING_RULES, to_string(rules), + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); } else if (!user_config.rules.empty()) { rules = user_config.rules; + result.metadata[ConfigName::SPAN_SAMPLING_RULES] = + ConfigMetadata(ConfigName::SPAN_SAMPLING_RULES, to_string(rules), + ConfigMetadata::Origin::CODE); } for (const auto &rule : rules) { diff --git a/src/datadog/span_sampler_config.h b/src/datadog/span_sampler_config.h index 065049ac..a89e25f8 100644 --- a/src/datadog/span_sampler_config.h +++ b/src/datadog/span_sampler_config.h @@ -7,8 +7,10 @@ // `SpanSamplerConfig` is specified as the `span_sampler` property of // `TracerConfig`. +#include #include +#include "config.h" #include "expected.h" #include "json_fwd.hpp" #include "logger.h" @@ -48,6 +50,7 @@ class FinalizedSpanSamplerConfig { }; std::vector rules; + std::unordered_map metadata; }; Expected finalize_config(const SpanSamplerConfig&, diff --git a/src/datadog/string_util.cpp b/src/datadog/string_util.cpp new file mode 100644 index 00000000..9384c81f --- /dev/null +++ b/src/datadog/string_util.cpp @@ -0,0 +1,74 @@ +#include "string_util.h" + +#include +#include + +namespace datadog { +namespace tracing { +namespace { + +template +std::string join(const Sequence& elements, StringView separator, + Func&& append_element) { + auto iter = std::begin(elements); + const auto end = std::end(elements); + std::string result; + if (iter == end) { + return result; + } + append_element(result, *iter); + for (++iter; iter != end; ++iter) { + append(result, separator); + append_element(result, *iter); + } + return result; +} + +} // namespace + +std::string to_string(bool b) { return b ? "true" : "false"; } + +std::string to_string(double d, size_t precision) { + std::stringstream stream; + stream << std::fixed << std::setprecision(precision) << d; + return stream.str(); +} + +std::string join(const std::vector& values, StringView separator) { + return join(values, separator, [](std::string& result, StringView value) { + append(result, value); + }); +} + +std::string join_propagation_styles( + const std::vector& values) { + return join(values, ",", [](std::string& result, PropagationStyle style) { + switch (style) { + case PropagationStyle::B3: + result += "b3"; + break; + case PropagationStyle::DATADOG: + result += "datadog"; + break; + case PropagationStyle::W3C: + result += "tracecontext"; + break; + case PropagationStyle::NONE: + result += "none"; + break; + } + }); +} + +std::string join_tags( + const std::unordered_map& values) { + return join(values, ",", [](std::string& result, const auto& entry) { + const auto& [key, value] = entry; + result += key; + result += ':'; + result += value; + }); +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/string_util.h b/src/datadog/string_util.h new file mode 100644 index 00000000..255a4b3c --- /dev/null +++ b/src/datadog/string_util.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "propagation_style.h" + +namespace datadog { +namespace tracing { + +// Return a string representation of the specified boolean `value`. +// The result is "true" for `true` and "false" for `false`. +std::string to_string(bool b); + +// Converts a double value to a string +std::string to_string(double d, size_t precision); + +// Joins elements of a vector into a single string with a specified separator +std::string join(const std::vector& values, StringView separator); + +// Joins propagation styles into a single comma-separated string +std::string join_propagation_styles(const std::vector&); + +// Joins key-value pairs into a single comma-separated string +std::string join_tags( + const std::unordered_map& values); + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/trace_sampler_config.cpp b/src/datadog/trace_sampler_config.cpp index 97a65e64..ac4afcd6 100644 --- a/src/datadog/trace_sampler_config.cpp +++ b/src/datadog/trace_sampler_config.cpp @@ -6,6 +6,7 @@ #include "environment.h" #include "json.hpp" #include "parse_util.h" +#include "string_util.h" namespace datadog { namespace tracing { @@ -130,13 +131,22 @@ Expected load_trace_sampler_env_config() { return env_config; } +std::string to_string(const std::vector &rules) { + nlohmann::json res; + for (const auto &r : rules) { + res.emplace_back(r.to_json()); + } + + return res.dump(); +} + } // namespace TraceSamplerConfig::Rule::Rule(const SpanMatcher &base) : SpanMatcher(base) {} Expected finalize_config( const TraceSamplerConfig &config) { - auto env_config = load_trace_sampler_env_config(); + Expected env_config = load_trace_sampler_env_config(); if (auto error = env_config.if_error()) { return *error; } @@ -147,8 +157,14 @@ Expected finalize_config( if (!env_config->rules.empty()) { rules = std::move(env_config->rules); + result.metadata[ConfigName::TRACE_SAMPLING_RULES] = + ConfigMetadata(ConfigName::TRACE_SAMPLING_RULES, to_string(rules), + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); } else if (!config.rules.empty()) { rules = std::move(config.rules); + result.metadata[ConfigName::TRACE_SAMPLING_RULES] = + ConfigMetadata(ConfigName::TRACE_SAMPLING_RULES, to_string(rules), + ConfigMetadata::Origin::CODE); } for (const auto &rule : rules) { @@ -172,8 +188,14 @@ Expected finalize_config( Optional sample_rate; if (env_config->sample_rate) { sample_rate = env_config->sample_rate; + result.metadata[ConfigName::TRACE_SAMPLING_RATE] = ConfigMetadata( + ConfigName::TRACE_SAMPLING_RATE, to_string(*sample_rate, 1), + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); } else if (config.sample_rate) { sample_rate = config.sample_rate; + result.metadata[ConfigName::TRACE_SAMPLING_RATE] = ConfigMetadata( + ConfigName::TRACE_SAMPLING_RATE, to_string(*sample_rate, 1), + ConfigMetadata::Origin::CODE); } // If `sample_rate` was specified, then it translates to a "catch-all" rule @@ -191,8 +213,10 @@ Expected finalize_config( result.rules.push_back(std::move(catch_all)); } - auto max_per_second = - value_or(env_config->max_per_second, config.max_per_second, 200); + const auto [origin, max_per_second] = + pick(env_config->max_per_second, config.max_per_second, 200); + result.metadata[ConfigName::TRACE_SAMPLING_LIMIT] = ConfigMetadata( + ConfigName::TRACE_SAMPLING_LIMIT, std::to_string(max_per_second), origin); const auto allowed_types = {FP_NORMAL, FP_SUBNORMAL}; if (!(max_per_second > 0) || diff --git a/src/datadog/trace_sampler_config.h b/src/datadog/trace_sampler_config.h index 5fab1845..bfe93a50 100644 --- a/src/datadog/trace_sampler_config.h +++ b/src/datadog/trace_sampler_config.h @@ -7,8 +7,10 @@ // `TraceSamplerConfig` is specified as the `trace_sampler` property of // `TracerConfig`. +#include #include +#include "config.h" #include "expected.h" #include "json_fwd.hpp" #include "optional.h" @@ -45,6 +47,8 @@ class FinalizedTraceSamplerConfig { std::vector rules; double max_per_second; + + std::unordered_map metadata; }; Expected finalize_config( diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 6a35d0dc..583ecd74 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -67,7 +67,7 @@ Tracer::Tracer(const FinalizedTracerConfig& config, collector_ = agent; if (tracer_telemetry_->enabled()) { - agent->send_app_started(); + agent->send_app_started(config.metadata); } } diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index 244bdd41..5df8ee3e 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -14,6 +14,7 @@ #include "environment.h" #include "json.hpp" #include "parse_util.h" +#include "string_util.h" #include "string_view.h" namespace datadog { @@ -26,44 +27,6 @@ bool falsy(StringView text) { return lower == "0" || lower == "false" || lower == "no"; } -// List items are separated by an optional comma (",") and any amount of -// whitespace. -// Leading and trailing whitespace is ignored. -std::vector parse_list(StringView input) { - using uchar = unsigned char; - - input = strip(input); - std::vector items; - if (input.empty()) { - return items; - } - - const char *const end = input.end(); - - const char *current = input.begin(); - const char *begin_delim; - do { - const char *begin_item = - std::find_if(current, end, [](uchar ch) { return !std::isspace(ch); }); - begin_delim = std::find_if(begin_item, end, [](uchar ch) { - return std::isspace(ch) || ch == ','; - }); - - items.emplace_back(begin_item, std::size_t(begin_delim - begin_item)); - - const char *end_delim = std::find_if( - begin_delim, end, [](uchar ch) { return !std::isspace(ch); }); - - if (end_delim != end && *end_delim == ',') { - ++end_delim; - } - - current = end_delim; - } while (begin_delim != end); - - return items; -} - Expected> parse_propagation_styles( StringView input) { std::vector styles; @@ -108,33 +71,6 @@ Expected> parse_propagation_styles( return styles; } -Expected> parse_tags( - StringView input) { - std::unordered_map tags; - - // Within a tag, the key and value are separated by a colon (":"). - for (const StringView &token : parse_list(input)) { - const auto separator = std::find(token.begin(), token.end(), ':'); - if (separator == token.end()) { - std::string message; - message += "Unable to parse a key/value from the tag text \""; - append(message, token); - message += - "\" because it does not contain the separator character \":\". " - "Error occurred in list of tags \""; - append(message, input); - message += "\"."; - return Error{Error::TAG_MISSING_SEPARATOR, std::move(message)}; - } - std::string key{token.begin(), separator}; - std::string value{separator + 1, token.end()}; - // If there are duplicate values, then the last one wins. - tags.insert_or_assign(std::move(key), std::move(value)); - } - - return tags; -} - // Return a `std::vector` parsed from the specified `env_var`. // If `env_var` is not in the environment, return `nullopt`. If an error occurs, // throw an `Error`. @@ -268,7 +204,7 @@ Expected load_tracer_env_config(Logger &logger) { if (!value_override) { continue; } - // TODO: log + const auto var_name = name(var); const auto var_name_override = name(var_override); @@ -318,71 +254,130 @@ Expected finalize_config(const TracerConfig &user_config, auto logger = user_config.logger ? user_config.logger : std::make_shared(); - auto env = load_tracer_env_config(*logger); - if (auto error = env.if_error()) { + Expected env_config = load_tracer_env_config(*logger); + if (auto error = env_config.if_error()) { return *error; } FinalizedTracerConfig final_config; - final_config.clock = clock; final_config.logger = logger; - final_config.defaults.service = - value_or(env->service, user_config.service, ""); - final_config.defaults.service_type = - value_or(env->service_type, user_config.service_type, "web"); - final_config.defaults.environment = - value_or(env->environment, user_config.environment, ""); - final_config.defaults.version = - value_or(env->version, user_config.version, ""); - final_config.defaults.name = value_or(env->name, user_config.name, ""); - final_config.defaults.tags = - value_or(env->tags, user_config.tags, - std::unordered_map{}); - final_config.extraction_styles = - value_or(env->extraction_styles, user_config.extraction_styles, - std::vector{PropagationStyle::DATADOG, - PropagationStyle::W3C}); - final_config.injection_styles = - value_or(env->injection_styles, user_config.injection_styles, - std::vector{PropagationStyle::DATADOG, - PropagationStyle::W3C}); - final_config.log_on_startup = - value_or(env->log_on_startup, user_config.log_on_startup, true); - final_config.report_traces = - value_or(env->report_traces, user_config.report_traces, true); - final_config.report_telemetry = - value_or(env->report_telemetry, user_config.report_telemetry, true); - final_config.report_hostname = - value_or(env->report_hostname, user_config.report_hostname, false); - final_config.delegate_trace_sampling = value_or( - env->delegate_trace_sampling, user_config.delegate_trace_sampling, false); - final_config.tags_header_size = value_or( - env->max_tags_header_size, user_config.max_tags_header_size, 512); - final_config.generate_128bit_trace_ids = - value_or(env->generate_128bit_trace_ids, - user_config.generate_128bit_trace_ids, true); - final_config.integration_name = - value_or(env->integration_name, user_config.integration_name, ""); - final_config.integration_version = - value_or(env->integration_version, user_config.integration_version, ""); - if (user_config.runtime_id) { - final_config.runtime_id = user_config.runtime_id; - } + ConfigMetadata::Origin origin; + + std::tie(origin, final_config.defaults.service) = + pick(env_config->service, user_config.service, ""); if (final_config.defaults.service.empty()) { return Error{Error::SERVICE_NAME_REQUIRED, "Service name is required."}; } + final_config.metadata[ConfigName::SERVICE_NAME] = ConfigMetadata( + ConfigName::SERVICE_NAME, final_config.defaults.service, origin); + + final_config.defaults.service_type = + value_or(env_config->service_type, user_config.service_type, "web"); + + // DD_ENV + std::tie(origin, final_config.defaults.environment) = + pick(env_config->environment, user_config.environment, ""); + final_config.metadata[ConfigName::SERVICE_ENV] = ConfigMetadata( + ConfigName::SERVICE_ENV, final_config.defaults.environment, origin); + + // DD_VERSION + std::tie(origin, final_config.defaults.version) = + pick(env_config->version, user_config.version, ""); + final_config.metadata[ConfigName::SERVICE_VERSION] = ConfigMetadata( + ConfigName::SERVICE_VERSION, final_config.defaults.version, origin); + + final_config.defaults.name = value_or(env_config->name, user_config.name, ""); + + // DD_TAGS + std::tie(origin, final_config.defaults.tags) = + pick(env_config->tags, user_config.tags, + std::unordered_map{}); + final_config.metadata[ConfigName::TAGS] = ConfigMetadata( + ConfigName::TAGS, join_tags(final_config.defaults.tags), origin); + + // Extraction Styles + const std::vector default_propagation_styles{ + PropagationStyle::DATADOG, PropagationStyle::W3C}; + + std::tie(origin, final_config.extraction_styles) = + pick(env_config->extraction_styles, user_config.extraction_styles, + default_propagation_styles); if (final_config.extraction_styles.empty()) { return Error{Error::MISSING_SPAN_EXTRACTION_STYLE, "At least one extraction style must be specified."}; } + final_config.metadata[ConfigName::EXTRACTION_STYLES] = ConfigMetadata( + ConfigName::EXTRACTION_STYLES, + join_propagation_styles(final_config.extraction_styles), origin); + + // Injection Styles + std::tie(origin, final_config.injection_styles) = + pick(env_config->injection_styles, user_config.injection_styles, + default_propagation_styles); if (final_config.injection_styles.empty()) { return Error{Error::MISSING_SPAN_INJECTION_STYLE, "At least one injection style must be specified."}; } + final_config.metadata[ConfigName::INJECTION_STYLES] = ConfigMetadata( + ConfigName::INJECTION_STYLES, + join_propagation_styles(final_config.injection_styles), origin); + + // Startup Logs + std::tie(origin, final_config.log_on_startup) = + pick(env_config->log_on_startup, user_config.log_on_startup, true); + final_config.metadata[ConfigName::STARTUP_LOGS] = ConfigMetadata( + ConfigName::STARTUP_LOGS, to_string(final_config.log_on_startup), origin); + + // Report traces + std::tie(origin, final_config.report_traces) = + pick(env_config->report_traces, user_config.report_traces, true); + final_config.metadata[ConfigName::REPORT_TRACES] = ConfigMetadata( + ConfigName::REPORT_TRACES, to_string(final_config.report_traces), origin); + + // Report telemetry + std::tie(origin, final_config.report_telemetry) = + pick(env_config->report_telemetry, user_config.report_telemetry, true); + final_config.metadata[ConfigName::REPORT_TELEMETRY] = + ConfigMetadata(ConfigName::REPORT_TELEMETRY, + to_string(final_config.report_traces), origin); + + // Report hostname + final_config.report_hostname = + value_or(env_config->report_hostname, user_config.report_hostname, false); + + // Delegate Sampling + std::tie(origin, final_config.delegate_trace_sampling) = + pick(env_config->delegate_trace_sampling, + user_config.delegate_trace_sampling, false); + final_config.metadata[ConfigName::DELEGATE_SAMPLING] = + ConfigMetadata(ConfigName::DELEGATE_SAMPLING, + to_string(final_config.delegate_trace_sampling), origin); + + // Tags Header Size + final_config.tags_header_size = value_or( + env_config->max_tags_header_size, user_config.max_tags_header_size, 512); + + // 128b Trace IDs + std::tie(origin, final_config.generate_128bit_trace_ids) = + pick(env_config->generate_128bit_trace_ids, + user_config.generate_128bit_trace_ids, true); + final_config.metadata[ConfigName::GENEREATE_128BIT_TRACE_IDS] = + ConfigMetadata(ConfigName::GENEREATE_128BIT_TRACE_IDS, + to_string(final_config.delegate_trace_sampling), origin); + + // Integration name & version + final_config.integration_name = + value_or(env_config->integration_name, user_config.integration_name, ""); + final_config.integration_version = value_or( + env_config->integration_version, user_config.integration_version, ""); + + if (user_config.runtime_id) { + final_config.runtime_id = user_config.runtime_id; + } if (!user_config.collector) { auto finalized = @@ -391,11 +386,13 @@ Expected finalize_config(const TracerConfig &user_config, return std::move(*error); } final_config.collector = *finalized; + final_config.metadata.merge(finalized->metadata); } else { final_config.collector = user_config.collector; } if (auto trace_sampler_config = finalize_config(user_config.trace_sampler)) { + final_config.metadata.merge(trace_sampler_config->metadata); final_config.trace_sampler = std::move(*trace_sampler_config); } else { return std::move(trace_sampler_config.error()); @@ -403,6 +400,7 @@ Expected finalize_config(const TracerConfig &user_config, if (auto span_sampler_config = finalize_config(user_config.span_sampler, *logger)) { + final_config.metadata.merge(span_sampler_config->metadata); final_config.span_sampler = std::move(*span_sampler_config); } else { return std::move(span_sampler_config.error()); diff --git a/src/datadog/tracer_config.h b/src/datadog/tracer_config.h index 8c448361..49cf6ab2 100644 --- a/src/datadog/tracer_config.h +++ b/src/datadog/tracer_config.h @@ -10,6 +10,7 @@ #include #include "clock.h" +#include "config.h" #include "datadog_agent_config.h" #include "error.h" #include "expected.h" @@ -42,7 +43,7 @@ struct TracerConfig { // Example: `prod`, `pre-prod` or `staging`. Optional environment; - // Set the application version. + // Set the service version. // // Overriden by the `DD_VERSION` environment variable. // Example values: `1.2.3`, `6c44da20`, `2020.02.13`. @@ -197,6 +198,7 @@ class FinalizedTracerConfig final { std::string integration_version; bool delegate_trace_sampling; bool report_traces; + std::unordered_map metadata; }; // Return a `FinalizedTracerConfig` from the specified `config` and from any diff --git a/src/datadog/tracer_telemetry.cpp b/src/datadog/tracer_telemetry.cpp index 9ebda3c5..d58ab82e 100644 --- a/src/datadog/tracer_telemetry.cpp +++ b/src/datadog/tracer_telemetry.cpp @@ -7,6 +7,50 @@ namespace datadog { namespace tracing { +namespace { + +std::string to_string(datadog::tracing::ConfigName name) { + switch (name) { + case ConfigName::SERVICE_NAME: + return "service"; + case ConfigName::SERVICE_ENV: + return "env"; + case ConfigName::SERVICE_VERSION: + return "application_version"; + case ConfigName::REPORT_TRACES: + return "trace_enabled"; + case ConfigName::TAGS: + return "trace_tags"; + case ConfigName::EXTRACTION_STYLES: + return "trace_propagation_style_extract"; + case ConfigName::INJECTION_STYLES: + return "trace_propagation_style_inject"; + case ConfigName::STARTUP_LOGS: + return "trace_startup_logs_enabled"; + case ConfigName::REPORT_TELEMETRY: + return "instrumentation_telemetry_enabled"; + case ConfigName::DELEGATE_SAMPLING: + return "DD_TRACE_DELEGATE_SAMPLING"; + case ConfigName::GENEREATE_128BIT_TRACE_IDS: + return "trace_128_bits_id_enabled"; + case ConfigName::AGENT_URL: + return "trace_agent_url"; + case ConfigName::RC_POLL_INTERVAL: + return "remote_config_poll_interval"; + case ConfigName::TRACE_SAMPLING_RATE: + return "trace_sample_rate"; + case ConfigName::TRACE_SAMPLING_LIMIT: + return "trace_rate_limit"; + case ConfigName::SPAN_SAMPLING_RULES: + return "span_sample_rules"; + case ConfigName::TRACE_SAMPLING_RULES: + return "trace_sample_rules"; + } + + std::abort(); +} + +} // namespace TracerTelemetry::TracerTelemetry(bool enabled, const Clock& clock, const std::shared_ptr& logger, @@ -83,12 +127,59 @@ nlohmann::json TracerTelemetry::generate_telemetry_body( }); } -std::string TracerTelemetry::app_started() { +nlohmann::json TracerTelemetry::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]; + + 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) { + // clang-format off + j["error"] = { + {"code", config_metadata.error->code}, + {"message", config_metadata.error->message} + }; + // clang-format on + } + + return j; +} + +std::string TracerTelemetry::app_started( + const std::unordered_map& configurations) { + auto configuration_json = nlohmann::json::array(); + for (const auto& [_, config_metadata] : configurations) { + // if (config_metadata.value.empty()) continue; + + configuration_json.emplace_back( + generate_configuration_field(config_metadata)); + } + // clang-format off auto app_started_msg = nlohmann::json{ {"request_type", "app-started"}, {"payload", nlohmann::json{ - {"configuration", nlohmann::json::array()} + {"configuration", configuration_json} }} }; @@ -241,5 +332,22 @@ std::string TracerTelemetry::app_closing() { return message_batch_payload; } +std::string TracerTelemetry::configuration_change( + const std::vector& new_configuration) { + auto configuration_json = nlohmann::json::array(); + for (const auto& config_metadata : new_configuration) { + // if (config_metadata.value.empty()) continue; + configuration_json.emplace_back( + generate_configuration_field(config_metadata)); + } + + auto configuration_change = + generate_telemetry_body("app-client-configuration-change"); + configuration_change["payload"] = + nlohmann::json{{"configuration", configuration_json}}; + + return configuration_change.dump(); +} + } // namespace tracing } // namespace datadog diff --git a/src/datadog/tracer_telemetry.h b/src/datadog/tracer_telemetry.h index f29231a6..6a6ffb33 100644 --- a/src/datadog/tracer_telemetry.h +++ b/src/datadog/tracer_telemetry.h @@ -13,6 +13,7 @@ // - `app-heartbeat` // - `generate-metrics` // - `app-closing` +// - `app-client-configuration-change` // // `app-started` messages are sent as part of initializing the tracer. // @@ -25,9 +26,12 @@ // last `app-heartbeat` event, a `generate-metrics` message is also included in // the batch. // +// `app-client-configuration-change` messages are sent as soon as the tracer +// configuration has been updated by a Remote Configuration event. #include #include "clock.h" +#include "config.h" #include "json.hpp" #include "metrics.h" #include "runtime_id.h" @@ -48,7 +52,10 @@ class TracerTelemetry { std::string hostname_; std::string integration_name_; std::string integration_version_; + // Track sequence id per payload generated uint64_t seq_id_ = 0; + // Track sequence id per configuration field + std::unordered_map config_seq_ids; // This structure contains all the metrics that are exposed by tracer // telemetry. struct { @@ -99,6 +106,9 @@ class TracerTelemetry { nlohmann::json generate_telemetry_body(std::string request_type); + nlohmann::json generate_configuration_field( + const ConfigMetadata& config_metadata); + public: TracerTelemetry(bool enabled, const Clock& clock, const std::shared_ptr& logger, @@ -111,17 +121,21 @@ class TracerTelemetry { auto& metrics() { return metrics_; }; // Constructs an `app-started` message using information provided when // constructed and the tracer_config value passed in. - std::string app_started(); - // This is used to take a snapshot of the current state of metrics and collect - // timestamped "points" of values. These values are later submitted in - // `generate-metrics` messages. + std::string app_started( + const std::unordered_map& configurations); + // This is used to take a snapshot of the current state of metrics and + // collect timestamped "points" of values. These values are later submitted + // in `generate-metrics` messages. void capture_metrics(); - // Constructs a messsage-batch containing `app-heartbeat`, and if metrics have - // been modified, a `generate-metrics` message. + // 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-closing`, and if metrics have // been modified, a `generate-metrics` message. std::string app_closing(); + // Construct an `app-client-configuration-change` message. + std::string configuration_change( + const std::vector& new_configuration); }; } // namespace tracing diff --git a/test/test_datadog_agent.cpp b/test/test_datadog_agent.cpp index d45bae12..cc97acb3 100644 --- a/test/test_datadog_agent.cpp +++ b/test/test_datadog_agent.cpp @@ -1,8 +1,10 @@ #include +#include #include #include #include +#include #include #include "mocks/event_schedulers.h" @@ -11,8 +13,9 @@ #include "test.h" using namespace datadog::tracing; +using namespace std::chrono_literals; -TEST_CASE("CollectorResponse") { +TEST_CASE("CollectorResponse", "[datadog_agent]") { TracerConfig config; config.service = "testsvc"; const auto logger = @@ -166,3 +169,63 @@ TEST_CASE("CollectorResponse") { REQUIRE(logger->first_error().code == error.code); } } + +// NOTE: `report_telemetry` is too vague for now. +// Does it mean no telemetry at all or just metrics are not generated? +// +// TODO: Use cases to implement: +// - telemetry is disabled, no event scheduled. +// - telemetry is enabled, after x sec generate metrics is called. +// - send_app_started? +TEST_CASE("Remote Configuration", "[datadog_agent]") { + const auto logger = + std::make_shared(std::cerr, MockLogger::ERRORS_ONLY); + logger->echo = nullptr; + const auto event_scheduler = std::make_shared(); + const auto http_client = std::make_shared(); + + TracerConfig config; + config.service = "testsvc"; + config.logger = logger; + config.agent.event_scheduler = event_scheduler; + config.agent.http_client = http_client; + config.report_telemetry = false; + + auto finalized = finalize_config(config); + REQUIRE(finalized); + + const TracerSignature signature(RuntimeID::generate(), "testsvc", "test"); + auto config_manager = std::make_shared(*finalized); + + auto telemetry = std::make_shared( + finalized->report_telemetry, finalized->clock, finalized->logger, + signature, "", ""); + + const auto& agent_config = + std::get(finalized->collector); + DatadogAgent agent(agent_config, telemetry, config.logger, signature, + config_manager); + + SECTION("404 do not log an error") { + http_client->response_status = 404; + agent.get_and_apply_remote_configuration_updates(); + http_client->drain(std::chrono::steady_clock::now()); + CHECK(logger->error_count() == 0); + } + + SECTION("5xx log an error") { + http_client->response_status = 500; + agent.get_and_apply_remote_configuration_updates(); + http_client->drain(std::chrono::steady_clock::now()); + CHECK(logger->error_count() == 1); + } + + SECTION("non json input") { + http_client->response_status = 200; + http_client->response_body << "hello, mars!"; + + agent.get_and_apply_remote_configuration_updates(); + http_client->drain(std::chrono::steady_clock::now()); + CHECK(logger->error_count() == 1); + } +} diff --git a/test/test_remote_config.cpp b/test/test_remote_config.cpp index 1a8fde74..2778f4f6 100644 --- a/test/test_remote_config.cpp +++ b/test/test_remote_config.cpp @@ -151,7 +151,8 @@ REMOTE_CONFIG_TEST("response processing") { /* allow_exceptions = */ false); REQUIRE(!response_json.is_discarded()); - rc.process_response(response_json); + const auto config_updated = rc.process_response(response_json); + CHECK(config_updated.empty()); // Next payload should contains an error. const auto payload = rc.make_request_payload(); @@ -201,7 +202,8 @@ REMOTE_CONFIG_TEST("response processing") { const auto old_trace_sampler = config_manager->trace_sampler(); const auto old_span_defaults = config_manager->span_defaults(); const auto old_report_traces = config_manager->report_traces(); - rc.process_response(response_json); + const auto config_updated = rc.process_response(response_json); + REQUIRE(config_updated.size() == 3); const auto new_trace_sampler = config_manager->trace_sampler(); const auto new_span_defaults = config_manager->span_defaults(); const auto new_report_traces = config_manager->report_traces(); @@ -210,7 +212,7 @@ REMOTE_CONFIG_TEST("response processing") { CHECK(new_span_defaults != old_span_defaults); CHECK(new_report_traces != old_report_traces); - SECTION("reset confguration") { + SECTION("reset configuration") { SECTION( "missing from client_configs -> all configurations should be reset") { // clang-format off @@ -227,7 +229,9 @@ REMOTE_CONFIG_TEST("response processing") { REQUIRE(!response_json.is_discarded()); - rc.process_response(response_json); + const auto config_updated = rc.process_response(response_json); + REQUIRE(config_updated.size() == 3); + const auto current_trace_sampler = config_manager->trace_sampler(); const auto current_span_defaults = config_manager->span_defaults(); const auto current_report_traces = config_manager->report_traces(); @@ -237,7 +241,9 @@ REMOTE_CONFIG_TEST("response processing") { CHECK(old_report_traces == current_report_traces); } - SECTION("missing configuration field -> field should be reset") { + SECTION( + "missing the trace_sampling_rate field -> only this field should be " + "reset") { // clang-format off const std::string json_input = R"({ "targets": "ewogICAgInNpZ25lZCI6IHsKICAgICAgICAiY3VzdG9tIjogewogICAgICAgICAgICAiYWdlbnRfcmVmcmVzaF9pbnRlcnZhbCI6IDUsCiAgICAgICAgICAgICJvcGFxdWVfYmFja2VuZF9zdGF0ZSI6ICJleUoyWlhKemFXOXVJam95TENKemRHRjBaU0k2ZXlKbWFXeGxYMmhoYzJobGN5STZleUprWVhSaFpHOW5MekV3TURBeE1qVTROREF2UVZCTlgxUlNRVU5KVGtjdk9ESTNaV0ZqWmpoa1ltTXpZV0l4TkRNMFpETXlNV05pT0RGa1ptSm1OMkZtWlRZMU5HRTBZall4TVRGalpqRTJOakJpTnpGalkyWTRPVGM0TVRrek9DOHlPVEE0Tm1Ka1ltVTFNRFpsTmpoaU5UQm1NekExTlRneU0yRXpaR0UxWTJVd05USTRaakUyTkRCa05USmpaamc0TmpFNE1UWmhZV0U1Wm1ObFlXWTBJanBiSW05WVpESnBlVU16ZUM5b1JXc3hlWFZoWTFoR04xbHFjWEpwVGs5QldVdHVaekZ0V0UwMU5WWktUSGM5SWwxOWZYMD0iCiAgICAgICAgfSwKICAgICAgICAic3BlY192ZXJzaW9uIjogIjEuMC4wIiwKICAgICAgICAidGFyZ2V0cyI6IHsKICAgICAgICAgICAgImZvby9BUE1fVFJBQ0lORy8zMCI6IHsKICAgICAgICAgICAgICAgICJoYXNoZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgInNoYTI1NiI6ICI2OWUzNDZiNWZmY2U4NDVlMjk5ODRlNzU5YjcxZDdiMDdjNTYxOTc5ZmFlOWU4MmVlZDA4MmMwMzhkODZlNmIwIgogICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICJsZW5ndGgiOiAzNzQKICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgInZlcnNpb24iOiA2NjIwNDMyMAogICAgfQp9", @@ -245,7 +251,7 @@ REMOTE_CONFIG_TEST("response processing") { "target_files": [ { "path": "foo/APM_TRACING/30", - "raw": "eyAiaWQiOiAiODI3ZWFjZjhkYmMzYWIxNDM0ZDMyMWNiODFkZmJmN2FmZTY1NGE0YjYxMTFjZjE2NjBiNzFjY2Y4OTc4MTkzOCIsICJyZXZpc2lvbiI6IDE2OTgxNjcxMjYwNjQsICJzY2hlbWFfdmVyc2lvbiI6ICJ2MS4wLjAiLCAiYWN0aW9uIjogImVuYWJsZSIsICJsaWJfY29uZmlnIjogeyAibGlicmFyeV9sYW5ndWFnZSI6ICJhbGwiLCAibGlicmFyeV92ZXJzaW9uIjogImxhdGVzdCIsICJzZXJ2aWNlX25hbWUiOiAidGVzdHN2YyIsICJlbnYiOiAidGVzdCIsICJ0cmFjaW5nX2VuYWJsZWQiOiB0cnVlIH0sICJzZXJ2aWNlX3RhcmdldCI6IHsgInNlcnZpY2UiOiAidGVzdHN2YyIsICJlbnYiOiAidGVzdCIgfSB9" + "raw": "eyAiaWQiOiAiODI3ZWFjZjhkYmMzYWIxNDM0ZDMyMWNiODFkZmJmN2FmZTY1NGE0YjYxMTFjZjE2NjBiNzFjY2Y4OTc4MTkzOCIsICJyZXZpc2lvbiI6IDE2OTgxNjcxMjYwNjQsICJzY2hlbWFfdmVyc2lvbiI6ICJ2MS4wLjAiLCAiYWN0aW9uIjogImVuYWJsZSIsICJsaWJfY29uZmlnIjogeyAibGlicmFyeV9sYW5ndWFnZSI6ICJhbGwiLCAibGlicmFyeV92ZXJzaW9uIjogImxhdGVzdCIsICJzZXJ2aWNlX25hbWUiOiAidGVzdHN2YyIsICJlbnYiOiAidGVzdCIsICJ0cmFjaW5nX2VuYWJsZWQiOiBmYWxzZSwgInRyYWNpbmdfdGFncyI6IFsiaGVsbG86d29ybGQiLCAiZm9vOmJhciJdIH0sICJzZXJ2aWNlX3RhcmdldCI6IHsgInNlcnZpY2UiOiAidGVzdHN2YyIsICJlbnYiOiAidGVzdCIgfSB9" } ] })"; @@ -258,7 +264,8 @@ REMOTE_CONFIG_TEST("response processing") { REQUIRE(!response_json.is_discarded()); - rc.process_response(response_json); + const auto config_updated = rc.process_response(response_json); + REQUIRE(config_updated.size() == 1); const auto current_trace_sampler = config_manager->trace_sampler(); CHECK(old_trace_sampler == current_trace_sampler); } @@ -303,9 +310,10 @@ REMOTE_CONFIG_TEST("response processing") { REQUIRE(!response_json.is_discarded()); const auto old_sampling_rate = config_manager->trace_sampler(); - rc.process_response(response_json); + const auto config_updated = rc.process_response(response_json); const auto new_sampling_rate = config_manager->trace_sampler(); + CHECK(config_updated.empty()); CHECK(new_sampling_rate == old_sampling_rate); } } diff --git a/test/test_smoke.cpp b/test/test_smoke.cpp index c71fb7bf..924a9312 100644 --- a/test/test_smoke.cpp +++ b/test/test_smoke.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -12,6 +13,7 @@ TEST_CASE("smoke") { TracerConfig config; config.service = "testsvc"; config.logger = std::make_shared(); + config.collector = std::make_shared(); auto maybe_config = finalize_config(config); REQUIRE(maybe_config); diff --git a/test/test_trace_segment.cpp b/test/test_trace_segment.cpp index 50c949da..eddaffd9 100644 --- a/test/test_trace_segment.cpp +++ b/test/test_trace_segment.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -467,6 +468,7 @@ TEST_CASE("independent of Tracer") { TracerConfig config; config.service = "testsvc"; config.name = "do.thing"; + config.collector = std::make_shared(); config.logger = std::make_shared(); auto maybe_tracer = finalize_config(config); diff --git a/test/test_tracer_telemetry.cpp b/test/test_tracer_telemetry.cpp index 5fe1b144..534a17d4 100644 --- a/test/test_tracer_telemetry.cpp +++ b/test/test_tracer_telemetry.cpp @@ -35,17 +35,20 @@ TEST_CASE("Tracer telemetry", "[telemetry]") { SECTION("generates app-started message") { SECTION("Without a defined integration") { - auto app_started_message = tracer_telemetry.app_started(); + auto app_started_message = tracer_telemetry.app_started({}); auto app_started = nlohmann::json::parse(app_started_message); REQUIRE(app_started["request_type"] == "message-batch"); REQUIRE(app_started["payload"].size() == 1); - CHECK(app_started["payload"][0]["request_type"] == "app-started"); + + auto& app_started_payload = app_started["payload"][0]; + CHECK(app_started_payload["request_type"] == "app-started"); + CHECK(app_started_payload["payload"]["configuration"].empty()); } SECTION("With an integration") { TracerTelemetry tracer_telemetry{ true, clock, logger, tracer_signature, "nginx", "1.25.2"}; - auto app_started_message = tracer_telemetry.app_started(); + auto app_started_message = tracer_telemetry.app_started({}); auto app_started = nlohmann::json::parse(app_started_message); REQUIRE(app_started["request_type"] == "message-batch"); REQUIRE(app_started["payload"].size() == 2); @@ -57,6 +60,90 @@ TEST_CASE("Tracer telemetry", "[telemetry]") { CHECK(expected.find(payload["request_type"]) != expected.cend()); } } + + SECTION("With configuration") { + std::unordered_map configuration{ + {ConfigName::SERVICE_NAME, + ConfigMetadata(ConfigName::SERVICE_NAME, "foo", + ConfigMetadata::Origin::CODE)}}; + + auto app_started_message = tracer_telemetry.app_started(configuration); + + auto app_started = nlohmann::json::parse(app_started_message); + REQUIRE(app_started["request_type"] == "message-batch"); + REQUIRE(app_started["payload"].is_array()); + REQUIRE(app_started["payload"].size() == 1); + + auto& app_started_payload = app_started["payload"][0]; + CHECK(app_started_payload["request_type"] == "app-started"); + + auto cfg_payload = app_started_payload["payload"]["configuration"]; + REQUIRE(cfg_payload.is_array()); + REQUIRE(cfg_payload.size() == 1); + + // clang-format off + const auto expected_conf = nlohmann::json({ + {"name", "service"}, + {"value", "foo"}, + {"seq_id", 1}, + {"origin", "code"}, + }); + // clang-format on + + CHECK(cfg_payload[0] == expected_conf); + + SECTION("generates a configuration change event") { + SECTION("empty configuration generate a valid payload") { + auto config_change_message = nlohmann::json::parse( + tracer_telemetry.configuration_change({}), nullptr, false); + REQUIRE(config_change_message.is_discarded() == false); + + CHECK(config_change_message["request_type"] == + "app-client-configuration-change"); + CHECK(config_change_message["payload"]["configuration"].is_array()); + CHECK(config_change_message["payload"]["configuration"].empty()); + } + + SECTION("valid configurations update") { + const std::vector new_config{ + {ConfigName::SERVICE_NAME, "increase seq_id", + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE}, + {ConfigName::REPORT_TRACES, "", ConfigMetadata::Origin::DEFAULT, + Error{Error::Code::OTHER, "empty field"}}}; + + auto config_change_message = nlohmann::json::parse( + tracer_telemetry.configuration_change(new_config), nullptr, + false); + REQUIRE(config_change_message.is_discarded() == false); + + CHECK(config_change_message["request_type"] == + "app-client-configuration-change"); + CHECK(config_change_message["payload"]["configuration"].is_array()); + CHECK(config_change_message["payload"]["configuration"].size() == 2); + + const std::unordered_map expected_json{ + {"service", nlohmann::json{{"name", "service"}, + {"value", "increase seq_id"}, + {"seq_id", 2}, + {"origin", "env_var"}}}, + {"trace_enabled", + nlohmann::json{{"name", "trace_enabled"}, + {"value", ""}, + {"seq_id", 1}, + {"origin", "default"}, + {"error", + {{"code", Error::Code::OTHER}, + {"message", "empty field"}}}}}}; + + for (const auto& conf : + config_change_message["payload"]["configuration"]) { + auto expected_conf = expected_json.find(conf["name"]); + REQUIRE(expected_conf != expected_json.cend()); + CHECK(expected_conf->second == conf); + } + } + } + } } SECTION("generates a heartbeat message") {