Skip to content

Commit b754683

Browse files
bm1549claude
andcommitted
refactor: extract YAML parser and integrate all configs with stable config precedence
Task 1: Separate the YAML parser from stable_config.cpp into its own module (yaml_parser.h/cpp) with a clean interface that takes a string and returns parsed data without depending on Logger or StableConfig. Add comprehensive parser tests in test_yaml_parser.cpp. Task 2: Update all remaining configs to use the 5-parameter resolve_and_record_config with stable config sources: - DD_TAGS (tags map type) - DD_TRACE_PROPAGATION_STYLE_EXTRACT/INJECT and variants (propagation styles) - DD_TRACE_BAGGAGE_MAX_ITEMS/BYTES (uint64) - DD_TRACE_SAMPLE_RATE, DD_TRACE_RATE_LIMIT (double, via trace_sampler_config) - DD_TRACE_RESOURCE_RENAMING_ENABLED (bool) - DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT (bool) JSON array configs (DD_TRACE_SAMPLING_RULES, DD_SPAN_SAMPLING_RULES) are left on the old path with TODO comments since the YAML parser skips non-scalar values. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bdc8d36 commit b754683

11 files changed

Lines changed: 648 additions & 200 deletions

BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ cc_library(
8383
"src/datadog/tracer.cpp",
8484
"src/datadog/stable_config.cpp",
8585
"src/datadog/stable_config.h",
86+
"src/datadog/yaml_parser.cpp",
87+
"src/datadog/yaml_parser.h",
8688
"src/datadog/tracer_config.cpp",
8789
"src/datadog/version.cpp",
8890
"src/datadog/w3c_propagation.cpp",

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ target_sources(dd-trace-cpp-objects
211211
src/datadog/tag_propagation.cpp
212212
src/datadog/threaded_event_scheduler.cpp
213213
src/datadog/stable_config.cpp
214+
src/datadog/yaml_parser.cpp
214215
src/datadog/tracer_config.cpp
215216
src/datadog/tracer.cpp
216217
src/datadog/trace_id.cpp

include/datadog/trace_sampler_config.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
namespace datadog {
2121
namespace tracing {
2222

23+
struct StableConfigs;
24+
2325
struct TraceSamplerRule final {
2426
Rate rate;
2527
SpanMatcher matcher;
@@ -42,7 +44,7 @@ struct TraceSamplerConfig {
4244

4345
class FinalizedTraceSamplerConfig {
4446
friend Expected<FinalizedTraceSamplerConfig> finalize_config(
45-
const TraceSamplerConfig& config);
47+
const TraceSamplerConfig& config, const StableConfigs* stable_configs);
4648
friend class FinalizedTracerConfig;
4749

4850
FinalizedTraceSamplerConfig() = default;
@@ -58,7 +60,8 @@ class FinalizedTraceSamplerConfig {
5860
};
5961

6062
Expected<FinalizedTraceSamplerConfig> finalize_config(
61-
const TraceSamplerConfig& config);
63+
const TraceSamplerConfig& config,
64+
const StableConfigs* stable_configs = nullptr);
6265

6366
} // namespace tracing
6467
} // namespace datadog

src/datadog/span_sampler_config.cpp

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ namespace datadog {
1414
namespace tracing {
1515
namespace {
1616

17-
std::string to_string(const std::vector<SpanSamplerConfig::Rule> &rules) {
17+
std::string to_string(const std::vector<SpanSamplerConfig::Rule>& rules) {
1818
nlohmann::json res;
19-
for (const auto &r : rules) {
19+
for (const auto& r : rules) {
2020
nlohmann::json j = r;
2121
j["sample_rate"] = r.sample_rate;
2222
if (r.max_per_second) {
@@ -37,7 +37,7 @@ Expected<std::vector<SpanSamplerConfig::Rule>> parse_rules(StringView rules_raw,
3737

3838
try {
3939
json_rules = nlohmann::json::parse(rules_raw);
40-
} catch (const nlohmann::json::parse_error &error) {
40+
} catch (const nlohmann::json::parse_error& error) {
4141
std::string message;
4242
message += "Unable to parse JSON from ";
4343
append(message, env_var);
@@ -63,9 +63,9 @@ Expected<std::vector<SpanSamplerConfig::Rule>> parse_rules(StringView rules_raw,
6363
const std::unordered_set<std::string> allowed_properties{
6464
"service", "name", "resource", "tags", "sample_rate", "max_per_second"};
6565

66-
for (const auto &json_rule : json_rules) {
66+
for (const auto& json_rule : json_rules) {
6767
auto matcher = from_json(json_rule);
68-
if (auto *error = matcher.if_error()) {
68+
if (auto* error = matcher.if_error()) {
6969
std::string prefix;
7070
prefix += "Unable to create a rule from ";
7171
append(prefix, env_var);
@@ -118,7 +118,7 @@ Expected<std::vector<SpanSamplerConfig::Rule>> parse_rules(StringView rules_raw,
118118
}
119119

120120
// Look for unexpected properties.
121-
for (const auto &[key, value] : json_rule.items()) {
121+
for (const auto& [key, value] : json_rule.items()) {
122122
if (allowed_properties.count(key)) {
123123
continue;
124124
}
@@ -143,14 +143,14 @@ Expected<std::vector<SpanSamplerConfig::Rule>> parse_rules(StringView rules_raw,
143143
return rules;
144144
}
145145

146-
Expected<SpanSamplerConfig> load_span_sampler_env_config(Logger &logger) {
146+
Expected<SpanSamplerConfig> load_span_sampler_env_config(Logger& logger) {
147147
SpanSamplerConfig env_config;
148148

149149
auto rules_env = lookup(environment::DD_SPAN_SAMPLING_RULES);
150150
if (rules_env) {
151151
auto maybe_rules =
152152
parse_rules(*rules_env, name(environment::DD_SPAN_SAMPLING_RULES));
153-
if (auto *error = maybe_rules.if_error()) {
153+
if (auto* error = maybe_rules.if_error()) {
154154
return std::move(*error);
155155
}
156156
env_config.rules = std::move(*maybe_rules);
@@ -174,7 +174,7 @@ Expected<SpanSamplerConfig> load_span_sampler_env_config(Logger &logger) {
174174
} else {
175175
const auto span_rules_file = std::string(*file_env);
176176

177-
const auto file_error = [&](const char *operation) {
177+
const auto file_error = [&](const char* operation) {
178178
std::string message;
179179
message += "Unable to ";
180180
message += operation;
@@ -199,7 +199,7 @@ Expected<SpanSamplerConfig> load_span_sampler_env_config(Logger &logger) {
199199

200200
auto maybe_rules = parse_rules(
201201
rules_stream.str(), name(environment::DD_SPAN_SAMPLING_RULES_FILE));
202-
if (auto *error = maybe_rules.if_error()) {
202+
if (auto* error = maybe_rules.if_error()) {
203203
std::string prefix;
204204
prefix += "With ";
205205
append(prefix, name(environment::DD_SPAN_SAMPLING_RULES_FILE));
@@ -218,10 +218,10 @@ Expected<SpanSamplerConfig> load_span_sampler_env_config(Logger &logger) {
218218

219219
} // namespace
220220

221-
SpanSamplerConfig::Rule::Rule(const SpanMatcher &base) : SpanMatcher(base) {}
221+
SpanSamplerConfig::Rule::Rule(const SpanMatcher& base) : SpanMatcher(base) {}
222222

223223
Expected<FinalizedSpanSamplerConfig> finalize_config(
224-
const SpanSamplerConfig &user_config, Logger &logger) {
224+
const SpanSamplerConfig& user_config, Logger& logger) {
225225
Expected<SpanSamplerConfig> env_config = load_span_sampler_env_config(logger);
226226
if (auto error = env_config.if_error()) {
227227
return *error;
@@ -237,15 +237,18 @@ Expected<FinalizedSpanSamplerConfig> finalize_config(
237237
user_rules = user_config.rules;
238238
}
239239

240+
// TODO: DD_SPAN_SAMPLING_RULES is a JSON array; stable config integration
241+
// for span sampling rules is deferred because the YAML parser only handles
242+
// scalar values (flow sequences/mappings are skipped).
240243
std::vector<SpanSamplerConfig::Rule> rules = resolve_and_record_config(
241244
env_rules, user_rules, &result.metadata, ConfigName::SPAN_SAMPLING_RULES,
242-
nullptr, [](const std::vector<SpanSamplerConfig::Rule> &r) {
245+
nullptr, [](const std::vector<SpanSamplerConfig::Rule>& r) {
243246
return to_string(r);
244247
});
245248

246-
for (const auto &rule : rules) {
249+
for (const auto& rule : rules) {
247250
auto maybe_rate = Rate::from(rule.sample_rate);
248-
if (auto *error = maybe_rate.if_error()) {
251+
if (auto* error = maybe_rate.if_error()) {
249252
std::string prefix;
250253
prefix +=
251254
"Unable to parse sample_rate in span sampling rule with span "
@@ -272,17 +275,17 @@ Expected<FinalizedSpanSamplerConfig> finalize_config(
272275
}
273276

274277
FinalizedSpanSamplerConfig::Rule finalized;
275-
static_cast<SpanMatcher &>(finalized) = rule;
278+
static_cast<SpanMatcher&>(finalized) = rule;
276279
finalized.sample_rate = *maybe_rate;
277280
finalized.max_per_second = rule.max_per_second;
278281
result.rules.push_back(std::move(finalized));
279282
}
280283
return result;
281284
}
282285

283-
std::string to_string(const FinalizedSpanSamplerConfig::Rule &rule) {
286+
std::string to_string(const FinalizedSpanSamplerConfig::Rule& rule) {
284287
// Get the base class's fields, then add our own.
285-
nlohmann::json result = static_cast<const SpanMatcher &>(rule);
288+
nlohmann::json result = static_cast<const SpanMatcher&>(rule);
286289
result["sample_rate"] = double(rule.sample_rate);
287290
if (rule.max_per_second) {
288291
result["max_per_second"] = *rule.max_per_second;

src/datadog/stable_config.cpp

Lines changed: 7 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#include "stable_config.h"
22

3-
#include <algorithm>
43
#include <fstream>
5-
#include <sstream>
64
#include <string>
75

6+
#include "yaml_parser.h"
7+
88
#ifdef _WIN32
99
#include <windows.h>
1010
// windows.h defines ERROR as a macro which conflicts with our enum.
@@ -15,9 +15,6 @@ namespace datadog {
1515
namespace tracing {
1616
namespace {
1717

18-
// Maximum file size: 256KB.
19-
constexpr std::size_t kMaxFileSize = 256 * 1024;
20-
2118
#ifdef _WIN32
2219

2320
std::string get_windows_agent_dir() {
@@ -67,149 +64,6 @@ std::string get_windows_agent_dir() {
6764

6865
#endif // _WIN32
6966

70-
// Remove leading and trailing whitespace from `s`.
71-
std::string trim(const std::string& s) {
72-
const auto begin = s.find_first_not_of(" \t\r\n");
73-
if (begin == std::string::npos) return "";
74-
const auto end = s.find_last_not_of(" \t\r\n");
75-
return s.substr(begin, end - begin + 1);
76-
}
77-
78-
// If `s` is surrounded by matching quotes (single or double), remove them and
79-
// return the inner content. Otherwise return `s` as-is.
80-
std::string unquote(const std::string& s) {
81-
if (s.size() >= 2) {
82-
const char front = s.front();
83-
const char back = s.back();
84-
if ((front == '"' && back == '"') || (front == '\'' && back == '\'')) {
85-
return s.substr(1, s.size() - 2);
86-
}
87-
}
88-
return s;
89-
}
90-
91-
// Strip an inline comment from a value string. Handles quoted values so that
92-
// a '#' inside quotes is not treated as a comment.
93-
std::string strip_inline_comment(const std::string& s) {
94-
if (s.empty()) return s;
95-
96-
// If the value starts with a quote, find the closing quote first.
97-
if (s[0] == '"' || s[0] == '\'') {
98-
const char quote = s[0];
99-
auto close = s.find(quote, 1);
100-
if (close != std::string::npos) {
101-
// Return just the quoted value (anything after closing quote + whitespace
102-
// + '#' is comment).
103-
return s.substr(0, close + 1);
104-
}
105-
// No closing quote — return as-is (will be treated as a parse issue
106-
// elsewhere or kept verbatim).
107-
return s;
108-
}
109-
110-
// Unquoted value: '#' starts a comment.
111-
auto pos = s.find('#');
112-
if (pos != std::string::npos) {
113-
auto result = s.substr(0, pos);
114-
// Trim trailing whitespace before the comment.
115-
auto end = result.find_last_not_of(" \t");
116-
if (end != std::string::npos) {
117-
return result.substr(0, end + 1);
118-
}
119-
return "";
120-
}
121-
return s;
122-
}
123-
124-
enum class ParseResult { OK, PARSE_ERROR };
125-
126-
// Parse a YAML file's contents into a StableConfig.
127-
// Returns OK on success (including empty/missing apm_configuration_default).
128-
// Returns ERROR on malformed input.
129-
ParseResult parse_yaml(const std::string& content, StableConfig& out) {
130-
std::istringstream stream(content);
131-
std::string line;
132-
bool in_apm_config = false;
133-
134-
while (std::getline(stream, line)) {
135-
// Remove carriage return if present (Windows line endings).
136-
if (!line.empty() && line.back() == '\r') {
137-
line.pop_back();
138-
}
139-
140-
// Strip comments from lines that are entirely comments.
141-
auto trimmed = trim(line);
142-
if (trimmed.empty() || trimmed[0] == '#') {
143-
continue;
144-
}
145-
146-
// Detect indentation to know if we're in a map or at the top level.
147-
const auto first_non_space = line.find_first_not_of(" \t");
148-
const bool is_indented = (first_non_space > 0);
149-
150-
if (!is_indented) {
151-
// Top-level key.
152-
in_apm_config = false;
153-
154-
auto colon_pos = trimmed.find(':');
155-
if (colon_pos == std::string::npos) {
156-
// Malformed line at top level.
157-
return ParseResult::PARSE_ERROR;
158-
}
159-
160-
auto key = trim(trimmed.substr(0, colon_pos));
161-
auto value = trim(trimmed.substr(colon_pos + 1));
162-
163-
// Strip inline comment from value.
164-
value = strip_inline_comment(value);
165-
value = trim(value);
166-
167-
if (key == "apm_configuration_default") {
168-
in_apm_config = true;
169-
// The value after the colon should be empty (map follows on next
170-
// lines). If it's not empty, that's malformed for our purposes.
171-
if (!value.empty()) {
172-
return ParseResult::PARSE_ERROR;
173-
}
174-
} else if (key == "config_id") {
175-
out.config_id = unquote(value);
176-
}
177-
// Unknown top-level keys are silently ignored.
178-
} else if (in_apm_config) {
179-
// Indented line under apm_configuration_default.
180-
auto colon_pos = trimmed.find(':');
181-
if (colon_pos == std::string::npos) {
182-
// Malformed entry.
183-
return ParseResult::PARSE_ERROR;
184-
}
185-
186-
auto key = trim(trimmed.substr(0, colon_pos));
187-
auto value = trim(trimmed.substr(colon_pos + 1));
188-
189-
// Strip inline comment.
190-
value = strip_inline_comment(value);
191-
value = trim(value);
192-
193-
// Check for non-scalar values (flow sequences/mappings).
194-
if (!value.empty() && (value[0] == '[' || value[0] == '{' ||
195-
value[0] == '|' || value[0] == '>')) {
196-
// Skip non-scalar values silently (as per spec: "log warning, skip
197-
// that entry").
198-
continue;
199-
}
200-
201-
// Unquote the value.
202-
value = unquote(value);
203-
204-
// Store the key-value pair. Last value wins for duplicates.
205-
out.values[key] = value;
206-
}
207-
// Indented lines under unknown top-level keys are silently ignored.
208-
}
209-
210-
return ParseResult::OK;
211-
}
212-
21367
// Read a file and parse it into a StableConfig. Logs warnings on errors.
21468
// Returns an empty StableConfig if the file doesn't exist or can't be read.
21569
StableConfig load_one(const std::string& path, Logger& logger) {
@@ -231,7 +85,7 @@ StableConfig load_one(const std::string& path, Logger& logger) {
23185
return result;
23286
}
23387

234-
if (static_cast<std::size_t>(size) > kMaxFileSize) {
88+
if (static_cast<std::size_t>(size) > kMaxYamlFileSize) {
23589
logger.log_error([&path](std::ostream& log) {
23690
log << "Stable config: file " << path
23791
<< " exceeds 256KB size limit; skipping.";
@@ -248,13 +102,16 @@ StableConfig load_one(const std::string& path, Logger& logger) {
248102
return result;
249103
}
250104

251-
if (parse_yaml(content, result) != ParseResult::OK) {
105+
YamlParseResult parsed;
106+
if (parse_yaml(content, parsed) != YamlParseStatus::OK) {
252107
logger.log_error([&path](std::ostream& log) {
253108
log << "Stable config: malformed YAML in " << path << "; skipping.";
254109
});
255110
return {}; // Return empty config on parse error.
256111
}
257112

113+
result.config_id = std::move(parsed.config_id);
114+
result.values = std::move(parsed.values);
258115
return result;
259116
}
260117

0 commit comments

Comments
 (0)