Skip to content

Commit dc5e762

Browse files
feat: ensure tracecontext headers take precedence over datadog (AIT-10281) (#142)
Adds support for "W3C Phase 3" so that when multiple trace contexts are extracted (with the same trace-id) and the `tracecontext` format is used, the resulting trace context will have: - `span.parent_id` set to the `tracecontext` parent-id value - `span.tags["_dd.parent_id"]` set to either the value of `tracestate[dd][p]` or `hex(x-datadog-parent-id)`, in that order of precedence
1 parent f6d6836 commit dc5e762

File tree

6 files changed

+199
-49
lines changed

6 files changed

+199
-49
lines changed

src/datadog/extraction_util.cpp

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <unordered_map>
88

99
#include "extracted_data.h"
10+
#include "hex.h"
1011
#include "logger.h"
1112
#include "parse_util.h"
1213
#include "string_util.h"
@@ -250,51 +251,47 @@ void AuditedReader::visit(
250251
});
251252
}
252253

253-
ExtractedData merge(const std::vector<ExtractedData>& contexts) {
254+
ExtractedData merge(
255+
const PropagationStyle first_style,
256+
const std::unordered_map<PropagationStyle, ExtractedData>& contexts) {
254257
ExtractedData result;
255-
256-
const auto found = std::find_if(
257-
contexts.begin(), contexts.end(),
258-
[](const ExtractedData& data) { return data.trace_id.has_value(); });
259-
258+
const auto found = contexts.find(first_style);
260259
if (found == contexts.end()) {
261-
// Nothing extracted a trace ID. Return the first context that includes a
262-
// parent ID, if any, or otherwise just return an empty `ExtractedData`.
263-
// The purpose of looking for a parent ID is to allow for the error
264-
// "extracted a parent ID without a trace ID," if that's what happened.
265-
const auto other = std::find_if(
266-
contexts.begin(), contexts.end(),
267-
[](const ExtractedData& data) { return data.parent_id.has_value(); });
268-
if (other != contexts.end()) {
269-
result = *other;
270-
}
271260
return result;
272261
}
273262

274263
// `found` refers to the first extracted context that yielded a trace ID.
275264
// This will be our main context.
276265
//
277-
// If the style of `found` is not W3C, then examine the remaining contexts
278-
// for W3C-style tracestate that we might want to include in `result`.
279-
result = *found;
280-
if (result.style == PropagationStyle::W3C) {
281-
return result;
282-
}
266+
// If the W3C style is present and its trace-id matches, we'll update the main
267+
// context with tracestate information that we want to include in `result`. We
268+
// may also need to use Datadog header information (only when the trace-id
269+
// matches).
270+
result = found->second;
283271

284-
const auto other =
285-
std::find_if(found + 1, contexts.end(), [&](const ExtractedData& data) {
286-
return data.style == PropagationStyle::W3C &&
287-
data.trace_id == found->trace_id;
288-
});
272+
const auto w3c = contexts.find(PropagationStyle::W3C);
273+
const auto dd = contexts.find(PropagationStyle::DATADOG);
289274

290-
if (other != contexts.end()) {
291-
result.datadog_w3c_parent_id = other->datadog_w3c_parent_id;
292-
result.additional_w3c_tracestate = other->additional_w3c_tracestate;
275+
if (w3c != contexts.end() && w3c->second.trace_id == result.trace_id) {
276+
result.additional_w3c_tracestate = w3c->second.additional_w3c_tracestate;
293277
result.additional_datadog_w3c_tracestate =
294-
other->additional_datadog_w3c_tracestate;
278+
w3c->second.additional_datadog_w3c_tracestate;
295279
result.headers_examined.insert(result.headers_examined.end(),
296-
other->headers_examined.begin(),
297-
other->headers_examined.end());
280+
w3c->second.headers_examined.begin(),
281+
w3c->second.headers_examined.end());
282+
283+
if (result.parent_id != w3c->second.parent_id) {
284+
if (w3c->second.datadog_w3c_parent_id &&
285+
w3c->second.datadog_w3c_parent_id != "0000000000000000") {
286+
result.datadog_w3c_parent_id = w3c->second.datadog_w3c_parent_id;
287+
} else if (dd != contexts.end() &&
288+
dd->second.trace_id == result.trace_id &&
289+
dd->second.parent_id.has_value()) {
290+
result.datadog_w3c_parent_id = hex_padded(dd->second.parent_id.value());
291+
}
292+
293+
result.parent_id = w3c->second.parent_id;
294+
}
298295
}
299296

300297
return result;

src/datadog/extraction_util.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,12 @@ struct AuditedReader : public DictReader {
6969
// Combine the specified trace `contexts`, each of which was extracted in a
7070
// particular propagation style, into one `ExtractedData` that includes fields
7171
// from compatible elements of `contexts`, and return the resulting
72-
// `ExtractedData`. The order of the elements of `contexts` must correspond to
73-
// the order of the configured extraction propagation styles.
74-
ExtractedData merge(const std::vector<ExtractedData>& contexts);
72+
// `ExtractedData`. The `first_style` specifies the first configured extraction
73+
// propagation style that has been extracted and the other contexts will be
74+
// merged with it, so long as the trace-ids match.
75+
ExtractedData merge(
76+
const PropagationStyle first_style,
77+
const std::unordered_map<PropagationStyle, ExtractedData>& contexts);
7578

7679
} // namespace tracing
7780
} // namespace datadog

src/datadog/tracer.cpp

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ Expected<Span> Tracer::extract_span(const DictReader& reader,
150150
AuditedReader audited_reader{reader};
151151

152152
auto span_data = std::make_unique<SpanData>();
153-
std::vector<ExtractedData> extracted_contexts;
153+
Optional<PropagationStyle> first_style_with_trace_id;
154+
Optional<PropagationStyle> first_style_with_parent_id;
155+
std::unordered_map<PropagationStyle, ExtractedData> extracted_contexts;
154156

155157
for (const auto style : extraction_styles_) {
156158
using Extractor = decltype(&extract_datadog); // function pointer
@@ -175,11 +177,33 @@ Expected<Span> Tracer::extract_span(const DictReader& reader,
175177
return error->with_prefix(
176178
extraction_error_prefix(style, audited_reader.entries_found));
177179
}
178-
extracted_contexts.push_back(std::move(*data));
179-
extracted_contexts.back().headers_examined = audited_reader.entries_found;
180+
181+
if (!first_style_with_trace_id && data->trace_id.has_value()) {
182+
first_style_with_trace_id = style;
183+
}
184+
185+
if (!first_style_with_parent_id && data->parent_id.has_value()) {
186+
first_style_with_parent_id = style;
187+
}
188+
189+
data->headers_examined = audited_reader.entries_found;
190+
extracted_contexts.emplace(style, std::move(*data));
180191
}
181192

182-
auto merged_context = merge(extracted_contexts);
193+
ExtractedData merged_context;
194+
if (!first_style_with_trace_id) {
195+
// Nothing extracted a trace ID. Return the first context that includes a
196+
// parent ID, if any, or otherwise just return an empty `ExtractedData`.
197+
// The purpose of looking for a parent ID is to allow for the error
198+
// "extracted a parent ID without a trace ID," if that's what happened.
199+
if (first_style_with_parent_id) {
200+
auto other = extracted_contexts.find(*first_style_with_parent_id);
201+
assert(other != extracted_contexts.end());
202+
merged_context = other->second;
203+
}
204+
} else {
205+
merged_context = merge(*first_style_with_trace_id, extracted_contexts);
206+
}
183207

184208
// Some information might be missing.
185209
// Here are the combinations considered:

test/test_curl.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ TEST_CASE("setopt failures", "[curl]") {
359359
#define CASE(OPTION) \
360360
{ OPTION, #OPTION }
361361

362-
const auto &[which_fails, name] = GENERATE(
362+
auto test_case = GENERATE(
363363
values<TestCase>({CASE(CURLOPT_ERRORBUFFER), CASE(CURLOPT_HEADERDATA),
364364
CASE(CURLOPT_HEADERFUNCTION), CASE(CURLOPT_HTTPHEADER),
365365
CASE(CURLOPT_POST), CASE(CURLOPT_POSTFIELDS),
@@ -369,17 +369,17 @@ TEST_CASE("setopt failures", "[curl]") {
369369

370370
#undef CASE
371371

372-
CAPTURE(name);
372+
CAPTURE(test_case.name);
373373
MockCurlLibrary library;
374-
library.fail = which_fails;
374+
library.fail = test_case.which_fails;
375375

376376
const auto clock = default_clock;
377377
const auto logger = std::make_shared<NullLogger>();
378378
const auto client = std::make_shared<Curl>(logger, clock, library);
379379

380380
const auto ignore = [](auto &&...) {};
381381
HTTPClient::URL url;
382-
if (which_fails == CURLOPT_UNIX_SOCKET_PATH) {
382+
if (test_case.which_fails == CURLOPT_UNIX_SOCKET_PATH) {
383383
url.scheme = "unix";
384384
url.path = "/foo/bar.sock";
385385
} else {

test/test_span.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -633,13 +633,14 @@ TEST_CASE("injecting W3C traceparent header") {
633633
int sampling_priority;
634634
std::string expected_flags;
635635
};
636-
const auto& [sampling_priority, expected_flags] = GENERATE(
636+
auto test_case = GENERATE(
637637
values<TestCase>({{-1, "00"}, {0, "00"}, {1, "01"}, {2, "01"}}));
638638

639-
CAPTURE(sampling_priority);
640-
CAPTURE(expected_flags);
639+
CAPTURE(test_case.sampling_priority);
640+
CAPTURE(test_case.expected_flags);
641641

642-
span.trace_segment().override_sampling_priority(sampling_priority);
642+
span.trace_segment().override_sampling_priority(
643+
test_case.sampling_priority);
643644

644645
MockDictWriter writer;
645646
span.inject(writer);
@@ -649,7 +650,7 @@ TEST_CASE("injecting W3C traceparent header") {
649650
// The "cafebabe"s come from `expected_id`.
650651
const std::string expected =
651652
"00-000000000000000000000000cafebabe-00000000cafebabe-" +
652-
expected_flags;
653+
test_case.expected_flags;
653654
REQUIRE(found->second == expected);
654655
}
655656
}

test/test_tracer.cpp

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,131 @@ TEST_CASE("span extraction") {
11091109
REQUIRE(span_tags.empty());
11101110
}
11111111

1112+
SECTION("W3C Phase 3 support - Preferring tracecontext") {
1113+
// Tests behavior from system-test
1114+
// test_headers_tracecontext.py::test_tracestate_w3c_p_extract_datadog_w3c
1115+
struct TestCase {
1116+
int line;
1117+
std::string name;
1118+
std::string traceparent;
1119+
Optional<std::string> tracestate;
1120+
Optional<std::string> dd_trace_id;
1121+
Optional<std::string> dd_parent_id;
1122+
Optional<std::string> dd_tags;
1123+
Optional<std::uint64_t> expected_parent_id;
1124+
Optional<std::string> expected_datadog_w3c_parent_id = {};
1125+
};
1126+
1127+
auto test_case = GENERATE(values<TestCase>({
1128+
{
1129+
__LINE__, "identical trace info",
1130+
"00-11111111111111110000000000000001-000000003ade68b1-"
1131+
"01", // traceparent
1132+
"dd=s:2;p:000000003ade68b1,foo=1", // tracestate
1133+
"1", // x-datadog-trace-id
1134+
"987654321", // x-datadog-parent-id
1135+
"_dd.p.tid=1111111111111111", // x-datadog-tags
1136+
987654321, // expected_parent_id
1137+
nullopt, // expected_datadog_w3c_parent_id,
1138+
},
1139+
1140+
{
1141+
__LINE__, "trace ids do not match",
1142+
"00-11111111111111110000000000000002-000000003ade68b1-"
1143+
"01", // traceparent
1144+
"dd=s:2;p:000000000000000a,foo=1", // tracestate
1145+
"2", // x-datadog-trace-id
1146+
"10", // x-datadog-parent-id
1147+
"_dd.p.tid=2222222222222222", // x-datadog-tags
1148+
10, // expected_parent_id
1149+
nullopt, // expected_datadog_w3c_parent_id,
1150+
},
1151+
1152+
{
1153+
__LINE__, "same trace, non-matching parent ids",
1154+
"00-11111111111111110000000000000003-000000003ade68b1-"
1155+
"01", // traceparent
1156+
"dd=s:2;p:000000000000000a,foo=1", // tracestate
1157+
"3", // x-datadog-trace-id
1158+
"10", // x-datadog-parent-id
1159+
"_dd.p.tid=1111111111111111", // x-datadog-tags
1160+
987654321, // expected_parent_id
1161+
"000000000000000a", // expected_datadog_w3c_parent_id,
1162+
},
1163+
1164+
{
1165+
__LINE__, "non-matching span, missing p value",
1166+
"00-00000000000000000000000000000004-000000003ade68b1-"
1167+
"01", // traceparent
1168+
"dd=s:2,foo=1", // tracestate
1169+
"4", // x-datadog-trace-id
1170+
"10", // x-datadog-parent-id
1171+
nullopt, // x-datadog-tags
1172+
987654321, // expected_parent_id
1173+
"000000000000000a", // expected_datadog_w3c_parent_id,
1174+
},
1175+
1176+
{
1177+
__LINE__, "non-matching span, non-matching p value",
1178+
"00-00000000000000000000000000000005-000000003ade68b1-"
1179+
"01", // traceparent
1180+
"dd=s:2;p:8fffffffffffffff,foo=1", // tracestate
1181+
"5", // x-datadog-trace-id
1182+
"10", // x-datadog-parent-id
1183+
nullopt, // x-datadog-tags
1184+
987654321, // expected_parent_id
1185+
"8fffffffffffffff", // expected_datadog_w3c_parent_id,
1186+
},
1187+
}));
1188+
1189+
CAPTURE(test_case.name);
1190+
CAPTURE(test_case.line);
1191+
CAPTURE(test_case.traceparent);
1192+
CAPTURE(test_case.tracestate);
1193+
CAPTURE(test_case.dd_trace_id);
1194+
CAPTURE(test_case.dd_parent_id);
1195+
CAPTURE(test_case.dd_tags);
1196+
1197+
const auto collector = std::make_shared<MockCollector>();
1198+
const auto logger = std::make_shared<MockLogger>();
1199+
1200+
TracerConfig config;
1201+
config.collector = collector;
1202+
config.logger = logger;
1203+
config.service = "service1";
1204+
config.delegate_trace_sampling = false;
1205+
std::vector<PropagationStyle> extraction_styles{
1206+
PropagationStyle::DATADOG, PropagationStyle::B3, PropagationStyle::W3C};
1207+
config.extraction_styles = extraction_styles;
1208+
1209+
auto valid_config = finalize_config(config);
1210+
REQUIRE(valid_config);
1211+
Tracer tracer{*valid_config};
1212+
1213+
std::unordered_map<std::string, std::string> headers;
1214+
headers["traceparent"] = test_case.traceparent;
1215+
if (test_case.tracestate) {
1216+
headers["tracestate"] = *test_case.tracestate;
1217+
}
1218+
if (test_case.dd_trace_id) {
1219+
headers["x-datadog-trace-id"] = *test_case.dd_trace_id;
1220+
}
1221+
if (test_case.dd_parent_id) {
1222+
headers["x-datadog-parent-id"] = *test_case.dd_parent_id;
1223+
}
1224+
if (test_case.dd_tags) {
1225+
headers["x-datadog-tags"] = *test_case.dd_tags;
1226+
}
1227+
MockDictReader reader{headers};
1228+
1229+
const auto span = tracer.extract_span(reader);
1230+
REQUIRE(span);
1231+
1232+
REQUIRE(span->parent_id() == test_case.expected_parent_id);
1233+
REQUIRE(span->lookup_tag(tags::internal::w3c_parent_id) ==
1234+
test_case.expected_datadog_w3c_parent_id);
1235+
}
1236+
11121237
SECTION("_dd.parent_id") {
11131238
auto finalized_config = finalize_config(config);
11141239
REQUIRE(finalized_config);

0 commit comments

Comments
 (0)