diff --git a/BUILD.bazel b/BUILD.bazel index 0a854130..7c641772 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,4 +1,35 @@ -load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") + +cc_library( + name = "dd_trace_cpp_stable_config", + srcs = [ + "src/datadog/stable_config_loader.cpp", + "src/datadog/stable_config_loader_internal.h", + "src/datadog/stable_config_parser.cpp", + ], + hdrs = ["include/datadog/stable_config_loader.h"], + strip_include_prefix = "include/", + visibility = ["//visibility:public"], + linkopts = select({ + "@platforms//os:windows": ["-lshell32"], + "//conditions:default": [], + }), + deps = [ + ":dd_trace_cpp", + "@yaml-cpp//:yaml-cpp", + ], +) + +cc_test( + name = "stable_config_loader_test", + size = "small", + srcs = ["test/bazel_stable_config_test.cpp"], + data = [ + "test/fixtures/stable_config/fleet.yaml", + "test/fixtures/stable_config/local.yaml", + ], + deps = [":dd_trace_cpp_stable_config"], +) cc_library( name = "dd_trace_cpp", @@ -67,6 +98,7 @@ cc_library( "src/datadog/span_sampler.cpp", "src/datadog/span_sampler.h", "src/datadog/span_sampler_config.cpp", + "src/datadog/stable_config.cpp", "src/datadog/string_util.cpp", "src/datadog/string_util.h", "src/datadog/tag_propagation.cpp", @@ -145,6 +177,7 @@ cc_library( "include/datadog/span_defaults.h", "include/datadog/span_matcher.h", "include/datadog/span_sampler_config.h", + "include/datadog/stable_config.h", "include/datadog/string_view.h", "include/datadog/telemetry/configuration.h", "include/datadog/telemetry/metrics.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index 787f91fb..95af3983 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ project( option(BUILD_SHARED_LIBS "Build shared libraries" ON) option(BUILD_STATIC_LIBS "Build static libraries" ON) option(DD_TRACE_BUILD_C_BINDING "Build C binding" OFF) +option(DD_TRACE_BUILD_STABLE_CONFIG "Build the native stable config loader" OFF) if (WIN32) option(DD_TRACE_STATIC_CRT "Build dd-trace-cpp with static CRT with MSVC" OFF) @@ -166,6 +167,7 @@ target_sources(dd-trace-cpp-objects include/datadog/span_defaults.h include/datadog/span_matcher.h include/datadog/span_sampler_config.h + include/datadog/stable_config.h include/datadog/string_view.h include/datadog/trace_id.h include/datadog/trace_sampler_config.h @@ -214,6 +216,7 @@ target_sources(dd-trace-cpp-objects src/datadog/span_matcher.cpp src/datadog/span_sampler_config.cpp src/datadog/span_sampler.cpp + src/datadog/stable_config.cpp src/datadog/string_util.cpp src/datadog/tags.cpp src/datadog/tag_propagation.cpp @@ -367,6 +370,43 @@ if (BUILD_STATIC_LIBS) ) endif () +if (DD_TRACE_BUILD_STABLE_CONFIG) + include(cmake/deps/yaml-cpp.cmake) + add_library(dd-trace-cpp-stable-config STATIC + src/datadog/stable_config_loader.cpp + src/datadog/stable_config_parser.cpp) + add_library(dd-trace-cpp::stable-config ALIAS dd-trace-cpp-stable-config) + set_target_properties(dd-trace-cpp-stable-config PROPERTIES EXPORT_NAME stable-config) + target_compile_features(dd-trace-cpp-stable-config PUBLIC cxx_std_17) + target_sources(dd-trace-cpp-stable-config PUBLIC + FILE_SET public_headers TYPE HEADERS BASE_DIRS include + FILES include/datadog/stable_config_loader.h) + target_link_libraries(dd-trace-cpp-stable-config PUBLIC yaml-cpp::yaml-cpp) + if (WIN32) + target_link_libraries(dd-trace-cpp-stable-config PUBLIC shell32) + endif () + if (BUILD_STATIC_LIBS) + target_link_libraries(dd-trace-cpp-stable-config PUBLIC dd-trace-cpp::static) + elseif (BUILD_SHARED_LIBS) + target_link_libraries(dd-trace-cpp-stable-config PUBLIC dd-trace-cpp::shared) + else () + message(FATAL_ERROR "Stable config requires a dd-trace-cpp library") + endif () + install(TARGETS dd-trace-cpp-stable-config + EXPORT dd-trace-cpp-targets + FILE_SET public_headers DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(FILES "${yaml-cpp_SOURCE_DIR}/LICENSE" + DESTINATION ${CMAKE_INSTALL_DATADIR}/dd-trace-cpp/licenses + RENAME yaml-cpp-LICENSE) + if (DD_TRACE_BUILD_TESTING) + target_sources(tests PRIVATE test/test_stable_config.cpp) + target_link_libraries(tests PRIVATE dd-trace-cpp::stable-config) + target_compile_definitions(tests PRIVATE + "DD_TRACE_SOURCE_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"") + endif () +endif () + # install target write_basic_package_version_file( "${PROJECT_NAME}-config-version.cmake" diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 1dc24387..157b2389 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -3,4 +3,5 @@ vendored,nlohmann/json,MIT,Copyright (c) 2013-2022 Niels Lohmann + +datadog::tracing::TracerConfig config; +auto finalized = + datadog::tracing::finalize_config_with_stable_config(config, "cpp"); +if (!finalized) { + // Handle finalized.error(). +} +``` + +The loader reads the platform's local and fleet `application_monitoring.yaml` +files by default. Pass explicit paths as the third and fourth arguments to +`finalize_config_with_stable_config`, or the second and third arguments to +`load_stable_config`. Files may contain `config_id`, +`apm_configuration_default`, `tags`, and `rules`. Rules select by language, +process arguments, environment variables, or tags. The first matching rule in +each file applies, and its values can contain `{{ language }}` or indexed +variables such as `{{ environment_variables[REGION] }}`. + +Settings resolve in this order: defaults, local YAML, environment variables, +fleet YAML, and values in `TracerConfig`. Stable configuration records the +winning source and config ID in configuration telemetry. The existing +`finalize_config(config)` overload keeps its original behavior. Missing or +oversized files are ignored; malformed YAML and other read errors are reported. + ### Optional: Linking to the shared library In case you decided to build the shared library: diff --git a/WORKSPACE b/WORKSPACE index 39f0fb10..bbee86dd 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -14,6 +14,12 @@ # accessed December 6, 2022. load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +http_archive( + name = "yaml-cpp", + sha256 = "298593d9c440fd9034b8b193d96318b76d49bc97c6ceadb7b0836edf0b6d7539", + urls = ["https://github.com/jbeder/yaml-cpp/releases/download/yaml-cpp-0.9.0/yaml-cpp-yaml-cpp-0.9.0.tar.gz"], +) + http_archive( name = "com_google_absl", sha256 = "1692f77d1739bacf3f94337188b78583cf09bab7e420d2dc6c5605a4f86785a1", diff --git a/cmake/dd-trace-cpp-config.cmake.in b/cmake/dd-trace-cpp-config.cmake.in index 7ed689a2..eba5aeff 100644 --- a/cmake/dd-trace-cpp-config.cmake.in +++ b/cmake/dd-trace-cpp-config.cmake.in @@ -9,4 +9,8 @@ if(DD_TRACE_TRANSPORT STREQUAL "curl") find_dependency(CURL) endif() +if(@DD_TRACE_BUILD_STABLE_CONFIG@) + find_dependency(yaml-cpp 0.9.0 CONFIG) +endif() + include("${CMAKE_CURRENT_LIST_DIR}/dd-trace-cpp-targets.cmake") diff --git a/cmake/deps/yaml-cpp.cmake b/cmake/deps/yaml-cpp.cmake new file mode 100644 index 00000000..96db94c2 --- /dev/null +++ b/cmake/deps/yaml-cpp.cmake @@ -0,0 +1,18 @@ +include(FetchContent) + +set(YAML_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(YAML_CPP_INSTALL ON CACHE BOOL "" FORCE) +set(YAML_CPP_DISABLE_UNINSTALL ON CACHE BOOL "" FORCE) +set(YAML_ENABLE_PIC ON CACHE BOOL "" FORCE) +if (DD_TRACE_STATIC_CRT) + set(YAML_MSVC_SHARED_RT OFF CACHE BOOL "" FORCE) +endif () + +FetchContent_Declare(yaml-cpp + URL https://github.com/jbeder/yaml-cpp/releases/download/yaml-cpp-0.9.0/yaml-cpp-yaml-cpp-0.9.0.tar.gz + URL_HASH SHA256=298593d9c440fd9034b8b193d96318b76d49bc97c6ceadb7b0836edf0b6d7539 + SYSTEM +) +FetchContent_MakeAvailable(yaml-cpp) diff --git a/include/datadog/config.h b/include/datadog/config.h index 3489a69c..e2e6cac2 100644 --- a/include/datadog/config.h +++ b/include/datadog/config.h @@ -45,7 +45,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, + FLEET_STABLE_CONFIG }; // Name of the configuration parameter @@ -56,11 +58,17 @@ struct ConfigMetadata { Origin origin; // Optional error associated with the configuration parameter Optional error; + Optional config_id; ConfigMetadata() = default; ConfigMetadata(ConfigName n, std::string v, Origin orig, - Optional err = nullopt) - : name(n), value(std::move(v)), origin(orig), error(std::move(err)) {} + Optional err = nullopt, + Optional id = nullopt) + : name(n), + value(std::move(v)), + origin(orig), + error(std::move(err)), + config_id(std::move(id)) {} }; // Returns the final configuration value using the following @@ -96,7 +104,11 @@ 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) { + Stringifier to_string_fn = nullptr, + ConfigMetadata::Origin from_env_origin = + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE, + Optional from_env_config_id = nullopt, + bool programmatic_wins = false) { auto stringify = [&](const Value& v) -> std::string { if constexpr (!std::is_same_v) { return to_string_fn(v); // use provided function @@ -112,9 +124,11 @@ Value resolve_and_record_config( std::vector metadata_entries; Optional chosen_value; - auto add_entry = [&](ConfigMetadata::Origin origin, const Value& val) { + auto add_entry = [&](ConfigMetadata::Origin origin, const Value& val, + Optional config_id = nullopt) { std::string val_str = stringify(val); - metadata_entries.emplace_back(ConfigMetadata{config_name, val_str, origin}); + metadata_entries.emplace_back( + ConfigMetadata{config_name, val_str, origin, nullopt, config_id}); chosen_value = val; }; @@ -123,12 +137,12 @@ Value resolve_and_record_config( 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 (programmatic_wins) { + if (from_env) add_entry(from_env_origin, *from_env, from_env_config_id); + if (from_user) add_entry(ConfigMetadata::Origin::CODE, *from_user); + } else { + if (from_user) add_entry(ConfigMetadata::Origin::CODE, *from_user); + if (from_env) add_entry(from_env_origin, *from_env, from_env_config_id); } if (!metadata_entries.empty()) { diff --git a/include/datadog/datadog_agent_config.h b/include/datadog/datadog_agent_config.h index a6d1483c..afd3a847 100644 --- a/include/datadog/datadog_agent_config.h +++ b/include/datadog/datadog_agent_config.h @@ -23,6 +23,7 @@ namespace datadog::tracing { class EventScheduler; class Logger; +class StableConfig; struct DatadogAgentConfig { // The `HTTPClient` used to submit traces to the Datadog Agent. If this @@ -64,7 +65,8 @@ struct DatadogAgentConfig { class FinalizedDatadogAgentConfig { friend Expected finalize_config( - const DatadogAgentConfig&, const std::shared_ptr&, const Clock&); + const DatadogAgentConfig&, const std::shared_ptr&, const Clock&, + const StableConfig*); FinalizedDatadogAgentConfig() = default; @@ -93,6 +95,6 @@ class FinalizedDatadogAgentConfig { Expected finalize_config( const DatadogAgentConfig& config, const std::shared_ptr& logger, - const Clock& clock); + const Clock& clock, const StableConfig* stable_config = nullptr); } // namespace datadog::tracing diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 4b4e8dbb..5e13f64e 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -16,6 +16,7 @@ namespace datadog { namespace tracing { +class StableConfig; namespace environment { // Central registry for supported environment variables. @@ -117,6 +118,8 @@ StringView name(Variable variable); // Return the value of the specified environment `variable`, or return // `nullopt` if that variable is not set in the environment. Optional lookup(Variable variable); +Optional lookup(Variable variable, + const StableConfig* stable_config); std::string to_json(); diff --git a/include/datadog/span_sampler_config.h b/include/datadog/span_sampler_config.h index 9dc85391..e77b5e32 100644 --- a/include/datadog/span_sampler_config.h +++ b/include/datadog/span_sampler_config.h @@ -20,6 +20,8 @@ namespace datadog { namespace tracing { +class StableConfig; + struct SpanSamplerConfig { struct Rule : public SpanMatcher { double sample_rate = 1.0; @@ -37,7 +39,7 @@ struct SpanSamplerConfig { class FinalizedSpanSamplerConfig { friend Expected finalize_config( - const SpanSamplerConfig&, Logger&); + const SpanSamplerConfig&, Logger&, const StableConfig*); friend class FinalizedTracerConfig; FinalizedSpanSamplerConfig() = default; @@ -52,8 +54,8 @@ class FinalizedSpanSamplerConfig { std::unordered_map> metadata; }; -Expected finalize_config(const SpanSamplerConfig&, - Logger&); +Expected finalize_config( + const SpanSamplerConfig&, Logger&, const StableConfig* = nullptr); std::string to_string(const FinalizedSpanSamplerConfig::Rule&); diff --git a/include/datadog/stable_config.h b/include/datadog/stable_config.h new file mode 100644 index 00000000..68326821 --- /dev/null +++ b/include/datadog/stable_config.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +#include "config.h" +#include "environment.h" +#include "string_view.h" + +namespace datadog::tracing { + +enum class StableConfigSource { LOCAL, FLEET }; + +struct StableConfigEntry { + std::string value; + StableConfigSource source; + std::string config_id; +}; + +struct ConfigValueSource { + ConfigMetadata::Origin origin; + Optional config_id; +}; + +// Values returned by the stable configuration parser. +class StableConfig { + public: + void set(std::string name, StableConfigEntry entry); + const StableConfigEntry* find(StringView name) const; + + private: + std::unordered_map entries_; +}; + +ConfigValueSource config_value_source(environment::Variable variable, + const StableConfig* stable_config); + +template +Value resolve_with_stable_config( + const Optional& from_environment, const Optional& from_user, + std::unordered_map>* metadata, + ConfigName config_name, environment::Variable variable, + const StableConfig* stable_config, DefaultValue fallback = nullptr, + Stringifier to_string_fn = nullptr) { + const ConfigValueSource source = config_value_source(variable, stable_config); + return resolve_and_record_config( + from_environment, from_user, metadata, config_name, fallback, + to_string_fn, source.origin, source.config_id, stable_config != nullptr); +} + +template +Value choose_with_stable_config(const Optional& from_environment, + const Optional& from_user, + Fallback fallback, + const StableConfig* stable_config) { + if (stable_config) { + return value_or(from_user, from_environment, fallback); + } + return value_or(from_environment, from_user, fallback); +} + +} // namespace datadog::tracing diff --git a/include/datadog/stable_config_loader.h b/include/datadog/stable_config_loader.h new file mode 100644 index 00000000..e474f9b8 --- /dev/null +++ b/include/datadog/stable_config_loader.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace datadog::tracing { + +// Load local and fleet stable configuration from YAML. +// Empty paths select the platform defaults. +Expected load_stable_config(StringView language, + StringView local_path = {}, + StringView fleet_path = {}); + +// Load stable config and use it to finalize the tracer configuration. +Expected finalize_config_with_stable_config( + const TracerConfig& config, StringView language, StringView local_path = {}, + StringView fleet_path = {}); + +} // namespace datadog::tracing diff --git a/include/datadog/trace_sampler_config.h b/include/datadog/trace_sampler_config.h index 57dd6f6e..ae748628 100644 --- a/include/datadog/trace_sampler_config.h +++ b/include/datadog/trace_sampler_config.h @@ -20,6 +20,8 @@ namespace datadog { namespace tracing { +class StableConfig; + struct TraceSamplerRule final { Rate rate; SpanMatcher matcher; @@ -42,7 +44,7 @@ struct TraceSamplerConfig { class FinalizedTraceSamplerConfig { friend Expected finalize_config( - const TraceSamplerConfig& config); + const TraceSamplerConfig& config, const StableConfig* stable_config); friend class FinalizedTracerConfig; FinalizedTraceSamplerConfig() = default; @@ -58,7 +60,8 @@ class FinalizedTraceSamplerConfig { }; Expected finalize_config( - const TraceSamplerConfig& config); + const TraceSamplerConfig& config, + const StableConfig* stable_config = nullptr); } // namespace tracing } // namespace datadog diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index de36e6fc..a23f36cc 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -28,6 +28,7 @@ namespace tracing { class Collector; class Logger; +class StableConfig; class SpanSampler; class TraceSampler; @@ -217,6 +218,9 @@ struct TracerConfig { class FinalizedTracerConfig final { friend Expected finalize_config( const TracerConfig& config, const Clock& clock); + friend Expected finalize_config( + const TracerConfig& config, const StableConfig* stable_config, + const Clock& clock); FinalizedTracerConfig() = default; public: @@ -265,6 +269,12 @@ class FinalizedTracerConfig final { Expected finalize_config(const TracerConfig& config); Expected finalize_config(const TracerConfig& config, const Clock& clock); +Expected finalize_config( + const TracerConfig& config, const StableConfig& stable_config, + const Clock& clock = default_clock); +Expected finalize_config( + const TracerConfig& config, const StableConfig* stable_config, + const Clock& clock); } // namespace tracing } // namespace datadog diff --git a/src/datadog/datadog_agent_config.cpp b/src/datadog/datadog_agent_config.cpp index 4e3abd59..035c0b61 100644 --- a/src/datadog/datadog_agent_config.cpp +++ b/src/datadog/datadog_agent_config.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include #include #include "datadog_agent_config_internal.h" @@ -36,15 +38,45 @@ std::pair select_agent_url( detect_default_agent_url(default_socket_path)); } -Optional build_agent_url_from_environment_variables() { - Optional url_env = lookup(environment::DD_TRACE_AGENT_URL); - if (url_env && !url_env->empty()) { - return std::string{*url_env}; +namespace { + +int agent_url_source_rank(environment::Variable variable, + const StableConfig* stable_config) { + const Optional value = lookup(variable, stable_config); + if (!value || value->empty()) return -1; + // Rank sources as local < environment < fleet. + const auto origin = config_value_source(variable, stable_config).origin; + if (origin == ConfigMetadata::Origin::FLEET_STABLE_CONFIG) return 2; + return origin == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE ? 1 : 0; +} + +environment::Variable agent_url_source(const StableConfig* stable_config) { + const int url_rank = + agent_url_source_rank(environment::DD_TRACE_AGENT_URL, stable_config); + const int host_rank = + agent_url_source_rank(environment::DD_AGENT_HOST, stable_config); + const int port_rank = + agent_url_source_rank(environment::DD_TRACE_AGENT_PORT, stable_config); + if (url_rank >= 0 && url_rank >= std::max(host_rank, port_rank)) { + return environment::DD_TRACE_AGENT_URL; + } + return host_rank >= port_rank ? environment::DD_AGENT_HOST + : environment::DD_TRACE_AGENT_PORT; +} + +} // namespace + +Optional build_agent_url_from_environment_variables( + const StableConfig* stable_config) { + if (agent_url_source(stable_config) == environment::DD_TRACE_AGENT_URL) { + return std::string{*lookup(environment::DD_TRACE_AGENT_URL, stable_config)}; } - Optional env_host = lookup(environment::DD_AGENT_HOST); + Optional env_host = + lookup(environment::DD_AGENT_HOST, stable_config); if (env_host && env_host->empty()) env_host = nullopt; - Optional env_port = lookup(environment::DD_TRACE_AGENT_PORT); + Optional env_port = + lookup(environment::DD_TRACE_AGENT_PORT, stable_config); if (env_port && env_port->empty()) env_port = nullopt; if (env_host || env_port) { std::string agent_url = "http://"; @@ -66,15 +98,17 @@ Optional build_agent_url_from_environment_variables() { return nullopt; } -Expected load_datadog_agent_env_config() { +Expected load_datadog_agent_env_config( + const StableConfig* stable_config) { DatadogAgentConfig env_config; - if (auto rc_enabled = lookup(environment::DD_REMOTE_CONFIGURATION_ENABLED)) { + if (auto rc_enabled = + lookup(environment::DD_REMOTE_CONFIGURATION_ENABLED, stable_config)) { env_config.remote_configuration_enabled = !falsy(*rc_enabled); } - if (auto raw_rc_poll_interval_value = - lookup(environment::DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS)) { + if (auto raw_rc_poll_interval_value = lookup( + environment::DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS, stable_config)) { auto res = parse_double(*raw_rc_poll_interval_value); if (auto error = res.if_error()) { return error->with_prefix( @@ -85,7 +119,7 @@ Expected load_datadog_agent_env_config() { } if (Optional agent_url = - build_agent_url_from_environment_variables()) { + build_agent_url_from_environment_variables(stable_config)) { env_config.url = *std::move(agent_url); } @@ -94,8 +128,10 @@ Expected load_datadog_agent_env_config() { Expected finalize_config( const DatadogAgentConfig& user_config, - const std::shared_ptr& logger, const Clock& clock) { - Expected env_config = load_datadog_agent_env_config(); + const std::shared_ptr& logger, const Clock& clock, + const StableConfig* stable_config) { + Expected env_config = + load_datadog_agent_env_config(stable_config); if (auto error = env_config.if_error()) { return *error; } @@ -126,9 +162,9 @@ Expected finalize_config( result.remote_configuration_listeners = user_config.remote_configuration_listeners; - if (auto flush_interval_milliseconds = - value_or(env_config->flush_interval_milliseconds, - user_config.flush_interval_milliseconds, 2000); + if (auto flush_interval_milliseconds = choose_with_stable_config( + env_config->flush_interval_milliseconds, + user_config.flush_interval_milliseconds, 2000, stable_config); flush_interval_milliseconds > 0) { result.flush_interval = std::chrono::milliseconds(flush_interval_milliseconds); @@ -138,9 +174,9 @@ Expected finalize_config( "milliseconds."}; } - if (auto request_timeout_milliseconds = - value_or(env_config->request_timeout_milliseconds, - user_config.request_timeout_milliseconds, 2000); + if (auto request_timeout_milliseconds = choose_with_stable_config( + env_config->request_timeout_milliseconds, + user_config.request_timeout_milliseconds, 2000, stable_config); request_timeout_milliseconds > 0) { result.request_timeout = std::chrono::milliseconds(request_timeout_milliseconds); @@ -150,9 +186,9 @@ Expected finalize_config( "milliseconds."}; } - if (auto shutdown_timeout_milliseconds = - value_or(env_config->shutdown_timeout_milliseconds, - user_config.shutdown_timeout_milliseconds, 2000); + if (auto shutdown_timeout_milliseconds = choose_with_stable_config( + env_config->shutdown_timeout_milliseconds, + user_config.shutdown_timeout_milliseconds, 2000, stable_config); shutdown_timeout_milliseconds > 0) { result.shutdown_timeout = std::chrono::milliseconds(shutdown_timeout_milliseconds); @@ -162,9 +198,10 @@ Expected finalize_config( "milliseconds."}; } - if (double rc_poll_interval_seconds = - value_or(env_config->remote_configuration_poll_interval_seconds, - user_config.remote_configuration_poll_interval_seconds, 5.0); + if (double rc_poll_interval_seconds = choose_with_stable_config( + env_config->remote_configuration_poll_interval_seconds, + user_config.remote_configuration_poll_interval_seconds, 5.0, + stable_config); rc_poll_interval_seconds >= 0.0) { result.remote_configuration_poll_interval = std::chrono::duration_cast( @@ -175,25 +212,36 @@ Expected finalize_config( "positive number of seconds."}; } - result.remote_configuration_enabled = - value_or(env_config->remote_configuration_enabled, - user_config.remote_configuration_enabled, true); + result.remote_configuration_enabled = choose_with_stable_config( + env_config->remote_configuration_enabled, + user_config.remote_configuration_enabled, true, stable_config); - const auto [origin, url] = + auto [origin, url] = select_agent_url(env_config->url, user_config.url, std::filesystem::path{default_agent_socket_path}); + Optional config_id; + if (stable_config && user_config.url) { + // With stable config, code wins over the environment. + origin = ConfigMetadata::Origin::CODE; + url = *user_config.url; + } else if (origin == ConfigMetadata::Origin::ENVIRONMENT_VARIABLE) { + const ConfigValueSource source = + config_value_source(agent_url_source(stable_config), stable_config); + origin = source.origin; + config_id = source.config_id; + } auto parsed_url = HTTPClient::URL::parse(url); if (auto* error = parsed_url.if_error()) { return std::move(*error); } result.url = *parsed_url; result.metadata[ConfigName::AGENT_URL] = { - ConfigMetadata(ConfigName::AGENT_URL, url, origin)}; + ConfigMetadata(ConfigName::AGENT_URL, url, origin, nullopt, config_id)}; // Starting Datadog Agent 7.62.0, the admission controller inject a unique // identifier through `DD_EXTERNAL_ENV`. This uid is used for origin // detection. - if (auto external_env = lookup(environment::DD_EXTERNAL_ENV)) { + if (auto external_env = lookup(environment::DD_EXTERNAL_ENV, stable_config)) { result.admission_controller_uid = std::string(*external_env); } diff --git a/src/datadog/datadog_agent_config_internal.h b/src/datadog/datadog_agent_config_internal.h index 7d024650..9980260b 100644 --- a/src/datadog/datadog_agent_config_internal.h +++ b/src/datadog/datadog_agent_config_internal.h @@ -7,6 +7,8 @@ namespace datadog::tracing { +class StableConfig; + // The well-known path where the Datadog Agent listens on a Unix domain socket. // Single-step instrumentation mounts the socket here. inline constexpr char default_agent_socket_path[] = @@ -15,7 +17,8 @@ inline constexpr char default_agent_socket_path[] = // Return the Agent URL configured by the environment, or null if the // environment does not configure one. `DD_TRACE_AGENT_URL` wins over // `DD_AGENT_HOST` and `DD_TRACE_AGENT_PORT`. Empty values count as unset. -Optional build_agent_url_from_environment_variables(); +Optional build_agent_url_from_environment_variables( + const StableConfig* stable_config = nullptr); // Return the origin and value of the Agent URL, given the URL from the // environment, the URL from programmatic configuration, and the path where the diff --git a/src/datadog/environment.cpp b/src/datadog/environment.cpp index 4dc3eb3f..73649962 100644 --- a/src/datadog/environment.cpp +++ b/src/datadog/environment.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -19,6 +20,18 @@ Optional lookup(Variable variable) { return StringView{value}; } +Optional lookup(Variable variable, + const StableConfig *stable_config) { + const StableConfigEntry *entry = + stable_config ? stable_config->find(name(variable)) : nullptr; + // Fleet config wins over the environment. Local config loses to it. + const Optional value = lookup(variable); + if (entry && (entry->source == StableConfigSource::FLEET || !value)) { + return StringView{entry->value}; + } + return value; +} + std::string to_json() { auto result = nlohmann::json::object({}); diff --git a/src/datadog/span_sampler_config.cpp b/src/datadog/span_sampler_config.cpp index 28d5c4ef..6984fef0 100644 --- a/src/datadog/span_sampler_config.cpp +++ b/src/datadog/span_sampler_config.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -143,10 +144,11 @@ Expected> parse_rules(StringView rules_raw, return rules; } -Expected load_span_sampler_env_config(Logger &logger) { +Expected load_span_sampler_env_config( + Logger &logger, const StableConfig *stable_config) { SpanSamplerConfig env_config; - auto rules_env = lookup(environment::DD_SPAN_SAMPLING_RULES); + auto rules_env = lookup(environment::DD_SPAN_SAMPLING_RULES, stable_config); if (rules_env) { auto maybe_rules = parse_rules(*rules_env, name(environment::DD_SPAN_SAMPLING_RULES)); @@ -156,7 +158,8 @@ Expected load_span_sampler_env_config(Logger &logger) { env_config.rules = std::move(*maybe_rules); } - if (auto file_env = lookup(environment::DD_SPAN_SAMPLING_RULES_FILE)) { + if (auto file_env = + lookup(environment::DD_SPAN_SAMPLING_RULES_FILE, stable_config)) { if (rules_env) { const auto rules_file_name = name(environment::DD_SPAN_SAMPLING_RULES_FILE); @@ -221,8 +224,10 @@ 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) { - Expected env_config = load_span_sampler_env_config(logger); + const SpanSamplerConfig &user_config, Logger &logger, + const StableConfig *stable_config) { + Expected env_config = + load_span_sampler_env_config(logger, stable_config); if (auto error = env_config.if_error()) { return *error; } @@ -237,9 +242,13 @@ Expected finalize_config( user_rules = user_config.rules; } - std::vector rules = resolve_and_record_config( + std::vector rules = resolve_with_stable_config( env_rules, user_rules, &result.metadata, ConfigName::SPAN_SAMPLING_RULES, - nullptr, [](const std::vector &r) { + lookup(environment::DD_SPAN_SAMPLING_RULES, stable_config) + ? environment::DD_SPAN_SAMPLING_RULES + : environment::DD_SPAN_SAMPLING_RULES_FILE, + stable_config, 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..9b85c064 --- /dev/null +++ b/src/datadog/stable_config.cpp @@ -0,0 +1,30 @@ +#include + +namespace datadog::tracing { + +void StableConfig::set(std::string name, StableConfigEntry entry) { + entries_.insert_or_assign(std::move(name), std::move(entry)); +} + +const StableConfigEntry* StableConfig::find(StringView name) const { + const auto entry = entries_.find(std::string(name.data(), name.size())); + return entry == entries_.end() ? nullptr : &entry->second; +} + +ConfigValueSource config_value_source(environment::Variable variable, + const StableConfig* stable_config) { + const StableConfigEntry* entry = + stable_config ? stable_config->find(environment::name(variable)) + : nullptr; + if (entry && (entry->source == StableConfigSource::FLEET || + !environment::lookup(variable))) { + return {entry->source == StableConfigSource::FLEET + ? ConfigMetadata::Origin::FLEET_STABLE_CONFIG + : ConfigMetadata::Origin::LOCAL_STABLE_CONFIG, + entry->config_id.empty() ? nullopt + : Optional{entry->config_id}}; + } + return {ConfigMetadata::Origin::ENVIRONMENT_VARIABLE, nullopt}; +} + +} // namespace datadog::tracing diff --git a/src/datadog/stable_config_loader.cpp b/src/datadog/stable_config_loader.cpp new file mode 100644 index 00000000..79de311d --- /dev/null +++ b/src/datadog/stable_config_loader.cpp @@ -0,0 +1,161 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#elif defined(_WIN32) +#include +#include +#endif + +#include "stable_config_loader_internal.h" + +#if !defined(_WIN32) && !defined(__APPLE__) +extern char** environ; +#endif + +namespace datadog::tracing { +namespace { + +constexpr std::size_t max_config_file_size = 100 * 1024 * 1024; + +Expected read_config_file(const std::string& path, + const char* source) { + const std::string prefix = + "Unable to load stable configuration: failed to read " + + std::string{source} + " config file: "; +#if defined(_WIN32) + if (path.size() > INT_MAX) + return Error{Error::OTHER, prefix + "path is too long"}; + const int length = + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.data(), + static_cast(path.size()), nullptr, 0); + if (length == 0) return Error{Error::OTHER, prefix + "invalid UTF-8 path"}; + std::wstring wide_path(static_cast(length), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.data(), + static_cast(path.size()), wide_path.data(), length); + const std::filesystem::path native_path{wide_path}; +#else + const std::filesystem::path native_path{path}; +#endif + std::error_code error; + const std::uintmax_t size = std::filesystem::file_size(native_path, error); + if (error == std::errc::no_such_file_or_directory) return std::string{}; + if (error) return Error{Error::OTHER, prefix + error.message()}; + if (size > max_config_file_size) return std::string{}; + std::ifstream file(native_path, std::ios::binary); + if (!file) return Error{Error::OTHER, prefix + std::strerror(errno)}; + std::string content(static_cast(size), '\0'); + file.read(content.data(), static_cast(content.size())); + if (file.bad()) return Error{Error::OTHER, prefix + "read failed"}; + content.resize(static_cast(file.gcount())); + return content; +} + +std::string local_default_path() { +#if defined(_WIN32) + return "C:\\ProgramData\\Datadog\\application_monitoring.yaml"; +#elif defined(__APPLE__) + return "/opt/datadog-agent/etc/application_monitoring.yaml"; +#else + return "/etc/datadog-agent/application_monitoring.yaml"; +#endif +} + +std::string fleet_default_path() { +#if defined(_WIN32) + return "C:\\ProgramData\\Datadog\\managed\\datadog-" + "agent\\stable\\application_monitoring.yaml"; +#elif defined(__APPLE__) + return "/opt/datadog-agent/etc/stable/application_monitoring.yaml"; +#else + return "/etc/datadog-agent/managed/datadog-agent/stable/" + "application_monitoring.yaml"; +#endif +} + +#if defined(_WIN32) +std::string utf8(const wchar_t* value) { + const int length = static_cast(std::wcslen(value)); + const int size = WideCharToMultiByte(CP_UTF8, 0, value, length, nullptr, 0, + nullptr, nullptr); + std::string result(static_cast(size), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value, length, result.data(), size, nullptr, + nullptr); + return result; +} +#endif + +stable_config_internal::ProcessInfo current_process(StringView language) { + stable_config_internal::ProcessInfo result; + result.language = std::string{language}; +#if defined(_WIN32) + int count = 0; + wchar_t** args = CommandLineToArgvW(GetCommandLineW(), &count); + if (args) { + for (int index = 0; index < count; ++index) { + result.args.push_back(utf8(args[index])); + } + LocalFree(args); + } + wchar_t* environment = GetEnvironmentStringsW(); + if (environment) { + for (const wchar_t* entry = environment; *entry; + entry += std::wcslen(entry) + 1) { + result.environment.push_back(utf8(entry)); + } + FreeEnvironmentStringsW(environment); + } +#elif defined(__APPLE__) + for (int index = 0; index < *_NSGetArgc(); ++index) { + result.args.emplace_back((*_NSGetArgv())[index]); + } + for (char** entry = *_NSGetEnviron(); entry && *entry; ++entry) { + result.environment.emplace_back(*entry); + } +#else + std::ifstream args("/proc/self/cmdline", std::ios::binary); + std::string item; + while (std::getline(args, item, '\0')) result.args.push_back(item); + for (char** entry = environ; entry && *entry; ++entry) { + result.environment.emplace_back(*entry); + } +#endif + return result; +} + +} // namespace + +Expected load_stable_config(StringView language, + StringView local_path, + StringView fleet_path) { + Expected local_yaml = read_config_file( + local_path.empty() ? local_default_path() : std::string{local_path}, + "local"); + if (Error* error = local_yaml.if_error()) return *error; + Expected fleet_yaml = read_config_file( + fleet_path.empty() ? fleet_default_path() : std::string{fleet_path}, + "fleet"); + if (Error* error = fleet_yaml.if_error()) return *error; + return stable_config_internal::load_yaml(*local_yaml, *fleet_yaml, + current_process(language)); +} + +Expected finalize_config_with_stable_config( + const TracerConfig& config, StringView language, StringView local_path, + StringView fleet_path) { + Expected stable_config = + load_stable_config(language, local_path, fleet_path); + if (Error* error = stable_config.if_error()) return *error; + return finalize_config(config, *stable_config); +} + +} // namespace datadog::tracing diff --git a/src/datadog/stable_config_loader_internal.h b/src/datadog/stable_config_loader_internal.h new file mode 100644 index 00000000..3d8621f7 --- /dev/null +++ b/src/datadog/stable_config_loader_internal.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include +#include + +namespace datadog::tracing::stable_config_internal { + +struct ProcessInfo { + std::string language; + std::vector args; + std::vector environment; +}; + +Expected load_yaml(StringView local_yaml, StringView fleet_yaml, + const ProcessInfo& process); + +} // namespace datadog::tracing::stable_config_internal diff --git a/src/datadog/stable_config_parser.cpp b/src/datadog/stable_config_parser.cpp new file mode 100644 index 00000000..fb3cfd6c --- /dev/null +++ b/src/datadog/stable_config_parser.cpp @@ -0,0 +1,296 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "stable_config_loader_internal.h" + +namespace datadog::tracing::stable_config_internal { +namespace { + +using Map = std::map; + +struct Selector { + std::string origin; + Optional key; + std::string operation; + std::vector matches; +}; + +struct Rule { + std::vector selectors; + Map configuration; +}; + +struct ParsedConfig { + std::string config_id; + Map defaults; + Map tags; + std::vector rules; +}; + +// Values a selector or template can read by key. +struct Context { + const ProcessInfo& process; + const Map& tags; + Map args; + Map environment; + + const Map* values(const std::string& origin) const { + if (origin == "tags") return &tags; + if (origin == "process_arguments") return &args; + if (origin == "environment_variables") return &environment; + return nullptr; + } +}; + +std::string scalar(const YAML::Node& node, const char* name) { + if (!node || !node.IsScalar()) { + throw std::runtime_error(std::string{name} + " must be a scalar"); + } + return node.Scalar(); +} + +std::string one_of(const YAML::Node& node, const char* name, + std::initializer_list allowed) { + std::string value = scalar(node, name); + if (std::find(allowed.begin(), allowed.end(), value) == allowed.end()) { + throw std::runtime_error("unknown " + std::string{name} + ": " + value); + } + return value; +} + +// Later duplicate keys win. Non-scalar keys are skipped. +Map parse_map(const YAML::Node& node, const char* name) { + if (!node || !node.IsMap()) { + throw std::runtime_error(std::string{name} + " must be a map"); + } + Map entries; + for (YAML::const_iterator entry = node.begin(); entry != node.end(); + ++entry) { + if (!entry->first.IsScalar()) continue; + entries.insert_or_assign(entry->first.Scalar(), + scalar(entry->second, "map value")); + } + return entries; +} + +Selector parse_selector(const YAML::Node& node) { + if (!node.IsMap()) throw std::runtime_error("selector must be a map"); + Selector result; + result.origin = one_of( + node["origin"], "selector origin", + {"language", "process_arguments", "environment_variables", "tags"}); + if (const YAML::Node key = node["key"]; key && !key.IsNull()) { + result.key = scalar(key, "selector key"); + } + result.operation = + one_of(node["operator"], "selector operator", + {"exists", "equals", "prefix_matches", "suffix_matches"}); + if (result.operation != "exists") { + const YAML::Node matches = node["matches"]; + if (!matches || !matches.IsSequence()) { + throw std::runtime_error("selector matches must be a sequence"); + } + for (const YAML::Node& match : matches) { + result.matches.push_back(scalar(match, "selector match")); + } + } + return result; +} + +Rule parse_rule(const YAML::Node& node) { + if (!node.IsMap()) throw std::runtime_error("rule must be a map"); + const YAML::Node selectors = node["selectors"]; + if (!selectors || !selectors.IsSequence()) { + throw std::runtime_error("rule selectors must be a sequence"); + } + Rule rule; + for (const YAML::Node& selector : selectors) { + rule.selectors.push_back(parse_selector(selector)); + } + rule.configuration = parse_map(node["configuration"], "rule configuration"); + return rule; +} + +ParsedConfig parse_yaml(StringView content) { + const YAML::Node document = YAML::Load(std::string{content}); + ParsedConfig result; + if (!document || document.IsNull()) return result; + if (!document.IsMap()) throw std::runtime_error("root must be a map"); + if (const YAML::Node id = document["config_id"]; id && !id.IsNull()) { + result.config_id = scalar(id, "config_id"); + } + if (document["apm_configuration_default"]) { + result.defaults = parse_map(document["apm_configuration_default"], + "apm_configuration_default"); + } + if (document["tags"]) result.tags = parse_map(document["tags"], "tags"); + if (const YAML::Node rules = document["rules"]; rules) { + if (!rules.IsSequence()) + throw std::runtime_error("rules must be a sequence"); + for (const YAML::Node& item : rules) { + result.rules.push_back(parse_rule(item)); + } + } + return result; +} + +bool valid_utf8(const std::string& value) { + for (std::size_t index = 0; index < value.size();) { + const unsigned char first = static_cast(value[index]); + if (first < 0x80) { + ++index; + continue; + } + const std::size_t length = first >= 0xf0 ? 4 : first >= 0xe0 ? 3 : 2; + if (first < 0xc2 || first > 0xf4 || index + length > value.size()) + return false; + for (std::size_t offset = 1; offset < length; ++offset) { + const unsigned char next = + static_cast(value[index + offset]); + if ((next & 0xc0) != 0x80) return false; + } + const unsigned char second = static_cast(value[index + 1]); + if ((first == 0xe0 && second < 0xa0) || (first == 0xed && second >= 0xa0) || + (first == 0xf0 && second < 0x90) || (first == 0xf4 && second >= 0x90)) + return false; + index += length; + } + return true; +} + +Map split_entries(const std::vector& entries, + bool include_without_equals) { + Map result; + for (const std::string& entry : entries) { + if (!valid_utf8(entry)) continue; + const std::size_t equal = entry.find('='); + if (equal == std::string::npos && !include_without_equals) continue; + result.insert_or_assign( + entry.substr(0, equal), + equal == std::string::npos ? "" : entry.substr(equal + 1)); + } + return result; +} + +bool string_match(const Selector& selector, const std::string& value) { + if (selector.operation == "exists") return true; + for (const std::string& match : selector.matches) { + if (selector.operation == "equals" && value == match) return true; + if (selector.operation == "prefix_matches" && + value.compare(0, match.size(), match) == 0) + return true; + if (selector.operation == "suffix_matches" && + value.size() >= match.size() && + value.compare(value.size() - match.size(), match.size(), match) == 0) { + return true; + } + } + return false; +} + +bool selector_matches(const Selector& selector, const Context& context) { + if (selector.origin == "language") { + return string_match(selector, context.process.language); + } + if (selector.key) { + const Map& values = *context.values(selector.origin); + const auto found = values.find(*selector.key); + return found != values.end() && string_match(selector, found->second); + } + if (selector.origin == "tags") return false; + const std::vector& values = + selector.origin == "process_arguments" ? context.process.args + : context.process.environment; + return std::any_of(values.begin(), values.end(), [&](const std::string& v) { + return string_match(selector, v); + }); +} + +std::string trim(std::string value) { + const std::size_t start = value.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) return ""; + const std::size_t end = value.find_last_not_of(" \t\n\r"); + return value.substr(start, end - start + 1); +} + +// Resolve `language` or `origin[key]`. Unknown values become "UNDEFINED". +std::string resolve_template_variable(const std::string& variable, + const Context& context) { + const std::size_t bracket = variable.find('['); + const std::string name = variable.substr(0, bracket); + if (name == "language") return context.process.language; + const std::size_t end = variable.find(']', bracket); + const Map* values = context.values(name); + if (end == std::string::npos || !values) return "UNDEFINED"; + const auto found = + values->find(trim(variable.substr(bracket + 1, end - bracket - 1))); + return found == values->end() ? "UNDEFINED" : found->second; +} + +std::string template_value(const std::string& input, const Context& context) { + std::string output; + std::size_t cursor = 0; + while (true) { + const std::size_t open = input.find("{{", cursor); + if (open == std::string::npos) return output + input.substr(cursor); + output += input.substr(cursor, open - cursor); + const std::size_t close = input.find("}}", open + 2); + if (close == std::string::npos) + throw std::runtime_error("unterminated template in config"); + const std::string variable = trim(input.substr(open + 2, close - open - 2)); + output += resolve_template_variable(variable, context); + cursor = close + 2; + } +} + +// Apply the defaults, then the first rule that matches. +void apply_source(StableConfig& result, const ParsedConfig& source, + StableConfigSource origin, const ProcessInfo& process) { + for (const auto& [name, value] : source.defaults) { + result.set(name, {value, origin, source.config_id}); + } + const Context context{process, source.tags, + split_entries(process.args, false), + split_entries(process.environment, true)}; + for (const Rule& rule : source.rules) { + if (!std::all_of(rule.selectors.begin(), rule.selectors.end(), + [&](const Selector& selector) { + return selector_matches(selector, context); + })) { + continue; + } + for (const auto& [name, value] : rule.configuration) { + result.set(name, + {template_value(value, context), origin, source.config_id}); + } + return; + } +} + +} // namespace + +Expected load_yaml(StringView local_yaml, StringView fleet_yaml, + const ProcessInfo& process) { + try { + if (!valid_utf8(process.language)) { + throw std::runtime_error("language is not UTF-8"); + } + const ParsedConfig local = parse_yaml(local_yaml); + const ParsedConfig fleet = parse_yaml(fleet_yaml); + StableConfig result; + apply_source(result, local, StableConfigSource::LOCAL, process); + apply_source(result, fleet, StableConfigSource::FLEET, process); + return result; + } catch (const std::exception& error) { + return Error{Error::OTHER, "Unable to load stable configuration: " + + std::string{error.what()}}; + } +} + +} // namespace datadog::tracing::stable_config_internal diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 33159ea3..45ba6099 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -761,6 +761,16 @@ 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"; + break; + } + + if (config_metadata.config_id) { + j["config_id"] = *config_metadata.config_id; } if (config_metadata.error) { diff --git a/src/datadog/trace_sampler_config.cpp b/src/datadog/trace_sampler_config.cpp index b573bae8..b3288edf 100644 --- a/src/datadog/trace_sampler_config.cpp +++ b/src/datadog/trace_sampler_config.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -15,10 +16,12 @@ namespace datadog { namespace tracing { namespace { -Expected load_trace_sampler_env_config() { +Expected load_trace_sampler_env_config( + const StableConfig *stable_config) { TraceSamplerConfig env_config; - if (auto rules_env = lookup(environment::DD_TRACE_SAMPLING_RULES)) { + if (auto rules_env = + lookup(environment::DD_TRACE_SAMPLING_RULES, stable_config)) { nlohmann::json json_rules; try { json_rules = nlohmann::json::parse(*rules_env); @@ -107,7 +110,8 @@ Expected load_trace_sampler_env_config() { } } - if (auto sample_rate_env = lookup(environment::DD_TRACE_SAMPLE_RATE)) { + if (auto sample_rate_env = + lookup(environment::DD_TRACE_SAMPLE_RATE, stable_config)) { auto maybe_sample_rate = parse_double(*sample_rate_env); if (auto *error = maybe_sample_rate.if_error()) { std::string prefix; @@ -119,7 +123,8 @@ Expected load_trace_sampler_env_config() { env_config.sample_rate = *maybe_sample_rate; } - if (auto limit_env = lookup(environment::DD_TRACE_RATE_LIMIT)) { + if (auto limit_env = + lookup(environment::DD_TRACE_RATE_LIMIT, stable_config)) { auto maybe_max_per_second = parse_double(*limit_env); if (auto *error = maybe_max_per_second.if_error()) { std::string prefix; @@ -150,8 +155,9 @@ std::string to_string(const std::vector &rules) { TraceSamplerConfig::Rule::Rule(const SpanMatcher &base) : SpanMatcher(base) {} Expected finalize_config( - const TraceSamplerConfig &config) { - Expected env_config = load_trace_sampler_env_config(); + const TraceSamplerConfig &config, const StableConfig *stable_config) { + Expected env_config = + load_trace_sampler_env_config(stable_config); if (auto error = env_config.if_error()) { return *error; } @@ -160,11 +166,15 @@ Expected finalize_config( std::vector rules; - if (!env_config->rules.empty()) { + // With stable config, code rules win over the environment. + const bool code_wins = stable_config && !config.rules.empty(); + if (!code_wins && !env_config->rules.empty()) { rules = std::move(env_config->rules); + const ConfigValueSource source = config_value_source( + environment::DD_TRACE_SAMPLING_RULES, stable_config); result.metadata[ConfigName::TRACE_SAMPLING_RULES] = { ConfigMetadata(ConfigName::TRACE_SAMPLING_RULES, to_string(rules), - ConfigMetadata::Origin::ENVIRONMENT_VARIABLE)}; + source.origin, nullopt, source.config_id)}; } else if (!config.rules.empty()) { rules = std::move(config.rules); result.metadata[ConfigName::TRACE_SAMPLING_RULES] = { @@ -191,10 +201,10 @@ Expected finalize_config( result.rules.emplace_back(std::move(finalized_rule)); } - Optional sample_rate = resolve_and_record_config( + Optional sample_rate = resolve_with_stable_config( env_config->sample_rate, config.sample_rate, &result.metadata, - ConfigName::TRACE_SAMPLING_RATE, 1.0, - [](const double &d) { return to_string(d, 1); }); + ConfigName::TRACE_SAMPLING_RATE, environment::DD_TRACE_SAMPLE_RATE, + stable_config, 1.0, [](const double &d) { return to_string(d, 1); }); bool is_sample_rate_provided = env_config->sample_rate || config.sample_rate; // If `sample_rate` was specified, then it translates to a "catch-all" rule @@ -214,10 +224,10 @@ Expected finalize_config( result.rules.emplace_back(std::move(finalized_rule)); } - double max_per_second = resolve_and_record_config( + double max_per_second = resolve_with_stable_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); }); + ConfigName::TRACE_SAMPLING_LIMIT, environment::DD_TRACE_RATE_LIMIT, + stable_config, 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 4fe4f2fc..5aca583b 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -71,8 +72,8 @@ Expected> parse_propagation_styles( // If `env_var` is not in the environment, return `nullopt`. If an error occurs, // throw an `Error`. Optional> styles_from_env( - environment::Variable env_var) { - const auto styles_env = lookup(env_var); + environment::Variable env_var, const StableConfig *stable_config) { + const auto styles_env = lookup(env_var, stable_config); if (!styles_env) { return {}; } @@ -94,21 +95,30 @@ std::string json_quoted(StringView text) { return nlohmann::json(std::move(unquoted)).dump(); } -Expected load_tracer_env_config(Logger &logger) { +environment::Variable propagation_style_source( + environment::Variable specific, environment::Variable legacy, + const StableConfig *stable_config) { + if (lookup(specific, stable_config)) return specific; + if (lookup(legacy, stable_config)) return legacy; + return environment::DD_TRACE_PROPAGATION_STYLE; +} + +Expected load_tracer_env_config( + Logger &logger, const StableConfig *stable_config) { TracerConfig env_cfg; - if (auto service_env = lookup(environment::DD_SERVICE)) { + if (auto service_env = lookup(environment::DD_SERVICE, stable_config)) { env_cfg.service = std::string{*service_env}; } - if (auto environment_env = lookup(environment::DD_ENV)) { + if (auto environment_env = lookup(environment::DD_ENV, stable_config)) { env_cfg.environment = std::string{*environment_env}; } - if (auto version_env = lookup(environment::DD_VERSION)) { + if (auto version_env = lookup(environment::DD_VERSION, stable_config)) { env_cfg.version = std::string{*version_env}; } - if (auto tags_env = lookup(environment::DD_TAGS)) { + if (auto tags_env = lookup(environment::DD_TAGS, stable_config)) { auto tags = parse_tags(*tags_env); if (auto *error = tags.if_error()) { std::string prefix; @@ -120,34 +130,38 @@ Expected load_tracer_env_config(Logger &logger) { env_cfg.tags = std::move(*tags); } - if (auto startup_env = lookup(environment::DD_TRACE_STARTUP_LOGS)) { + if (auto startup_env = + lookup(environment::DD_TRACE_STARTUP_LOGS, stable_config)) { env_cfg.log_on_startup = !falsy(*startup_env); } - if (auto enabled_env = lookup(environment::DD_TRACE_ENABLED)) { + if (auto enabled_env = lookup(environment::DD_TRACE_ENABLED, stable_config)) { env_cfg.report_traces = !falsy(*enabled_env); } if (auto enabled_env = - lookup(environment::DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED)) { + lookup(environment::DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, + stable_config)) { env_cfg.generate_128bit_trace_ids = !falsy(*enabled_env); } - if (auto apm_enabled_env = lookup(environment::DD_APM_TRACING_ENABLED)) { + if (auto apm_enabled_env = + lookup(environment::DD_APM_TRACING_ENABLED, stable_config)) { env_cfg.tracing_enabled = !falsy(*apm_enabled_env); } - if (auto resource_renaming_enabled_env = - lookup(environment::DD_TRACE_RESOURCE_RENAMING_ENABLED)) { + if (auto resource_renaming_enabled_env = lookup( + environment::DD_TRACE_RESOURCE_RENAMING_ENABLED, stable_config)) { env_cfg.resource_renaming_enabled = !falsy(*resource_renaming_enabled_env); } if (auto resource_renaming_always_simplified_endpoint_env = lookup( - environment::DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT)) { + environment::DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, + stable_config)) { env_cfg.resource_renaming_always_simplified_endpoint = !falsy(*resource_renaming_always_simplified_endpoint_env); } // Baggage if (auto baggage_items_env = - lookup(environment::DD_TRACE_BAGGAGE_MAX_ITEMS)) { + lookup(environment::DD_TRACE_BAGGAGE_MAX_ITEMS, stable_config)) { auto maybe_value = parse_uint64(*baggage_items_env, 10); if (auto *error = maybe_value.if_error()) { return *error; @@ -157,7 +171,7 @@ Expected load_tracer_env_config(Logger &logger) { } if (auto baggage_bytes_env = - lookup(environment::DD_TRACE_BAGGAGE_MAX_BYTES)) { + lookup(environment::DD_TRACE_BAGGAGE_MAX_BYTES, stable_config)) { auto maybe_value = parse_uint64(*baggage_bytes_env, 10); if (auto *error = maybe_value.if_error()) { return *error; @@ -219,11 +233,11 @@ Expected load_tracer_env_config(Logger &logger) { }; for (const auto &[var, var_override] : questionable_combinations) { - const auto value = lookup(var); + const auto value = lookup(var, stable_config); if (!value) { continue; } - const auto value_override = lookup(var_override); + const auto value_override = lookup(var_override, stable_config); if (!value_override) { continue; } @@ -237,7 +251,7 @@ Expected load_tracer_env_config(Logger &logger) { } const auto propagation_behavior_extract = - lookup(environment::DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT); + lookup(environment::DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT, stable_config); if (propagation_behavior_extract.has_value()) { env_cfg.propagation_behavior_extract = parse_propagation_behavior_extract( propagation_behavior_extract.value()); @@ -245,23 +259,23 @@ Expected load_tracer_env_config(Logger &logger) { try { const auto global_styles = - styles_from_env(environment::DD_TRACE_PROPAGATION_STYLE); + styles_from_env(environment::DD_TRACE_PROPAGATION_STYLE, stable_config); - if (auto trace_extraction_styles = - styles_from_env(environment::DD_TRACE_PROPAGATION_STYLE_EXTRACT)) { + if (auto trace_extraction_styles = styles_from_env( + environment::DD_TRACE_PROPAGATION_STYLE_EXTRACT, stable_config)) { env_cfg.extraction_styles = std::move(*trace_extraction_styles); - } else if (auto extraction_styles = - styles_from_env(environment::DD_PROPAGATION_STYLE_EXTRACT)) { + } else if (auto extraction_styles = styles_from_env( + environment::DD_PROPAGATION_STYLE_EXTRACT, stable_config)) { env_cfg.extraction_styles = std::move(*extraction_styles); } else { env_cfg.extraction_styles = global_styles; } - if (auto trace_injection_styles = - styles_from_env(environment::DD_TRACE_PROPAGATION_STYLE_INJECT)) { + if (auto trace_injection_styles = styles_from_env( + environment::DD_TRACE_PROPAGATION_STYLE_INJECT, stable_config)) { env_cfg.injection_styles = std::move(*trace_injection_styles); - } else if (auto injection_styles = - styles_from_env(environment::DD_PROPAGATION_STYLE_INJECT)) { + } else if (auto injection_styles = styles_from_env( + environment::DD_PROPAGATION_STYLE_INJECT, stable_config)) { env_cfg.injection_styles = std::move(*injection_styles); } else { env_cfg.injection_styles = global_styles; @@ -276,15 +290,28 @@ Expected load_tracer_env_config(Logger &logger) { } // namespace Expected finalize_config(const TracerConfig &config) { - return finalize_config(config, default_clock); + return finalize_config(config, nullptr, default_clock); } -Expected finalize_config(const TracerConfig &user_config, +Expected finalize_config(const TracerConfig &config, const Clock &clock) { + return finalize_config(config, nullptr, clock); +} + +Expected finalize_config( + const TracerConfig &config, const StableConfig &stable_config, + const Clock &clock) { + return finalize_config(config, &stable_config, clock); +} + +Expected finalize_config( + const TracerConfig &user_config, const StableConfig *stable_config, + const Clock &clock) { auto logger = user_config.logger ? user_config.logger : std::make_shared(); - Expected env_config = load_tracer_env_config(*logger); + Expected env_config = + load_tracer_env_config(*logger, stable_config); if (auto error = env_config.if_error()) { return *error; } @@ -294,31 +321,34 @@ Expected finalize_config(const TracerConfig &user_config, final_config.logger = logger; // DD_SERVICE - final_config.defaults.service = resolve_and_record_config( + final_config.defaults.service = resolve_with_stable_config( env_config->service, user_config.service, &final_config.metadata, - ConfigName::SERVICE_NAME, get_process_name()); + ConfigName::SERVICE_NAME, environment::DD_SERVICE, stable_config, + get_process_name()); // Service type - final_config.defaults.service_type = - value_or(env_config->service_type, user_config.service_type, "web"); + final_config.defaults.service_type = choose_with_stable_config( + env_config->service_type, user_config.service_type, "web", stable_config); // DD_ENV - final_config.defaults.environment = resolve_and_record_config( + final_config.defaults.environment = resolve_with_stable_config( env_config->environment, user_config.environment, &final_config.metadata, - ConfigName::SERVICE_ENV); + ConfigName::SERVICE_ENV, environment::DD_ENV, stable_config); // DD_VERSION - final_config.defaults.version = resolve_and_record_config( + final_config.defaults.version = resolve_with_stable_config( env_config->version, user_config.version, &final_config.metadata, - ConfigName::SERVICE_VERSION); + ConfigName::SERVICE_VERSION, environment::DD_VERSION, stable_config); // Span name - final_config.defaults.name = value_or(env_config->name, user_config.name, ""); + final_config.defaults.name = choose_with_stable_config( + env_config->name, user_config.name, "", stable_config); // DD_TAGS - final_config.defaults.tags = resolve_and_record_config( + final_config.defaults.tags = resolve_with_stable_config( env_config->tags, user_config.tags, &final_config.metadata, - ConfigName::TAGS, std::unordered_map{}, + ConfigName::TAGS, environment::DD_TAGS, stable_config, + std::unordered_map{}, [](const auto &tags) { return join_tags(tags); }); // Extraction Styles @@ -326,10 +356,13 @@ Expected finalize_config(const TracerConfig &user_config, PropagationStyle::DATADOG, PropagationStyle::W3C, PropagationStyle::BAGGAGE}; - final_config.extraction_styles = resolve_and_record_config( + final_config.extraction_styles = resolve_with_stable_config( env_config->extraction_styles, user_config.extraction_styles, &final_config.metadata, ConfigName::EXTRACTION_STYLES, - default_propagation_styles, + propagation_style_source(environment::DD_TRACE_PROPAGATION_STYLE_EXTRACT, + environment::DD_PROPAGATION_STYLE_EXTRACT, + stable_config), + stable_config, default_propagation_styles, [](const std::vector &styles) { return join_propagation_styles(styles); }); @@ -340,10 +373,13 @@ Expected finalize_config(const TracerConfig &user_config, } // Injection Styles - final_config.injection_styles = resolve_and_record_config( + final_config.injection_styles = resolve_with_stable_config( env_config->injection_styles, user_config.injection_styles, &final_config.metadata, ConfigName::INJECTION_STYLES, - default_propagation_styles, + propagation_style_source(environment::DD_TRACE_PROPAGATION_STYLE_INJECT, + environment::DD_PROPAGATION_STYLE_INJECT, + stable_config), + stable_config, default_propagation_styles, [](const std::vector &styles) { return join_propagation_styles(styles); }); @@ -354,49 +390,57 @@ Expected finalize_config(const TracerConfig &user_config, } // Startup Logs - final_config.log_on_startup = resolve_and_record_config( + final_config.log_on_startup = resolve_with_stable_config( env_config->log_on_startup, user_config.log_on_startup, - &final_config.metadata, ConfigName::STARTUP_LOGS, true, + &final_config.metadata, ConfigName::STARTUP_LOGS, + environment::DD_TRACE_STARTUP_LOGS, stable_config, true, [](const bool &b) { return to_string(b); }); // Report traces - final_config.report_traces = resolve_and_record_config( + final_config.report_traces = resolve_with_stable_config( env_config->report_traces, user_config.report_traces, - &final_config.metadata, ConfigName::REPORT_TRACES, true, + &final_config.metadata, ConfigName::REPORT_TRACES, + environment::DD_TRACE_ENABLED, stable_config, true, [](const bool &b) { return to_string(b); }); // Report hostname - final_config.report_hostname = - value_or(env_config->report_hostname, user_config.report_hostname, false); + final_config.report_hostname = choose_with_stable_config( + env_config->report_hostname, user_config.report_hostname, false, + stable_config); // Tags Header Size - final_config.tags_header_size = value_or( - env_config->max_tags_header_size, user_config.max_tags_header_size, 512); + final_config.tags_header_size = choose_with_stable_config( + env_config->max_tags_header_size, user_config.max_tags_header_size, + std::size_t{512}, stable_config); // 128b Trace IDs - final_config.generate_128bit_trace_ids = resolve_and_record_config( + final_config.generate_128bit_trace_ids = resolve_with_stable_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); }); + ConfigName::GENEREATE_128BIT_TRACE_IDS, + environment::DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED, stable_config, + true, [](const bool &b) { return to_string(b); }); // Integration name & version - final_config.integration_name = value_or( - env_config->integration_name, user_config.integration_name, "datadog"); - final_config.integration_version = - value_or(env_config->integration_version, user_config.integration_version, - tracer_version); + final_config.integration_name = choose_with_stable_config( + env_config->integration_name, user_config.integration_name, "datadog", + stable_config); + final_config.integration_version = choose_with_stable_config( + env_config->integration_version, user_config.integration_version, + tracer_version, stable_config); // Baggage - max items - final_config.baggage_opts.max_items = resolve_and_record_config( + final_config.baggage_opts.max_items = resolve_with_stable_config( env_config->baggage_max_items, user_config.baggage_max_items, - &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_ITEMS, 64UL, + &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_ITEMS, + environment::DD_TRACE_BAGGAGE_MAX_ITEMS, stable_config, 64UL, [](const size_t &i) { return std::to_string(i); }); // Baggage - max bytes - final_config.baggage_opts.max_bytes = resolve_and_record_config( + final_config.baggage_opts.max_bytes = resolve_with_stable_config( env_config->baggage_max_bytes, user_config.baggage_max_bytes, - &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_BYTES, 8192UL, + &final_config.metadata, ConfigName::TRACE_BAGGAGE_MAX_BYTES, + environment::DD_TRACE_BAGGAGE_MAX_BYTES, stable_config, 8192UL, [](const size_t &i) { return std::to_string(i); }); if (final_config.baggage_opts.max_items <= 0 || @@ -412,10 +456,11 @@ Expected finalize_config(const TracerConfig &user_config, final_config.injection_styles.erase(it); } - final_config.propagation_behavior_extract = resolve_and_record_config( + final_config.propagation_behavior_extract = resolve_with_stable_config( env_config->propagation_behavior_extract, user_config.propagation_behavior_extract, &final_config.metadata, ConfigName::PROPAGATION_BEHAVIOR_EXTRACT, + environment::DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT, stable_config, PropagationBehaviorExtract::CONTINUE, [](const PropagationBehaviorExtract &behavior) { return std::string{to_string_view(behavior)}; @@ -425,13 +470,14 @@ Expected finalize_config(const TracerConfig &user_config, final_config.root_session_id = user_config.root_session_id; final_config.process_tags = user_config.process_tags; - auto agent_finalized = - finalize_config(user_config.agent, final_config.logger, clock); + auto agent_finalized = finalize_config(user_config.agent, final_config.logger, + clock, stable_config); if (auto *error = agent_finalized.if_error()) { 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_config)) { // Merge metadata vectors for (auto &[key, values] : trace_sampler_config->metadata) { auto &dest = final_config.metadata[key]; @@ -443,7 +489,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_config)) { // Merge metadata vectors for (auto &[key, values] : span_sampler_config->metadata) { auto &dest = final_config.metadata[key]; @@ -469,35 +515,35 @@ 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.products.emplace_back(telemetry::Product{ - telemetry::Product::Name::tracing, true, tracer_version, nullopt, - nullopt, final_config.metadata}); } else { return std::move(telemetry_final_config.error()); } // APM Tracing Enabled - final_config.tracing_enabled = resolve_and_record_config( + final_config.tracing_enabled = resolve_with_stable_config( env_config->tracing_enabled, user_config.tracing_enabled, - &final_config.metadata, ConfigName::APM_TRACING_ENABLED, true, + &final_config.metadata, ConfigName::APM_TRACING_ENABLED, + environment::DD_APM_TRACING_ENABLED, stable_config, true, [](const bool &b) { return to_string(b); }); { // Resource Renaming Enabled - const bool resource_renaming_enabled = resolve_and_record_config( + const bool resource_renaming_enabled = resolve_with_stable_config( env_config->resource_renaming_enabled, user_config.resource_renaming_enabled, &final_config.metadata, - ConfigName::TRACE_RESOURCE_RENAMING_ENABLED, false, + ConfigName::TRACE_RESOURCE_RENAMING_ENABLED, + environment::DD_TRACE_RESOURCE_RENAMING_ENABLED, stable_config, false, [](const bool &b) { return to_string(b); }); // Resource Renaming Always Simplified Endpoint const bool resource_renaming_always_simplified_endpoint = - resolve_and_record_config( + resolve_with_stable_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); }); + environment::DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, + stable_config, false, [](const bool &b) { return to_string(b); }); if (!resource_renaming_enabled) { final_config.resource_renaming_mode = @@ -535,6 +581,10 @@ Expected finalize_config(const TracerConfig &user_config, final_config.collector = user_config.collector; } + final_config.telemetry.products.emplace_back(telemetry::Product{ + telemetry::Product::Name::tracing, true, tracer_version, nullopt, nullopt, + final_config.metadata}); + return final_config; } diff --git a/test/bazel_stable_config_test.cpp b/test/bazel_stable_config_test.cpp new file mode 100644 index 00000000..5d083575 --- /dev/null +++ b/test/bazel_stable_config_test.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include +#include + +using namespace datadog::tracing; + +namespace { + +struct TestHTTPClient : HTTPClient { + Expected post(const URL&, HeadersSetter, std::string, ResponseHandler, + ErrorHandler, + std::chrono::steady_clock::time_point) override { + return {}; + } + + void drain(std::chrono::steady_clock::time_point) override {} + + std::string config() const override { return "{}"; } +}; + +} // namespace + +int main() { + const char* runfiles = std::getenv("TEST_SRCDIR"); + const char* workspace = std::getenv("TEST_WORKSPACE"); + if (!runfiles || !workspace) return 1; + const std::string base = + std::string(runfiles) + "/" + workspace + "/test/fixtures/stable_config/"; + + TracerConfig config; + config.agent.http_client = std::make_shared(); + auto finalized = finalize_config_with_stable_config( + config, "cpp", base + "local.yaml", base + "fleet.yaml"); + if (!finalized) return 2; + if (finalized->defaults.service != "fleet-service") return 3; + if (finalized->extraction_styles.size() != 1 || + finalized->extraction_styles.front() != PropagationStyle::B3) { + return 4; + } + return 0; +} diff --git a/test/fixtures/stable_config/fleet.yaml b/test/fixtures/stable_config/fleet.yaml new file mode 100644 index 00000000..ab6b9dc2 --- /dev/null +++ b/test/fixtures/stable_config/fleet.yaml @@ -0,0 +1,4 @@ +config_id: fleet-test +apm_configuration_default: + DD_SERVICE: fleet-service + DD_TRACE_SAMPLE_RATE: "0.5" diff --git a/test/fixtures/stable_config/invalid.yaml b/test/fixtures/stable_config/invalid.yaml new file mode 100644 index 00000000..fe111806 --- /dev/null +++ b/test/fixtures/stable_config/invalid.yaml @@ -0,0 +1 @@ +rules: [ diff --git a/test/fixtures/stable_config/local.yaml b/test/fixtures/stable_config/local.yaml new file mode 100644 index 00000000..2486b575 --- /dev/null +++ b/test/fixtures/stable_config/local.yaml @@ -0,0 +1,15 @@ +config_id: local-test +apm_configuration_default: + DD_SERVICE: local-service + DD_ENV: local-env + DD_VERSION: local-version + DD_APM_TRACING_ENABLED: "false" + DD_TRACE_SAMPLE_RATE: "0.25" + DD_TRACE_AGENT_URL: http://local-agent:8126 +rules: + - selectors: + - origin: language + matches: ["cpp"] + operator: equals + configuration: + DD_TRACE_PROPAGATION_STYLE_EXTRACT: b3 diff --git a/test/test_stable_config.cpp b/test/test_stable_config.cpp new file mode 100644 index 00000000..72a28bd0 --- /dev/null +++ b/test/test_stable_config.cpp @@ -0,0 +1,257 @@ +#include + +#include +#include +#include + +#include "common/environment.h" +#include "mocks/http_clients.h" +#include "stable_config_loader_internal.h" +#include "test.h" + +using namespace datadog::tracing; +using datadog::test::EnvGuard; + +#define STABLE_CONFIG_TEST(name) TEST_CASE(name, "[stable.config]") + +STABLE_CONFIG_TEST("stable config observes source precedence") { + const std::string local = std::string{DD_TRACE_SOURCE_DIR} + + "/test/fixtures/stable_config/local.yaml"; + const std::string fleet = std::string{DD_TRACE_SOURCE_DIR} + + "/test/fixtures/stable_config/fleet.yaml"; + EnvGuard service_env{"DD_SERVICE", "env-service"}; + EnvGuard environment_env{"DD_ENV", "env-environment"}; + EnvGuard version_env{"DD_VERSION"}; + EnvGuard sample_rate_env{"DD_TRACE_SAMPLE_RATE", "0.75"}; + EnvGuard apm_enabled_env{"DD_APM_TRACING_ENABLED"}; + EnvGuard agent_url_env{"DD_TRACE_AGENT_URL"}; + EnvGuard agent_host_env{"DD_AGENT_HOST", "env-agent"}; + EnvGuard agent_port_env{"DD_TRACE_AGENT_PORT"}; + + auto loaded = load_stable_config("cpp", local, fleet); + REQUIRE(loaded); + + TracerConfig code; + code.service = "code-service"; + code.agent.http_client = std::make_shared(); + auto with_code = finalize_config(code, *loaded); + REQUIRE(with_code); + CHECK(with_code->defaults.service == "code-service"); + CHECK(with_code->defaults.environment == "env-environment"); + CHECK(with_code->defaults.version == "local-version"); + CHECK(with_code->agent_url.authority == "env-agent:8126"); + CHECK(with_code->metadata.at(ConfigName::AGENT_URL).back().origin == + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + REQUIRE(with_code->extraction_styles.size() == 1); + CHECK(with_code->extraction_styles.front() == PropagationStyle::B3); + CHECK_FALSE(with_code->tracing_enabled); + CHECK(with_code->metadata.at(ConfigName::SERVICE_NAME).back().origin == + ConfigMetadata::Origin::CODE); + const auto& sample_rate = + with_code->metadata.at(ConfigName::TRACE_SAMPLING_RATE).back(); + CHECK(sample_rate.origin == ConfigMetadata::Origin::FLEET_STABLE_CONFIG); + REQUIRE(sample_rate.config_id); + CHECK(*sample_rate.config_id == "fleet-test"); + const auto& extraction = + with_code->metadata.at(ConfigName::EXTRACTION_STYLES).back(); + CHECK(extraction.origin == ConfigMetadata::Origin::LOCAL_STABLE_CONFIG); + REQUIRE(extraction.config_id); + CHECK(*extraction.config_id == "local-test"); + + code.trace_sampler.sample_rate = 0.9; + auto with_code_rate = finalize_config(code, *loaded); + REQUIRE(with_code_rate); + CHECK(with_code_rate->metadata.at(ConfigName::TRACE_SAMPLING_RATE) + .back() + .origin == ConfigMetadata::Origin::CODE); + + TracerConfig no_code; + no_code.agent.http_client = code.agent.http_client; + auto without_code = finalize_config(no_code, *loaded); + REQUIRE(without_code); + CHECK(without_code->defaults.service == "fleet-service"); + CHECK(without_code->metadata.at(ConfigName::SERVICE_NAME).back().origin == + ConfigMetadata::Origin::FLEET_STABLE_CONFIG); +} + +STABLE_CONFIG_TEST("missing stable config files are ignored") { + auto loaded = load_stable_config("cpp", "/nonexistent/local.yaml", + "/nonexistent/fleet.yaml"); + REQUIRE(loaded); + CHECK_FALSE(loaded->find("DD_SERVICE")); +} + +STABLE_CONFIG_TEST("invalid stable config is reported") { + const std::string invalid = std::string{DD_TRACE_SOURCE_DIR} + + "/test/fixtures/stable_config/invalid.yaml"; + auto loaded = load_stable_config("cpp", invalid, "/nonexistent/fleet.yaml"); + CHECK_FALSE(loaded); +} + +STABLE_CONFIG_TEST("stable config applies one matching rule per source") { + stable_config_internal::ProcessInfo process{"cpp", {}, {}}; + auto loaded = stable_config_internal::load_yaml( + R"(config_id: local +apm_configuration_default: + DD_SERVICE: local-default + DD_ENV: local-env +rules: + - selectors: + - {origin: language, operator: equals, matches: [java]} + configuration: {DD_SERVICE: ignored} + - selectors: + - {origin: language, operator: equals, matches: [cpp]} + configuration: {DD_SERVICE: local-rule, DD_VERSION: local-version} + - selectors: [] + configuration: {DD_SERVICE: later-rule, DD_VERSION: later-version} +)", + R"(config_id: fleet +apm_configuration_default: + DD_ENV: fleet-env +rules: + - selectors: [] + configuration: {DD_SERVICE: fleet-rule} +)", + process); + REQUIRE(loaded); + REQUIRE(loaded->find("DD_SERVICE")); + CHECK(loaded->find("DD_SERVICE")->value == "fleet-rule"); + CHECK(loaded->find("DD_SERVICE")->source == StableConfigSource::FLEET); + CHECK(loaded->find("DD_SERVICE")->config_id == "fleet"); + CHECK(loaded->find("DD_ENV")->value == "fleet-env"); + CHECK(loaded->find("DD_VERSION")->value == "local-version"); + CHECK(loaded->find("DD_VERSION")->source == StableConfigSource::LOCAL); +} + +STABLE_CONFIG_TEST("stable config selectors use all origins and operators") { + stable_config_internal::ProcessInfo process{ + "cpp", + {"--mode=fast", "run", "--mode=final"}, + {"REGION=eu-west", "FLAG", "REGION=us-east"}}; + auto loaded = stable_config_internal::load_yaml( + R"(tags: {cluster: blue-prod} +rules: + - selectors: + - {origin: language, operator: equals, matches: [java, cpp]} + - {origin: process_arguments, key: --mode, operator: suffix_matches, matches: [final]} + - {origin: process_arguments, operator: equals, matches: [run]} + - {origin: environment_variables, key: REGION, operator: prefix_matches, matches: [us-]} + - {origin: environment_variables, operator: exists} + - {origin: environment_variables, key: FLAG, operator: exists} + - {origin: tags, key: cluster, operator: prefix_matches, matches: [blue]} + configuration: {DD_SERVICE: matched} +)", + "", process); + REQUIRE(loaded); + REQUIRE(loaded->find("DD_SERVICE")); + CHECK(loaded->find("DD_SERVICE")->value == "matched"); + + auto no_match = stable_config_internal::load_yaml( + R"(rules: + - selectors: + - {origin: tags, operator: exists} + configuration: {DD_SERVICE: wrong} +)", + "", process); + REQUIRE(no_match); + CHECK_FALSE(no_match->find("DD_SERVICE")); +} + +STABLE_CONFIG_TEST("stable config templates only matched rule values") { + stable_config_internal::ProcessInfo process{ + "cpp", {"--name=worker=one"}, {"ZONE=east"}}; + auto loaded = stable_config_internal::load_yaml( + R"(apm_configuration_default: {DD_ENV: "{{ language }}"} +tags: {cluster: alpha} +rules: + - selectors: [] + configuration: + DD_SERVICE: "{{ language }}-{{ tags[cluster] }}-{{ process_arguments[--name] }}-{{ environment_variables[ZONE] }}-{{ missing }}" + DD_VERSION: "{{ tags[absent] }}" + DD_TRACE_AGENT_URL: "prefix{{ tags[ cluster ] }}{{ language[ }}suffix" + DD_TAGS: "{{ tags }}-{{ tags[cluster }}-{{ unknown[key] }}" +)", + "", process); + REQUIRE(loaded); + CHECK(loaded->find("DD_ENV")->value == "{{ language }}"); + CHECK(loaded->find("DD_SERVICE")->value == + "cpp-alpha-worker=one-east-UNDEFINED"); + CHECK(loaded->find("DD_VERSION")->value == "UNDEFINED"); + CHECK(loaded->find("DD_TRACE_AGENT_URL")->value == "prefixalphacppsuffix"); + CHECK(loaded->find("DD_TAGS")->value == "UNDEFINED-UNDEFINED-UNDEFINED"); + + auto invalid = stable_config_internal::load_yaml( + "rules: [{selectors: [], configuration: {DD_SERVICE: '{{ broken'}}]", "", + process); + CHECK_FALSE(invalid); +} + +STABLE_CONFIG_TEST("stable config accepts scalar values and duplicate keys") { + stable_config_internal::ProcessInfo process{"cpp", {}, {}}; + auto loaded = stable_config_internal::load_yaml( + R"(unknown_field: ignored +apm_configuration_default: + DD_SERVICE: first + DD_SERVICE: second + DD_TRACE_ENABLED: true + DD_TRACE_SAMPLE_RATE: 0.25 +)", + "", process); + REQUIRE(loaded); + CHECK(loaded->find("DD_SERVICE")->value == "second"); + CHECK(loaded->find("DD_TRACE_ENABLED")->value == "true"); + CHECK(loaded->find("DD_TRACE_SAMPLE_RATE")->value == "0.25"); +} + +STABLE_CONFIG_TEST("stable config validates YAML fields") { + stable_config_internal::ProcessInfo process{"cpp", {}, {}}; + const std::vector invalid_documents = { + "[not, a, map]", + "apm_configuration_default: []", + "apm_configuration_default: {DD_SERVICE: null}", + "tags: []", + "rules: {}", + "rules: [null]", + "rules: [{selectors: {}, configuration: {}}]", + "rules: [{configuration: {DD_SERVICE: x}}]", + "rules: [{selectors: [], configuration: []}]", + "rules: [{selectors: [{origin: invalid, operator: exists}], " + "configuration: {}}]", + "rules: [{selectors: [{origin: language, operator: invalid}], " + "configuration: {}}]", + "rules: [{selectors: [{origin: language, operator: equals}], " + "configuration: {}}]", + }; + for (const std::string& document : invalid_documents) { + INFO(document); + CHECK_FALSE(stable_config_internal::load_yaml(document, "", process)); + } + CHECK(stable_config_internal::load_yaml("", "# comment\n", process)); +} + +STABLE_CONFIG_TEST("stable config skips oversized files and reads fleet") { + const std::filesystem::path oversized = + std::filesystem::temp_directory_path() / + ("dd-trace-cpp-stable-config-" + + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + { + std::ofstream file(oversized); + REQUIRE(file.good()); + } + std::filesystem::resize_file(oversized, 100 * 1024 * 1024 + 1); + const std::string fleet = std::string{DD_TRACE_SOURCE_DIR} + + "/test/fixtures/stable_config/fleet.yaml"; + auto loaded = load_stable_config("cpp", oversized.string(), fleet); + std::filesystem::remove(oversized); + REQUIRE(loaded); + REQUIRE(loaded->find("DD_SERVICE")); + CHECK(loaded->find("DD_SERVICE")->value == "fleet-service"); + CHECK(loaded->find("DD_SERVICE")->source == StableConfigSource::FLEET); +} + +STABLE_CONFIG_TEST("stable config reports file read errors") { + auto loaded = + load_stable_config("cpp", DD_TRACE_SOURCE_DIR, "/nonexistent/fleet.yaml"); + CHECK_FALSE(loaded); +}