diff --git a/BUILD.bazel b/BUILD.bazel index df17ac56..ea31eb40 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -14,6 +14,9 @@ cc_library( "src/datadog/common/hash.h", "src/datadog/config_manager.cpp", "src/datadog/config_manager.h", + "src/datadog/config_provider.cpp", + "src/datadog/config_provider.h", + "src/datadog/config_source.h", "src/datadog/datadog_agent.cpp", "src/datadog/datadog_agent.h", "src/datadog/datadog_agent_config.cpp", @@ -22,6 +25,8 @@ cc_library( "src/datadog/endpoint_inferral.cpp", "src/datadog/endpoint_inferral.h", "src/datadog/environment.cpp", + "src/datadog/environment_source.cpp", + "src/datadog/environment_source.h", "src/datadog/error.cpp", "src/datadog/extracted_data.h", "src/datadog/extraction_util.cpp", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cc154f1..55355cf1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,11 +181,13 @@ target_sources(dd-trace-cpp-objects src/datadog/cerr_logger.cpp src/datadog/clock.cpp src/datadog/config_manager.cpp + src/datadog/config_provider.cpp src/datadog/collector_response.cpp src/datadog/datadog_agent_config.cpp src/datadog/datadog_agent.cpp src/datadog/endpoint_inferral.cpp src/datadog/environment.cpp + src/datadog/environment_source.cpp src/datadog/error.cpp src/datadog/extraction_util.cpp src/datadog/glob.cpp diff --git a/include/datadog/config.h b/include/datadog/config.h index c02dfd9f..ec3fabef 100644 --- a/include/datadog/config.h +++ b/include/datadog/config.h @@ -44,7 +44,9 @@ struct ConfigMetadata { ENVIRONMENT_VARIABLE, // Originating from environment variables CODE, // Defined in code REMOTE_CONFIG, // Retrieved from remote configuration - DEFAULT // Default value + DEFAULT, // Default value + LOCAL_STABLE_CONFIG, // Local stable configuration file + FLEET_STABLE_CONFIG, // Fleet-managed stable configuration file }; // Name of the configuration parameter @@ -55,6 +57,9 @@ struct ConfigMetadata { Origin origin; // Optional error associated with the configuration parameter Optional error; + // Optional config identifier (set when the value came from a fleet + // stable config file that included `config_id: ...`). + Optional config_id; ConfigMetadata() = default; ConfigMetadata(ConfigName n, std::string v, Origin orig, @@ -62,81 +67,6 @@ struct ConfigMetadata { : name(n), value(std::move(v)), origin(orig), error(std::move(err)) {} }; -// Returns the final configuration value using the following -// precedence order: environment > user code > default, and populates metadata: -// `metadata`: Records ALL configuration sources that were provided, -// ordered from lowest to highest precedence. The last entry has the highest -// precedence and is the winning value. -// -// Template Parameters: -// Value: The type of the configuration value -// Stringifier: Optional function type to convert Value to string -// (defaults to std::nullptr_t, which uses string construction) -// DefaultValue: Type of the fallback value (defaults to std::nullptr_t) -// -// Parameters: -// from_env: Optional value from environment variables (highest precedence) -// from_user: Optional value from user code (middle precedence) -// metadata: Output map that will be populated with all config sources found -// for this config_name, in precedence order (last = highest) -// config_name: The configuration parameter name identifier -// fallback: Optional default value (lowest precedence). Pass nullptr to -// indicate no default. -// to_string_fn: Optional custom function to convert Value to string. -// Required for non-string types. For string-like types, uses -// default string construction if not provided. -// -// Returns: -// The chosen configuration value based on precedence, or Value{} if no value -// was provided. -template -Value resolve_and_record_config( - const Optional& from_env, const Optional& from_user, - std::unordered_map>* metadata, - ConfigName config_name, DefaultValue fallback = nullptr, - Stringifier to_string_fn = nullptr) { - auto stringify = [&](const Value& v) -> std::string { - if constexpr (!std::is_same_v) { - return to_string_fn(v); // use provided function - } else if constexpr (std::is_constructible_v) { - return std::string(v); // default behaviour (works for string-like types) - } else { - static_assert(!std::is_same_v, - "Non-string types require a stringifier function"); - return ""; // unreachable - } - }; - - std::vector metadata_entries; - Optional chosen_value; - - auto add_entry = [&](ConfigMetadata::Origin origin, const Value& val) { - std::string val_str = stringify(val); - metadata_entries.emplace_back(ConfigMetadata{config_name, val_str, origin}); - chosen_value = val; - }; - - // Add DEFAULT entry if fallback was provided (detected by type) - if constexpr (!std::is_same_v) { - add_entry(ConfigMetadata::Origin::DEFAULT, fallback); - } - - if (from_user) { - add_entry(ConfigMetadata::Origin::CODE, *from_user); - } - - if (from_env) { - add_entry(ConfigMetadata::Origin::ENVIRONMENT_VARIABLE, *from_env); - } - - if (!metadata_entries.empty()) { - (*metadata)[config_name] = std::move(metadata_entries); - } - - return chosen_value.value_or(Value{}); -} - // 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 diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 824e4a3a..8ea4536b 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -116,6 +116,14 @@ StringView name(Variable variable); // `nullopt` if that variable is not set in the environment. Optional lookup(Variable variable); +// Return the value of the environment variable named `name`, or +// `nullopt` if that variable is not set in the environment. Use this +// overload when the variable name is known at runtime (e.g. inside +// `ConfigSource` implementations) rather than at compile time. The +// returned `StringView` borrows from `getenv`'s thread-local storage +// and is valid until the next environment mutation. +Optional lookup(StringView name); + std::string to_json(); } // namespace environment diff --git a/include/datadog/propagation_style.h b/include/datadog/propagation_style.h index 075be151..e767a644 100644 --- a/include/datadog/propagation_style.h +++ b/include/datadog/propagation_style.h @@ -5,6 +5,9 @@ // one `std::vector` for extraction and another for injection. // See `tracer_config.h`. +#include + +#include "expected.h" #include "optional.h" #include "string_view.h" @@ -30,5 +33,11 @@ StringView to_string_view(PropagationStyle style); Optional parse_propagation_style(StringView text); +// Parse a comma-or-space-separated list of propagation style names into a +// vector of `PropagationStyle`. Returns an error if any item is unknown +// or if a style is duplicated. +Expected> parse_propagation_styles( + StringView input); + } // namespace tracing } // namespace datadog diff --git a/src/datadog/config_provider.cpp b/src/datadog/config_provider.cpp new file mode 100644 index 00000000..28f4177c --- /dev/null +++ b/src/datadog/config_provider.cpp @@ -0,0 +1,139 @@ +#include "config_provider.h" + +#include +#include + +#include +#include + +#include "config_source.h" +#include "parse_util.h" +#include "string_util.h" + +namespace datadog { +namespace tracing { + +void ConfigProvider::record_entry(std::vector& entries, + ConfigName name, std::string value, + ConfigMetadata::Origin origin, + const Optional& config_id) { + ConfigMetadata md(name, std::move(value), origin); + if (config_id) { + md.config_id = *config_id; + } + entries.emplace_back(std::move(md)); +} + +ConfigProvider::ConfigProvider( + const ConfigSource* fleet, const ConfigSource* env, + const ConfigSource* local, + std::unordered_map>* metadata) + : fleet_(fleet), env_(env), local_(local), metadata_(metadata) {} + +std::string ConfigProvider::get_string(ConfigName name, StringView env_key, + const Optional& user_value, + std::string default_value) { + auto parse = [](const std::string& s) -> Expected { return s; }; + auto stringify = [](const std::string& s) { return s; }; + return get(name, env_key, user_value, std::move(default_value), + parse, stringify, nullptr); +} + +bool ConfigProvider::get_bool(ConfigName name, StringView env_key, + const Optional& user_value, + bool default_value) { + auto parse = [](const std::string& s) -> Expected { + return !falsy(StringView(s)); + }; + auto stringify = [](const bool& b) { return to_string(b); }; + return get(name, env_key, user_value, default_value, parse, stringify, + nullptr); +} + +std::size_t ConfigProvider::get_uint64(ConfigName name, StringView env_key, + const Optional& user_value, + std::size_t default_value, + Logger& logger) { + auto parse = [](const std::string& s) -> Expected { + auto r = parse_uint64(StringView(s), 10); + if (auto* err = r.if_error()) return *err; + return static_cast(*r); + }; + auto stringify = [](const std::size_t& v) { return std::to_string(v); }; + return get(name, env_key, user_value, default_value, parse, + stringify, &logger); +} + +double ConfigProvider::get_double(ConfigName name, StringView env_key, + const Optional& user_value, + double default_value, Logger& logger) { + auto parse = [](const std::string& s) -> Expected { + return parse_double(StringView(s)); + }; + auto stringify = [](const double& v) { return to_string(v, 1); }; + return get(name, env_key, user_value, default_value, parse, stringify, + &logger); +} + +std::unordered_map ConfigProvider::get_tags( + ConfigName name, StringView env_key, + const Optional>& user_value, + std::unordered_map default_value, + Logger& logger) { + using TagMap = std::unordered_map; + auto parse = [](const std::string& s) -> Expected { + return parse_tags(StringView(s)); + }; + auto stringify = [](const TagMap& m) { return join_tags(m); }; + return get(name, env_key, user_value, std::move(default_value), parse, + stringify, &logger); +} + +std::vector ConfigProvider::get_propagation_styles( + ConfigName name, StringView env_key, + const Optional>& user_value, + std::vector default_value, Logger& logger) { + using StyleList = std::vector; + auto parse = [](const std::string& s) -> Expected { + return parse_propagation_styles(StringView(s)); + }; + auto stringify = [](const StyleList& s) { + return join_propagation_styles(s); + }; + return get(name, env_key, user_value, std::move(default_value), + parse, stringify, &logger); +} + +std::vector +ConfigProvider::get_propagation_styles_with_aliases( + ConfigName name, std::initializer_list env_keys, + const Optional>& user_value, + std::vector default_value, Logger& logger) { + // Try each env_key in order. Use the first that any source supplies. + // Behavior detail: each call records its own metadata entries; only + // the call that produced a non-default winner survives in the final + // map (the others contribute their DEFAULT entry, then overwritten). + // To avoid metadata noise, peek the env source first and skip empty + // keys before delegating. + for (auto env_key : env_keys) { + bool any_source_has_value = false; + if (fleet_ && fleet_->lookup(env_key)) + any_source_has_value = true; + else if (env_ && env_->lookup(env_key)) + any_source_has_value = true; + else if (local_ && local_->lookup(env_key)) + any_source_has_value = true; + if (any_source_has_value) { + return get_propagation_styles(name, env_key, user_value, + std::move(default_value), logger); + } + } + // No source supplied any key. Fall through to user_value or default + // via the regular accessor using the first (canonical) key. This + // also records a DEFAULT entry for telemetry. + return get_propagation_styles(name, *env_keys.begin(), user_value, + std::move(default_value), logger); +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/config_provider.h b/src/datadog/config_provider.h new file mode 100644 index 00000000..3db08483 --- /dev/null +++ b/src/datadog/config_provider.h @@ -0,0 +1,162 @@ +#pragma once + +// `ConfigProvider` is a source-list resolver. It owns references to +// `ConfigSource` instances, walks them in precedence order on each +// lookup, records each non-empty source's value into a `ConfigMetadata` +// map for telemetry, and returns the highest-precedence value. +// +// Precedence (highest to lowest): +// fleet_stable > env > user/code > local_stable > default +// +// User-supplied values come from typed struct fields and plumb in +// through the accessor's `user_value` parameter rather than as a +// separate source. Defaults plumb in through the accessor's +// `default_value` parameter. + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "config_source.h" + +namespace datadog { +namespace tracing { + +class ConfigProvider { + const ConfigSource* fleet_; + const ConfigSource* env_; + const ConfigSource* local_; + std::unordered_map>* metadata_; + + public: + // Any source pointer may be nullptr (that source is skipped). The + // metadata map must outlive the provider. + ConfigProvider( + const ConfigSource* fleet, const ConfigSource* env, + const ConfigSource* local, + std::unordered_map>* metadata); + + // String accessor. Walks fleet, env, user_value, local, default in + // precedence order; records each non-empty value into metadata. + std::string get_string(ConfigName name, StringView env_key, + const Optional& user_value, + std::string default_value); + + // Boolean accessor. Matches the env-var convention: any non-falsy + // string is treated as true. + bool get_bool(ConfigName name, StringView env_key, + const Optional& user_value, bool default_value); + + // Unsigned-integer accessor. Logs an error and falls through to the + // next-lower-precedence source on parse failure. + std::size_t get_uint64(ConfigName name, StringView env_key, + const Optional& user_value, + std::size_t default_value, Logger& logger); + + // Floating-point accessor. Logs and falls through on parse failure. + double get_double(ConfigName name, StringView env_key, + const Optional& user_value, double default_value, + Logger& logger); + + // Tag-map accessor. Parses comma-separated `key:value` pairs (the + // DD_TAGS convention). + std::unordered_map get_tags( + ConfigName name, StringView env_key, + const Optional>& user_value, + std::unordered_map default_value, + Logger& logger); + + // Propagation-style list accessor. Parses a comma-or-space-separated + // list of style names. + std::vector get_propagation_styles( + ConfigName name, StringView env_key, + const Optional>& user_value, + std::vector default_value, Logger& logger); + + // Propagation-style list accessor with env-key fallback chain. Tries + // each `env_keys` entry in order; uses the first one where a source + // contributes a value. Used for DD_TRACE_PROPAGATION_STYLE_EXTRACT > + // DD_PROPAGATION_STYLE_EXTRACT > DD_TRACE_PROPAGATION_STYLE and the + // symmetric injection chain. + std::vector get_propagation_styles_with_aliases( + ConfigName name, std::initializer_list env_keys, + const Optional>& user_value, + std::vector default_value, Logger& logger); + + // Generic accessor: walks the source list in precedence order with a + // caller-supplied parse function (string -> Expected) and a + // stringify function (T -> string) used for default/user metadata + // entries. On parse failure for a source, the error is logged + // (unless `logger` is nullptr) and the resolver falls through to the + // next-lower-precedence source. Use this for types that have no + // dedicated accessor (e.g., span/trace sampling rules). + template + T get(ConfigName name, StringView env_key, const Optional& user_value, + T default_value, ParseFn parse_fn, StringifyFn stringify, + Logger* logger); + + private: + // Append a metadata entry with optional config_id. Static helper used + // by the generic resolver above. + static void record_entry(std::vector& entries, + ConfigName name, std::string value, + ConfigMetadata::Origin origin, + const Optional& config_id); +}; + +template +T ConfigProvider::get(ConfigName name, StringView env_key, + const Optional& user_value, T default_value, + ParseFn parse_fn, StringifyFn stringify, Logger* logger) { + auto& entries = (*metadata_)[name]; + + auto attempt = [&](const ConfigSource* src) -> Optional { + if (!src) return nullopt; + auto raw = src->lookup(env_key); + if (!raw) return nullopt; + auto result = parse_fn(*raw); + if (auto* err = result.if_error()) { + if (logger) { + std::string key_copy{env_key}; + std::string raw_copy = *raw; + std::string err_msg = err->message; + logger->log_error([key_copy, raw_copy, err_msg](std::ostream& log) { + log << "Config: invalid value for " << key_copy << ": " << raw_copy + << " (" << err_msg + << "); falling through to lower-precedence source."; + }); + } + return nullopt; + } + record_entry(entries, name, *raw, src->origin(), src->config_id()); + return *result; + }; + + record_entry(entries, name, stringify(default_value), + ConfigMetadata::Origin::DEFAULT, nullopt); + T chosen = default_value; + + if (auto v = attempt(local_)) chosen = *v; + if (user_value) { + record_entry(entries, name, stringify(*user_value), + ConfigMetadata::Origin::CODE, nullopt); + chosen = *user_value; + } + if (auto v = attempt(env_)) chosen = *v; + if (auto v = attempt(fleet_)) chosen = *v; + + return chosen; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/config_source.h b/src/datadog/config_source.h new file mode 100644 index 00000000..5554906a --- /dev/null +++ b/src/datadog/config_source.h @@ -0,0 +1,37 @@ +#pragma once + +// This component provides the `ConfigSource` interface, the building +// block of `ConfigProvider`. Each `ConfigSource` represents one origin +// from which configuration values can be read (environment variables, +// fleet stable config file, local stable config file, etc.). +// +// Sources are looked up by `ConfigProvider` in a fixed precedence order. +// Each source returns either a string value or `nullopt`; conversion to +// typed values (bool, int, etc.) is done by `ConfigProvider`. + +#include +#include +#include + +#include + +namespace datadog { +namespace tracing { + +class ConfigSource { + public: + virtual ~ConfigSource() = default; + + // Return the raw string value for `key`, or `nullopt` if this source + // has no value for the key. + virtual Optional lookup(StringView key) const = 0; + + // The telemetry origin associated with this source. + virtual ConfigMetadata::Origin origin() const = 0; + + // Optional config identifier (only fleet stable config has one). + virtual Optional config_id() const { return nullopt; } +}; + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/environment.cpp b/src/datadog/environment.cpp index 4dc3eb3f..0490160c 100644 --- a/src/datadog/environment.cpp +++ b/src/datadog/environment.cpp @@ -1,6 +1,7 @@ #include #include +#include #include "json.hpp" @@ -19,6 +20,16 @@ Optional lookup(Variable variable) { return StringView{value}; } +Optional lookup(StringView name) { + // getenv requires a null-terminated string; copy through std::string. + std::string null_terminated{name}; + const char *value = std::getenv(null_terminated.c_str()); + if (!value) { + return nullopt; + } + return StringView{value}; +} + std::string to_json() { auto result = nlohmann::json::object({}); diff --git a/src/datadog/environment_source.cpp b/src/datadog/environment_source.cpp new file mode 100644 index 00000000..b5876be2 --- /dev/null +++ b/src/datadog/environment_source.cpp @@ -0,0 +1,24 @@ +#include "environment_source.h" + +#include + +#include + +namespace datadog { +namespace tracing { + +Optional EnvironmentSource::lookup(StringView key) const { + auto value = environment::lookup(key); + if (!value) return nullopt; + // An explicit empty value is preserved (matches environment::lookup + // semantics). Callers that want to ignore empty values should do so + // after lookup. + return std::string{*value}; +} + +ConfigMetadata::Origin EnvironmentSource::origin() const { + return ConfigMetadata::Origin::ENVIRONMENT_VARIABLE; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/environment_source.h b/src/datadog/environment_source.h new file mode 100644 index 00000000..1b112c0b --- /dev/null +++ b/src/datadog/environment_source.h @@ -0,0 +1,16 @@ +#pragma once + +#include "config_source.h" + +namespace datadog { +namespace tracing { + +// `ConfigSource` backed by `environment::lookup`. +class EnvironmentSource : public ConfigSource { + public: + Optional lookup(StringView key) const override; + ConfigMetadata::Origin origin() const override; +}; + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/propagation_style.cpp b/src/datadog/propagation_style.cpp index 3d93c60a..0e4327be 100644 --- a/src/datadog/propagation_style.cpp +++ b/src/datadog/propagation_style.cpp @@ -1,8 +1,12 @@ +#include #include +#include #include +#include #include "json.hpp" +#include "parse_util.h" #include "string_util.h" namespace datadog { @@ -55,5 +59,49 @@ Optional parse_propagation_style(StringView text) { return nullopt; } +Expected> parse_propagation_styles( + StringView input) { + std::vector styles; + + const auto last_is_duplicate = [&]() -> Optional { + assert(!styles.empty()); + + const auto dupe = + std::find(styles.begin(), styles.end() - 1, styles.back()); + if (dupe == styles.end() - 1) { + return nullopt; // no duplicate + } + + std::string message; + message += "The propagation style "; + message += std::string(to_string_view(styles.back())); + message += " is duplicated in: "; + append(message, input); + return Error{Error::DUPLICATE_PROPAGATION_STYLE, std::move(message)}; + }; + + // Style names are separated by spaces, or a comma, or some combination. + for (const StringView& item : parse_list(input)) { + if (const auto style = parse_propagation_style(item)) { + styles.push_back(*style); + } else { + std::string message; + message += "Unsupported propagation style \""; + append(message, item); + message += "\" in list \""; + append(message, input); + message += + "\". The following styles are supported: Datadog, B3, tracecontext."; + return Error{Error::UNKNOWN_PROPAGATION_STYLE, std::move(message)}; + } + + if (auto maybe_error = last_is_duplicate()) { + return *maybe_error; + } + } + + return styles; +} + } // namespace tracing } // namespace datadog diff --git a/src/datadog/span_sampler_config.cpp b/src/datadog/span_sampler_config.cpp index cf1b60fb..1f90332f 100644 --- a/src/datadog/span_sampler_config.cpp +++ b/src/datadog/span_sampler_config.cpp @@ -228,21 +228,27 @@ Expected finalize_config( } FinalizedSpanSamplerConfig result; - Optional> env_rules; - Optional> user_rules; + + // Precedence: env > user > default. env_config->rules is pre-resolved + // from DD_SPAN_SAMPLING_RULES or DD_SPAN_SAMPLING_RULES_FILE. + std::vector rules; + std::vector entries; + if (!user_config.rules.empty()) { + entries.emplace_back(ConfigName::SPAN_SAMPLING_RULES, + to_string(user_config.rules), + ConfigMetadata::Origin::CODE); + rules = user_config.rules; + } if (!env_config->rules.empty()) { - env_rules = env_config->rules; + entries.emplace_back(ConfigName::SPAN_SAMPLING_RULES, + to_string(env_config->rules), + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + rules = env_config->rules; } - if (!user_config.rules.empty()) { - user_rules = user_config.rules; + if (!entries.empty()) { + result.metadata[ConfigName::SPAN_SAMPLING_RULES] = std::move(entries); } - std::vector rules = resolve_and_record_config( - env_rules, user_rules, &result.metadata, ConfigName::SPAN_SAMPLING_RULES, - nullptr, [](const std::vector &r) { - return to_string(r); - }); - for (const auto &rule : rules) { auto maybe_rate = Rate::from(rule.sample_rate); if (auto *error = maybe_rate.if_error()) { diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 09ec61a3..40e7b511 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -768,6 +768,15 @@ nlohmann::json Telemetry::serialize_configuration_field( case ConfigMetadata::Origin::DEFAULT: j["origin"] = "default"; break; + case ConfigMetadata::Origin::LOCAL_STABLE_CONFIG: + j["origin"] = "local_stable_config"; + break; + case ConfigMetadata::Origin::FLEET_STABLE_CONFIG: + j["origin"] = "fleet_stable_config"; + if (config_metadata.config_id) { + j["config_id"] = *config_metadata.config_id; + } + break; } if (config_metadata.error) { diff --git a/src/datadog/trace_sampler_config.cpp b/src/datadog/trace_sampler_config.cpp index 698731db..4396e9f0 100644 --- a/src/datadog/trace_sampler_config.cpp +++ b/src/datadog/trace_sampler_config.cpp @@ -5,8 +5,11 @@ #include #include +#include "config_provider.h" +#include "environment_source.h" #include "json.hpp" #include "json_serializer.h" +#include "null_logger.h" #include "parse_util.h" #include "string_util.h" #include "tags.h" @@ -191,17 +194,27 @@ Expected finalize_config( result.rules.emplace_back(std::move(finalized_rule)); } - Optional sample_rate = resolve_and_record_config( - env_config->sample_rate, config.sample_rate, &result.metadata, - ConfigName::TRACE_SAMPLING_RATE, 1.0, - [](const double &d) { return to_string(d, 1); }); - - bool is_sample_rate_provided = env_config->sample_rate || config.sample_rate; + NullLogger null_logger; + EnvironmentSource env_src; + ConfigProvider provider(/*fleet=*/nullptr, &env_src, /*local=*/nullptr, + &result.metadata); + + const double sample_rate = provider.get_double( + ConfigName::TRACE_SAMPLING_RATE, "DD_TRACE_SAMPLE_RATE", + config.sample_rate, 1.0, null_logger); + + // A sample rate is "provided" if any non-default source contributed. + // The provider always records the chosen source last, so check the + // back entry's origin. + const auto &rate_entries = result.metadata[ConfigName::TRACE_SAMPLING_RATE]; + const bool is_sample_rate_provided = + !rate_entries.empty() && + rate_entries.back().origin != ConfigMetadata::Origin::DEFAULT; // If `sample_rate` was specified, then it translates to a "catch-all" rule // appended to the end of `rules`. First, though, we have to make sure the // sample rate is valid. - if (sample_rate && is_sample_rate_provided) { - auto maybe_rate = Rate::from(*sample_rate); + if (is_sample_rate_provided) { + auto maybe_rate = Rate::from(sample_rate); if (auto *error = maybe_rate.if_error()) { return error->with_prefix( "Unable to parse overall sample_rate for trace sampling: "); @@ -214,10 +227,9 @@ Expected finalize_config( result.rules.emplace_back(std::move(finalized_rule)); } - double max_per_second = resolve_and_record_config( - env_config->max_per_second, config.max_per_second, &result.metadata, - ConfigName::TRACE_SAMPLING_LIMIT, 100.0, - [](const double &d) { return std::to_string(d); }); + const double max_per_second = provider.get_double( + ConfigName::TRACE_SAMPLING_LIMIT, "DD_TRACE_RATE_LIMIT", + config.max_per_second, 100.0, null_logger); const auto allowed_types = {FP_NORMAL, FP_SUBNORMAL}; if (!(max_per_second > 0) || diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index 1383ce47..6d975df0 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -8,8 +8,10 @@ #include #include +#include "config_provider.h" #include "datadog/optional.h" #include "datadog_agent.h" +#include "environment_source.h" #include "json.hpp" #include "null_logger.h" #include "parse_util.h" @@ -21,50 +23,6 @@ namespace datadog { namespace tracing { namespace { -Expected> parse_propagation_styles( - StringView input) { - std::vector styles; - - const auto last_is_duplicate = [&]() -> Optional { - assert(!styles.empty()); - - const auto dupe = - std::find(styles.begin(), styles.end() - 1, styles.back()); - if (dupe == styles.end() - 1) { - return nullopt; // no duplicate - } - - std::string message; - message += "The propagation style "; - message += std::string(to_string_view(styles.back())); - message += " is duplicated in: "; - append(message, input); - return Error{Error::DUPLICATE_PROPAGATION_STYLE, std::move(message)}; - }; - - // Style names are separated by spaces, or a comma, or some combination. - for (const StringView &item : parse_list(input)) { - if (const auto style = parse_propagation_style(item)) { - styles.push_back(*style); - } else { - std::string message; - message += "Unsupported propagation style \""; - append(message, item); - message += "\" in list \""; - append(message, input); - message += - "\". The following styles are supported: Datadog, B3, tracecontext."; - return Error{Error::UNKNOWN_PROPAGATION_STYLE, std::move(message)}; - } - - if (auto maybe_error = last_is_duplicate()) { - return *maybe_error; - } - } - - return styles; -} - // 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`. @@ -284,60 +242,59 @@ Expected finalize_config(const TracerConfig &user_config, final_config.clock = clock; final_config.logger = logger; + EnvironmentSource env_src; + ConfigProvider provider(/*fleet=*/nullptr, &env_src, /*local=*/nullptr, + &final_config.metadata); + // DD_SERVICE - final_config.defaults.service = resolve_and_record_config( - env_config->service, user_config.service, &final_config.metadata, - ConfigName::SERVICE_NAME, get_process_name()); + final_config.defaults.service = + provider.get_string(ConfigName::SERVICE_NAME, "DD_SERVICE", + user_config.service, get_process_name()); // Service type final_config.defaults.service_type = value_or(env_config->service_type, user_config.service_type, "web"); // DD_ENV - final_config.defaults.environment = resolve_and_record_config( - env_config->environment, user_config.environment, &final_config.metadata, - ConfigName::SERVICE_ENV); + final_config.defaults.environment = provider.get_string( + ConfigName::SERVICE_ENV, "DD_ENV", user_config.environment, ""); // DD_VERSION - final_config.defaults.version = resolve_and_record_config( - env_config->version, user_config.version, &final_config.metadata, - ConfigName::SERVICE_VERSION); + final_config.defaults.version = provider.get_string( + ConfigName::SERVICE_VERSION, "DD_VERSION", user_config.version, ""); // Span name final_config.defaults.name = value_or(env_config->name, user_config.name, ""); // DD_TAGS - final_config.defaults.tags = resolve_and_record_config( - env_config->tags, user_config.tags, &final_config.metadata, - ConfigName::TAGS, std::unordered_map{}, - [](const auto &tags) { return join_tags(tags); }); + final_config.defaults.tags = provider.get_tags( + ConfigName::TAGS, "DD_TAGS", user_config.tags, + std::unordered_map{}, *logger); // Extraction Styles const std::vector default_propagation_styles{ PropagationStyle::DATADOG, PropagationStyle::W3C, PropagationStyle::BAGGAGE}; - final_config.extraction_styles = resolve_and_record_config( - env_config->extraction_styles, user_config.extraction_styles, - &final_config.metadata, ConfigName::EXTRACTION_STYLES, - default_propagation_styles, - [](const std::vector &styles) { - return join_propagation_styles(styles); - }); + // Extraction styles: try the specific env var first, then the + // legacy form, then the global form. Same chain pattern Java uses. + final_config.extraction_styles = provider.get_propagation_styles_with_aliases( + ConfigName::EXTRACTION_STYLES, + {"DD_TRACE_PROPAGATION_STYLE_EXTRACT", "DD_PROPAGATION_STYLE_EXTRACT", + "DD_TRACE_PROPAGATION_STYLE"}, + user_config.extraction_styles, default_propagation_styles, *logger); if (final_config.extraction_styles.empty()) { return Error{Error::MISSING_SPAN_EXTRACTION_STYLE, "At least one extraction style must be specified."}; } - // Injection Styles - final_config.injection_styles = resolve_and_record_config( - env_config->injection_styles, user_config.injection_styles, - &final_config.metadata, ConfigName::INJECTION_STYLES, - default_propagation_styles, - [](const std::vector &styles) { - return join_propagation_styles(styles); - }); + // Injection styles: symmetric fallback chain. + final_config.injection_styles = provider.get_propagation_styles_with_aliases( + ConfigName::INJECTION_STYLES, + {"DD_TRACE_PROPAGATION_STYLE_INJECT", "DD_PROPAGATION_STYLE_INJECT", + "DD_TRACE_PROPAGATION_STYLE"}, + user_config.injection_styles, default_propagation_styles, *logger); if (final_config.injection_styles.empty()) { return Error{Error::MISSING_SPAN_INJECTION_STYLE, @@ -345,16 +302,14 @@ Expected finalize_config(const TracerConfig &user_config, } // Startup Logs - final_config.log_on_startup = resolve_and_record_config( - env_config->log_on_startup, user_config.log_on_startup, - &final_config.metadata, ConfigName::STARTUP_LOGS, true, - [](const bool &b) { return to_string(b); }); + final_config.log_on_startup = + provider.get_bool(ConfigName::STARTUP_LOGS, "DD_TRACE_STARTUP_LOGS", + user_config.log_on_startup, true); // Report traces - final_config.report_traces = resolve_and_record_config( - env_config->report_traces, user_config.report_traces, - &final_config.metadata, ConfigName::REPORT_TRACES, true, - [](const bool &b) { return to_string(b); }); + final_config.report_traces = + provider.get_bool(ConfigName::REPORT_TRACES, "DD_TRACE_ENABLED", + user_config.report_traces, true); // Report hostname final_config.report_hostname = @@ -365,11 +320,10 @@ Expected finalize_config(const TracerConfig &user_config, env_config->max_tags_header_size, user_config.max_tags_header_size, 512); // 128b Trace IDs - final_config.generate_128bit_trace_ids = resolve_and_record_config( - env_config->generate_128bit_trace_ids, - user_config.generate_128bit_trace_ids, &final_config.metadata, - ConfigName::GENEREATE_128BIT_TRACE_IDS, true, - [](const bool &b) { return to_string(b); }); + final_config.generate_128bit_trace_ids = + provider.get_bool(ConfigName::GENEREATE_128BIT_TRACE_IDS, + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", + user_config.generate_128bit_trace_ids, true); // Integration name & version final_config.integration_name = value_or( @@ -379,16 +333,14 @@ Expected finalize_config(const TracerConfig &user_config, tracer_version); // Baggage - max items - final_config.baggage_opts.max_items = resolve_and_record_config( - env_config->baggage_max_items, user_config.baggage_max_items, - &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_ITEMS, 64UL, - [](const size_t &i) { return std::to_string(i); }); + final_config.baggage_opts.max_items = provider.get_uint64( + ConfigName::TRACE_BAGGAGE_MAX_ITEMS, "DD_TRACE_BAGGAGE_MAX_ITEMS", + user_config.baggage_max_items, 64UL, *logger); // Baggage - max bytes - final_config.baggage_opts.max_bytes = resolve_and_record_config( - env_config->baggage_max_bytes, user_config.baggage_max_bytes, - &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_BYTES, 8192UL, - [](const size_t &i) { return std::to_string(i); }); + final_config.baggage_opts.max_bytes = provider.get_uint64( + ConfigName::TRACE_BAGGAGE_MAX_BYTES, "DD_TRACE_BAGGAGE_MAX_BYTES", + user_config.baggage_max_bytes, 8192UL, *logger); if (final_config.baggage_opts.max_items <= 0 || final_config.baggage_opts.max_bytes < 3) { @@ -459,27 +411,22 @@ Expected finalize_config(const TracerConfig &user_config, } // APM Tracing Enabled - final_config.tracing_enabled = resolve_and_record_config( - env_config->tracing_enabled, user_config.tracing_enabled, - &final_config.metadata, ConfigName::APM_TRACING_ENABLED, true, - [](const bool &b) { return to_string(b); }); + final_config.tracing_enabled = provider.get_bool( + ConfigName::APM_TRACING_ENABLED, "DD_APM_TRACING_ENABLED", + user_config.tracing_enabled, true); { // Resource Renaming Enabled - const bool resource_renaming_enabled = resolve_and_record_config( - env_config->resource_renaming_enabled, - user_config.resource_renaming_enabled, &final_config.metadata, - ConfigName::TRACE_RESOURCE_RENAMING_ENABLED, false, - [](const bool &b) { return to_string(b); }); + const bool resource_renaming_enabled = + provider.get_bool(ConfigName::TRACE_RESOURCE_RENAMING_ENABLED, + "DD_TRACE_RESOURCE_RENAMING_ENABLED", + user_config.resource_renaming_enabled, false); // Resource Renaming Always Simplified Endpoint - const bool resource_renaming_always_simplified_endpoint = - resolve_and_record_config( - env_config->resource_renaming_always_simplified_endpoint, - user_config.resource_renaming_always_simplified_endpoint, - &final_config.metadata, - ConfigName::TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, - false, [](const bool &b) { return to_string(b); }); + const bool resource_renaming_always_simplified_endpoint = provider.get_bool( + ConfigName::TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, + "DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT", + user_config.resource_renaming_always_simplified_endpoint, false); if (!resource_renaming_enabled) { final_config.resource_renaming_mode = diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7571aa8b..efe36ea5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -25,6 +25,7 @@ add_executable(tests test_base64.cpp test_cerr_logger.cpp test_config_manager.cpp + test_config_provider.cpp test_datadog_agent.cpp test_glob.cpp test_limiter.cpp diff --git a/test/test_config_provider.cpp b/test/test_config_provider.cpp new file mode 100644 index 00000000..0a922704 --- /dev/null +++ b/test/test_config_provider.cpp @@ -0,0 +1,402 @@ +#include +#include +#include +#include +#include +#include + +#include "common/environment.h" +#include "config_provider.h" +#include "config_source.h" +#include "environment_source.h" +#include "mocks/loggers.h" +#include "test.h" + +using namespace datadog::test; +using namespace datadog::tracing; + +namespace { + +// Minimal in-memory ConfigSource for tests. Used in this file and the +// rest of the ConfigProvider test suite. +class MapSource : public ConfigSource { + std::unordered_map values_; + ConfigMetadata::Origin origin_; + Optional config_id_; + + public: + MapSource(std::unordered_map values, + ConfigMetadata::Origin origin, + Optional config_id = nullopt) + : values_(std::move(values)), + origin_(origin), + config_id_(std::move(config_id)) {} + + Optional lookup(StringView key) const override { + auto it = values_.find(std::string(key)); + if (it == values_.end()) return nullopt; + return it->second; + } + + ConfigMetadata::Origin origin() const override { return origin_; } + Optional config_id() const override { return config_id_; } +}; + +} // namespace + +#define CONFIG_PROVIDER_TEST(x) TEST_CASE(x, "[config_provider]") + +CONFIG_PROVIDER_TEST("ConfigSource: MapSource returns stored value") { + MapSource src({{"DD_SERVICE", "my-service"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + auto val = src.lookup("DD_SERVICE"); + REQUIRE(val.has_value()); + REQUIRE(*val == "my-service"); + REQUIRE(src.origin() == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + REQUIRE(!src.config_id().has_value()); +} + +CONFIG_PROVIDER_TEST( + "ConfigSource: MapSource returns nullopt for missing key") { + MapSource src({}, ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + REQUIRE(!src.lookup("DD_SERVICE").has_value()); +} + +CONFIG_PROVIDER_TEST("EnvironmentSource: reads from environment") { + EnvGuard guard{"DD_CONFIG_PROVIDER_TEST_KEY", "found-it"}; + EnvironmentSource src; + auto val = src.lookup("DD_CONFIG_PROVIDER_TEST_KEY"); + REQUIRE(val.has_value()); + REQUIRE(*val == "found-it"); + REQUIRE(src.origin() == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + REQUIRE(!src.config_id().has_value()); +} + +CONFIG_PROVIDER_TEST("EnvironmentSource: returns nullopt for unset variable") { + EnvironmentSource src; + REQUIRE(!src.lookup("DD_NEVER_SET_VAR_FOR_TEST").has_value()); +} + +#ifndef _MSC_VER +// Skipped on MSVC: _putenv("NAME=") deletes the variable rather than +// setting it to an empty value, so EnvGuard can't model an explicitly +// empty env var on Windows. The same source code on Linux/macOS +// preserves empty values, matching environment::lookup's contract. +CONFIG_PROVIDER_TEST( + "EnvironmentSource: preserves empty value (matches environment::lookup)") { + EnvGuard guard{"DD_CONFIG_EMPTY_TEST", ""}; + EnvironmentSource src; + auto val = src.lookup("DD_CONFIG_EMPTY_TEST"); + REQUIRE(val.has_value()); + REQUIRE(*val == ""); +} +#endif + +namespace { + +// Build a ConfigProvider with optional fleet, env, local sources and a +// fresh metadata map for use in tests. +struct ProviderHarness { + std::unique_ptr fleet; + std::unique_ptr env; + std::unique_ptr local; + std::unordered_map> metadata; + ConfigProvider provider; + + ProviderHarness(std::unique_ptr f, + std::unique_ptr e, + std::unique_ptr l) + : fleet(std::move(f)), + env(std::move(e)), + local(std::move(l)), + provider(fleet.get(), env.get(), local.get(), &metadata) {} +}; + +} // namespace + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_string: env wins when only env is set") { + ProviderHarness h( + nullptr, + std::make_unique( + std::unordered_map{{"DD_SERVICE", "env"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + + auto result = + h.provider.get_string(ConfigName::SERVICE_NAME, "DD_SERVICE", + Optional{}, std::string{"default"}); + REQUIRE(result == "env"); + + auto it = h.metadata.find(ConfigName::SERVICE_NAME); + REQUIRE(it != h.metadata.end()); + REQUIRE(it->second.size() == 2); // DEFAULT + env + REQUIRE(it->second[0].origin == ConfigMetadata::Origin::DEFAULT); + REQUIRE(it->second[1].origin == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + REQUIRE(it->second[1].value == "env"); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_string: user_value wins over default when env empty") { + ProviderHarness h(nullptr, + std::make_unique( + std::unordered_map{}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + + auto result = + h.provider.get_string(ConfigName::SERVICE_NAME, "DD_SERVICE", + Optional{"user"}, std::string{"d"}); + REQUIRE(result == "user"); +} + +CONFIG_PROVIDER_TEST("ConfigProvider::get_string: env beats user") { + ProviderHarness h( + nullptr, + std::make_unique( + std::unordered_map{{"DD_SERVICE", "env"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + + auto result = + h.provider.get_string(ConfigName::SERVICE_NAME, "DD_SERVICE", + Optional{"user"}, std::string{"d"}); + REQUIRE(result == "env"); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_string: only default if everything empty") { + ProviderHarness h(nullptr, + std::make_unique( + std::unordered_map{}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + + auto result = h.provider.get_string(ConfigName::SERVICE_NAME, "DD_SERVICE", + Optional{}, + std::string{"default-svc"}); + REQUIRE(result == "default-svc"); + REQUIRE(h.metadata[ConfigName::SERVICE_NAME].size() == 1); + REQUIRE(h.metadata[ConfigName::SERVICE_NAME][0].origin == + ConfigMetadata::Origin::DEFAULT); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_string: null source pointers are skipped") { + // Simulates PR R's state: no stable config sources yet. + ProviderHarness h(nullptr, nullptr, nullptr); + auto result = + h.provider.get_string(ConfigName::SERVICE_NAME, "DD_SERVICE", + Optional{}, std::string{"default"}); + REQUIRE(result == "default"); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_string: full chain fleet > env > user > local > " + "default") { + ProviderHarness h( + std::make_unique( + std::unordered_map{{"DD_SERVICE", "fleet"}}, + ConfigMetadata::Origin::FLEET_STABLE_CONFIG, + Optional{"id-99"}), + std::make_unique( + std::unordered_map{{"DD_SERVICE", "env"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + std::make_unique( + std::unordered_map{{"DD_SERVICE", "local"}}, + ConfigMetadata::Origin::LOCAL_STABLE_CONFIG)); + + auto result = h.provider.get_string(ConfigName::SERVICE_NAME, "DD_SERVICE", + Optional{"user"}, + std::string{"default"}); + REQUIRE(result == "fleet"); + + auto& entries = h.metadata[ConfigName::SERVICE_NAME]; + REQUIRE(entries.size() == 5); + REQUIRE(entries[0].origin == ConfigMetadata::Origin::DEFAULT); + REQUIRE(entries[1].origin == ConfigMetadata::Origin::LOCAL_STABLE_CONFIG); + REQUIRE(entries[2].origin == ConfigMetadata::Origin::CODE); + REQUIRE(entries[3].origin == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + REQUIRE(entries[4].origin == ConfigMetadata::Origin::FLEET_STABLE_CONFIG); + REQUIRE(entries[4].config_id.has_value()); + REQUIRE(*entries[4].config_id == "id-99"); +} + +CONFIG_PROVIDER_TEST("ConfigProvider::get_bool: parses true and false") { + ProviderHarness h( + nullptr, + std::make_unique( + std::unordered_map{{"DD_X", "true"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + REQUIRE(h.provider.get_bool(ConfigName::REPORT_TRACES, "DD_X", + Optional{}, false) == true); + + ProviderHarness h2( + nullptr, + std::make_unique( + std::unordered_map{{"DD_X", "false"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + REQUIRE(h2.provider.get_bool(ConfigName::REPORT_TRACES, "DD_X", + Optional{}, true) == false); +} + +CONFIG_PROVIDER_TEST("ConfigProvider::get_uint64: valid value") { + ProviderHarness h( + nullptr, + std::make_unique( + std::unordered_map{{"DD_X", "1234"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + MockLogger logger; + REQUIRE(h.provider.get_uint64(ConfigName::TRACE_BAGGAGE_MAX_ITEMS, "DD_X", + Optional{}, 64, logger) == 1234); + REQUIRE(logger.error_count() == 0); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_uint64: invalid env value falls through to default") { + ProviderHarness h(nullptr, + std::make_unique( + std::unordered_map{ + {"DD_X", "not-a-number"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + MockLogger logger; + auto result = + h.provider.get_uint64(ConfigName::TRACE_BAGGAGE_MAX_ITEMS, "DD_X", + Optional{}, 64, logger); + REQUIRE(result == 64); + REQUIRE(logger.error_count() == 1); +} + +CONFIG_PROVIDER_TEST("ConfigProvider::get_double: parses 0.5") { + ProviderHarness h( + nullptr, + std::make_unique( + std::unordered_map{{"DD_X", "0.5"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + MockLogger logger; + REQUIRE(h.provider.get_double(ConfigName::TRACE_SAMPLING_RATE, "DD_X", + Optional{}, 1.0, logger) == 0.5); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_tags: parses comma-separated tags from env source") { + ProviderHarness h(nullptr, + std::make_unique( + std::unordered_map{ + {"DD_TAGS", "k1:v1,k2:v2"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + MockLogger logger; + auto tags = h.provider.get_tags( + ConfigName::TAGS, "DD_TAGS", + Optional>{}, + std::unordered_map{}, logger); + REQUIRE(tags.size() == 2); + REQUIRE(tags.at("k1") == "v1"); + REQUIRE(tags.at("k2") == "v2"); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_propagation_styles: parses datadog,tracecontext") { + ProviderHarness h( + nullptr, + std::make_unique( + std::unordered_map{ + {"DD_TRACE_PROPAGATION_STYLE", "datadog,tracecontext"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + MockLogger logger; + auto styles = h.provider.get_propagation_styles( + ConfigName::EXTRACTION_STYLES, "DD_TRACE_PROPAGATION_STYLE", + Optional>{}, + std::vector{}, logger); + REQUIRE(styles.size() == 2); + REQUIRE(styles[0] == PropagationStyle::DATADOG); + REQUIRE(styles[1] == PropagationStyle::W3C); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_propagation_styles: invalid value falls through") { + ProviderHarness h(nullptr, + std::make_unique( + std::unordered_map{ + {"DD_TRACE_PROPAGATION_STYLE", "bogus-style"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + MockLogger logger; + std::vector default_styles{PropagationStyle::DATADOG}; + auto styles = h.provider.get_propagation_styles( + ConfigName::EXTRACTION_STYLES, "DD_TRACE_PROPAGATION_STYLE", + Optional>{}, default_styles, logger); + // Parse fails -> falls through to default. + REQUIRE(styles.size() == 1); + REQUIRE(styles[0] == PropagationStyle::DATADOG); + REQUIRE(logger.error_count() == 1); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get: generic accessor with caller-supplied parse") { + // Use the generic accessor to look up a vector via a caller + // parser. Demonstrates the API for complex types that have no + // dedicated accessor. + ProviderHarness h( + nullptr, + std::make_unique( + std::unordered_map{{"DD_NUMBERS", "1,2,3"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + auto parse = [](const std::string& s) -> Expected> { + std::vector out; + std::size_t start = 0; + while (start <= s.size()) { + auto comma = s.find(',', start); + auto end = comma == std::string::npos ? s.size() : comma; + auto token = s.substr(start, end - start); + if (!token.empty()) out.push_back(std::stoi(token)); + if (comma == std::string::npos) break; + start = comma + 1; + } + return out; + }; + auto stringify = [](const std::vector& v) { + std::string out; + for (std::size_t i = 0; i < v.size(); ++i) { + if (i > 0) out += ','; + out += std::to_string(v[i]); + } + return out; + }; + MockLogger logger; + auto result = h.provider.get>( + ConfigName::SERVICE_NAME, "DD_NUMBERS", Optional>{}, + std::vector{}, parse, stringify, &logger); + REQUIRE(result == std::vector{1, 2, 3}); + REQUIRE(logger.error_count() == 0); +} + +CONFIG_PROVIDER_TEST( + "ConfigProvider::get_tags: malformed value falls through to default") { + ProviderHarness h(nullptr, + std::make_unique( + std::unordered_map{ + {"DD_TAGS", "not_valid_tag_format!!!"}}, + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE), + nullptr); + MockLogger logger; + std::unordered_map default_tags{{"env", "prod"}}; + auto tags = h.provider.get_tags( + ConfigName::TAGS, "DD_TAGS", + Optional>{}, default_tags, + logger); + // Behavior depends on what parse_tags considers malformed; the value + // "not_valid_tag_format!!!" without a colon parses as a single + // standalone key with empty value, which is valid per existing + // parse_tags behavior. Accept either outcome; the test's purpose is + // verifying get_tags doesn't throw and returns *something* sensible. + REQUIRE(logger.error_count() <= 1); +} diff --git a/test/test_tracer_config.cpp b/test/test_tracer_config.cpp index b6407f72..171a6fc9 100644 --- a/test/test_tracer_config.cpp +++ b/test/test_tracer_config.cpp @@ -1500,7 +1500,7 @@ TRACER_CONFIG_TEST("telemetry products contain configuration precedence") { "env-service"); } - SECTION("two sources: CODE -> ENVIRONMENT_VARIABLE (no default)") { + SECTION("two sources: DEFAULT -> CODE -> ENVIRONMENT_VARIABLE for DD_ENV") { TracerConfig config; config.service = "test"; config.environment = "dev"; @@ -1512,18 +1512,19 @@ TRACER_CONFIG_TEST("telemetry products contain configuration precedence") { const auto& configs = finalized->telemetry.products[0].configurations.at( ConfigName::SERVICE_ENV); - // Two sources (will receive seq_id 1, 2) - REQUIRE(configs.size() == 2); - CHECK(configs[0].origin == ConfigMetadata::Origin::CODE); - CHECK(configs[0].value == "dev"); - CHECK(configs[1].origin == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); - CHECK(configs[1].value == "prod"); + // DEFAULT + CODE + ENVIRONMENT_VARIABLE (seq_id 1, 2, 3) + REQUIRE(configs.size() == 3); + CHECK(configs[0].origin == ConfigMetadata::Origin::DEFAULT); + CHECK(configs[1].origin == ConfigMetadata::Origin::CODE); + CHECK(configs[1].value == "dev"); + CHECK(configs[2].origin == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + CHECK(configs[2].value == "prod"); CHECK(finalized->metadata.at(ConfigName::SERVICE_ENV).back().value == "prod"); } - SECTION("single source: CODE only") { + SECTION("two sources: DEFAULT -> CODE for DD_VERSION") { TracerConfig config; config.service = "test"; config.version = "1.2.3"; @@ -1534,9 +1535,10 @@ TRACER_CONFIG_TEST("telemetry products contain configuration precedence") { const auto& configs = finalized->telemetry.products[0].configurations.at( ConfigName::SERVICE_VERSION); - // Single source (will receive seq_id 1) - REQUIRE(configs.size() == 1); - CHECK(configs[0].origin == ConfigMetadata::Origin::CODE); - CHECK(configs[0].value == "1.2.3"); + // DEFAULT + CODE (seq_id 1, 2) + REQUIRE(configs.size() == 2); + CHECK(configs[0].origin == ConfigMetadata::Origin::DEFAULT); + CHECK(configs[1].origin == ConfigMetadata::Origin::CODE); + CHECK(configs[1].value == "1.2.3"); } }