Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1f8f323
feat: implement stable configuration (Phase 1)
bm1549 Mar 20, 2026
9338569
feat: report non-native stable config keys in telemetry
bm1549 Mar 20, 2026
8858572
fix: address pre-push review findings
bm1549 Mar 20, 2026
f380c1d
style: apply clang-format to all changed files
bm1549 Mar 20, 2026
d6e7ed8
fix: add stable_config to BUILD.bazel, remove fake product defaults
bm1549 Mar 20, 2026
c340dcc
fix: rename ParseResult::ERROR to avoid Windows macro conflict
bm1549 Mar 20, 2026
6e4fcc3
refactor: remove non-native config telemetry reporting
bm1549 Mar 20, 2026
e225a12
style: remove trailing blank lines after AdditionalConfigEntry removal
bm1549 Mar 20, 2026
1e32ee8
feat: emit config_id in telemetry for fleet stable config
bm1549 Mar 20, 2026
1dcbd5b
fix: cast std::tolower return to char for MSVC warning C4244
bm1549 Mar 20, 2026
bdc8d36
style: format request_handler.cpp after MSVC fix
bm1549 Mar 20, 2026
62d83fb
refactor: extract YAML parser and integrate all configs with stable c…
bm1549 Mar 20, 2026
5b4ccd0
refactor: apply code review feedback across stable config implementation
bm1549 Mar 21, 2026
0fc46ef
fix: use portable path suffix instead of getpid() for Windows compat
bm1549 Mar 21, 2026
f93dc68
refactor: replace custom YAML parser with yaml-cpp library
bm1549 Mar 21, 2026
41e97ea
chore: remove auto-generated CLAUDE.md from PR
bm1549 Mar 21, 2026
e4a7de9
fix: wire yaml-cpp for Bazel builds and fix LLVM stdlib mismatch
bm1549 Mar 21, 2026
7c99bf8
refactor: remove test-only fields from public FinalizedTracerConfig
bm1549 Mar 21, 2026
0119aae
fix: use correct Bazel label for yaml-cpp dependency
bm1549 Mar 21, 2026
551e9fc
fix: use repo_name alias for yaml-cpp Bazel dependency
bm1549 Mar 21, 2026
03cbbf1
fix: use yaml-cpp 0.8.0.bcr.1 for Bazel 9 compatibility
bm1549 Mar 21, 2026
1a9ed8e
fix: propagate ASAN flags to yaml-cpp on Windows
bm1549 Mar 21, 2026
0fbd7d3
style: reformat with clang-format-14 (project standard)
bm1549 Mar 21, 2026
ebe5b3c
docs: update yaml_parser.h comment to reflect yaml-cpp usage
bm1549 Mar 21, 2026
8f89c38
chore: remove TODO comment from catch block
bm1549 Mar 21, 2026
32d2d5b
refactor: apply review feedback — move rules parser, simplify helpers…
bm1549 Mar 21, 2026
20488e9
fix: propagate only sanitizer flags to yaml-cpp, not warning flags
bm1549 Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -153,5 +157,6 @@ cc_library(
deps = [
"@com_google_absl//absl/strings",
"@com_google_absl//absl/types:optional",
"@yaml_cpp//:yaml-cpp",
],
)
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -242,6 +248,7 @@ target_link_libraries(dd-trace-cpp-objects
Threads::Threads
PRIVATE
dd-trace-cpp::specs
$<BUILD_INTERFACE:yaml-cpp>
)

set_target_properties(dd-trace-cpp-objects
Expand Down
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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")
10 changes: 10 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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()
36 changes: 36 additions & 0 deletions cmake/deps/yaml.cmake
Original file line number Diff line number Diff line change
@@ -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()
43 changes: 38 additions & 5 deletions include/datadog/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -96,15 +103,33 @@ Value resolve_and_record_config(
std::unordered_map<ConfigName, std::vector<ConfigMetadata>>* 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<Value>{}, from_env, from_user,
Optional<Value>{}, 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 <typename Value, typename Stringifier = std::nullptr_t,
typename DefaultValue = std::nullptr_t>
Value resolve_and_record_config(
const Optional<Value>& from_fleet_stable, const Optional<Value>& from_env,
const Optional<Value>& from_user, const Optional<Value>& from_local_stable,
std::unordered_map<ConfigName, std::vector<ConfigMetadata>>* 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<Stringifier, std::nullptr_t>) {
return to_string_fn(v); // use provided function
return to_string_fn(v);
} else if constexpr (std::is_constructible_v<std::string, Value>) {
return std::string(v); // default behaviour (works for string-like types)
return std::string(v);
} else {
static_assert(!std::is_same_v<Value, Value>,
"Non-string types require a stringifier function");
return ""; // unreachable
return "";
}
};

Expand All @@ -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<DefaultValue, std::nullptr_t>) {
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);
}
Expand All @@ -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);
}
Expand Down
7 changes: 4 additions & 3 deletions include/datadog/span_sampler_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ struct SpanSamplerConfig {

class FinalizedSpanSamplerConfig {
friend Expected<FinalizedSpanSamplerConfig> finalize_config(
const SpanSamplerConfig&, Logger&);
const SpanSamplerConfig&, Logger&, const struct StableConfigs*);
friend class FinalizedTracerConfig;

FinalizedSpanSamplerConfig() = default;
Expand All @@ -52,8 +52,9 @@ class FinalizedSpanSamplerConfig {
std::unordered_map<ConfigName, std::vector<ConfigMetadata>> metadata;
};

Expected<FinalizedSpanSamplerConfig> finalize_config(const SpanSamplerConfig&,
Logger&);
Expected<FinalizedSpanSamplerConfig> finalize_config(
const SpanSamplerConfig&, Logger&,
const struct StableConfigs* stable_configs = nullptr);

std::string to_string(const FinalizedSpanSamplerConfig::Rule&);

Expand Down
5 changes: 4 additions & 1 deletion include/datadog/telemetry/configuration.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#pragma once

#include <datadog/config.h>
#include <datadog/expected.h>
#include <datadog/optional.h>
#include <datadog/telemetry/product.h>
Expand Down Expand Up @@ -56,6 +55,10 @@ struct FinalizedConfiguration {
std::string integration_version;
std::vector<Product> products;

// Fleet stable config ID, used to attach config_id to telemetry entries
// with FLEET_STABLE_CONFIG origin at serialization time.
tracing::Optional<std::string> fleet_stable_config_id;

// Onboarding metadata coming from `DD_INSTRUMENTATION_INSTALL_*` environment
// variables.
tracing::Optional<std::string> install_id;
Expand Down
9 changes: 7 additions & 2 deletions include/datadog/trace_sampler_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
namespace datadog {
namespace tracing {

class Logger;
struct StableConfigs;

struct TraceSamplerRule final {
Rate rate;
SpanMatcher matcher;
Expand All @@ -42,7 +45,8 @@ struct TraceSamplerConfig {

class FinalizedTraceSamplerConfig {
friend Expected<FinalizedTraceSamplerConfig> finalize_config(
const TraceSamplerConfig& config);
const TraceSamplerConfig& config, const StableConfigs* stable_configs,
Logger* logger);
friend class FinalizedTracerConfig;

FinalizedTraceSamplerConfig() = default;
Expand All @@ -58,7 +62,8 @@ class FinalizedTraceSamplerConfig {
};

Expected<FinalizedTraceSamplerConfig> finalize_config(
const TraceSamplerConfig& config);
const TraceSamplerConfig& config,
const StableConfigs* stable_configs = nullptr, Logger* logger = nullptr);

} // namespace tracing
} // namespace datadog
1 change: 1 addition & 0 deletions include/datadog/tracer_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <cstddef>
#include <memory>
#include <string>
#include <variant>
#include <vector>

Expand Down
78 changes: 75 additions & 3 deletions src/datadog/span_sampler_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename Rule, typename Json, typename Customize>
Optional<std::vector<Rule>> 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<Rule> 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<SpanSamplerConfig::Rule> &rules) {
nlohmann::json res;
for (const auto &r : rules) {
Expand Down Expand Up @@ -221,7 +271,8 @@ Expected<SpanSamplerConfig> load_span_sampler_env_config(Logger &logger) {
SpanSamplerConfig::Rule::Rule(const SpanMatcher &base) : SpanMatcher(base) {}

Expected<FinalizedSpanSamplerConfig> finalize_config(
const SpanSamplerConfig &user_config, Logger &logger) {
const SpanSamplerConfig &user_config, Logger &logger,
const StableConfigs *stable_configs) {
Expected<SpanSamplerConfig> env_config = load_span_sampler_env_config(logger);
if (auto error = env_config.if_error()) {
return *error;
Expand All @@ -237,9 +288,30 @@ Expected<FinalizedSpanSamplerConfig> finalize_config(
user_rules = user_config.rules;
}

Optional<std::vector<SpanSamplerConfig::Rule>> fleet_rules;
Optional<std::vector<SpanSamplerConfig::Rule>> local_rules;
if (stable_configs) {
auto parse_span_rules = [&logger](const StableConfig &cfg,
const std::string &key) {
return parse_stable_config_rules<SpanSamplerConfig::Rule, nlohmann::json>(
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<SpanSamplerConfig::Rule> rules = resolve_and_record_config(
env_rules, user_rules, &result.metadata, ConfigName::SPAN_SAMPLING_RULES,
nullptr, [](const std::vector<SpanSamplerConfig::Rule> &r) {
fleet_rules, env_rules, user_rules, local_rules, &result.metadata,
ConfigName::SPAN_SAMPLING_RULES, nullptr,
[](const std::vector<SpanSamplerConfig::Rule> &r) {
return to_string(r);
});

Expand Down
Loading