diff --git a/BUILD.bazel b/BUILD.bazel index df17ac56..1682bd65 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -55,6 +55,8 @@ cc_library( "src/datadog/span.cpp", "src/datadog/span_data.cpp", "src/datadog/span_data.h", + "src/datadog/span_link.cpp", + "src/datadog/span_link.h", "src/datadog/span_matcher.cpp", "src/datadog/span_sampler.cpp", "src/datadog/span_sampler.h", @@ -131,6 +133,7 @@ cc_library( "include/datadog/sampling_priority.h", "include/datadog/span.h", "include/datadog/span_config.h", + "include/datadog/span_context.h", "include/datadog/span_defaults.h", "include/datadog/span_matcher.h", "include/datadog/span_sampler_config.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cc154f1..1587fe74 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,6 +159,7 @@ target_sources(dd-trace-cpp-objects include/datadog/sampling_priority.h include/datadog/span.h include/datadog/span_config.h + include/datadog/span_context.h include/datadog/span_defaults.h include/datadog/span_matcher.h include/datadog/span_sampler_config.h @@ -203,6 +204,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 f5958b37..1d008106 100644 --- a/include/datadog/span.h +++ b/include/datadog/span.h @@ -43,9 +43,12 @@ #include #include #include +#include +#include #include "clock.h" #include "optional.h" +#include "span_context.h" #include "string_view.h" #include "trace_id.h" #include "trace_source.h" @@ -60,6 +63,9 @@ struct SpanConfig; struct SpanData; class TraceSegment; +// The map type used for user-supplied span-link attributes. +using SpanLinkAttributes = std::unordered_map; + class Span { std::shared_ptr trace_segment_; SpanData* data_; @@ -165,6 +171,14 @@ class Span { // Specifies the product (AppSec, DBM) that created this span. void set_source(Source); + // Return this span's identifying and propagation state. This does not make a + // sampling decision when one has not already been made. + SpanContext context() const; + + // Add a link to this span. + void add_link(const SpanContext& context, + const SpanLinkAttributes& attributes = {}); + // 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/include/datadog/span_context.h b/include/datadog/span_context.h new file mode 100644 index 00000000..b0a95dcd --- /dev/null +++ b/include/datadog/span_context.h @@ -0,0 +1,34 @@ +#pragma once + +// This component defines `SpanContext`, the identifying and propagation state +// of a span. It can be supplied to `Span::add_link` to associate a span with a +// span in this or another trace. + +#include +#include + +#include "trace_id.h" + +namespace datadog::tracing { + +class SpanContext { + public: + // 128-bit trace ID of the span. + TraceID trace_id; + // ID of the span within its trace. + std::uint64_t span_id; + // W3C `tracestate` header value, if any. + Optional tracestate; + // W3C trace flags, if any. + Optional flags; + + SpanContext(TraceID trace_id, std::uint64_t span_id, + Optional tracestate = nullopt, + Optional flags = nullopt) + : trace_id(trace_id), + span_id(span_id), + tracestate(tracestate), + flags(flags) {} +}; + +} // namespace datadog::tracing diff --git a/include/datadog/trace_segment.h b/include/datadog/trace_segment.h index ea27dd83..f7fa958a 100644 --- a/include/datadog/trace_segment.h +++ b/include/datadog/trace_segment.h @@ -56,6 +56,8 @@ class SpanSampler; class TraceSampler; class ConfigManager; +using W3CLinkContext = std::pair; + class TraceSegment { mutable std::mutex mutex_; @@ -75,8 +77,8 @@ class TraceSegment { std::vector> spans_; std::size_t num_finished_spans_; Optional sampling_decision_; - Optional additional_w3c_tracestate_; - Optional additional_datadog_w3c_tracestate_; + const Optional additional_w3c_tracestate_; + const Optional additional_datadog_w3c_tracestate_; std::shared_ptr config_manager_; @@ -108,6 +110,14 @@ class TraceSegment { const Optional& origin() const; Optional sampling_decision() const; + // Return the W3C tracestate encoding and derived trace flags for `span`, + // based on this segment's sampling decision. Unlike `inject`, this never + // makes a sampling decision if one hasn't been made yet -- it returns + // `nullopt` instead of forcing (and thereby permanently finalizing) one. + // Used by `Span::context` without prematurely deciding sampling for the + // trace. + Optional w3c_link_context(const SpanData& span) const; + Logger& logger() const; // Inject trace context for the specified `span` into the specified `writer`, diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index 7d076996..39f4153d 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -164,6 +164,23 @@ void Span::set_source(Source source) { to_tag(source)); } +SpanContext Span::context() const { + SpanContext context{trace_id(), id()}; + + if (Optional w3c_context = + trace_segment_->w3c_link_context(*data_)) { + context.tracestate = std::move(w3c_context->first); + context.flags = w3c_context->second; + } + + return context; +} + +void Span::add_link(const SpanContext& context, + const SpanLinkAttributes& attributes) { + data_->span_links.emplace_back(context, attributes); +} + TraceSegment& Span::trace_segment() { return *trace_segment_; } const TraceSegment& Span::trace_segment() const { return *trace_segment_; } diff --git a/src/datadog/span_data.cpp b/src/datadog/span_data.cpp index 7ed4ebb2..ad3eb00c 100644 --- a/src/datadog/span_data.cpp +++ b/src/datadog/span_data.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include #include "msgpack.h" #include "tags.h" @@ -74,66 +76,86 @@ void SpanData::apply_config(const SpanDefaults& defaults, } Expected msgpack_encode(std::string& destination, const SpanData& span) { - // clang-format off - msgpack::pack_map( - destination, - "service", [&](auto& destination) { - return msgpack::pack_string(destination, span.service); - }, - "name", [&](auto& destination) { - return msgpack::pack_string(destination, span.name); - }, - "resource", [&](auto& destination) { - return msgpack::pack_string(destination, span.resource); - }, - "trace_id", [&](auto& destination) { - msgpack::pack_integer(destination, span.trace_id.low); - return Expected{}; - }, - "span_id", [&](auto& destination) { - msgpack::pack_integer(destination, span.span_id); - return Expected{}; - }, - "parent_id", [&](auto& destination) { - msgpack::pack_integer(destination, span.parent_id); - return Expected{}; - }, - "start", [&](auto& destination) { - msgpack::pack_integer( - destination, std::uint64_t(std::chrono::duration_cast( - span.start.wall.time_since_epoch()) - .count())); - return Expected{}; - }, - "duration", [&](auto& destination) { - msgpack::pack_integer( - destination, - std::uint64_t(std::chrono::duration_cast(span.duration) - .count())); - return Expected{}; - }, - "error", [&](auto& destination) { - msgpack::pack_integer(destination, std::int32_t(span.error)); - return Expected{}; - }, - "meta", [&](auto& destination) { - return msgpack::pack_map(destination, span.tags, - [](std::string& destination, const auto& value) { - return msgpack::pack_string(destination, value); - }); - }, "metrics", - [&](auto& destination) { - return msgpack::pack_map(destination, span.numeric_tags, - [](std::string& destination, const auto& value) { - msgpack::pack_double(destination, value); - return Expected{}; - }); - }, "type", [&](auto& destination) { - return msgpack::pack_string(destination, span.service_type); - }); - // clang-format on - - return nullopt; + auto pack_service = [&](auto& destination) { + return msgpack::pack_string(destination, span.service); + }; + auto pack_name = [&](auto& destination) { + return msgpack::pack_string(destination, span.name); + }; + auto pack_resource = [&](auto& destination) { + return msgpack::pack_string(destination, span.resource); + }; + auto pack_trace_id = [&](auto& destination) { + msgpack::pack_integer(destination, span.trace_id.low); + return Expected{}; + }; + auto pack_span_id = [&](auto& destination) { + msgpack::pack_integer(destination, span.span_id); + return Expected{}; + }; + auto pack_parent_id = [&](auto& destination) { + msgpack::pack_integer(destination, span.parent_id); + return Expected{}; + }; + auto pack_start = [&](auto& destination) { + msgpack::pack_integer( + destination, + std::uint64_t(std::chrono::duration_cast( + span.start.wall.time_since_epoch()) + .count())); + return Expected{}; + }; + auto pack_duration = [&](auto& destination) { + msgpack::pack_integer( + destination, + std::uint64_t( + std::chrono::duration_cast(span.duration) + .count())); + return Expected{}; + }; + auto pack_error = [&](auto& destination) { + msgpack::pack_integer(destination, std::int32_t(span.error)); + return Expected{}; + }; + auto pack_meta = [&](auto& destination) { + return msgpack::pack_map(destination, span.tags, + [](std::string& destination, const auto& value) { + return msgpack::pack_string(destination, value); + }); + }; + auto pack_metrics = [&](auto& destination) { + return msgpack::pack_map(destination, span.numeric_tags, + [](std::string& destination, const auto& value) { + msgpack::pack_double(destination, value); + return Expected{}; + }); + }; + auto pack_type = [&](auto& destination) { + return msgpack::pack_string(destination, span.service_type); + }; + + if (span.span_links.empty()) { + return msgpack::pack_map( + destination, "service", pack_service, "name", pack_name, "resource", + pack_resource, "trace_id", pack_trace_id, "span_id", pack_span_id, + "parent_id", pack_parent_id, "start", pack_start, "duration", + pack_duration, "error", pack_error, "meta", pack_meta, "metrics", + pack_metrics, "type", pack_type); + } else { + auto pack_span_links = [&](auto& destination) { + return msgpack::pack_array( + destination, span.span_links, + [](std::string& destination, const SpanLink& link) { + return msgpack_encode(destination, link); + }); + }; + return msgpack::pack_map( + destination, "service", pack_service, "name", pack_name, "resource", + pack_resource, "trace_id", pack_trace_id, "span_id", pack_span_id, + "parent_id", pack_parent_id, "start", pack_start, "duration", + pack_duration, "error", pack_error, "meta", pack_meta, "metrics", + pack_metrics, "type", pack_type, "span_links", pack_span_links); + } } Expected msgpack_encode( diff --git a/src/datadog/span_data.h b/src/datadog/span_data.h index 36950427..440e050a 100644 --- a/src/datadog/span_data.h +++ b/src/datadog/span_data.h @@ -14,6 +14,8 @@ #include #include +#include "span_link.h" + namespace datadog { namespace tracing { @@ -33,6 +35,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; diff --git a/src/datadog/span_link.cpp b/src/datadog/span_link.cpp new file mode 100644 index 00000000..8419e39d --- /dev/null +++ b/src/datadog/span_link.cpp @@ -0,0 +1,78 @@ +#include "span_link.h" + +#include +#include + +#include "msgpack.h" + +namespace datadog::tracing { + +Expected msgpack_encode(std::string& destination, const SpanLink& link) { + const bool has_attributes = !link.attributes.empty(); + const bool has_tracestate = + link.context.tracestate.has_value() && !link.context.tracestate->empty(); + const bool has_flags = link.context.flags.has_value(); + + std::size_t size = 3; // trace_id, span_id, and trace_id_high are always + // present. + if (has_attributes) ++size; + if (has_tracestate) ++size; + if (has_flags) ++size; + + Expected result = msgpack::pack_map(destination, size); + if (!result) return result; + + result = msgpack::pack_map_suffix( + destination, "trace_id", + [&](std::string& destination) { + msgpack::pack_integer(destination, link.context.trace_id.low); + return Expected{}; + }, + "span_id", + [&](std::string& destination) { + msgpack::pack_integer(destination, link.context.span_id); + return Expected{}; + }, + "trace_id_high", + [&](std::string& destination) { + msgpack::pack_integer(destination, link.context.trace_id.high); + return Expected{}; + }); + if (!result) return result; + + if (has_attributes) { + result = msgpack::pack_map_suffix( + destination, "attributes", [&](std::string& destination) { + return msgpack::pack_map( + destination, link.attributes, + [](std::string& destination, const std::string& value) { + return msgpack::pack_string(destination, value); + }); + }); + if (!result) return result; + } + + if (has_tracestate) { + result = msgpack::pack_map_suffix( + destination, "tracestate", [&](std::string& destination) { + return msgpack::pack_string(destination, *link.context.tracestate); + }); + if (!result) return result; + } + + if (has_flags) { + result = msgpack::pack_map_suffix( + destination, "flags", [&](std::string& destination) { + // 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.context.flags | (1u << 31))); + return Expected{}; + }); + if (!result) return result; + } + + return result; +} + +} // namespace datadog::tracing diff --git a/src/datadog/span_link.h b/src/datadog/span_link.h new file mode 100644 index 00000000..8b049d6f --- /dev/null +++ b/src/datadog/span_link.h @@ -0,0 +1,22 @@ +#pragma once + +// This component defines the internal span-link representation and its +// MessagePack serialization. Applications use `SpanContext` and +// `Span::add_link` instead. + +#include + +namespace datadog::tracing { + +class SpanLink { + public: + SpanContext context; + SpanLinkAttributes attributes; + + SpanLink(SpanContext context, SpanLinkAttributes attributes = {}) + : context(std::move(context)), attributes(std::move(attributes)) {} +}; + +Expected msgpack_encode(std::string& destination, const SpanLink& link); + +} // namespace datadog::tracing diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index 4a7181b0..e620c431 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -202,6 +202,35 @@ Optional TraceSegment::sampling_decision() const { return sampling_decision_; } +Optional> TraceSegment::w3c_link_context( + const SpanData& span) const { + int sampling_priority; + std::vector> trace_tags; + { + std::lock_guard lock(mutex_); + if (!sampling_decision_) { + return nullopt; + } + sampling_priority = sampling_decision_->priority; + trace_tags = trace_tags_; + + auto& local_root_tags = spans_.front()->tags; + auto ts_tag_found = std::find_if( + local_root_tags.cbegin(), local_root_tags.cend(), + [](const auto& p) { return p.first == tags::internal::trace_source; }); + if (ts_tag_found != local_root_tags.cend()) { + trace_tags.emplace_back(tags::internal::trace_source, + ts_tag_found->second); + } + } + + return std::make_pair( + encode_tracestate(span.span_id, sampling_priority, origin_, trace_tags, + additional_datadog_w3c_tracestate_, + additional_w3c_tracestate_), + sampling_priority > 0 ? 1u : 0u); +} + Logger& TraceSegment::logger() const { return *logger_; } void TraceSegment::register_span(std::unique_ptr span) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7571aa8b..1496765d 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 diff --git a/test/system-tests/main.cpp b/test/system-tests/main.cpp index de7d421e..18ba5846 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 df605583..e2c4728d 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,82 @@ void RequestHandler::on_set_meta(const httplib::Request& req, res.status = 200; } +// Parse optional attributes, supporting flat strings and arrays. +static datadog::tracing::SpanLinkAttributes parse_link_attributes( + nlohmann::basic_json<> request_json) { + datadog::tracing::SpanLinkAttributes link_attributes; + + auto attributes = request_json.find("attributes"); + if (attributes == request_json.cend() || !attributes->is_object()) { + return link_attributes; + } + + 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())); + } + } + } + } + + return link_attributes; +} + +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_link_attributes = parse_link_attributes(request_json); + + auto span = active_spans_.find(*span_id); + if (span == active_spans_.cend()) { + const auto msg = + "on_add_link: span not found for id " + std::to_string(*span_id); + VALIDATION_ERROR(res, msg); + } + + // 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()) { + span->second.add_link(linked_it->second.context(), span_link_attributes); + } 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); + } + span->second.add_link(context_it->second, span_link_attributes); + } + + res.status = 200; +} + void RequestHandler::on_set_metric(const httplib::Request& req, httplib::Response& res) { const auto request_json = nlohmann::json::parse(req.body); @@ -333,6 +410,42 @@ void RequestHandler::on_inject_headers(const httplib::Request& req, res.set_content(response_json.dump(), "application/json"); } +datadog::tracing::SpanContext RequestHandler::make_link_context( + const datadog::tracing::Expected& span, + const datadog::tracing::Optional& headers, + std::uint64_t upstream_id) { + datadog::tracing::SpanContext stored_context{span->trace_id(), upstream_id}; + datadog::tracing::Optional sampling_priority; + + for (const auto& hdr : *headers) { + if (hdr.size() != 2) continue; + const auto name = utils::tolower(hdr[0].get()); + if (name == "tracestate") { + stored_context.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 { + stored_context.flags = static_cast( + std::stoul(tp.substr(pos + 1), nullptr, 16)); + } catch (...) { + } + } + } else if (name == "x-datadog-sampling-priority") { + try { + sampling_priority = std::stoi(hdr[1].get()); + } catch (...) { + } + } + } + // Derive W3C flags from sampling priority when no traceparent flags present. + if (!stored_context.flags.has_value() && sampling_priority.has_value()) { + stored_context.flags = *sampling_priority > 0 ? 1u : 0u; + } + return stored_context; +} + void RequestHandler::on_extract_headers(const httplib::Request& req, httplib::Response& res) { const auto request_json = nlohmann::json::parse(req.body); @@ -345,18 +458,21 @@ 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 parent_id = span->parent_id(); + const auto upstream_id = parent_id.value_or(0); + const auto response_body = nlohmann::json{{"span_id", upstream_id}}; - tracing_context_[*span->parent_id()] = std::move(*http_headers); + // Fall back to the extracted span's own id when there's no parent id + // (e.g. root-context extraction), avoiding collisions. + const auto handle = parent_id.value_or(span->id()); + link_contexts_.insert_or_assign( + handle, make_link_context(span, http_headers, upstream_id)); + tracing_context_[handle] = std::move(*http_headers); // The span below will not be finished and flushed. blackhole_.emplace_back(std::move(*span)); @@ -369,6 +485,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 09baf975..c7ba7cc4 100644 --- a/test/system-tests/request_handler.h +++ b/test/system-tests/request_handler.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -24,6 +25,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); @@ -40,6 +42,7 @@ 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. @@ -53,5 +56,10 @@ class RequestHandler final { // explaining the name :) std::vector blackhole_; + static datadog::tracing::SpanContext make_link_context( + const datadog::tracing::Expected& span, + const datadog::tracing::Optional& headers, + std::uint64_t upstream_id); + #undef VALIDATION_ERROR }; diff --git a/test/test_span.cpp b/test/test_span.cpp index 237c9cf1..6608e60b 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -1084,3 +1086,97 @@ 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, without forcing the linked " + "trace's sampling decision") { + 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}; + + 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(); + + const auto linked_context = linked.context(); + const SpanLinkAttributes attrs{{"link.key", "value"}}; + span.add_link(linked_context, attrs); + + // `linked`'s trace hasn't been sampled yet (its trace segment is still + // open), and obtaining its context must not force a decision -- doing so + // would permanently finalize sampling before the trace is finished. + REQUIRE_FALSE(linked.trace_segment().sampling_decision().has_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.context.trace_id == linked_trace_id); + REQUIRE(link.context.span_id == linked_span_id); + REQUIRE(link.attributes.at("link.key") == "value"); + // Sampling hadn't been decided for the linked trace yet, so the link + // carries no flags/tracestate rather than ones derived from a forced, + // premature decision. + REQUIRE_FALSE(link.context.flags.has_value()); + REQUIRE_FALSE(link.context.tracestate.has_value()); +} + +TEST_SPAN( + "add_link populates flags/tracestate when the linked trace's sampling " + "decision is already made") { + 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(); + auto linked = tracer.create_span(); + linked.trace_segment().override_sampling_priority( + SamplingPriority::USER_KEEP); + + const auto linked_context = linked.context(); + const SpanLinkAttributes attrs{{"link.key", "value"}}; + span.add_link(linked_context, attrs); + } + + // 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.context.flags.has_value()); + REQUIRE(*link.context.flags == 1u); +} diff --git a/test/test_span_link.cpp b/test/test_span_link.cpp new file mode 100644 index 00000000..b14c5dfe --- /dev/null +++ b/test/test_span_link.cpp @@ -0,0 +1,145 @@ +// 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 "span_link.h" +#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{ + SpanContext(TraceID(0x1122334455667788ULL, 0xBBBBBBBBBBBBBBBBULL), 42)}; + + const auto j = encode_to_json(link); + + REQUIRE(j.is_object()); + REQUIRE(j.size() == 3); + REQUIRE(j["trace_id"].get() == 0x1122334455667788ULL); + REQUIRE(j["trace_id_high"].get() == 0xBBBBBBBBBBBBBBBBULL); + REQUIRE(j["span_id"].get() == 42); + 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{SpanContext( + TraceID(/*low=*/0xAAAAAAAAAAAAAAAAULL, /*high=*/0xBBBBBBBBBBBBBBBBULL), + 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{SpanContext(TraceID(1), 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{SpanContext(TraceID(1), 2)}; + + SECTION("non-empty") { + link.context.tracestate = "dd=s:1"; + const auto j = encode_to_json(link); + REQUIRE(j["tracestate"].get() == "dd=s:1"); + } + + SECTION("empty string -> omitted") { + link.context.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{SpanContext(TraceID(1), 2)}; + + SECTION("sampled") { + link.context.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.context.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{SpanContext(TraceID(/*low=*/0x99, /*high=*/0x11), 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"); +}