From fad515b3c47d75eef3619b16769fc7cdd5bacf4b Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 10:39:57 +0200 Subject: [PATCH 01/14] add span link struct and test --- include/datadog/span_link.h | 38 +++++++++ src/datadog/span_link.cpp | 72 +++++++++++++++++ test/test_span_link.cpp | 157 ++++++++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 include/datadog/span_link.h create mode 100644 src/datadog/span_link.cpp create mode 100644 test/test_span_link.cpp diff --git a/include/datadog/span_link.h b/include/datadog/span_link.h new file mode 100644 index 000000000..d1a34ae4f --- /dev/null +++ b/include/datadog/span_link.h @@ -0,0 +1,38 @@ +#pragma once + +// This component defines `SpanLink`, a causal association between the span that +// owns the link and another span (possibly in another trace). Span links carry +// the linked span's identifiers plus optional W3C tracestate, trace flags, and +// arbitrary string attributes. They are serialized into the owning span under +// the `span_links` key in a format shared by all Datadog tracers. + +#include +#include +#include + +#include "expected.h" +#include "optional.h" +#include "trace_id.h" + +namespace datadog { +namespace tracing { + +struct SpanLink { + // 128-bit trace ID of the linked span. + TraceID trace_id; + // The linked span's ID. + std::uint64_t span_id = 0; + // W3C `tracestate` header value from the linked context, if any. + Optional tracestate; + // Additional string attributes associated with the link. + std::unordered_map attributes; + // W3C trace flags from the linked context, if any. + Optional flags; +}; + +// Append to the specified `destination` the MessagePack representation of the +// specified `link`. +Expected msgpack_encode(std::string& destination, const SpanLink& link); + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/span_link.cpp b/src/datadog/span_link.cpp new file mode 100644 index 000000000..827a91c0f --- /dev/null +++ b/src/datadog/span_link.cpp @@ -0,0 +1,72 @@ +#include + +#include +#include + +#include "msgpack.h" + +namespace datadog { +namespace tracing { + +Expected msgpack_encode(std::string& destination, const SpanLink& link) { + const bool has_trace_id_high = link.trace_id.high != 0; + const bool has_attributes = !link.attributes.empty(); + const bool has_tracestate = link.tracestate && !link.tracestate->empty(); + const bool has_flags = link.flags.has_value(); + + std::size_t size = 2; // trace_id + span_id are always present + if (has_trace_id_high) ++size; + if (has_attributes) ++size; + if (has_tracestate) ++size; + if (has_flags) ++size; + + auto result = msgpack::pack_map(destination, size); + if (!result) return result; + + // trace_id (low 64 bits) + result = msgpack::pack_string(destination, "trace_id"); + if (!result) return result; + msgpack::pack_integer(destination, link.trace_id.low); + + if (has_trace_id_high) { + result = msgpack::pack_string(destination, "trace_id_high"); + if (!result) return result; + msgpack::pack_integer(destination, link.trace_id.high); + } + + result = msgpack::pack_string(destination, "span_id"); + if (!result) return result; + msgpack::pack_integer(destination, link.span_id); + + if (has_attributes) { + result = msgpack::pack_string(destination, "attributes"); + if (!result) return result; + result = msgpack::pack_map( + destination, link.attributes, + [](std::string& destination, const auto& value) { + return msgpack::pack_string(destination, value); + }); + if (!result) return result; + } + + if (has_tracestate) { + result = msgpack::pack_string(destination, "tracestate"); + if (!result) return result; + result = msgpack::pack_string(destination, *link.tracestate); + if (!result) return result; + } + + if (has_flags) { + result = msgpack::pack_string(destination, "flags"); + if (!result) return result; + // The high bit marks "flags is present" so a receiver can distinguish an + // explicit value of 0 from an omitted field. + msgpack::pack_integer(destination, + std::uint64_t(*link.flags | (1u << 31))); + } + + return nullopt; +} + +} // namespace tracing +} // namespace datadog diff --git a/test/test_span_link.cpp b/test/test_span_link.cpp new file mode 100644 index 000000000..67a2b220c --- /dev/null +++ b/test/test_span_link.cpp @@ -0,0 +1,157 @@ +// Tests for `SpanLink` msgpack serialization. The on-the-wire field names and +// omission rules must match the other Datadog tracers (dd-trace-go, -py, -rs). + +#include + +#include +#include + +#include + +#include "test.h" + +using namespace datadog::tracing; + +#define TEST_SPAN_LINK(x) TEST_CASE(x, "[span_link]") + +namespace { +// Encode a single link and decode it back to JSON for inspection. +nlohmann::json encode_to_json(const SpanLink& link) { + std::string buffer; + const auto result = msgpack_encode(buffer, link); + REQUIRE(result); + return nlohmann::json::from_msgpack(buffer); +} +} // namespace + +TEST_SPAN_LINK("minimal link encodes only trace_id and span_id") { + SpanLink link; + link.trace_id = TraceID(0x1122334455667788ULL); // high == 0 + link.span_id = 42; + + const auto j = encode_to_json(link); + + REQUIRE(j.is_object()); + REQUIRE(j.size() == 2); + REQUIRE(j["trace_id"].get() == 0x1122334455667788ULL); + REQUIRE(j["span_id"].get() == 42); + REQUIRE_FALSE(j.contains("trace_id_high")); + REQUIRE_FALSE(j.contains("attributes")); + REQUIRE_FALSE(j.contains("tracestate")); + REQUIRE_FALSE(j.contains("flags")); +} + +TEST_SPAN_LINK("128-bit trace id emits trace_id_high") { + SpanLink link; + link.trace_id = TraceID(/*low=*/0xAAAAAAAAAAAAAAAAULL, + /*high=*/0xBBBBBBBBBBBBBBBBULL); + link.span_id = 7; + + const auto j = encode_to_json(link); + + REQUIRE(j["trace_id"].get() == 0xAAAAAAAAAAAAAAAAULL); + REQUIRE(j["trace_id_high"].get() == 0xBBBBBBBBBBBBBBBBULL); + REQUIRE(j["span_id"].get() == 7); +} + +TEST_SPAN_LINK("attributes encode as a string map and omit when empty") { + SpanLink link; + link.trace_id = TraceID(1); + link.span_id = 2; + + SECTION("present") { + link.attributes = {{"link.key", "value"}, {"k2", "v2"}}; + const auto j = encode_to_json(link); + REQUIRE(j["attributes"]["link.key"].get() == "value"); + REQUIRE(j["attributes"]["k2"].get() == "v2"); + } + + SECTION("empty -> omitted") { + const auto j = encode_to_json(link); + REQUIRE_FALSE(j.contains("attributes")); + } +} + +TEST_SPAN_LINK("tracestate omitted when empty, present otherwise") { + SpanLink link; + link.trace_id = TraceID(1); + link.span_id = 2; + + SECTION("non-empty") { + link.tracestate = "dd=s:1"; + const auto j = encode_to_json(link); + REQUIRE(j["tracestate"].get() == "dd=s:1"); + } + + SECTION("empty string -> omitted") { + link.tracestate = ""; + const auto j = encode_to_json(link); + REQUIRE_FALSE(j.contains("tracestate")); + } + + SECTION("unset -> omitted") { + const auto j = encode_to_json(link); + REQUIRE_FALSE(j.contains("tracestate")); + } +} + +TEST_SPAN_LINK("flags set the high bit when present") { + SpanLink link; + link.trace_id = TraceID(1); + link.span_id = 2; + + SECTION("sampled") { + link.flags = 1u; + const auto j = encode_to_json(link); + REQUIRE(j["flags"].get() == (1u | (1u << 31))); + } + + SECTION("zero is still present with high bit") { + link.flags = 0u; + const auto j = encode_to_json(link); + REQUIRE(j["flags"].get() == (1u << 31)); + } + + SECTION("unset -> omitted") { + const auto j = encode_to_json(link); + REQUIRE_FALSE(j.contains("flags")); + } +} + +#include "span_data.h" // internal header; available via test include dirs + +TEST_SPAN_LINK("SpanData omits span_links when there are none") { + SpanData span; + std::string buffer; + const auto result = msgpack_encode(buffer, span); + REQUIRE(result); + + const auto j = nlohmann::json::from_msgpack(buffer); + REQUIRE(j.is_object()); + REQUIRE_FALSE(j.contains("span_links")); +} + +TEST_SPAN_LINK("SpanData emits span_links array when present") { + SpanData span; + + SpanLink link; + link.trace_id = TraceID(/*low=*/0x99, /*high=*/0x11); + link.span_id = 123; + link.attributes = {{"link.key", "value"}}; + span.span_links.push_back(link); + + std::string buffer; + const auto result = msgpack_encode(buffer, span); + REQUIRE(result); + + const auto j = nlohmann::json::from_msgpack(buffer); + REQUIRE(j.contains("span_links")); + REQUIRE(j["span_links"].is_array()); + REQUIRE(j["span_links"].size() == 1); + + const auto& encoded = j["span_links"][0]; + REQUIRE(encoded["trace_id"].get() == 0x99); + REQUIRE(encoded["trace_id_high"].get() == 0x11); + REQUIRE(encoded["span_id"].get() == 123); + REQUIRE(encoded["attributes"]["link.key"].get() == "value"); +} From 07969736e4062c898d357b690ccc29efabfd1d00 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 10:40:43 +0200 Subject: [PATCH 02/14] enable span link tests --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7571aa8b7..1496765d2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable(tests test_parse_util.cpp test_smoke.cpp test_span.cpp + test_span_link.cpp test_span_sampler.cpp test_trace_id.cpp test_trace_segment.cpp From 0fecaca822435034c10b56b7bc020ab83d0a49f0 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 10:45:32 +0200 Subject: [PATCH 03/14] implement Span::add_link --- CMakeLists.txt | 1 + include/datadog/span.h | 5 +++++ src/datadog/span.cpp | 6 ++++++ src/datadog/span_data.h | 2 ++ 4 files changed, 14 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cc154f17..aa290a306 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,6 +203,7 @@ target_sources(dd-trace-cpp-objects src/datadog/runtime_id.cpp src/datadog/span.cpp src/datadog/span_data.cpp + src/datadog/span_link.cpp src/datadog/span_matcher.cpp src/datadog/span_sampler_config.cpp src/datadog/span_sampler.cpp diff --git a/include/datadog/span.h b/include/datadog/span.h index 0018230f6..748a4a4dd 100644 --- a/include/datadog/span.h +++ b/include/datadog/span.h @@ -59,6 +59,7 @@ class DictReader; class DictWriter; struct SpanConfig; struct SpanData; +struct SpanLink; class TraceSegment; class Span { @@ -166,6 +167,10 @@ class Span { // Specifies the product (AppSec, DBM) that created this span. void set_source(Source); + // Add a link to this span. The link is serialized with this span under + // `span_links`. + void add_link(const SpanLink& link); + // Write information about this span and its trace into the specified `writer` // using all of the configured injection propagation styles. void inject(DictWriter& writer) const; diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index 7d076996b..cdfceb659 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -8,6 +8,8 @@ #include #include +#include + #include "span_data.h" #include "tags.h" @@ -164,6 +166,10 @@ void Span::set_source(Source source) { to_tag(source)); } +void Span::add_link(const SpanLink& link) { + data_->span_links.push_back(link); +} + TraceSegment& Span::trace_segment() { return *trace_segment_; } const TraceSegment& Span::trace_segment() const { return *trace_segment_; } diff --git a/src/datadog/span_data.h b/src/datadog/span_data.h index 9413e173b..3140fb616 100644 --- a/src/datadog/span_data.h +++ b/src/datadog/span_data.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,7 @@ struct SpanData { bool error = false; std::unordered_map tags; std::unordered_map numeric_tags; + std::vector span_links; Optional environment() const; Optional version() const; From f2545626531e7a0eb0d21b1ff2e61e7d62945ca9 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 10:47:35 +0200 Subject: [PATCH 04/14] add span link to msg pack encoding --- src/datadog/span_data.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/datadog/span_data.cpp b/src/datadog/span_data.cpp index 7ed4ebb21..c54fb9058 100644 --- a/src/datadog/span_data.cpp +++ b/src/datadog/span_data.cpp @@ -75,7 +75,13 @@ void SpanData::apply_config(const SpanDefaults& defaults, Expected msgpack_encode(std::string& destination, const SpanData& span) { // clang-format off - msgpack::pack_map( + const bool has_links = !span.span_links.empty(); + + // 12 always-present fields, plus span_links when there are any. + auto result = msgpack::pack_map(destination, has_links ? 13u : 12u); + if (!result) return result; + + result = msgpack::pack_map_suffix( destination, "service", [&](auto& destination) { return msgpack::pack_string(destination, span.service); @@ -131,6 +137,18 @@ Expected msgpack_encode(std::string& destination, const SpanData& span) { }, "type", [&](auto& destination) { return msgpack::pack_string(destination, span.service_type); }); + if (!result) return result; + + if (has_links) { + result = msgpack::pack_string(destination, "span_links"); + if (!result) return result; + result = msgpack::pack_array( + destination, span.span_links, + [](std::string& destination, const SpanLink& link) { + return msgpack_encode(destination, link); + }); + if (!result) return result; + } // clang-format on return nullopt; From 58cf95d35f5a47ab37abfdc7d3a5d606433880da Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 10:48:16 +0200 Subject: [PATCH 05/14] add spn link test for the Span class --- test/test_span.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/test_span.cpp b/test/test_span.cpp index 4af524ed7..589b9d1df 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1084,3 +1085,32 @@ TEST_SPAN("injection behaviour when apm tracing is disabled") { CHECK(contains_tracing_context(writer.items)); } } + +TEST_SPAN("add_link records links on the span data") { + TracerConfig config; + config.service = "testsvc"; + const auto collector = std::make_shared(); + config.collector = collector; + config.logger = std::make_shared(); + + auto finalized_config = finalize_config(config); + REQUIRE(finalized_config); + Tracer tracer{*finalized_config}; + + { + auto span = tracer.create_span(); + + SpanLink link; + link.trace_id = TraceID(/*low=*/0xABC, /*high=*/0xDEF); + link.span_id = 99; + link.attributes = {{"link.key", "value"}}; + span.add_link(link); + } + + REQUIRE(collector->chunks.size() == 1); + const auto& span_data = collector->first_span(); + REQUIRE(span_data.span_links.size() == 1); + REQUIRE(span_data.span_links[0].trace_id == TraceID(0xABC, 0xDEF)); + REQUIRE(span_data.span_links[0].span_id == 99); + REQUIRE(span_data.span_links[0].attributes.at("link.key") == "value"); +} From 84a7ef4e3814f578708ee6cd46965ca7b3cd8a4a Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 10:48:52 +0200 Subject: [PATCH 06/14] implenent add_link endpoint for parametric tests --- test/system-tests/main.cpp | 4 ++ test/system-tests/request_handler.cpp | 61 +++++++++++++++++++++++++++ test/system-tests/request_handler.h | 1 + 3 files changed, 66 insertions(+) diff --git a/test/system-tests/main.cpp b/test/system-tests/main.cpp index de7d421e1..18ba58466 100644 --- a/test/system-tests/main.cpp +++ b/test/system-tests/main.cpp @@ -128,6 +128,10 @@ int main(int argc, char* argv[]) { [&handler](const httplib::Request& req, httplib::Response& res) { handler.on_manual_keep(req, res); }); + svr.Post("/trace/span/add_link", + [&handler](const httplib::Request& req, httplib::Response& res) { + handler.on_add_link(req, res); + }); // Not implemented svr.Post("/trace/span/set_metric", diff --git a/test/system-tests/request_handler.cpp b/test/system-tests/request_handler.cpp index df6055837..8f1e1aa4a 100644 --- a/test/system-tests/request_handler.cpp +++ b/test/system-tests/request_handler.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -236,6 +237,66 @@ void RequestHandler::on_set_meta(const httplib::Request& req, res.status = 200; } +void RequestHandler::on_add_link(const httplib::Request& req, + httplib::Response& res) { + const auto request_json = nlohmann::json::parse(req.body); + + auto span_id = utils::get_if_exists(request_json, "span_id"); + if (!span_id) { + VALIDATION_ERROR(res, "on_add_link: missing `span_id` field."); + } + + auto parent_id = utils::get_if_exists(request_json, "parent_id"); + if (!parent_id) { + VALIDATION_ERROR(res, "on_add_link: missing `parent_id` field."); + } + + auto span_it = active_spans_.find(*span_id); + if (span_it == active_spans_.cend()) { + const auto msg = + "on_add_link: span not found for id " + std::to_string(*span_id); + VALIDATION_ERROR(res, msg); + } + + datadog::tracing::SpanLink link; + + // The linked context is either another active span or a previously extracted + // propagation context, both keyed by `parent_id`. + auto linked_it = active_spans_.find(*parent_id); + if (linked_it != active_spans_.cend()) { + link.trace_id = linked_it->second.trace_id(); + link.span_id = linked_it->second.id(); + } else { + auto context_it = tracing_context_.find(*parent_id); + if (context_it == tracing_context_.cend()) { + const auto msg = "on_add_link: linked context not found for parent_id " + + std::to_string(*parent_id); + VALIDATION_ERROR(res, msg); + } + auto linked = tracer_.extract_span(utils::HeaderReader(context_it->second)); + if (!linked) { + VALIDATION_ERROR(res, + "on_add_link: unable to extract linked context for " + "parent_id " + + std::to_string(*parent_id)); + } + link.trace_id = linked->trace_id(); + link.span_id = linked->parent_id().value_or(0); + } + + if (auto attributes = request_json.find("attributes"); + attributes != request_json.cend() && attributes->is_object()) { + for (const auto& [key, value] : attributes->items()) { + if (value.is_string()) { + link.attributes.emplace(key, value.get()); + } + } + } + + span_it->second.add_link(link); + res.status = 200; +} + void RequestHandler::on_set_metric(const httplib::Request& req, httplib::Response& res) { const auto request_json = nlohmann::json::parse(req.body); diff --git a/test/system-tests/request_handler.h b/test/system-tests/request_handler.h index 09baf9753..ba501cbf9 100644 --- a/test/system-tests/request_handler.h +++ b/test/system-tests/request_handler.h @@ -24,6 +24,7 @@ class RequestHandler final { void on_span_start(const httplib::Request& req, httplib::Response& res); void on_span_end(const httplib::Request& req, httplib::Response& res); void on_set_meta(const httplib::Request& req, httplib::Response& res); + void on_add_link(const httplib::Request& req, httplib::Response& res); void on_set_metric(const httplib::Request& /* req */, httplib::Response& res); void on_manual_keep(const httplib::Request& req, httplib::Response& res); void on_manual_drop(const httplib::Request& req, httplib::Response& res); From 65382c437377d66da8e8debc185ede80cad02660 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 11:55:34 +0200 Subject: [PATCH 07/14] flatten array --- test/system-tests/request_handler.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/system-tests/request_handler.cpp b/test/system-tests/request_handler.cpp index 8f1e1aa4a..09d846882 100644 --- a/test/system-tests/request_handler.cpp +++ b/test/system-tests/request_handler.cpp @@ -289,6 +289,22 @@ void RequestHandler::on_add_link(const httplib::Request& req, for (const auto& [key, value] : attributes->items()) { if (value.is_string()) { link.attributes.emplace(key, value.get()); + } else if (value.is_array()) { + std::size_t idx = 0; + for (const auto& elem : value) { + std::string flat_key = key + "." + std::to_string(idx++); + if (elem.is_string()) { + link.attributes.emplace(flat_key, elem.get()); + } else if (elem.is_boolean()) { + link.attributes.emplace(flat_key, elem.get() ? "true" : "false"); + } else if (elem.is_number_integer()) { + link.attributes.emplace(flat_key, + std::to_string(elem.get())); + } else if (elem.is_number_float()) { + link.attributes.emplace(flat_key, + std::to_string(elem.get())); + } + } } } } From ecfb7cc71e18406d39d217885434306097a69720 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 14:21:01 +0200 Subject: [PATCH 08/14] cleaner spanlink construction --- include/datadog/span.h | 16 +++- include/datadog/span_link.h | 10 +++ src/datadog/span.cpp | 25 ++++-- src/datadog/span_link.cpp | 31 +++++++ test/system-tests/request_handler.cpp | 115 +++++++++++++++++--------- test/system-tests/request_handler.h | 3 + test/test_span.cpp | 36 +++++--- 7 files changed, 175 insertions(+), 61 deletions(-) diff --git a/include/datadog/span.h b/include/datadog/span.h index 748a4a4dd..6cca22299 100644 --- a/include/datadog/span.h +++ b/include/datadog/span.h @@ -47,6 +47,7 @@ #include "clock.h" #include "optional.h" +#include "span_link.h" #include "string_view.h" #include "trace_id.h" #include "trace_source.h" @@ -54,12 +55,12 @@ namespace datadog { namespace tracing { +struct ExtractedContext; struct InjectionOptions; class DictReader; class DictWriter; struct SpanConfig; struct SpanData; -struct SpanLink; class TraceSegment; class Span { @@ -167,14 +168,21 @@ class Span { // Specifies the product (AppSec, DBM) that created this span. void set_source(Source); - // Add a link to this span. The link is serialized with this span under - // `span_links`. - void add_link(const SpanLink& link); + // Add a span link to this span, filled from the specified live span. + // The linked span's trace ID, span ID, tracestate, and trace flags are + // populated automatically. `attrs` is optional user-supplied attributes. + void add_link(const Span& linked, const SpanLinkAttributes& attrs = {}); + // Add a span link to this span, filled from the specified extracted + // distributed-tracing context. + void add_link(const ExtractedContext& ctx, + const SpanLinkAttributes& attrs = {}); // Write information about this span and its trace into the specified `writer` // using all of the configured injection propagation styles. void inject(DictWriter& writer) const; void inject(DictWriter& writer, const InjectionOptions& options) const; + // Return the injected headers as a map. + std::unordered_map inject() const; // Return a reference to this span's trace segment. The trace segment has // member functions that affect the trace as a whole, such as diff --git a/include/datadog/span_link.h b/include/datadog/span_link.h index d1a34ae4f..47d9bd59b 100644 --- a/include/datadog/span_link.h +++ b/include/datadog/span_link.h @@ -17,6 +17,12 @@ namespace datadog { namespace tracing { +class Span; +struct ExtractedContext; + +// Convenience alias: the map type used for span link user attributes. +using SpanLinkAttributes = std::unordered_map; + struct SpanLink { // 128-bit trace ID of the linked span. TraceID trace_id; @@ -28,6 +34,10 @@ struct SpanLink { std::unordered_map attributes; // W3C trace flags from the linked context, if any. Optional flags; + + SpanLink() = default; + SpanLink(const Span& linked, const SpanLinkAttributes& attrs = {}); + SpanLink(const ExtractedContext& ctx, const SpanLinkAttributes& attrs = {}); }; // Append to the specified `destination` the MessagePack representation of the diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index cdfceb659..6974a1ced 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -1,15 +1,14 @@ -#include +#include #include #include #include +#include #include #include #include #include -#include - #include "span_data.h" #include "tags.h" @@ -63,6 +62,17 @@ void Span::inject(DictWriter& writer) const { trace_segment_->inject(writer, *data_); } +std::unordered_map Span::inject() const { + struct MapWriter : DictWriter { + std::unordered_map map; + void set(StringView k, StringView v) override { + map[std::string(k)] = std::string(v); + } + } writer; + inject(writer); + return std::move(writer.map); +} + void Span::inject(DictWriter& writer, const InjectionOptions& options) const { trace_segment_->inject(writer, *data_, options); } @@ -166,8 +176,13 @@ void Span::set_source(Source source) { to_tag(source)); } -void Span::add_link(const SpanLink& link) { - data_->span_links.push_back(link); +void Span::add_link(const Span& linked, const SpanLinkAttributes& attrs) { + data_->span_links.emplace_back(linked, attrs); +} + +void Span::add_link(const ExtractedContext& ctx, + const SpanLinkAttributes& attrs) { + data_->span_links.emplace_back(ctx, attrs); } TraceSegment& Span::trace_segment() { return *trace_segment_; } diff --git a/src/datadog/span_link.cpp b/src/datadog/span_link.cpp index 827a91c0f..4c779cfa7 100644 --- a/src/datadog/span_link.cpp +++ b/src/datadog/span_link.cpp @@ -1,13 +1,44 @@ +#include +#include #include #include #include +#include #include "msgpack.h" namespace datadog { namespace tracing { +SpanLink::SpanLink(const Span& linked, const SpanLinkAttributes& attrs) + : trace_id(linked.trace_id()), span_id(linked.id()), attributes(attrs) { + const auto headers = linked.inject(); + + if (auto it = headers.find("tracestate"); it != headers.end()) { + tracestate = it->second; + } + if (auto it = headers.find("traceparent"); it != headers.end()) { + const auto& tp = it->second; + const auto pos = tp.rfind('-'); + if (pos != std::string::npos && pos + 1 < tp.size()) { + try { + flags = static_cast( + std::stoul(tp.substr(pos + 1), nullptr, 16)); + } catch (...) { + } + } + } +} + +SpanLink::SpanLink(const ExtractedContext& ctx, const SpanLinkAttributes& attrs) + : trace_id(ctx.trace_id), + span_id(ctx.span_id), + tracestate(ctx.tracestate), + attributes(attrs), + flags(ctx.flags) {} + + Expected msgpack_encode(std::string& destination, const SpanLink& link) { const bool has_trace_id_high = link.trace_id.high != 0; const bool has_attributes = !link.attributes.empty(); diff --git a/test/system-tests/request_handler.cpp b/test/system-tests/request_handler.cpp index 09d846882..ca2824d9d 100644 --- a/test/system-tests/request_handler.cpp +++ b/test/system-tests/request_handler.cpp @@ -1,5 +1,6 @@ #include "request_handler.h" +#include #include #include #include @@ -258,58 +259,66 @@ void RequestHandler::on_add_link(const httplib::Request& req, VALIDATION_ERROR(res, msg); } - datadog::tracing::SpanLink link; - - // The linked context is either another active span or a previously extracted - // propagation context, both keyed by `parent_id`. - auto linked_it = active_spans_.find(*parent_id); - if (linked_it != active_spans_.cend()) { - link.trace_id = linked_it->second.trace_id(); - link.span_id = linked_it->second.id(); - } else { - auto context_it = tracing_context_.find(*parent_id); - if (context_it == tracing_context_.cend()) { - const auto msg = "on_add_link: linked context not found for parent_id " + - std::to_string(*parent_id); - VALIDATION_ERROR(res, msg); - } - auto linked = tracer_.extract_span(utils::HeaderReader(context_it->second)); - if (!linked) { - VALIDATION_ERROR(res, - "on_add_link: unable to extract linked context for " - "parent_id " + - std::to_string(*parent_id)); - } - link.trace_id = linked->trace_id(); - link.span_id = linked->parent_id().value_or(0); - } - + // Parse optional attributes, supporting flat strings and arrays. + datadog::tracing::SpanLinkAttributes attrs; if (auto attributes = request_json.find("attributes"); attributes != request_json.cend() && attributes->is_object()) { for (const auto& [key, value] : attributes->items()) { if (value.is_string()) { - link.attributes.emplace(key, value.get()); + attrs.emplace(key, value.get()); } else if (value.is_array()) { std::size_t idx = 0; for (const auto& elem : value) { std::string flat_key = key + "." + std::to_string(idx++); if (elem.is_string()) { - link.attributes.emplace(flat_key, elem.get()); + attrs.emplace(flat_key, elem.get()); } else if (elem.is_boolean()) { - link.attributes.emplace(flat_key, elem.get() ? "true" : "false"); + attrs.emplace(flat_key, elem.get() ? "true" : "false"); } else if (elem.is_number_integer()) { - link.attributes.emplace(flat_key, - std::to_string(elem.get())); + attrs.emplace(flat_key, std::to_string(elem.get())); } else if (elem.is_number_float()) { - link.attributes.emplace(flat_key, - std::to_string(elem.get())); + attrs.emplace(flat_key, std::to_string(elem.get())); } } } } } - span_it->second.add_link(link); + // The linked context is either another active span or a previously extracted + // propagation context, both keyed by `parent_id`. + auto linked_it = active_spans_.find(*parent_id); + if (linked_it != active_spans_.cend()) { + // Propagate sampling priority from the linked span's trace. + nlohmann::json linked_hdrs = nlohmann::json::array(); + utils::HeaderWriter linked_writer(linked_hdrs); + linked_it->second.inject(linked_writer); + for (const auto& hdr : linked_hdrs) { + if (hdr.size() == 2 && + hdr[0].get() == "x-datadog-sampling-priority") { + try { + span_it->second.trace_segment().override_sampling_priority( + std::stoi(hdr[1].get())); + } catch (...) { + } + break; + } + } + span_it->second.add_link(linked_it->second, attrs); + } else { + auto context_it = link_contexts_.find(*parent_id); + if (context_it == link_contexts_.cend()) { + const auto msg = "on_add_link: linked context not found for parent_id " + + std::to_string(*parent_id); + VALIDATION_ERROR(res, msg); + } + const auto& ctx = context_it->second; + if (ctx.sampling_priority.has_value()) { + span_it->second.trace_segment().override_sampling_priority( + *ctx.sampling_priority); + } + span_it->second.add_link(ctx, attrs); + } + res.status = 200; } @@ -422,18 +431,41 @@ void RequestHandler::on_extract_headers(const httplib::Request& req, auto span = tracer_.extract_span(utils::HeaderReader(*http_headers)); if (span.if_error()) { - const auto response_body_fail = nlohmann::json{ - {"span_id", nullptr}, - }; + const auto response_body_fail = nlohmann::json{{"span_id", nullptr}}; res.set_content(response_body_fail.dump(), "application/json"); return; } - const auto response_body = nlohmann::json{ - {"span_id", span->parent_id().value()}, - }; + const auto upstream_id = span->parent_id().value_or(0); + datadog::tracing::ExtractedContext ctx; + ctx.trace_id = span->trace_id(); + ctx.span_id = upstream_id; + for (const auto& hdr : *http_headers) { + if (hdr.size() != 2) continue; + const auto name = hdr[0].get(); + if (name == "tracestate") { + ctx.tracestate = hdr[1].get(); + } else if (name == "traceparent") { + const auto tp = hdr[1].get(); + const auto pos = tp.rfind('-'); + if (pos != std::string::npos && pos + 1 < tp.size()) { + try { + ctx.flags = static_cast( + std::stoul(tp.substr(pos + 1), nullptr, 16)); + } catch (...) { + } + } + } else if (name == "x-datadog-sampling-priority") { + try { + ctx.sampling_priority = std::stoi(hdr[1].get()); + } catch (...) { + } + } + } - tracing_context_[*span->parent_id()] = std::move(*http_headers); + const auto response_body = nlohmann::json{{"span_id", upstream_id}}; + tracing_context_[upstream_id] = std::move(*http_headers); + link_contexts_[upstream_id] = ctx; // The span below will not be finished and flushed. blackhole_.emplace_back(std::move(*span)); @@ -446,6 +478,7 @@ void RequestHandler::on_span_flush(const httplib::Request& /* req */, scheduler_->flush_telemetry(); active_spans_.clear(); tracing_context_.clear(); + link_contexts_.clear(); res.status = 200; } diff --git a/test/system-tests/request_handler.h b/test/system-tests/request_handler.h index ba501cbf9..d30924415 100644 --- a/test/system-tests/request_handler.h +++ b/test/system-tests/request_handler.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -41,6 +42,8 @@ class RequestHandler final { std::shared_ptr logger_; std::unordered_map active_spans_; std::unordered_map tracing_context_; + std::unordered_map + link_contexts_; // Previously, `/trace/span/start` was used to create new spans or create // child spans from the extracted tracing context. diff --git a/test/test_span.cpp b/test/test_span.cpp index 589b9d1df..dd4c94d78 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -1097,20 +1097,34 @@ TEST_SPAN("add_link records links on the span data") { REQUIRE(finalized_config); Tracer tracer{*finalized_config}; + TraceID linked_trace_id; + std::uint64_t linked_span_id = 0; + { auto span = tracer.create_span(); + auto linked = tracer.create_span(); + linked_trace_id = linked.trace_id(); + linked_span_id = linked.id(); - SpanLink link; - link.trace_id = TraceID(/*low=*/0xABC, /*high=*/0xDEF); - link.span_id = 99; - link.attributes = {{"link.key", "value"}}; - span.add_link(link); + const SpanLinkAttributes attrs{{"link.key", "value"}}; + span.add_link(linked, attrs); } - REQUIRE(collector->chunks.size() == 1); - const auto& span_data = collector->first_span(); - REQUIRE(span_data.span_links.size() == 1); - REQUIRE(span_data.span_links[0].trace_id == TraceID(0xABC, 0xDEF)); - REQUIRE(span_data.span_links[0].span_id == 99); - REQUIRE(span_data.span_links[0].attributes.at("link.key") == "value"); + // Find the span that carries the link across all chunks. + const SpanData* span_with_link = nullptr; + for (const auto& chunk : collector->chunks) { + for (const auto& sd : chunk) { + if (!sd->span_links.empty()) { + span_with_link = sd.get(); + } + } + } + REQUIRE(span_with_link != nullptr); + REQUIRE(span_with_link->span_links.size() == 1); + const auto& link = span_with_link->span_links[0]; + REQUIRE(link.trace_id == linked_trace_id); + REQUIRE(link.span_id == linked_span_id); + REQUIRE(link.attributes.at("link.key") == "value"); + // traceparent is always injected with W3C propagation, so flags must be set. + REQUIRE(link.flags.has_value()); } From eecfe932a10233e5ccb4af8a96a7c8f32e1abe82 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 14:31:27 +0200 Subject: [PATCH 09/14] add extracted context struct --- include/datadog/extracted_context.h | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 include/datadog/extracted_context.h diff --git a/include/datadog/extracted_context.h b/include/datadog/extracted_context.h new file mode 100644 index 000000000..4dff49846 --- /dev/null +++ b/include/datadog/extracted_context.h @@ -0,0 +1,30 @@ +#pragma once + +// Tracing context extracted from incoming distributed headers. Unlike `Span`, +// this type holds only the propagation fields of the upstream context and does +// not represent a locally-owned span. + +#include +#include + +#include "optional.h" +#include "trace_id.h" + +namespace datadog { +namespace tracing { + +struct ExtractedContext { + TraceID trace_id; + // The upstream span's ID (e.g. x-datadog-parent-id or traceparent + // parent-id field). + std::uint64_t span_id = 0; + // W3C tracestate, if present in the incoming headers. + Optional tracestate; + // W3C trace flags byte, if present in the incoming traceparent header. + Optional flags; + // Sampling priority extracted from the incoming headers. + Optional sampling_priority; +}; + +} // namespace tracing +} // namespace datadog From c4ae457ef521ccb96577e6e417c0b4d0dd7b795c Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 14:51:47 +0200 Subject: [PATCH 10/14] cleaning up (move logic to span.cpp) --- include/datadog/span.h | 2 -- include/datadog/span_link.h | 7 ------ src/datadog/span.cpp | 49 ++++++++++++++++++++++++++++--------- src/datadog/span_link.cpp | 30 ----------------------- 4 files changed, 37 insertions(+), 51 deletions(-) diff --git a/include/datadog/span.h b/include/datadog/span.h index 6cca22299..9f2126b43 100644 --- a/include/datadog/span.h +++ b/include/datadog/span.h @@ -181,8 +181,6 @@ class Span { // using all of the configured injection propagation styles. void inject(DictWriter& writer) const; void inject(DictWriter& writer, const InjectionOptions& options) const; - // Return the injected headers as a map. - std::unordered_map inject() const; // Return a reference to this span's trace segment. The trace segment has // member functions that affect the trace as a whole, such as diff --git a/include/datadog/span_link.h b/include/datadog/span_link.h index 47d9bd59b..198be47f8 100644 --- a/include/datadog/span_link.h +++ b/include/datadog/span_link.h @@ -17,9 +17,6 @@ namespace datadog { namespace tracing { -class Span; -struct ExtractedContext; - // Convenience alias: the map type used for span link user attributes. using SpanLinkAttributes = std::unordered_map; @@ -34,10 +31,6 @@ struct SpanLink { std::unordered_map attributes; // W3C trace flags from the linked context, if any. Optional flags; - - SpanLink() = default; - SpanLink(const Span& linked, const SpanLinkAttributes& attrs = {}); - SpanLink(const ExtractedContext& ctx, const SpanLinkAttributes& attrs = {}); }; // Append to the specified `destination` the MessagePack representation of the diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index 6974a1ced..ae1b89607 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -62,16 +63,6 @@ void Span::inject(DictWriter& writer) const { trace_segment_->inject(writer, *data_); } -std::unordered_map Span::inject() const { - struct MapWriter : DictWriter { - std::unordered_map map; - void set(StringView k, StringView v) override { - map[std::string(k)] = std::string(v); - } - } writer; - inject(writer); - return std::move(writer.map); -} void Span::inject(DictWriter& writer, const InjectionOptions& options) const { trace_segment_->inject(writer, *data_, options); @@ -177,12 +168,46 @@ void Span::set_source(Source source) { } void Span::add_link(const Span& linked, const SpanLinkAttributes& attrs) { - data_->span_links.emplace_back(linked, attrs); + SpanLink link; + link.trace_id = linked.trace_id(); + link.span_id = linked.id(); + link.attributes = attrs; + + // Capture injected headers (tracestate, traceparent flags) into a map. + struct : DictWriter { + std::unordered_map map; + void set(StringView k, StringView v) override { + map[std::string(k)] = std::string(v); + } + } w; + linked.inject(w); + if (auto it = w.map.find("tracestate"); it != w.map.end()) { + link.tracestate = it->second; + } + if (auto it = w.map.find("traceparent"); it != w.map.end()) { + const auto& tp = it->second; + const auto pos = tp.rfind('-'); + if (pos != std::string::npos && pos + 1 < tp.size()) { + try { + link.flags = static_cast( + std::stoul(tp.substr(pos + 1), nullptr, 16)); + } catch (...) { + } + } + } + + data_->span_links.push_back(std::move(link)); } void Span::add_link(const ExtractedContext& ctx, const SpanLinkAttributes& attrs) { - data_->span_links.emplace_back(ctx, attrs); + SpanLink link; + link.trace_id = ctx.trace_id; + link.span_id = ctx.span_id; + link.tracestate = ctx.tracestate; + link.attributes = attrs; + link.flags = ctx.flags; + data_->span_links.push_back(std::move(link)); } TraceSegment& Span::trace_segment() { return *trace_segment_; } diff --git a/src/datadog/span_link.cpp b/src/datadog/span_link.cpp index 4c779cfa7..1a7b80cd2 100644 --- a/src/datadog/span_link.cpp +++ b/src/datadog/span_link.cpp @@ -1,9 +1,6 @@ -#include -#include #include #include -#include #include #include "msgpack.h" @@ -11,33 +8,6 @@ namespace datadog { namespace tracing { -SpanLink::SpanLink(const Span& linked, const SpanLinkAttributes& attrs) - : trace_id(linked.trace_id()), span_id(linked.id()), attributes(attrs) { - const auto headers = linked.inject(); - - if (auto it = headers.find("tracestate"); it != headers.end()) { - tracestate = it->second; - } - if (auto it = headers.find("traceparent"); it != headers.end()) { - const auto& tp = it->second; - const auto pos = tp.rfind('-'); - if (pos != std::string::npos && pos + 1 < tp.size()) { - try { - flags = static_cast( - std::stoul(tp.substr(pos + 1), nullptr, 16)); - } catch (...) { - } - } - } -} - -SpanLink::SpanLink(const ExtractedContext& ctx, const SpanLinkAttributes& attrs) - : trace_id(ctx.trace_id), - span_id(ctx.span_id), - tracestate(ctx.tracestate), - attributes(attrs), - flags(ctx.flags) {} - Expected msgpack_encode(std::string& destination, const SpanLink& link) { const bool has_trace_id_high = link.trace_id.high != 0; From 86d189efd13ae26d4020dabb40d46b24e4a048f3 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 15:23:45 +0200 Subject: [PATCH 11/14] format --- src/datadog/span.cpp | 1 - src/datadog/span_data.h | 2 +- src/datadog/span_link.cpp | 14 ++++++-------- test/test_span_link.cpp | 3 +-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index ae1b89607..1ce19e3d7 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -63,7 +63,6 @@ void Span::inject(DictWriter& writer) const { trace_segment_->inject(writer, *data_); } - void Span::inject(DictWriter& writer, const InjectionOptions& options) const { trace_segment_->inject(writer, *data_, options); } diff --git a/src/datadog/span_data.h b/src/datadog/span_data.h index 3140fb616..efbda770e 100644 --- a/src/datadog/span_data.h +++ b/src/datadog/span_data.h @@ -6,8 +6,8 @@ #include #include #include -#include #include +#include #include #include diff --git a/src/datadog/span_link.cpp b/src/datadog/span_link.cpp index 1a7b80cd2..b3b1c91a5 100644 --- a/src/datadog/span_link.cpp +++ b/src/datadog/span_link.cpp @@ -8,7 +8,6 @@ namespace datadog { namespace tracing { - Expected msgpack_encode(std::string& destination, const SpanLink& link) { const bool has_trace_id_high = link.trace_id.high != 0; const bool has_attributes = !link.attributes.empty(); @@ -42,11 +41,11 @@ Expected msgpack_encode(std::string& destination, const SpanLink& link) { if (has_attributes) { result = msgpack::pack_string(destination, "attributes"); if (!result) return result; - result = msgpack::pack_map( - destination, link.attributes, - [](std::string& destination, const auto& value) { - return msgpack::pack_string(destination, value); - }); + result = + msgpack::pack_map(destination, link.attributes, + [](std::string& destination, const auto& value) { + return msgpack::pack_string(destination, value); + }); if (!result) return result; } @@ -62,8 +61,7 @@ Expected msgpack_encode(std::string& destination, const SpanLink& link) { if (!result) return result; // The high bit marks "flags is present" so a receiver can distinguish an // explicit value of 0 from an omitted field. - msgpack::pack_integer(destination, - std::uint64_t(*link.flags | (1u << 31))); + msgpack::pack_integer(destination, std::uint64_t(*link.flags | (1u << 31))); } return nullopt; diff --git a/test/test_span_link.cpp b/test/test_span_link.cpp index 67a2b220c..8fbf9cb04 100644 --- a/test/test_span_link.cpp +++ b/test/test_span_link.cpp @@ -4,9 +4,8 @@ #include #include -#include - #include +#include #include "test.h" From dff6677e6945192ebecc8a3a8692eaa89a364019 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 15:44:11 +0200 Subject: [PATCH 12/14] fix BAZEL build --- BUILD.bazel | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BUILD.bazel b/BUILD.bazel index df17ac563..0b467a696 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -55,6 +55,7 @@ cc_library( "src/datadog/span.cpp", "src/datadog/span_data.cpp", "src/datadog/span_data.h", + "src/datadog/span_link.cpp", "src/datadog/span_matcher.cpp", "src/datadog/span_sampler.cpp", "src/datadog/span_sampler.h", @@ -112,6 +113,7 @@ cc_library( "include/datadog/environment.h", "include/datadog/error.h", "include/datadog/event_scheduler.h", + "include/datadog/extracted_context.h", "include/datadog/expected.h", "include/datadog/http_client.h", "include/datadog/http_endpoint_calculation_mode.h", @@ -132,6 +134,7 @@ cc_library( "include/datadog/span.h", "include/datadog/span_config.h", "include/datadog/span_defaults.h", + "include/datadog/span_link.h", "include/datadog/span_matcher.h", "include/datadog/span_sampler_config.h", "include/datadog/string_view.h", From 1a36ade09f6841b5c62fd205a7fc3c3177dcc59f Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 16:21:12 +0200 Subject: [PATCH 13/14] derive W3C flags from sampling priority in add_link(ExtractedContext) --- src/datadog/span.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index 1ce19e3d7..ec2eb8a90 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -205,7 +205,11 @@ void Span::add_link(const ExtractedContext& ctx, link.span_id = ctx.span_id; link.tracestate = ctx.tracestate; link.attributes = attrs; - link.flags = ctx.flags; + if (ctx.flags.has_value()) { + link.flags = ctx.flags; + } else if (ctx.sampling_priority.has_value()) { + link.flags = *ctx.sampling_priority > 0 ? 1u : 0u; + } data_->span_links.push_back(std::move(link)); } From 6c2f121762743b78e1a0850ace09982aafa8ed03 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 30 Jun 2026 17:33:16 +0200 Subject: [PATCH 14/14] resolve https://github.com/DataDog/dd-trace-cpp/pull/330#discussion_r3499942743 --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index aa290a306..8a1bd78a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ target_sources(dd-trace-cpp-objects include/datadog/span.h include/datadog/span_config.h include/datadog/span_defaults.h + include/datadog/span_link.h include/datadog/span_matcher.h include/datadog/span_sampler_config.h include/datadog/string_view.h