Skip to content

Commit 274149a

Browse files
authored
fix(rc): improve trace sampling rules parsing (#232)
Correctly parse `tags` from remote trace sampling rules and improves overall rule parsing reliability through stricter validation and clearer error reporting. Changes: - Add support for `tags` in trace sampling rules. - Improve validation with explicit type checks and more descriptive error messages. - Update catch-all rule handling to replace an existing one instead of always prepending, ensuring consistent rule ordering. - Extended system test request handler to support `span_tags` from incoming requests. [APMAPI-863] [APMAPI-866]
1 parent 0eda5b5 commit 274149a

File tree

5 files changed

+227
-62
lines changed

5 files changed

+227
-62
lines changed

src/datadog/config_manager.cpp

Lines changed: 160 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
#include <datadog/telemetry/telemetry.h>
44

5-
#include "json_serializer.h"
65
#include "parse_util.h"
76
#include "string_util.h"
87
#include "trace_sampler.h"
@@ -26,69 +25,168 @@ nlohmann::json to_json(const SpanDefaults& defaults) {
2625
}
2726

2827
using Rules = std::vector<TraceSamplerRule>;
28+
using Tags = std::unordered_map<std::string, std::string>;
29+
30+
Expected<Tags> parse_tags_from_sampling_rules(const nlohmann::json& json_tags) {
31+
assert(json_tags.is_array());
32+
33+
Tags tags;
34+
for (const auto& json_tag_entry : json_tags) {
35+
auto key = json_tag_entry.find("key");
36+
if (key == json_tag_entry.cend() || key->is_string() == false) {
37+
std::string err_msg =
38+
"Failed to parse tags: the required \"key\" field is either missing "
39+
"or incorrectly formatted. (input: ";
40+
err_msg += json_tags.dump();
41+
err_msg += ")";
42+
return Error{Error::TRACE_SAMPLING_RULES_INVALID_JSON,
43+
std::move(err_msg)};
44+
}
2945

30-
Expected<Rules> parse_trace_sampling_rules(const nlohmann::json& json_rules) {
31-
Rules parsed_rules;
46+
auto value = json_tag_entry.find("value_glob");
47+
if (value == json_tag_entry.cend() || value->is_string() == false) {
48+
std::string err_msg =
49+
"Failed to parse tags: the required \"value_glob\" field is either "
50+
"missing or incorrectly formatted. (input: ";
51+
err_msg += json_tags.dump();
52+
err_msg += ")";
53+
return Error{Error::TRACE_SAMPLING_RULES_INVALID_JSON,
54+
std::move(err_msg)};
55+
}
56+
57+
tags.emplace(*key, *value);
58+
}
59+
60+
return tags;
61+
}
3262

33-
std::string type = json_rules.type_name();
34-
if (type != "array") {
63+
Expected<TraceSamplerRule> parse_rule(const nlohmann::json& json_rule) {
64+
assert(json_rule.is_object());
65+
66+
auto make_error = [&json_rule](StringView field_name) {
67+
std::string err_msg = "Failed to parse sampling rule: the required \"";
68+
append(err_msg, field_name);
69+
err_msg += "\" field is missing. (input: ";
70+
err_msg += json_rule.dump();
71+
err_msg += ")";
72+
return Error{Error::TRACE_SAMPLING_RULES_INVALID_JSON, std::move(err_msg)};
73+
};
74+
75+
const auto make_property_error = [&json_rule](StringView property,
76+
const nlohmann::json& value,
77+
StringView expected_type) {
3578
std::string message;
36-
return Error{Error::TRACE_SAMPLING_RULES_WRONG_TYPE, std::move(message)};
79+
message += "Rule property \"";
80+
append(message, property);
81+
message += "\" should have type \"";
82+
append(message, expected_type);
83+
message += "\", but has type \"";
84+
message += value.type_name();
85+
message += "\": ";
86+
message += value.dump();
87+
message += " in rule ";
88+
message += json_rule.dump();
89+
return Error{Error::RULE_PROPERTY_WRONG_TYPE, std::move(message)};
90+
};
91+
92+
TraceSamplerRule rule;
93+
94+
// Required: service, resource, sample_rate, provenance.
95+
if (auto service = json_rule.find("service"); service != json_rule.cend()) {
96+
if (service->is_string() == false) {
97+
return make_property_error("service", *service, "string");
98+
}
99+
rule.matcher.service = *service;
100+
} else {
101+
return make_error("service");
37102
}
38103

39-
for (const auto& json_rule : json_rules) {
40-
auto matcher = from_json(json_rule);
41-
if (auto* error = matcher.if_error()) {
42-
std::string prefix;
43-
return error->with_prefix(prefix);
104+
if (auto resource = json_rule.find("resource");
105+
resource != json_rule.cend()) {
106+
if (resource->is_string() == false) {
107+
return make_property_error("resource", *resource, "string");
44108
}
109+
rule.matcher.resource = *resource;
110+
} else {
111+
return make_error("resource");
112+
}
45113

46-
TraceSamplerRule rule;
47-
rule.matcher = std::move(*matcher);
48-
49-
if (auto sample_rate = json_rule.find("sample_rate");
50-
sample_rate != json_rule.end()) {
51-
type = sample_rate->type_name();
52-
if (type != "number") {
53-
std::string message;
54-
return Error{Error::TRACE_SAMPLING_RULES_SAMPLE_RATE_WRONG_TYPE,
55-
std::move(message)};
56-
}
114+
if (auto sample_rate = json_rule.find("sample_rate");
115+
sample_rate != json_rule.end()) {
116+
if (sample_rate->is_number() == false) {
117+
return make_property_error("sample_rate", *sample_rate, "number");
118+
}
57119

58-
auto maybe_rate = Rate::from(*sample_rate);
59-
if (auto error = maybe_rate.if_error()) {
60-
return *error;
61-
}
120+
auto maybe_rate = Rate::from(*sample_rate);
121+
if (auto error = maybe_rate.if_error()) {
122+
return *error;
123+
}
62124

63-
rule.rate = *maybe_rate;
125+
rule.rate = *maybe_rate;
126+
} else {
127+
return make_error("sample_rate");
128+
}
129+
130+
if (auto provenance_it = json_rule.find("provenance");
131+
provenance_it != json_rule.cend()) {
132+
if (!provenance_it->is_string()) {
133+
return make_property_error("provenance", *provenance_it, "string");
134+
}
135+
136+
auto provenance = to_lower(provenance_it->get<StringView>());
137+
if (provenance == "customer") {
138+
rule.mechanism = SamplingMechanism::REMOTE_RULE;
139+
} else if (provenance == "dynamic") {
140+
rule.mechanism = SamplingMechanism::REMOTE_ADAPTIVE_RULE;
64141
} else {
65-
return Error{Error::TRACE_SAMPLING_RULES_INVALID_JSON,
66-
"Missing \"sample_rate\" field"};
142+
std::string err_msg = "Failed to parse sampling rule: unknown \"";
143+
err_msg += provenance;
144+
err_msg += "\" value. Expected either \"customer\" or \"dynamic\"";
145+
return Error{Error::TRACE_SAMPLING_RULES_UNKNOWN_PROPERTY,
146+
std::move(err_msg)};
147+
}
148+
} else {
149+
return make_error("provenance");
150+
}
151+
152+
// Parse optional fields: name, tags
153+
if (auto name = json_rule.find("name"); name != json_rule.cend()) {
154+
if (name->is_string() == false) {
155+
return make_property_error("name", *name, "string");
67156
}
157+
rule.matcher.name = *name;
158+
}
68159

69-
if (auto provenance_it = json_rule.find("provenance");
70-
provenance_it != json_rule.cend()) {
71-
if (!provenance_it->is_string()) {
72-
std::string message;
73-
return Error{Error::TRACE_SAMPLING_RULES_SAMPLE_RATE_WRONG_TYPE,
74-
std::move(message)};
75-
}
160+
if (auto tags = json_rule.find("tags"); tags != json_rule.cend()) {
161+
if (tags->is_array() == false) {
162+
return make_property_error("tags", *tags, "array");
163+
}
76164

77-
auto provenance = to_lower(provenance_it->get<StringView>());
78-
if (provenance == "customer") {
79-
rule.mechanism = SamplingMechanism::REMOTE_RULE;
80-
} else if (provenance == "dynamic") {
81-
rule.mechanism = SamplingMechanism::REMOTE_ADAPTIVE_RULE;
82-
} else {
83-
return Error{Error::TRACE_SAMPLING_RULES_UNKNOWN_PROPERTY,
84-
"Unknown \"provenance\" value"};
85-
}
86-
} else {
87-
return Error{Error::TRACE_SAMPLING_RULES_INVALID_JSON,
88-
"Missing \"provenance\" field"};
165+
auto maybe_tags = parse_tags_from_sampling_rules(*tags);
166+
if (auto* error = maybe_tags.if_error()) {
167+
return *error;
168+
}
169+
170+
rule.matcher.tags = std::move(*maybe_tags);
171+
}
172+
173+
return rule;
174+
}
175+
176+
Expected<Rules> parse_trace_sampling_rules(const nlohmann::json& json_rules) {
177+
if (json_rules.is_array() == false) {
178+
std::string message;
179+
return Error{Error::TRACE_SAMPLING_RULES_WRONG_TYPE, std::move(message)};
180+
}
181+
182+
Rules parsed_rules;
183+
for (const auto& json_rule : json_rules) {
184+
auto maybe_rule = parse_rule(json_rule);
185+
if (auto error = maybe_rule.if_error()) {
186+
return *error;
89187
}
90188

91-
parsed_rules.emplace_back(std::move(rule));
189+
parsed_rules.emplace_back(std::move(*maybe_rule));
92190
}
93191

94192
return parsed_rules;
@@ -215,7 +313,19 @@ std::vector<ConfigMetadata> ConfigManager::apply_update(
215313
rule.rate = *rate;
216314
rule.matcher = catch_all;
217315
rule.mechanism = SamplingMechanism::RULE;
218-
rules.emplace(rules.cbegin(), std::move(rule));
316+
317+
// Convention: Catch-all rules should ALWAYS be the last in the list.
318+
// If a catch-all rule already exists, replace it.
319+
// If NOT, add the new one at the end of the rules list.
320+
if (rules.empty()) {
321+
rules.emplace_back(std::move(rule));
322+
} else {
323+
if (auto& last_rule = rules.back(); last_rule.matcher == catch_all) {
324+
last_rule = rule;
325+
} else {
326+
rules.emplace_back(std::move(rule));
327+
}
328+
}
219329

220330
metadata.emplace_back(std::move(trace_sampling_metadata));
221331
}

src/datadog/json_serializer.h

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,19 @@ inline void to_json(nlohmann::json& j, const SpanMatcher& matcher) {
1717
inline Expected<SpanMatcher> from_json(const nlohmann::json& json) {
1818
SpanMatcher result;
1919

20-
std::string type = json.type_name();
21-
if (type != "object") {
20+
if (json.is_object() == false) {
2221
std::string message;
2322
message += "A rule must be a JSON object, but this is of type \"";
24-
message += type;
23+
message += json.type_name();
2524
message += "\": ";
2625
message += json.dump();
2726
return Error{Error::RULE_WRONG_TYPE, std::move(message)};
2827
}
2928

3029
const auto check_property_type =
31-
[&](StringView property, const nlohmann::json& value,
32-
StringView expected_type) -> Optional<Error> {
33-
type = value.type_name();
30+
[&json](StringView property, const nlohmann::json& value,
31+
StringView expected_type) -> Optional<Error> {
32+
const StringView type = value.type_name();
3433
if (type == expected_type) {
3534
return nullopt;
3635
}
@@ -41,7 +40,7 @@ inline Expected<SpanMatcher> from_json(const nlohmann::json& json) {
4140
message += "\" should have type \"";
4241
append(message, expected_type);
4342
message += "\", but has type \"";
44-
message += type;
43+
append(message, type);
4544
message += "\": ";
4645
message += value.dump();
4746
message += " in rule ";
@@ -70,13 +69,12 @@ inline Expected<SpanMatcher> from_json(const nlohmann::json& json) {
7069
return *error;
7170
}
7271
for (const auto& [tag_name, tag_value] : value.items()) {
73-
type = tag_value.type_name();
74-
if (type != "string") {
72+
if (tag_value.is_string() == false) {
7573
std::string message;
7674
message += "Rule tag pattern must be a string, but ";
7775
message += tag_value.dump();
7876
message += " has type \"";
79-
message += type;
77+
message += tag_value.type_name();
8078
message += "\" for tag named \"";
8179
message += tag_name;
8280
message += "\" in rule: ";
@@ -86,7 +84,7 @@ inline Expected<SpanMatcher> from_json(const nlohmann::json& json) {
8684
result.tags.emplace(std::string(tag_name), std::string(tag_value));
8785
}
8886
} else {
89-
// Unknown properties are OK. `SpanMatcher` is used as a base class for
87+
// Unknown properties are OK. `SpanMatcher` is used as a base class for
9088
// trace sampling rules and span sampling rules. Those derived types
9189
// will have additional properties in their JSON representations.
9290
}

src/datadog/trace_sampler_config.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Expected<FinalizedTraceSamplerConfig> finalize_config(
209209
}
210210

211211
// If `sample_rate` was specified, then it translates to a "catch-all" rule
212-
// appended to the end of `rules`. First, though, we have to make sure the
212+
// appended to the end of `rules`. First, though, we have to make sure the
213213
// sample rate is valid.
214214
if (sample_rate) {
215215
auto maybe_rate = Rate::from(*sample_rate);

test/system-tests/request_handler.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ void RequestHandler::on_trace_config(const httplib::Request& /* req */,
7070
res.set_content(response_body.dump(), "application/json");
7171
}
7272

73+
// TODO: Refact endpoint handler to return 404 when an unknown field is passed
74+
// in the payload. that would send a clear message instead of silently creating
75+
// a span.
7376
void RequestHandler::on_span_start(const httplib::Request& req,
7477
httplib::Response& res) {
7578
const auto request_json = nlohmann::json::parse(req.body);
@@ -94,6 +97,23 @@ void RequestHandler::on_span_start(const httplib::Request& req,
9497
span_cfg.resource = *resource;
9598
}
9699

100+
if (auto tags = request_json.find("span_tags");
101+
tags != request_json.cend() && tags->is_array()) {
102+
for (const auto& tag : *tags) {
103+
if (tag.size() != 2) {
104+
// TODO: refactor to log errors
105+
continue;
106+
}
107+
108+
if (tag[0].is_string() == false || tag[1].is_string() == false) {
109+
// TODO: refactor to log errors
110+
continue;
111+
}
112+
113+
span_cfg.tags.emplace(tag[0], tag[1]);
114+
}
115+
}
116+
97117
auto success = [](const datadog::tracing::Span& span,
98118
httplib::Response& res) {
99119
// clang-format off

test/test_config_manager.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,41 @@ CONFIG_MANAGER_TEST("remote configuration handling") {
184184
const auto reverted_tracing_status = config_manager.report_traces();
185185
CHECK(old_tracing_status == reverted_tracing_status);
186186
}
187+
188+
SECTION("handling of `tracing_sampling_rules`") {
189+
SECTION("valid input") {
190+
config_update.content = R"({
191+
"lib_config": {
192+
"library_language": "all",
193+
"library_version": "latest",
194+
"service_name": "testsvc",
195+
"env": "test",
196+
"tracing_sampling_rules": [
197+
{
198+
"service": "foo",
199+
"resource": "GET /hello",
200+
"sample_rate": 0.1,
201+
"provenance": "customer",
202+
"name": "test",
203+
"tags": [
204+
{ "key": "tag1", "value_glob": "value1" }
205+
]
206+
}
207+
]
208+
},
209+
"service_target": {
210+
"service": "testsvc",
211+
"env": "test"
212+
}
213+
})";
214+
215+
const auto old_sampler_cfg =
216+
config_manager.trace_sampler()->config_json();
217+
const auto err = config_manager.on_update(config_update);
218+
const auto new_sampler_cfg =
219+
config_manager.trace_sampler()->config_json();
220+
221+
CHECK(old_sampler_cfg != new_sampler_cfg);
222+
}
223+
}
187224
}

0 commit comments

Comments
 (0)