diff --git a/BUILD.bazel b/BUILD.bazel index 5c5b72a5..659d2f06 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -81,6 +81,10 @@ cc_library( "src/datadog/trace_segment.cpp", "src/datadog/trace_source.cpp", "src/datadog/tracer.cpp", + "src/datadog/stable_config.cpp", + "src/datadog/stable_config.h", + "src/datadog/yaml_parser.cpp", + "src/datadog/yaml_parser.h", "src/datadog/tracer_config.cpp", "src/datadog/version.cpp", "src/datadog/w3c_propagation.cpp", @@ -153,5 +157,6 @@ cc_library( deps = [ "@com_google_absl//absl/strings", "@com_google_absl//absl/types:optional", + "@yaml_cpp//:yaml-cpp", ], ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cc154f1..f1d869ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,10 @@ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") include(cmake/compiler/gcc.cmake) endif () +# yaml-cpp must be included AFTER the compiler setup above, because +# clang.cmake sets -stdlib=libc++ which yaml-cpp needs to inherit. +include(cmake/deps/yaml.cmake) + if (DD_TRACE_BUILD_FUZZERS) add_subdirectory(fuzz) endif () @@ -210,6 +214,8 @@ target_sources(dd-trace-cpp-objects src/datadog/tags.cpp src/datadog/tag_propagation.cpp src/datadog/threaded_event_scheduler.cpp + src/datadog/stable_config.cpp + src/datadog/yaml_parser.cpp src/datadog/tracer_config.cpp src/datadog/tracer.cpp src/datadog/trace_id.cpp @@ -242,6 +248,7 @@ target_link_libraries(dd-trace-cpp-objects Threads::Threads PRIVATE dd-trace-cpp::specs + $ ) set_target_properties(dd-trace-cpp-objects diff --git a/MODULE.bazel b/MODULE.bazel index c8bb29bb..1104dd90 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -7,3 +7,4 @@ bazel_dep(name = "abseil-cpp", version = "20260107.1", repo_name = "com_google_a bazel_dep(name = "bazel_skylib", version = "1.9.0") bazel_dep(name = "platforms", version = "1.0.0") bazel_dep(name = "rules_cc", version = "0.2.17") +bazel_dep(name = "yaml-cpp", version = "0.8.0.bcr.1", repo_name = "yaml_cpp") diff --git a/WORKSPACE b/WORKSPACE index 39f0fb10..ae7fdd41 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -40,6 +40,16 @@ http_archive( urls = ["https://github.com/bazelbuild/rules_cc/releases/download/0.2.14/rules_cc-0.2.14.tar.gz"], ) +# This pulls the upstream yaml-cpp 0.8.0 tarball directly. +# MODULE.bazel uses "0.8.0.bcr.1" because that is the BCR (Bazel Central +# Registry) patched release; the underlying library version is the same 0.8.0. +http_archive( + name = "yaml_cpp", + sha256 = "fbe74bbdcee21d656715688706da3c8becfd946d92cd44705cc6098bb23b3a16", + strip_prefix = "yaml-cpp-0.8.0", + urls = ["https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz"], +) + load("@rules_cc//cc:extensions.bzl", "compatibility_proxy_repo") compatibility_proxy_repo() diff --git a/cmake/deps/yaml.cmake b/cmake/deps/yaml.cmake new file mode 100644 index 00000000..25dd0068 --- /dev/null +++ b/cmake/deps/yaml.cmake @@ -0,0 +1,36 @@ +include(FetchContent) + +set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE) +set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE) +set(YAML_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + +FetchContent_Declare(yaml-cpp + URL https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz + URL_HASH SHA256=fbe74bbdcee21d656715688706da3c8becfd946d92cd44705cc6098bb23b3a16 + EXCLUDE_FROM_ALL + SYSTEM +) + +# yaml-cpp 0.8.0 uses cmake_minimum_required(VERSION 3.4) which is rejected +# by CMake >= 4.0. Allow it via CMAKE_POLICY_VERSION_MINIMUM. +set(_yaml_saved_policy_min "${CMAKE_POLICY_VERSION_MINIMUM}") +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) +FetchContent_MakeAvailable(yaml-cpp) +set(CMAKE_POLICY_VERSION_MINIMUM "${_yaml_saved_policy_min}") + +# Ensure yaml-cpp is compiled with the same sanitizer flags as the main +# project. Without this, MSVC ASAN annotation mismatches cause linker +# errors (LNK2038). We add only the sanitizer flags — not the full set +# of compile options from dd-trace-cpp-specs (which includes -WX and +# warning levels that would break yaml-cpp's own code). +if (DD_TRACE_ENABLE_SANITIZE AND TARGET yaml-cpp) + if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "MSVC")) + target_compile_options(yaml-cpp PRIVATE /fsanitize=address) + target_link_options(yaml-cpp PRIVATE /fsanitize=address) + else() + target_compile_options(yaml-cpp PRIVATE -fsanitize=address,undefined) + target_link_options(yaml-cpp PRIVATE -fsanitize=address,undefined) + endif() +endif() diff --git a/include/datadog/config.h b/include/datadog/config.h index c02dfd9f..f584a363 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, // From local stable config file + FLEET_STABLE_CONFIG // From fleet stable config file }; // Name of the configuration parameter @@ -62,6 +64,11 @@ struct ConfigMetadata { : name(n), value(std::move(v)), origin(orig), error(std::move(err)) {} }; +// 3-parameter overload (env, user, default) kept for backward compatibility +// with external projects (e.g., nginx-datadog, httpd-datadog) that include +// this public header. New internal code should prefer the 5-parameter +// overload that also accepts fleet and local stable config sources. +// // 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, @@ -96,15 +103,33 @@ Value resolve_and_record_config( std::unordered_map>* metadata, ConfigName config_name, DefaultValue fallback = nullptr, Stringifier to_string_fn = nullptr) { + // Delegate to the 5-parameter overload with nullopt for fleet and local + // stable config sources. + return resolve_and_record_config(Optional{}, from_env, from_user, + Optional{}, metadata, config_name, + fallback, to_string_fn); +} + +// Extended version of resolve_and_record_config that includes stable +// configuration sources. Precedence order (highest to lowest): +// fleet_stable > env > user/code > local_stable > default +template +Value resolve_and_record_config( + const Optional& from_fleet_stable, const Optional& from_env, + const Optional& from_user, const Optional& from_local_stable, + 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 + return to_string_fn(v); } else if constexpr (std::is_constructible_v) { - return std::string(v); // default behaviour (works for string-like types) + return std::string(v); } else { static_assert(!std::is_same_v, "Non-string types require a stringifier function"); - return ""; // unreachable + return ""; } }; @@ -117,11 +142,15 @@ Value resolve_and_record_config( chosen_value = val; }; - // Add DEFAULT entry if fallback was provided (detected by type) + // Precedence: default < local_stable < user/code < env < fleet_stable if constexpr (!std::is_same_v) { add_entry(ConfigMetadata::Origin::DEFAULT, fallback); } + if (from_local_stable) { + add_entry(ConfigMetadata::Origin::LOCAL_STABLE_CONFIG, *from_local_stable); + } + if (from_user) { add_entry(ConfigMetadata::Origin::CODE, *from_user); } @@ -130,6 +159,10 @@ Value resolve_and_record_config( add_entry(ConfigMetadata::Origin::ENVIRONMENT_VARIABLE, *from_env); } + if (from_fleet_stable) { + add_entry(ConfigMetadata::Origin::FLEET_STABLE_CONFIG, *from_fleet_stable); + } + if (!metadata_entries.empty()) { (*metadata)[config_name] = std::move(metadata_entries); } diff --git a/include/datadog/span_sampler_config.h b/include/datadog/span_sampler_config.h index 7faaa8eb..a0c8f186 100644 --- a/include/datadog/span_sampler_config.h +++ b/include/datadog/span_sampler_config.h @@ -37,7 +37,7 @@ struct SpanSamplerConfig { class FinalizedSpanSamplerConfig { friend Expected finalize_config( - const SpanSamplerConfig&, Logger&); + const SpanSamplerConfig&, Logger&, const struct StableConfigs*); friend class FinalizedTracerConfig; FinalizedSpanSamplerConfig() = default; @@ -52,8 +52,9 @@ class FinalizedSpanSamplerConfig { std::unordered_map> metadata; }; -Expected finalize_config(const SpanSamplerConfig&, - Logger&); +Expected finalize_config( + const SpanSamplerConfig&, Logger&, + const struct StableConfigs* stable_configs = nullptr); std::string to_string(const FinalizedSpanSamplerConfig::Rule&); diff --git a/include/datadog/telemetry/configuration.h b/include/datadog/telemetry/configuration.h index 51693f26..0277dc60 100644 --- a/include/datadog/telemetry/configuration.h +++ b/include/datadog/telemetry/configuration.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -56,6 +55,10 @@ struct FinalizedConfiguration { std::string integration_version; std::vector products; + // Fleet stable config ID, used to attach config_id to telemetry entries + // with FLEET_STABLE_CONFIG origin at serialization time. + tracing::Optional fleet_stable_config_id; + // Onboarding metadata coming from `DD_INSTRUMENTATION_INSTALL_*` environment // variables. tracing::Optional install_id; diff --git a/include/datadog/trace_sampler_config.h b/include/datadog/trace_sampler_config.h index 685eeb82..dd9b2e37 100644 --- a/include/datadog/trace_sampler_config.h +++ b/include/datadog/trace_sampler_config.h @@ -20,6 +20,9 @@ namespace datadog { namespace tracing { +class Logger; +struct StableConfigs; + struct TraceSamplerRule final { Rate rate; SpanMatcher matcher; @@ -42,7 +45,8 @@ struct TraceSamplerConfig { class FinalizedTraceSamplerConfig { friend Expected finalize_config( - const TraceSamplerConfig& config); + const TraceSamplerConfig& config, const StableConfigs* stable_configs, + Logger* logger); friend class FinalizedTracerConfig; FinalizedTraceSamplerConfig() = default; @@ -58,7 +62,8 @@ class FinalizedTraceSamplerConfig { }; Expected finalize_config( - const TraceSamplerConfig& config); + const TraceSamplerConfig& config, + const StableConfigs* stable_configs = nullptr, Logger* logger = nullptr); } // namespace tracing } // namespace datadog diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index 2ee4cd02..e741d764 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -8,6 +8,7 @@ #include #include +#include #include #include diff --git a/src/datadog/span_sampler_config.cpp b/src/datadog/span_sampler_config.cpp index cf1b60fb..0870c973 100644 --- a/src/datadog/span_sampler_config.cpp +++ b/src/datadog/span_sampler_config.cpp @@ -9,11 +9,61 @@ #include "json.hpp" #include "json_serializer.h" +#include "stable_config.h" namespace datadog { namespace tracing { namespace { +// Parse a stable config JSON string as an array of sampling rules. +// `customize_rule` is a callable that receives (Rule&, const json_rule&) to set +// rule-specific fields beyond the base matcher and sample_rate. +// Returns nullopt on any parse error (stable config errors are non-fatal). +template +Optional> parse_stable_config_rules( + const StableConfig &cfg, const std::string &key, Logger &logger, + Customize customize_rule) { + auto val = cfg.lookup(key); + if (!val || val->empty()) return nullopt; + + try { + auto json_rules = Json::parse(*val); + if (!json_rules.is_array()) { + logger.log_error([&key](std::ostream &log) { + log << "Unable to parse JSON sampling rules from " << key + << ": expected a JSON array"; + }); + return nullopt; + } + + std::vector rules; + for (const auto &json_rule : json_rules) { + auto matcher = from_json(json_rule); + if (matcher.if_error()) { + logger.log_error([&key](std::ostream &log) { + log << "Unable to parse JSON sampling rules from " << key + << ": invalid rule matcher"; + }); + return nullopt; + } + + Rule rule{*matcher}; + if (auto sr = json_rule.find("sample_rate"); + sr != json_rule.end() && sr->is_number()) { + rule.sample_rate = *sr; + } + customize_rule(rule, json_rule); + rules.emplace_back(std::move(rule)); + } + return rules; + } catch (...) { + logger.log_error([&key](std::ostream &log) { + log << "Unable to parse JSON sampling rules from " << key; + }); + return nullopt; + } +} + std::string to_string(const std::vector &rules) { nlohmann::json res; for (const auto &r : rules) { @@ -221,7 +271,8 @@ Expected load_span_sampler_env_config(Logger &logger) { SpanSamplerConfig::Rule::Rule(const SpanMatcher &base) : SpanMatcher(base) {} Expected finalize_config( - const SpanSamplerConfig &user_config, Logger &logger) { + const SpanSamplerConfig &user_config, Logger &logger, + const StableConfigs *stable_configs) { Expected env_config = load_span_sampler_env_config(logger); if (auto error = env_config.if_error()) { return *error; @@ -237,9 +288,30 @@ Expected finalize_config( user_rules = user_config.rules; } + Optional> fleet_rules; + Optional> local_rules; + if (stable_configs) { + auto parse_span_rules = [&logger](const StableConfig &cfg, + const std::string &key) { + return parse_stable_config_rules( + cfg, key, logger, + [](SpanSamplerConfig::Rule &rule, const nlohmann::json &json_rule) { + if (auto mps = json_rule.find("max_per_second"); + mps != json_rule.end() && mps->is_number()) { + rule.max_per_second = *mps; + } + }); + }; + fleet_rules = + parse_span_rules(stable_configs->fleet, "DD_SPAN_SAMPLING_RULES"); + local_rules = + parse_span_rules(stable_configs->local, "DD_SPAN_SAMPLING_RULES"); + } + std::vector rules = resolve_and_record_config( - env_rules, user_rules, &result.metadata, ConfigName::SPAN_SAMPLING_RULES, - nullptr, [](const std::vector &r) { + fleet_rules, env_rules, user_rules, local_rules, &result.metadata, + ConfigName::SPAN_SAMPLING_RULES, nullptr, + [](const std::vector &r) { return to_string(r); }); diff --git a/src/datadog/stable_config.cpp b/src/datadog/stable_config.cpp new file mode 100644 index 00000000..d2d9de69 --- /dev/null +++ b/src/datadog/stable_config.cpp @@ -0,0 +1,159 @@ +#include "stable_config.h" + +#include +#include +#include + +#include "yaml_parser.h" + +#ifdef _WIN32 +#include +// windows.h defines ERROR as a macro which conflicts with our enum. +#undef ERROR +#endif + +namespace datadog { +namespace tracing { +namespace { + +// Maximum file size accepted for stable configuration files: 256KB. +// This is a file I/O concern, not a parser concern, so it lives here rather +// than in yaml_parser.h. +constexpr std::size_t kMaxYamlFileSize = 256 * 1024; + +#ifdef _WIN32 + +std::string get_windows_agent_dir() { + // Try to read the agent directory from the Windows registry. + // Keys: HKLM\SOFTWARE\Datadog\Datadog Agent -> ConfigRoot or InstallPath + HKEY key; + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Datadog\\Datadog Agent", 0, + KEY_READ, &key) == ERROR_SUCCESS) { + char buffer[MAX_PATH]; + DWORD size = sizeof(buffer); + DWORD type = 0; + + // Try ConfigRoot first. + if (RegQueryValueExA(key, "ConfigRoot", nullptr, &type, + reinterpret_cast(buffer), + &size) == ERROR_SUCCESS && + type == REG_SZ && size > 0) { + RegCloseKey(key); + std::string result(buffer, size - 1); // exclude null terminator + // Ensure trailing backslash. + if (!result.empty() && result.back() != '\\') { + result += '\\'; + } + return result; + } + + // Try InstallPath. + size = sizeof(buffer); + if (RegQueryValueExA(key, "InstallPath", nullptr, &type, + reinterpret_cast(buffer), + &size) == ERROR_SUCCESS && + type == REG_SZ && size > 0) { + RegCloseKey(key); + std::string result(buffer, size - 1); + if (!result.empty() && result.back() != '\\') { + result += '\\'; + } + return result; + } + + RegCloseKey(key); + } + + // Default path. + return "C:\\ProgramData\\Datadog\\"; +} + +#endif // _WIN32 + +// Read a file and parse it into a StableConfig. Logs warnings on errors. +// Returns an empty StableConfig if the file doesn't exist or can't be read. +StableConfig load_one(const std::string& path, Logger& logger) { + StableConfig result; + + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + // File not found or unreadable — silently skip. + return result; + } + + // Check file size. + const auto size = file.tellg(); + if (size < 0) { + logger.log_error([&path](std::ostream& log) { + log << "Stable config: unable to determine size of " << path + << "; skipping."; + }); + return result; + } + + if (static_cast(size) > kMaxYamlFileSize) { + logger.log_error([&path](std::ostream& log) { + log << "Stable config: file " << path + << " exceeds 256KB size limit; skipping."; + }); + return result; + } + + file.seekg(0); + std::string content(static_cast(size), '\0'); + if (!file.read(content.data(), size)) { + logger.log_error([&path](std::ostream& log) { + log << "Stable config: unable to read " << path << "; skipping."; + }); + return result; + } + + YamlParseResult parsed; + if (parse_yaml(content, parsed) != YamlParseStatus::OK) { + logger.log_error([&path](std::ostream& log) { + log << "Stable config: malformed YAML in " << path << "; skipping."; + }); + return {}; // Return empty config on parse error. + } + + result.config_id = std::move(parsed.config_id); + result.values = std::move(parsed.values); + return result; +} + +} // namespace + +StableConfigPaths get_stable_config_paths() { +#ifdef _WIN32 + const auto agent_dir = get_windows_agent_dir(); + return { + agent_dir + "application_monitoring.yaml", + agent_dir + "managed\\datadog-agent\\stable\\application_monitoring.yaml", + }; +#else + return { + "/etc/datadog-agent/application_monitoring.yaml", + "/etc/datadog-agent/managed/datadog-agent/stable/" + "application_monitoring.yaml", + }; +#endif +} + +Optional StableConfig::lookup(const std::string& key) const { + auto it = values.find(key); + if (it != values.end()) { + return it->second; + } + return nullopt; +} + +StableConfigs load_stable_configs(Logger& logger) { + const auto paths = get_stable_config_paths(); + StableConfigs configs; + configs.local = load_one(paths.local_path, logger); + configs.fleet = load_one(paths.fleet_path, logger); + return configs; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/stable_config.h b/src/datadog/stable_config.h new file mode 100644 index 00000000..e3a32f20 --- /dev/null +++ b/src/datadog/stable_config.h @@ -0,0 +1,56 @@ +#pragma once + +// This component provides support for reading "stable configuration" from +// YAML files on disk. Two files are read at tracer initialization: +// +// - A "local" (user-managed) file +// - A "fleet" (fleet-managed) file +// +// Each file may contain a flat map of DD_* environment variable names to +// scalar values under the `apm_configuration_default` key. These values +// participate in configuration precedence: +// +// fleet_stable > env > user/code > local_stable > default + +#include +#include + +#include +#include + +namespace datadog { +namespace tracing { + +// Paths to the two stable configuration files. +struct StableConfigPaths { + std::string local_path; + std::string fleet_path; +}; + +// Return the platform-specific paths for stable configuration files. +StableConfigPaths get_stable_config_paths(); + +// Parsed contents of one stable configuration file. +struct StableConfig { + // Config ID from the file (optional, for telemetry). + Optional config_id; + + // Map of environment variable names (e.g. "DD_SERVICE") to string values. + std::unordered_map values; + + // Look up a config key, returning nullopt if not present. + Optional lookup(const std::string& key) const; +}; + +// Holds both the local and fleet stable configs. +struct StableConfigs { + StableConfig local; + StableConfig fleet; +}; + +// Load and parse both stable configuration files. +// Returns empty configs (no error) if files don't exist. +StableConfigs load_stable_configs(Logger& logger); + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index d9464dd7..17c27036 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -735,6 +735,18 @@ nlohmann::json Telemetry::generate_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"; + break; + } + + if (config_metadata.origin == + tracing::ConfigMetadata::Origin::FLEET_STABLE_CONFIG && + config_.fleet_stable_config_id) { + j["config_id"] = *config_.fleet_stable_config_id; } if (config_metadata.error) { diff --git a/src/datadog/trace_sampler_config.cpp b/src/datadog/trace_sampler_config.cpp index 698731db..ca486d0e 100644 --- a/src/datadog/trace_sampler_config.cpp +++ b/src/datadog/trace_sampler_config.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -7,7 +8,9 @@ #include "json.hpp" #include "json_serializer.h" +#include "null_logger.h" #include "parse_util.h" +#include "stable_config.h" #include "string_util.h" #include "tags.h" @@ -145,12 +148,84 @@ std::string to_string(const std::vector &rules) { return res.dump(); } +// Convert a stable config string value to Optional. +Optional stable_config_double(const StableConfig &cfg, + const std::string &key) { + auto val = cfg.lookup(key); + if (!val || val->empty()) return nullopt; + auto result = parse_double(StringView(*val)); + if (result.if_error()) return nullopt; + return *result; +} + +// Parse a stable config JSON string as an array of sampling rules. +// `customize_rule` is a callable that receives (Rule&, const json_rule&) to set +// rule-specific fields beyond the base matcher and sample_rate. +// Returns nullopt on any parse error (stable config errors are non-fatal). +template +Optional> parse_stable_config_rules( + const StableConfig &cfg, const std::string &key, Logger &logger, + Customize customize_rule) { + auto val = cfg.lookup(key); + if (!val || val->empty()) return nullopt; + + try { + auto json_rules = Json::parse(*val); + if (!json_rules.is_array()) { + logger.log_error([&key](std::ostream &log) { + log << "Unable to parse JSON sampling rules from " << key + << ": expected a JSON array"; + }); + return nullopt; + } + + std::vector rules; + for (const auto &json_rule : json_rules) { + auto matcher = from_json(json_rule); + if (matcher.if_error()) { + logger.log_error([&key](std::ostream &log) { + log << "Unable to parse JSON sampling rules from " << key + << ": invalid rule matcher"; + }); + return nullopt; + } + + Rule rule{*matcher}; + if (auto sr = json_rule.find("sample_rate"); + sr != json_rule.end() && sr->is_number()) { + rule.sample_rate = *sr; + } + customize_rule(rule, json_rule); + rules.emplace_back(std::move(rule)); + } + return rules; + } catch (...) { + logger.log_error([&key](std::ostream &log) { + log << "Unable to parse JSON sampling rules from " << key; + }); + return nullopt; + } +} + +// Try to parse a stable config string value as trace sampling rules JSON. +// Returns nullopt on any parse error (stable config errors are non-fatal). +Optional> stable_config_sampling_rules( + const StableConfig &cfg, const std::string &key, Logger &logger) { + return parse_stable_config_rules( + cfg, key, logger, + [](TraceSamplerConfig::Rule &, const nlohmann::json &) {}); +} + } // namespace TraceSamplerConfig::Rule::Rule(const SpanMatcher &base) : SpanMatcher(base) {} Expected finalize_config( - const TraceSamplerConfig &config) { + const TraceSamplerConfig &config, const StableConfigs *stable_configs, + Logger *logger) { + NullLogger null_logger; + Logger &log = logger ? *logger : static_cast(null_logger); + Expected env_config = load_trace_sampler_env_config(); if (auto error = env_config.if_error()) { return *error; @@ -160,7 +235,22 @@ Expected finalize_config( std::vector rules; - if (!env_config->rules.empty()) { + // Precedence: fleet_stable > env > user/code > local_stable + Optional> fleet_rules; + Optional> local_rules; + if (stable_configs) { + fleet_rules = stable_config_sampling_rules(stable_configs->fleet, + "DD_TRACE_SAMPLING_RULES", log); + local_rules = stable_config_sampling_rules(stable_configs->local, + "DD_TRACE_SAMPLING_RULES", log); + } + + if (fleet_rules) { + rules = std::move(*fleet_rules); + result.metadata[ConfigName::TRACE_SAMPLING_RULES] = { + ConfigMetadata(ConfigName::TRACE_SAMPLING_RULES, to_string(rules), + ConfigMetadata::Origin::FLEET_STABLE_CONFIG)}; + } else 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), @@ -170,6 +260,11 @@ Expected finalize_config( result.metadata[ConfigName::TRACE_SAMPLING_RULES] = { ConfigMetadata(ConfigName::TRACE_SAMPLING_RULES, to_string(rules), ConfigMetadata::Origin::CODE)}; + } else if (local_rules) { + rules = std::move(*local_rules); + result.metadata[ConfigName::TRACE_SAMPLING_RULES] = { + ConfigMetadata(ConfigName::TRACE_SAMPLING_RULES, to_string(rules), + ConfigMetadata::Origin::LOCAL_STABLE_CONFIG)}; } for (const auto &rule : rules) { @@ -191,12 +286,22 @@ Expected finalize_config( result.rules.emplace_back(std::move(finalized_rule)); } + Optional fleet_sample_rate; + Optional local_sample_rate; + if (stable_configs) { + fleet_sample_rate = + stable_config_double(stable_configs->fleet, "DD_TRACE_SAMPLE_RATE"); + local_sample_rate = + stable_config_double(stable_configs->local, "DD_TRACE_SAMPLE_RATE"); + } + Optional sample_rate = resolve_and_record_config( - env_config->sample_rate, config.sample_rate, &result.metadata, - ConfigName::TRACE_SAMPLING_RATE, 1.0, + fleet_sample_rate, env_config->sample_rate, config.sample_rate, + local_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; + bool is_sample_rate_provided = fleet_sample_rate || env_config->sample_rate || + config.sample_rate || local_sample_rate; // 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. @@ -214,10 +319,19 @@ Expected finalize_config( result.rules.emplace_back(std::move(finalized_rule)); } + Optional fleet_rate_limit; + Optional local_rate_limit; + if (stable_configs) { + fleet_rate_limit = + stable_config_double(stable_configs->fleet, "DD_TRACE_RATE_LIMIT"); + local_rate_limit = + stable_config_double(stable_configs->local, "DD_TRACE_RATE_LIMIT"); + } + 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); }); + fleet_rate_limit, env_config->max_per_second, config.max_per_second, + local_rate_limit, &result.metadata, ConfigName::TRACE_SAMPLING_LIMIT, + 100.0, [](const double &d) { return std::to_string(d); }); 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 a10b17a8..9c5100a7 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -14,6 +14,7 @@ #include "null_logger.h" #include "parse_util.h" #include "platform_util.h" +#include "stable_config.h" #include "string_util.h" #include "threaded_event_scheduler.h" @@ -92,6 +93,46 @@ std::string json_quoted(StringView text) { return nlohmann::json(std::move(unquoted)).dump(); } +// Convert a stable config string value to Optional. +Optional stable_config_bool(const StableConfig &cfg, + const std::string &key) { + auto val = cfg.lookup(key); + if (!val || val->empty()) return nullopt; + return !falsy(StringView(*val)); +} + +// Convert a stable config string value to Optional. +Optional stable_config_uint64(const StableConfig &cfg, + const std::string &key) { + auto val = cfg.lookup(key); + if (!val || val->empty()) return nullopt; + auto result = parse_uint64(StringView(*val), 10); + if (result.if_error()) return nullopt; + return static_cast(*result); +} + +// Convert a stable config string value to +// Optional> (tags). +Optional> stable_config_tags( + const StableConfig &cfg, const std::string &key) { + auto val = cfg.lookup(key); + if (!val || val->empty()) return nullopt; + auto tags = parse_tags(StringView(*val)); + if (tags.if_error()) return nullopt; + return std::move(*tags); +} + +// Convert a stable config string value to +// Optional>. +Optional> stable_config_propagation_styles( + const StableConfig &cfg, const std::string &key) { + auto val = cfg.lookup(key); + if (!val || val->empty()) return nullopt; + auto styles = parse_propagation_styles(StringView(*val)); + if (styles.if_error()) return nullopt; + return std::move(*styles); +} + Expected load_tracer_env_config(Logger &logger) { TracerConfig env_cfg; @@ -275,6 +316,9 @@ Expected finalize_config(const TracerConfig &user_config, auto logger = user_config.logger ? user_config.logger : std::make_shared(); + // Load stable configs from YAML files. + auto stable_configs = load_stable_configs(*logger); + Expected env_config = load_tracer_env_config(*logger); if (auto error = env_config.if_error()) { return *error; @@ -286,8 +330,9 @@ Expected finalize_config(const TracerConfig &user_config, // 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()); + stable_configs.fleet.lookup("DD_SERVICE"), env_config->service, + user_config.service, stable_configs.local.lookup("DD_SERVICE"), + &final_config.metadata, ConfigName::SERVICE_NAME, get_process_name()); // Service type final_config.defaults.service_type = @@ -295,30 +340,54 @@ Expected finalize_config(const TracerConfig &user_config, // DD_ENV final_config.defaults.environment = resolve_and_record_config( - env_config->environment, user_config.environment, &final_config.metadata, - ConfigName::SERVICE_ENV); + stable_configs.fleet.lookup("DD_ENV"), env_config->environment, + user_config.environment, stable_configs.local.lookup("DD_ENV"), + &final_config.metadata, ConfigName::SERVICE_ENV); // DD_VERSION final_config.defaults.version = resolve_and_record_config( - env_config->version, user_config.version, &final_config.metadata, - ConfigName::SERVICE_VERSION); + stable_configs.fleet.lookup("DD_VERSION"), env_config->version, + user_config.version, stable_configs.local.lookup("DD_VERSION"), + &final_config.metadata, ConfigName::SERVICE_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); }); + { + auto fleet_tags = stable_config_tags(stable_configs.fleet, "DD_TAGS"); + auto local_tags = stable_config_tags(stable_configs.local, "DD_TAGS"); + final_config.defaults.tags = resolve_and_record_config( + fleet_tags, env_config->tags, user_config.tags, local_tags, + &final_config.metadata, ConfigName::TAGS, + std::unordered_map{}, + [](const auto &tags) { return join_tags(tags); }); + } - // Extraction Styles + // Extraction and Injection Styles const std::vector default_propagation_styles{ PropagationStyle::DATADOG, PropagationStyle::W3C, PropagationStyle::BAGGAGE}; + // Resolve stable config propagation styles using the same cascade as env + // vars: specific > legacy > global. + auto stable_propagation_styles = + [](const StableConfig &cfg, const std::string &specific_key, + const std::string &legacy_key, const std::string &global_key) + -> Optional> { + if (auto s = stable_config_propagation_styles(cfg, specific_key)) return s; + if (auto s = stable_config_propagation_styles(cfg, legacy_key)) return s; + return stable_config_propagation_styles(cfg, global_key); + }; + final_config.extraction_styles = resolve_and_record_config( + stable_propagation_styles( + stable_configs.fleet, "DD_TRACE_PROPAGATION_STYLE_EXTRACT", + "DD_PROPAGATION_STYLE_EXTRACT", "DD_TRACE_PROPAGATION_STYLE"), env_config->extraction_styles, user_config.extraction_styles, + stable_propagation_styles( + stable_configs.local, "DD_TRACE_PROPAGATION_STYLE_EXTRACT", + "DD_PROPAGATION_STYLE_EXTRACT", "DD_TRACE_PROPAGATION_STYLE"), &final_config.metadata, ConfigName::EXTRACTION_STYLES, default_propagation_styles, [](const std::vector &styles) { @@ -330,9 +399,14 @@ Expected finalize_config(const TracerConfig &user_config, "At least one extraction style must be specified."}; } - // Injection Styles final_config.injection_styles = resolve_and_record_config( + stable_propagation_styles( + stable_configs.fleet, "DD_TRACE_PROPAGATION_STYLE_INJECT", + "DD_PROPAGATION_STYLE_INJECT", "DD_TRACE_PROPAGATION_STYLE"), env_config->injection_styles, user_config.injection_styles, + stable_propagation_styles( + stable_configs.local, "DD_TRACE_PROPAGATION_STYLE_INJECT", + "DD_PROPAGATION_STYLE_INJECT", "DD_TRACE_PROPAGATION_STYLE"), &final_config.metadata, ConfigName::INJECTION_STYLES, default_propagation_styles, [](const std::vector &styles) { @@ -346,13 +420,17 @@ Expected finalize_config(const TracerConfig &user_config, // Startup Logs final_config.log_on_startup = resolve_and_record_config( + stable_config_bool(stable_configs.fleet, "DD_TRACE_STARTUP_LOGS"), env_config->log_on_startup, user_config.log_on_startup, + stable_config_bool(stable_configs.local, "DD_TRACE_STARTUP_LOGS"), &final_config.metadata, ConfigName::STARTUP_LOGS, true, [](const bool &b) { return to_string(b); }); // Report traces final_config.report_traces = resolve_and_record_config( + stable_config_bool(stable_configs.fleet, "DD_TRACE_ENABLED"), env_config->report_traces, user_config.report_traces, + stable_config_bool(stable_configs.local, "DD_TRACE_ENABLED"), &final_config.metadata, ConfigName::REPORT_TRACES, true, [](const bool &b) { return to_string(b); }); @@ -366,9 +444,13 @@ Expected finalize_config(const TracerConfig &user_config, // 128b Trace IDs final_config.generate_128bit_trace_ids = resolve_and_record_config( + stable_config_bool(stable_configs.fleet, + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED"), env_config->generate_128bit_trace_ids, - user_config.generate_128bit_trace_ids, &final_config.metadata, - ConfigName::GENEREATE_128BIT_TRACE_IDS, true, + user_config.generate_128bit_trace_ids, + stable_config_bool(stable_configs.local, + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED"), + &final_config.metadata, ConfigName::GENEREATE_128BIT_TRACE_IDS, true, [](const bool &b) { return to_string(b); }); // Integration name & version @@ -380,13 +462,17 @@ Expected finalize_config(const TracerConfig &user_config, // Baggage - max items final_config.baggage_opts.max_items = resolve_and_record_config( + stable_config_uint64(stable_configs.fleet, "DD_TRACE_BAGGAGE_MAX_ITEMS"), env_config->baggage_max_items, user_config.baggage_max_items, + stable_config_uint64(stable_configs.local, "DD_TRACE_BAGGAGE_MAX_ITEMS"), &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_ITEMS, 64UL, [](const size_t &i) { return std::to_string(i); }); // Baggage - max bytes final_config.baggage_opts.max_bytes = resolve_and_record_config( + stable_config_uint64(stable_configs.fleet, "DD_TRACE_BAGGAGE_MAX_BYTES"), env_config->baggage_max_bytes, user_config.baggage_max_bytes, + stable_config_uint64(stable_configs.local, "DD_TRACE_BAGGAGE_MAX_BYTES"), &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_BYTES, 8192UL, [](const size_t &i) { return std::to_string(i); }); @@ -415,7 +501,8 @@ Expected finalize_config(const TracerConfig &user_config, return std::move(*error); } - if (auto trace_sampler_config = finalize_config(user_config.trace_sampler)) { + if (auto trace_sampler_config = finalize_config( + user_config.trace_sampler, &stable_configs, logger.get())) { // Merge metadata vectors for (auto &[key, values] : trace_sampler_config->metadata) { auto &dest = final_config.metadata[key]; @@ -427,7 +514,7 @@ Expected finalize_config(const TracerConfig &user_config, } if (auto span_sampler_config = - finalize_config(user_config.span_sampler, *logger)) { + finalize_config(user_config.span_sampler, *logger, &stable_configs)) { // Merge metadata vectors for (auto &[key, values] : span_sampler_config->metadata) { auto &dest = final_config.metadata[key]; @@ -453,6 +540,8 @@ Expected finalize_config(const TracerConfig &user_config, if (auto telemetry_final_config = telemetry::finalize_config(user_config.telemetry)) { final_config.telemetry = std::move(*telemetry_final_config); + final_config.telemetry.fleet_stable_config_id = + stable_configs.fleet.config_id; final_config.telemetry.products.emplace_back(telemetry::Product{ telemetry::Product::Name::tracing, true, tracer_version, nullopt, nullopt, final_config.metadata}); @@ -462,23 +551,35 @@ Expected finalize_config(const TracerConfig &user_config, // APM Tracing Enabled final_config.tracing_enabled = resolve_and_record_config( + stable_config_bool(stable_configs.fleet, "DD_APM_TRACING_ENABLED"), env_config->tracing_enabled, user_config.tracing_enabled, + stable_config_bool(stable_configs.local, "DD_APM_TRACING_ENABLED"), &final_config.metadata, ConfigName::APM_TRACING_ENABLED, true, [](const bool &b) { return to_string(b); }); { // Resource Renaming Enabled const bool resource_renaming_enabled = resolve_and_record_config( + stable_config_bool(stable_configs.fleet, + "DD_TRACE_RESOURCE_RENAMING_ENABLED"), 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); }); + user_config.resource_renaming_enabled, + stable_config_bool(stable_configs.local, + "DD_TRACE_RESOURCE_RENAMING_ENABLED"), + &final_config.metadata, ConfigName::TRACE_RESOURCE_RENAMING_ENABLED, + false, [](const bool &b) { return to_string(b); }); // Resource Renaming Always Simplified Endpoint const bool resource_renaming_always_simplified_endpoint = resolve_and_record_config( + stable_config_bool( + stable_configs.fleet, + "DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT"), env_config->resource_renaming_always_simplified_endpoint, user_config.resource_renaming_always_simplified_endpoint, + stable_config_bool( + stable_configs.local, + "DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT"), &final_config.metadata, ConfigName::TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, false, [](const bool &b) { return to_string(b); }); diff --git a/src/datadog/yaml_parser.cpp b/src/datadog/yaml_parser.cpp new file mode 100644 index 00000000..066495b5 --- /dev/null +++ b/src/datadog/yaml_parser.cpp @@ -0,0 +1,67 @@ +#include "yaml_parser.h" + +#include + +#include +#include + +namespace datadog { +namespace tracing { + +YamlParseStatus parse_yaml(const std::string& content, YamlParseResult& out) { + if (content.empty()) { + return YamlParseStatus::OK; + } + + YAML::Node root; + try { + root = YAML::Load(content); + } catch (const std::exception&) { + return YamlParseStatus::PARSE_ERROR; + } catch (...) { + return YamlParseStatus::PARSE_ERROR; + } + + if (!root.IsDefined() || root.IsNull()) { + return YamlParseStatus::OK; + } + + if (!root.IsMap()) { + return YamlParseStatus::PARSE_ERROR; + } + + if (root["config_id"]) { + out.config_id = root["config_id"].as(); + } + + if (root["apm_configuration_default"]) { + const auto& apm = root["apm_configuration_default"]; + if (!apm.IsMap()) { + return YamlParseStatus::PARSE_ERROR; + } + + for (const auto& kv : apm) { + const auto& value_node = kv.second; + + // Skip non-scalar values (sequences, maps, etc.). + if (!value_node.IsScalar() && !value_node.IsNull()) { + continue; + } + + std::string value; + if (value_node.IsScalar()) { + // Use the scalar value directly. yaml-cpp preserves the original + // text representation, so booleans stay as "true"/"false" and + // numbers stay as their original string form. + value = value_node.Scalar(); + } + + out.values[kv.first.as()] = value; + } + } + + return YamlParseStatus::OK; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/yaml_parser.h b/src/datadog/yaml_parser.h new file mode 100644 index 00000000..d434633a --- /dev/null +++ b/src/datadog/yaml_parser.h @@ -0,0 +1,33 @@ +#pragma once + +// This component provides a YAML parser for stable configuration files, +// using yaml-cpp. It extracts a config_id and a flat key-value map from +// the `apm_configuration_default` section of a YAML document. + +#include + +#include +#include + +namespace datadog { +namespace tracing { + +// Result of parsing a YAML stable configuration document. +struct YamlParseResult { + // Config ID from the file (optional). + Optional config_id; + + // Map of environment variable names (e.g. "DD_SERVICE") to string values, + // extracted from the `apm_configuration_default` section. + std::unordered_map values; +}; + +enum class YamlParseStatus { OK, PARSE_ERROR }; + +// Parse the given YAML content string into a YamlParseResult. +// Returns OK on success (including when `apm_configuration_default` is absent). +// Returns PARSE_ERROR on malformed input. +YamlParseStatus parse_yaml(const std::string& content, YamlParseResult& out); + +} // namespace tracing +} // namespace datadog diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7571aa8b..744e4ffe 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -40,6 +40,8 @@ add_executable(tests test_tracer.cpp test_trace_sampler.cpp test_endpoint_inferral.cpp + test_stable_config.cpp + test_yaml_parser.cpp remote_config/test_remote_config.cpp ) diff --git a/test/system-tests/request_handler.cpp b/test/system-tests/request_handler.cpp index 0e318c38..65adff4f 100644 --- a/test/system-tests/request_handler.cpp +++ b/test/system-tests/request_handler.cpp @@ -7,8 +7,14 @@ #include #include +#include +#include +#include #include +#include +#include "datadog/null_logger.h" +#include "datadog/stable_config.h" #include "httplib.h" #include "utils.h" @@ -16,9 +22,14 @@ RequestHandler::RequestHandler( datadog::tracing::FinalizedTracerConfig& tracerConfig, std::shared_ptr scheduler, std::shared_ptr logger) - : tracer_(tracerConfig), - scheduler_(scheduler), - logger_(std::move(logger)) {} + : tracer_(tracerConfig), scheduler_(scheduler), logger_(std::move(logger)) { + // Load stable config values directly from disk rather than leaking them + // through the public FinalizedTracerConfig API. + datadog::tracing::NullLogger null_logger; + auto configs = datadog::tracing::load_stable_configs(null_logger); + local_stable_config_values_ = std::move(configs.local.values); + fleet_stable_config_values_ = std::move(configs.fleet.values); +} void RequestHandler::set_error(const char* const file, int line, const std::string& reason, @@ -47,6 +58,17 @@ void RequestHandler::on_trace_config(const httplib::Request& /* req */, httplib::Response& res) { auto tracer_cfg = nlohmann::json::parse(tracer_.config()); + // Helper: convert a DD_* key name to lowercase (e.g. "DD_SERVICE" -> + // "dd_service"). + auto to_lower_key = [](const std::string& key) -> std::string { + std::string result = key; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) -> char { + return static_cast(std::tolower(c)); + }); + return result; + }; + // clang-format off auto response_body = nlohmann::json{ { "config", { @@ -68,6 +90,60 @@ void RequestHandler::on_trace_config(const httplib::Request& /* req */, } } + // Helper: normalize boolean-like strings to lowercase. + auto normalize_value = [](const std::string& val) -> std::string { + std::string lower = val; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) -> char { + return static_cast(std::tolower(c)); + }); + if (lower == "true" || lower == "false") { + return lower; + } + return val; + }; + + // Merge stable config and env var values with correct precedence. + // Precedence: fleet_stable > env > local_stable + std::unordered_map effective_config; + + // 1. Collect all keys from both local and fleet configs. + std::unordered_map all_keys; + for (const auto& [key, value] : local_stable_config_values_) { + all_keys[key] = ""; + } + for (const auto& [key, value] : fleet_stable_config_values_) { + all_keys[key] = ""; + } + + // 2. Local stable config (lowest precedence) + for (const auto& [key, value] : local_stable_config_values_) { + effective_config[key] = normalize_value(value); + } + + // 3. Environment variables (check ALL tracked keys, not just those in + // effective_config so far -- fleet keys are also checked). + for (const auto& [key, _] : all_keys) { + const char* env_val = std::getenv(key.c_str()); + if (env_val != nullptr) { + effective_config[key] = normalize_value(std::string(env_val)); + } + } + + // 4. Fleet stable config (highest precedence) + for (const auto& [key, value] : fleet_stable_config_values_) { + effective_config[key] = normalize_value(value); + } + + // Write all effective config values to the response. + for (const auto& [key, value] : effective_config) { + auto lower_key = to_lower_key(key); + // Only set if not already present from the native config above. + if (!response_body["config"].contains(lower_key)) { + response_body["config"][lower_key] = value; + } + } + logger_->log_info(response_body.dump()); res.set_content(response_body.dump(), "application/json"); } diff --git a/test/system-tests/request_handler.h b/test/system-tests/request_handler.h index 09baf975..a3029098 100644 --- a/test/system-tests/request_handler.h +++ b/test/system-tests/request_handler.h @@ -6,6 +6,8 @@ #include #include +#include +#include #include "developer_noise.h" #include "httplib.h" @@ -53,5 +55,9 @@ class RequestHandler final { // explaining the name :) std::vector blackhole_; + // Stable configuration values for the /config endpoint. + std::unordered_map local_stable_config_values_; + std::unordered_map fleet_stable_config_values_; + #undef VALIDATION_ERROR }; diff --git a/test/test_stable_config.cpp b/test/test_stable_config.cpp new file mode 100644 index 00000000..400518ce --- /dev/null +++ b/test/test_stable_config.cpp @@ -0,0 +1,273 @@ +#include +#include + +#include +#include +#include +#include + +#include "common/environment.h" +#include "mocks/loggers.h" +#include "stable_config.h" +#include "test.h" +#include "yaml_parser.h" + +using namespace datadog::test; +using namespace datadog::tracing; + +namespace { + +namespace fs = std::filesystem; + +// Create a temporary directory for stable config test files. +class TempDir { + fs::path path_; + + public: + TempDir() { + path_ = fs::temp_directory_path() / + ("dd-trace-cpp-test-stable-config-" + + std::to_string(std::hash{}(__FILE__))); + fs::create_directories(path_); + } + + ~TempDir() { + std::error_code ec; + fs::remove_all(path_, ec); + } + + const fs::path& path() const { return path_; } +}; + +} // namespace + +#define STABLE_CONFIG_TEST(x) TEST_CASE(x, "[stable_config]") + +STABLE_CONFIG_TEST("StableConfig::lookup returns nullopt for missing key") { + StableConfig cfg; + REQUIRE(!cfg.lookup("DD_SERVICE").has_value()); +} + +STABLE_CONFIG_TEST("StableConfig::lookup returns value for present key") { + StableConfig cfg; + cfg.values["DD_SERVICE"] = "my-service"; + auto val = cfg.lookup("DD_SERVICE"); + REQUIRE(val.has_value()); + REQUIRE(*val == "my-service"); +} + +STABLE_CONFIG_TEST("parse valid YAML with apm_configuration_default") { + std::string yaml_content = R"( +apm_configuration_default: + DD_SERVICE: my-service + DD_ENV: production + DD_PROFILING_ENABLED: true + DD_TRACE_SAMPLE_RATE: 0.5 +)"; + + YamlParseResult parsed; + auto status = parse_yaml(yaml_content, parsed); + REQUIRE(status == YamlParseStatus::OK); + + REQUIRE(parsed.values.count("DD_SERVICE")); + REQUIRE(parsed.values["DD_SERVICE"] == "my-service"); + REQUIRE(parsed.values.count("DD_ENV")); + REQUIRE(parsed.values["DD_ENV"] == "production"); + REQUIRE(parsed.values.count("DD_PROFILING_ENABLED")); + REQUIRE(parsed.values["DD_PROFILING_ENABLED"] == "true"); + REQUIRE(parsed.values.count("DD_TRACE_SAMPLE_RATE")); + REQUIRE(parsed.values["DD_TRACE_SAMPLE_RATE"] == "0.5"); + REQUIRE(!parsed.values.count("DD_MISSING")); +} + +STABLE_CONFIG_TEST("config_id is stored") { + StableConfig cfg; + cfg.config_id = "fleet-policy-123"; + REQUIRE(cfg.config_id.has_value()); + REQUIRE(*cfg.config_id == "fleet-policy-123"); +} + +STABLE_CONFIG_TEST("duplicate keys: last value wins") { + StableConfig cfg; + cfg.values["DD_SERVICE"] = "first"; + cfg.values["DD_SERVICE"] = "second"; + REQUIRE(*cfg.lookup("DD_SERVICE") == "second"); +} + +STABLE_CONFIG_TEST("get_stable_config_paths returns platform paths") { + auto paths = get_stable_config_paths(); +#ifdef _WIN32 + // On Windows, paths should contain backslashes and + // application_monitoring.yaml. + REQUIRE(paths.local_path.find("application_monitoring.yaml") != + std::string::npos); + REQUIRE(paths.fleet_path.find("managed") != std::string::npos); +#else + REQUIRE(paths.local_path == "/etc/datadog-agent/application_monitoring.yaml"); + REQUIRE(paths.fleet_path == + "/etc/datadog-agent/managed/datadog-agent/stable/" + "application_monitoring.yaml"); +#endif +} + +STABLE_CONFIG_TEST("load_stable_configs with missing files returns empty") { + MockLogger logger; + // The default paths likely don't exist in the test environment, so this + // should return empty configs without errors. + auto configs = load_stable_configs(logger); + REQUIRE(configs.local.values.empty()); + REQUIRE(configs.fleet.values.empty()); + REQUIRE(!configs.local.config_id.has_value()); + REQUIRE(!configs.fleet.config_id.has_value()); +} + +STABLE_CONFIG_TEST( + "finalize_config: fleet stable config overrides env and local") { + // We can't easily write to /etc/datadog-agent/ in tests, so we test + // the precedence via the resolve_and_record_config function directly. + // The 5-arg overload handles fleet > env > user > local > default. + + std::unordered_map> metadata; + + Optional fleet_val("fleet-service"); + Optional env_val("env-service"); + Optional user_val("user-service"); + Optional local_val("local-service"); + + auto result = resolve_and_record_config( + fleet_val, env_val, user_val, local_val, &metadata, + ConfigName::SERVICE_NAME, std::string("default-service")); + + // Fleet should win. + REQUIRE(result == "fleet-service"); + + // Check metadata entries: should have all 5 sources in precedence order. + auto it = metadata.find(ConfigName::SERVICE_NAME); + REQUIRE(it != metadata.end()); + auto& entries = it->second; + REQUIRE(entries.size() == 5); + + // Order: default, local_stable, code, env, fleet_stable + REQUIRE(entries[0].origin == ConfigMetadata::Origin::DEFAULT); + REQUIRE(entries[0].value == "default-service"); + REQUIRE(entries[1].origin == ConfigMetadata::Origin::LOCAL_STABLE_CONFIG); + REQUIRE(entries[1].value == "local-service"); + REQUIRE(entries[2].origin == ConfigMetadata::Origin::CODE); + REQUIRE(entries[2].value == "user-service"); + REQUIRE(entries[3].origin == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + REQUIRE(entries[3].value == "env-service"); + REQUIRE(entries[4].origin == ConfigMetadata::Origin::FLEET_STABLE_CONFIG); + REQUIRE(entries[4].value == "fleet-service"); +} + +STABLE_CONFIG_TEST("precedence: env > local_stable") { + std::unordered_map> metadata; + + Optional fleet_val; // nullopt + Optional env_val("env-service"); + Optional user_val; // nullopt + Optional local_val("local-service"); + + auto result = resolve_and_record_config( + fleet_val, env_val, user_val, local_val, &metadata, + ConfigName::SERVICE_NAME, std::string("default-service")); + + REQUIRE(result == "env-service"); +} + +STABLE_CONFIG_TEST("precedence: user > local_stable") { + std::unordered_map> metadata; + + Optional fleet_val; + Optional env_val; + Optional user_val("user-service"); + Optional local_val("local-service"); + + auto result = resolve_and_record_config( + fleet_val, env_val, user_val, local_val, &metadata, + ConfigName::SERVICE_NAME, std::string("default-service")); + + REQUIRE(result == "user-service"); +} + +STABLE_CONFIG_TEST("precedence: local_stable > default") { + std::unordered_map> metadata; + + Optional fleet_val; + Optional env_val; + Optional user_val; + Optional local_val("local-service"); + + auto result = resolve_and_record_config( + fleet_val, env_val, user_val, local_val, &metadata, + ConfigName::SERVICE_NAME, std::string("default-service")); + + REQUIRE(result == "local-service"); +} + +STABLE_CONFIG_TEST("precedence: fleet > env") { + std::unordered_map> metadata; + + Optional fleet_val("fleet-service"); + Optional env_val("env-service"); + Optional user_val; + Optional local_val; + + auto result = + resolve_and_record_config(fleet_val, env_val, user_val, local_val, + &metadata, ConfigName::SERVICE_NAME); + + REQUIRE(result == "fleet-service"); +} + +STABLE_CONFIG_TEST("precedence: only default") { + std::unordered_map> metadata; + + Optional fleet_val; + Optional env_val; + Optional user_val; + Optional local_val; + + auto result = resolve_and_record_config( + fleet_val, env_val, user_val, local_val, &metadata, + ConfigName::SERVICE_NAME, std::string("default-service")); + + REQUIRE(result == "default-service"); +} + +STABLE_CONFIG_TEST("precedence: no values yields empty string") { + std::unordered_map> metadata; + + Optional fleet_val; + Optional env_val; + Optional user_val; + Optional local_val; + + auto result = + resolve_and_record_config(fleet_val, env_val, user_val, local_val, + &metadata, ConfigName::SERVICE_NAME); + + REQUIRE(result.empty()); + REQUIRE(metadata.empty()); +} + +STABLE_CONFIG_TEST("bool precedence: fleet > env > user > local > default") { + std::unordered_map> metadata; + auto to_str = [](const bool& b) { return b ? "true" : "false"; }; + + SECTION("fleet wins") { + auto result = resolve_and_record_config( + Optional(false), Optional(true), Optional(true), + Optional(true), &metadata, ConfigName::REPORT_TRACES, true, + to_str); + REQUIRE(result == false); + } + + SECTION("env wins when no fleet") { + auto result = resolve_and_record_config( + Optional(), Optional(false), Optional(true), + Optional(true), &metadata, ConfigName::REPORT_TRACES, true, + to_str); + REQUIRE(result == false); + } +} diff --git a/test/test_yaml_parser.cpp b/test/test_yaml_parser.cpp new file mode 100644 index 00000000..4e319d61 --- /dev/null +++ b/test/test_yaml_parser.cpp @@ -0,0 +1,291 @@ +#include + +#include "test.h" +#include "yaml_parser.h" + +using namespace datadog::tracing; + +#define YAML_PARSER_TEST(x) TEST_CASE(x, "[yaml_parser]") + +YAML_PARSER_TEST("parse empty string") { + YamlParseResult result; + REQUIRE(parse_yaml("", result) == YamlParseStatus::OK); + REQUIRE(!result.config_id.has_value()); + REQUIRE(result.values.empty()); +} + +YAML_PARSER_TEST("parse only comments and blank lines") { + YamlParseResult result; + auto status = parse_yaml( + "# This is a comment\n" + "\n" + " # indented comment\n" + "\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.empty()); +} + +YAML_PARSER_TEST("parse config_id at top level") { + YamlParseResult result; + auto status = parse_yaml("config_id: my-policy-123\n", result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.config_id.has_value()); + REQUIRE(*result.config_id == "my-policy-123"); +} + +YAML_PARSER_TEST("parse config_id with quotes") { + YamlParseResult result; + auto status = parse_yaml("config_id: \"quoted-policy-456\"\n", result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(*result.config_id == "quoted-policy-456"); +} + +YAML_PARSER_TEST("parse config_id with single quotes") { + YamlParseResult result; + auto status = parse_yaml("config_id: 'single-quoted'\n", result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(*result.config_id == "single-quoted"); +} + +YAML_PARSER_TEST("parse apm_configuration_default with entries") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n" + " DD_ENV: production\n" + " DD_TRACE_SAMPLE_RATE: 0.5\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.size() == 3); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); + REQUIRE(result.values.at("DD_ENV") == "production"); + REQUIRE(result.values.at("DD_TRACE_SAMPLE_RATE") == "0.5"); +} + +YAML_PARSER_TEST("parse with config_id and apm_configuration_default") { + YamlParseResult result; + auto status = parse_yaml( + "config_id: fleet-policy-789\n" + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n" + " DD_VERSION: 1.2.3\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(*result.config_id == "fleet-policy-789"); + REQUIRE(result.values.size() == 2); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); + REQUIRE(result.values.at("DD_VERSION") == "1.2.3"); +} + +YAML_PARSER_TEST("yaml parser: duplicate keys last value wins") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SERVICE: first\n" + " DD_SERVICE: second\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_SERVICE") == "second"); +} + +YAML_PARSER_TEST("inline comments are stripped") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SERVICE: my-service # this is a comment\n" + " DD_ENV: prod # env comment\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); + REQUIRE(result.values.at("DD_ENV") == "prod"); +} + +YAML_PARSER_TEST("quoted values preserve hash characters") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SERVICE: \"my#service\" # comment\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_SERVICE") == "my#service"); +} + +YAML_PARSER_TEST("single-quoted values preserve hash characters") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SERVICE: 'my#service' # comment\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_SERVICE") == "my#service"); +} + +YAML_PARSER_TEST("flow sequences are skipped") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n" + " DD_TAGS: [tag1, tag2]\n" + " DD_ENV: prod\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.count("DD_TAGS") == 0); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); + REQUIRE(result.values.at("DD_ENV") == "prod"); +} + +YAML_PARSER_TEST("flow mappings are skipped") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SOME_MAP: {key: value}\n" + " DD_SERVICE: my-service\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.count("DD_SOME_MAP") == 0); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); +} + +YAML_PARSER_TEST("Windows line endings are handled") { + YamlParseResult result; + auto status = parse_yaml( + "config_id: win-id\r\n" + "apm_configuration_default:\r\n" + " DD_SERVICE: my-service\r\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(*result.config_id == "win-id"); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); +} + +YAML_PARSER_TEST("malformed YAML returns PARSE_ERROR") { + YamlParseResult result; + auto status = parse_yaml("[invalid yaml: {{{", result); + REQUIRE(status == YamlParseStatus::PARSE_ERROR); +} + +YAML_PARSER_TEST("apm_configuration_default with scalar value is PARSE_ERROR") { + YamlParseResult result; + auto status = parse_yaml("apm_configuration_default: some_value\n", result); + REQUIRE(status == YamlParseStatus::PARSE_ERROR); +} + +YAML_PARSER_TEST("unknown top-level keys are silently ignored") { + YamlParseResult result; + auto status = parse_yaml( + "some_unknown_key: some_value\n" + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); +} + +YAML_PARSER_TEST("indented lines under unknown keys are ignored") { + YamlParseResult result; + auto status = parse_yaml( + "other_section:\n" + " key1: val1\n" + " key2: val2\n" + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.size() == 1); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); +} + +YAML_PARSER_TEST("empty values are stored") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_SERVICE:\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_SERVICE") == ""); +} + +YAML_PARSER_TEST("boolean-like values are stored as strings") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_TRACE_ENABLED: true\n" + " DD_TRACE_STARTUP_LOGS: false\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_TRACE_ENABLED") == "true"); + REQUIRE(result.values.at("DD_TRACE_STARTUP_LOGS") == "false"); +} + +YAML_PARSER_TEST("numeric values are stored as strings") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_TRACE_SAMPLE_RATE: 0.5\n" + " DD_TRACE_RATE_LIMIT: 100\n" + " DD_TRACE_BAGGAGE_MAX_ITEMS: 64\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_TRACE_SAMPLE_RATE") == "0.5"); + REQUIRE(result.values.at("DD_TRACE_RATE_LIMIT") == "100"); + REQUIRE(result.values.at("DD_TRACE_BAGGAGE_MAX_ITEMS") == "64"); +} + +YAML_PARSER_TEST("multiple top-level sections") { + YamlParseResult result; + auto status = parse_yaml( + "config_id: test-id\n" + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n" + "other_section:\n" + " key: value\n" + " another: value2\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(*result.config_id == "test-id"); + REQUIRE(result.values.size() == 1); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); +} + +YAML_PARSER_TEST("YAML document markers are handled") { + YamlParseResult result; + auto status = parse_yaml( + "---\n" + "config_id: doc-marker-id\n" + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n" + "...\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(*result.config_id == "doc-marker-id"); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); +} + +YAML_PARSER_TEST("quoted JSON strings are correctly unquoted") { + YamlParseResult result; + auto status = parse_yaml( + "apm_configuration_default:\n" + " DD_TRACE_SAMPLING_RULES: '[{\"rate\":1}]'\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(result.values.at("DD_TRACE_SAMPLING_RULES") == "[{\"rate\":1}]"); +} + +YAML_PARSER_TEST("non-map root returns PARSE_ERROR") { + YamlParseResult result; + auto status = parse_yaml("- item1\n- item2\n", result); + REQUIRE(status == YamlParseStatus::PARSE_ERROR); +} + +YAML_PARSER_TEST("YAML anchors and aliases are handled") { + YamlParseResult result; + auto status = parse_yaml( + "config_id: &id anchor-id\n" + "apm_configuration_default:\n" + " DD_SERVICE: my-service\n", + result); + REQUIRE(status == YamlParseStatus::OK); + REQUIRE(*result.config_id == "anchor-id"); + REQUIRE(result.values.at("DD_SERVICE") == "my-service"); +}