From 937d66270103fcbb63496a4a5af9c4818ab4ff68 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 25 Jun 2026 15:20:33 +0200 Subject: [PATCH 01/12] Add DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT to config inversion --- include/datadog/environment.h | 1 + supported-configurations.json | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 824e4a3aa..ff6ade3c7 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -52,6 +52,7 @@ namespace environment { ENV_DEFAULT_RESOLVED_IN_CODE("Defaults to process name when unset.")) \ MACRO(DD_SPAN_SAMPLING_RULES, ARRAY, "[]") \ MACRO(DD_SPAN_SAMPLING_RULES_FILE, STRING, "") \ + MACRO(DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT, STRING, "continue") \ MACRO(DD_TRACE_PROPAGATION_STYLE_EXTRACT, ARRAY, \ "datadog,tracecontext,baggage") \ MACRO(DD_TRACE_PROPAGATION_STYLE_INJECT, ARRAY, \ diff --git a/supported-configurations.json b/supported-configurations.json index 051ff8b5d..e7ac2d54e 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -203,6 +203,13 @@ "type": "boolean" } ], + "DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT": [ + { + "default": "continue", + "implementation": "B", + "type": "string" + } + ], "DD_TRACE_PROPAGATION_STYLE": [ { "default": "datadog,tracecontext,baggage", @@ -289,4 +296,4 @@ ] }, "version": "2" -} \ No newline at end of file +} From b02308e029ef826ef990f2f703918fab9cb9e224 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Wed, 1 Jul 2026 14:37:47 +0200 Subject: [PATCH 02/12] Add DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT to config --- BUILD.bazel | 2 + CMakeLists.txt | 2 + include/datadog/config.h | 1 + .../datadog/propagation_behavior_extract.h | 30 ++++++++++++++ include/datadog/tracer_config.h | 10 +++++ src/datadog/propagation_behavior_extract.cpp | 40 +++++++++++++++++++ src/datadog/telemetry/telemetry_impl.cpp | 2 + 7 files changed, 87 insertions(+) create mode 100644 include/datadog/propagation_behavior_extract.h create mode 100644 src/datadog/propagation_behavior_extract.cpp diff --git a/BUILD.bazel b/BUILD.bazel index 13159b803..40d7baa42 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -42,6 +42,7 @@ cc_library( "src/datadog/parse_util.cpp", "src/datadog/parse_util.h", "src/datadog/platform_util.h", + "src/datadog/propagation_behavior_extract.cpp", "src/datadog/propagation_style.cpp", "src/datadog/random.cpp", "src/datadog/random.h", @@ -121,6 +122,7 @@ cc_library( "include/datadog/logger.h", "include/datadog/null_collector.h", "include/datadog/optional.h", + "include/datadog/propagation_behavior_extract.h", "include/datadog/propagation_style.h", "include/datadog/rate.h", "include/datadog/remote_config/capability.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a1bd78a1..bd8049bf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,7 @@ target_sources(dd-trace-cpp-objects include/datadog/logger.h include/datadog/null_collector.h include/datadog/optional.h + include/datadog/propagation_behavior_extract.h include/datadog/propagation_style.h include/datadog/rate.h include/datadog/runtime_id.h @@ -196,6 +197,7 @@ target_sources(dd-trace-cpp-objects src/datadog/logger.cpp src/datadog/msgpack.cpp src/datadog/parse_util.cpp + src/datadog/propagation_behavior_extract.cpp src/datadog/propagation_style.cpp src/datadog/random.cpp src/datadog/rate.cpp diff --git a/include/datadog/config.h b/include/datadog/config.h index c02dfd9f3..3489a69c2 100644 --- a/include/datadog/config.h +++ b/include/datadog/config.h @@ -21,6 +21,7 @@ enum class ConfigName : char { TAGS, EXTRACTION_STYLES, INJECTION_STYLES, + PROPAGATION_BEHAVIOR_EXTRACT, STARTUP_LOGS, REPORT_TELEMETRY, DELEGATE_SAMPLING, diff --git a/include/datadog/propagation_behavior_extract.h b/include/datadog/propagation_behavior_extract.h new file mode 100644 index 000000000..210d104a9 --- /dev/null +++ b/include/datadog/propagation_behavior_extract.h @@ -0,0 +1,30 @@ +#pragma once + +// This component provides an `enum class`, `PropagationBehaviorExtract`, that +// indicates a trace context extraction or injection format to be used. +// `TracerConfig` has one `std::vector` for extraction and +// another for injection. See `tracer_config.h`. + +#include "optional.h" +#include "string_view.h" + +namespace datadog { +namespace tracing { + +enum class PropagationBehaviorExtract { + // Propagate extract4ed context normally + CONTINUE, + // Restart a new trace (new sampling decision, no parent) + // Reference previous trace through a span-link + RESTART, + // Discard entirely incoming context + IGNORE, +}; + +StringView to_string_view(PropagationBehaviorExtract behavior); + +// defaults to CONTINUE if empty or unsupported +Optional parse_propagation_behavior_extract(StringView text); + +} // namespace tracing +} // namespace datadog diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index 4f9196dbd..77af6ca00 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -16,6 +16,7 @@ #include "datadog_agent_config.h" #include "expected.h" #include "http_endpoint_calculation_mode.h" +#include "propagation_behavior_extract.h" #include "propagation_style.h" #include "runtime_id.h" #include "span_defaults.h" @@ -107,6 +108,13 @@ struct TracerConfig { // environment variables. Optional> extraction_styles; + // `propagation_behavior_extract` indicates how to handle incoming trace context. + // `continue`: default behavior, all trace contexts are propagated. + // `restart`: restart a new trace (new sampling decision) with a span link to the remote one. baggage is propagated. + // `ignore`: discard entirely any existing trace context and start a new trace. + // Overridden by DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT + Optional propagation_behavior_extract; + // `report_hostname` indicates whether the tracer will include the result of // `gethostname` with traces sent to the collector. Optional report_hostname; @@ -224,6 +232,8 @@ class FinalizedTracerConfig final { std::vector injection_styles; std::vector extraction_styles; + Optional propagation_behavior_extract; + bool report_hostname; std::size_t tags_header_size; std::shared_ptr logger; diff --git a/src/datadog/propagation_behavior_extract.cpp b/src/datadog/propagation_behavior_extract.cpp new file mode 100644 index 000000000..ea5eed794 --- /dev/null +++ b/src/datadog/propagation_behavior_extract.cpp @@ -0,0 +1,40 @@ +#include + +#include + +#include "json.hpp" +#include "string_util.h" + +namespace datadog { +namespace tracing { + +StringView to_string_view(PropagationBehaviorExtract behavior) { + switch (behavior) { + case PropagationBehaviorExtract::CONTINUE: + return "continue"; + case PropagationBehaviorExtract::RESTART: + return "restart"; + case PropagationBehaviorExtract::IGNORE: + return "ignore"; + } +} + +nlohmann::json to_json(PropagationBehaviorExtract behavior) { return to_string_view(behavior); } + + +Optional parse_propagation_behavior_extract(StringView text) { + auto token = std::string{text}; + to_lower(token); + + if (token == "continue" || token.empty()) { + return PropagationBehaviorExtract::CONTINUE; + } else if (token == "restart") { + return PropagationBehaviorExtract::RESTART; + } else if (token == "ignore") { + return PropagationBehaviorExtract::IGNORE; + } + return nullopt; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 9f1e99143..b76738f76 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -92,6 +92,8 @@ std::string to_string(datadog::tracing::ConfigName name) { return "trace_propagation_style_extract"; case ConfigName::INJECTION_STYLES: return "trace_propagation_style_inject"; + case ConfigName::PROPAGATION_BEHAVIOR_EXTRACT: + return "trace_propagation_behavior_extract"; case ConfigName::STARTUP_LOGS: return "trace_startup_logs_enabled"; case ConfigName::REPORT_TELEMETRY: From c7248f062666a02d7cecbbaca825ba6fa5d603a8 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Wed, 8 Jul 2026 17:31:23 +0200 Subject: [PATCH 03/12] rebase --- .../datadog/propagation_behavior_extract.h | 3 +- include/datadog/tracer.h | 5 + include/datadog/tracer_config.h | 13 ++- src/datadog/extracted_data.h | 1 + src/datadog/propagation_behavior_extract.cpp | 8 +- src/datadog/span.cpp | 13 +-- src/datadog/tracer.cpp | 106 ++++++++++++------ src/datadog/tracer_config.cpp | 4 + src/datadog/w3c_propagation.cpp | 2 + 9 files changed, 102 insertions(+), 53 deletions(-) diff --git a/include/datadog/propagation_behavior_extract.h b/include/datadog/propagation_behavior_extract.h index 210d104a9..3305d96b8 100644 --- a/include/datadog/propagation_behavior_extract.h +++ b/include/datadog/propagation_behavior_extract.h @@ -24,7 +24,8 @@ enum class PropagationBehaviorExtract { StringView to_string_view(PropagationBehaviorExtract behavior); // defaults to CONTINUE if empty or unsupported -Optional parse_propagation_behavior_extract(StringView text); +Optional parse_propagation_behavior_extract( + StringView text); } // namespace tracing } // namespace datadog diff --git a/include/datadog/tracer.h b/include/datadog/tracer.h index 6a032a133..938f679ed 100644 --- a/include/datadog/tracer.h +++ b/include/datadog/tracer.h @@ -18,6 +18,7 @@ #include "expected.h" #include "id_generator.h" #include "optional.h" +#include "propagation_behavior_extract.h" #include "span.h" #include "span_config.h" #include "tracer_config.h" @@ -45,6 +46,7 @@ class Tracer { Clock clock_; std::vector injection_styles_; std::vector extraction_styles_; + PropagationBehaviorExtract propagation_behavior_extract_; Optional hostname_; std::size_t tags_header_max_size_; // Store the tracer configuration in an in-memory file, allowing it to be @@ -75,6 +77,9 @@ class Tracer { // `config`. If there is no tracing information in `reader`, then return an // error with code `Error::NO_SPAN_TO_EXTRACT`. If a failure occurs, then // return an error with some other code. + // Depending of the propagation_behavior_restart config, it can continue the + // trace, restart a new trace (with link), or discard the span (returning + // `Error::NO_SPAN_TO_EXTRACT`) Expected extract_span(const DictReader& reader); Expected extract_span(const DictReader& reader, const SpanConfig& config); diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index 77af6ca00..2c8b36adf 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -108,11 +108,12 @@ struct TracerConfig { // environment variables. Optional> extraction_styles; - // `propagation_behavior_extract` indicates how to handle incoming trace context. - // `continue`: default behavior, all trace contexts are propagated. - // `restart`: restart a new trace (new sampling decision) with a span link to the remote one. baggage is propagated. - // `ignore`: discard entirely any existing trace context and start a new trace. - // Overridden by DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT + // `propagation_behavior_extract` indicates how to handle incoming trace + // context. `continue`: default behavior, all trace contexts are propagated. + // `restart`: restart a new trace (new sampling decision) with a span link to + // the remote one. baggage is propagated. `ignore`: discard entirely any + // existing trace context and start a new trace. Overridden by + // DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT Optional propagation_behavior_extract; // `report_hostname` indicates whether the tracer will include the result of @@ -232,7 +233,7 @@ class FinalizedTracerConfig final { std::vector injection_styles; std::vector extraction_styles; - Optional propagation_behavior_extract; + PropagationBehaviorExtract propagation_behavior_extract; bool report_hostname; std::size_t tags_header_size; diff --git a/src/datadog/extracted_data.h b/src/datadog/extracted_data.h index b2d17c303..a26274d78 100644 --- a/src/datadog/extracted_data.h +++ b/src/datadog/extracted_data.h @@ -40,6 +40,7 @@ struct ExtractedData { Optional additional_datadog_w3c_tracestate; // `style` is the extraction style used to obtain this `ExtractedData`. It's // for diagnostics. + Optional tracestate_full; Optional style; // `headers_examined` are the name/value pairs of HTTP headers (or equivalent // request meta-data) that were looked up and had values during the diff --git a/src/datadog/propagation_behavior_extract.cpp b/src/datadog/propagation_behavior_extract.cpp index ea5eed794..fe09c01ce 100644 --- a/src/datadog/propagation_behavior_extract.cpp +++ b/src/datadog/propagation_behavior_extract.cpp @@ -19,10 +19,12 @@ StringView to_string_view(PropagationBehaviorExtract behavior) { } } -nlohmann::json to_json(PropagationBehaviorExtract behavior) { return to_string_view(behavior); } - +nlohmann::json to_json(PropagationBehaviorExtract behavior) { + return to_string_view(behavior); +} -Optional parse_propagation_behavior_extract(StringView text) { +Optional parse_propagation_behavior_extract( + StringView text) { auto token = std::string{text}; to_lower(token); diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index df2950f1b..13786d4bd 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -166,19 +166,16 @@ void Span::set_source(Source source) { } void Span::add_link(const Span& linked, const SpanLinkAttributes& attrs) { - SpanLink link; - link.trace_id = linked.trace_id(); - link.span_id = linked.id(); - link.attributes = attrs; + Optional tracestate = nullopt; + Optional flags = nullopt; // Populate tracestate/flags from the linked trace's sampling decision, if // one has already been made. Do not force a decision here if (auto context = linked.trace_segment().w3c_link_context(*linked.data_)) { - link.tracestate = std::move(context->first); - link.flags = context->second; + tracestate = std::move(context->first); + flags = context->second; } - - data_->span_links.push_back(std::move(link)); + add_link(SpanLink{linked.trace_id(), linked.id(), tracestate, attrs, flags}); } void Span::add_link(const SpanLink& link) { data_->span_links.push_back(link); } diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 4209ac228..a03750b60 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -61,6 +61,7 @@ Tracer::Tracer(const FinalizedTracerConfig& config, clock_(config.clock), injection_styles_(config.injection_styles), extraction_styles_(config.extraction_styles), + propagation_behavior_extract_(config.propagation_behavior_extract), tags_header_max_size_(config.tags_header_size), baggage_opts_(config.baggage_opts), baggage_injection_enabled_(false), @@ -86,10 +87,12 @@ Tracer::Tracer(const FinalizedTracerConfig& config, collector_ = agent; } - for (const auto style : extraction_styles_) { - if (style == PropagationStyle::BAGGAGE) { - baggage_extraction_enabled_ = true; - break; + if (propagation_behavior_extract_ != PropagationBehaviorExtract::IGNORE) { + for (const auto style : extraction_styles_) { + if (style == PropagationStyle::BAGGAGE) { + baggage_extraction_enabled_ = true; + break; + } } } @@ -172,7 +175,7 @@ void Tracer::store_config( // clang-format off msgpack::pack_map( - buffer, + buffer, "schema_version", [&](auto& buffer) { msgpack::pack_integer(buffer, std::uint64_t(2)); return Expected{}; }, "runtime_id", [&](auto& buffer) { return msgpack::pack_string(buffer, runtime_id_.string()); }, "tracer_version", [&](auto& buffer) { return msgpack::pack_string(buffer, signature_.library_version); }, @@ -229,6 +232,13 @@ Expected Tracer::extract_span(const DictReader& reader) { Expected Tracer::extract_span(const DictReader& reader, const SpanConfig& config) { + // ignore: Discard incoming context, new span with new sampling decision + if (propagation_behavior_extract_ == PropagationBehaviorExtract::IGNORE) { + return Error{ + Error::NO_SPAN_TO_EXTRACT, + "Ignoring context extraction (propagation_behavior_extract=ignore)"}; + } + assert(!extraction_styles_.empty()); AuditedReader audited_reader{reader}; @@ -417,37 +427,63 @@ Expected Tracer::extract_span(const DictReader& reader, merged_context.trace_tags.erase(found); } - // When APM Tracing is disabled, the incoming sampling decision MAY be - // overridden based on locally generated spans. As such, the received sampling - // decision is intentionally ignored, and the tracer is expected to make its - // own decision in accordance with the locally enabled product configuration. - Optional sampling_decision; - if (tracing_enabled_ && merged_context.sampling_priority) { - SamplingDecision decision; - decision.priority = *merged_context.sampling_priority; - // `decision.mechanism` is null. We might be able to infer it once we - // extract `trace_tags`, but we would have no use for it, so we won't. - decision.origin = SamplingDecision::Origin::EXTRACTED; - - sampling_decision = decision; - } + switch (propagation_behavior_extract_) { + case PropagationBehaviorExtract::CONTINUE: { + // When APM Tracing is disabled, the incoming sampling decision MAY be + // overridden based on locally generated spans. As such, the received + // sampling decision is intentionally ignored, and the tracer is expected + // to make its own decision in accordance with the locally enabled product + // configuration. + Optional sampling_decision; + if (tracing_enabled_ && merged_context.sampling_priority) { + SamplingDecision decision; + decision.priority = *merged_context.sampling_priority; + // `decision.mechanism` is null. We might be able to infer it once we + // extract `trace_tags`, but we would have no use for it, so we won't. + decision.origin = SamplingDecision::Origin::EXTRACTED; + sampling_decision = decision; + } - const auto span_data_ptr = span_data.get(); - telemetry::counter::increment(metrics::tracer::trace_segments_created, - {"new_continued:continued"}); - const auto segment = std::make_shared( - logger_, collector_, config_manager_->trace_sampler(), span_sampler_, - config_manager_->span_defaults(), config_manager_, runtime_id_, - injection_styles_, hostname_, std::move(merged_context.origin), - tags_header_max_size_, std::move(merged_context.trace_tags), - std::move(sampling_decision), - std::move(merged_context.additional_w3c_tracestate), - std::move(merged_context.additional_datadog_w3c_tracestate), - std::move(span_data), resource_renaming_mode_, tracing_enabled_); - Span span{span_data_ptr, segment, - [generator = generator_]() { return generator->span_id(); }, - clock_}; - return span; + const auto span_data_ptr = span_data.get(); + telemetry::counter::increment(metrics::tracer::trace_segments_created, + {"new_continued:continued"}); + const auto segment = std::make_shared( + logger_, collector_, config_manager_->trace_sampler(), span_sampler_, + config_manager_->span_defaults(), config_manager_, runtime_id_, + injection_styles_, hostname_, std::move(merged_context.origin), + tags_header_max_size_, std::move(merged_context.trace_tags), + std::move(sampling_decision), + std::move(merged_context.additional_w3c_tracestate), + std::move(merged_context.additional_datadog_w3c_tracestate), + std::move(span_data), resource_renaming_mode_, tracing_enabled_); + + Span span{span_data_ptr, segment, + [generator = generator_]() { return generator->span_id(); }, + clock_}; + return span; + } + case PropagationBehaviorExtract::RESTART: { + // restart: create a new trace, with a span link to the previous one + + auto link_attributes = SpanLinkAttributes{}; + link_attributes.emplace("reason", "propagation_behavior_extract"); + link_attributes.emplace("context_headers", "todo"); + + auto tracestate = + extracted_contexts[PropagationStyle::W3C].tracestate_full; + + auto restarted_span = create_span(config); + restarted_span.add_link(SpanLink{span_data->trace_id, span_data->span_id, + tracestate, link_attributes, + nullopt}); // TODO: flags + return restarted_span; + } + default: + // Should be unreachable + return Error{ + Error::NO_SPAN_TO_EXTRACT, + "Ignoring context extraction (propagation_behavior_extract=ignore)"}; + } } Span Tracer::extract_or_create_span(const DictReader& reader) { diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index 1383ce478..728e05009 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -403,6 +403,10 @@ Expected finalize_config(const TracerConfig &user_config, final_config.injection_styles.erase(it); } + final_config.propagation_behavior_extract = + value_or(user_config.propagation_behavior_extract, + PropagationBehaviorExtract::CONTINUE); + final_config.runtime_id = user_config.runtime_id; final_config.root_session_id = user_config.root_session_id; final_config.process_tags = user_config.process_tags; diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index f6e1eb46e..26eb0a020 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -287,6 +287,8 @@ void extract_tracestate( } const auto tracestate = trim(*maybe_tracestate); + result.tracestate_full = tracestate; + auto maybe_parsed = parse_tracestate(tracestate); if (!maybe_parsed) { // No "dd" entry in `tracestate`, so there's nothing to extract. From 21ccf65e22c8ba8d13dc20ae30f89f009288a52d Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 7 Jul 2026 17:10:31 +0200 Subject: [PATCH 04/12] add branch for PropagationBehaviorExtract::to_string_view --- src/datadog/propagation_behavior_extract.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/datadog/propagation_behavior_extract.cpp b/src/datadog/propagation_behavior_extract.cpp index fe09c01ce..b0d3059d6 100644 --- a/src/datadog/propagation_behavior_extract.cpp +++ b/src/datadog/propagation_behavior_extract.cpp @@ -16,6 +16,8 @@ StringView to_string_view(PropagationBehaviorExtract behavior) { return "restart"; case PropagationBehaviorExtract::IGNORE: return "ignore"; + default: + std::abort(); } } From ccf96b3d5dd3b6aa5acf8c75eec3cc4775b7bfa4 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Wed, 8 Jul 2026 18:10:15 +0200 Subject: [PATCH 05/12] Remove trailing newline from supported-configurations.json Matches the exact output of the config-inversion tool, which the verify CI job diffs against byte-for-byte. --- supported-configurations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supported-configurations.json b/supported-configurations.json index e7ac2d54e..186db9f51 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -296,4 +296,4 @@ ] }, "version": "2" -} +} \ No newline at end of file From 153370ee26e8b942381b00c6d2015e7988c23078 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Fri, 10 Jul 2026 16:19:10 +0200 Subject: [PATCH 06/12] finalize tracer config --- src/datadog/tracer_config.cpp | 22 ++++++++-- test/test_tracer_config.cpp | 80 +++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index 728e05009..1ce09c40c 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -8,7 +8,9 @@ #include #include +#include "datadog/config.h" #include "datadog/optional.h" +#include "datadog/propagation_behavior_extract.h" #include "datadog_agent.h" #include "json.hpp" #include "null_logger.h" @@ -234,6 +236,15 @@ Expected load_tracer_env_config(Logger &logger) { warn_message(var_name, *value, var_name_override, *value_override)}); } + const auto propagation_behavior_extract = + lookup(environment::DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT); + if (propagation_behavior_extract.has_value()) { + env_cfg.propagation_behavior_extract = parse_propagation_behavior_extract( + propagation_behavior_extract.value()); + } else { + env_cfg.propagation_behavior_extract = nullopt; + } + try { const auto global_styles = styles_from_env(environment::DD_TRACE_PROPAGATION_STYLE); @@ -403,9 +414,14 @@ Expected finalize_config(const TracerConfig &user_config, final_config.injection_styles.erase(it); } - final_config.propagation_behavior_extract = - value_or(user_config.propagation_behavior_extract, - PropagationBehaviorExtract::CONTINUE); + final_config.propagation_behavior_extract = resolve_and_record_config( + env_config->propagation_behavior_extract, + user_config.propagation_behavior_extract, &final_config.metadata, + ConfigName::PROPAGATION_BEHAVIOR_EXTRACT, + PropagationBehaviorExtract::CONTINUE, + [](const PropagationBehaviorExtract &behavior) { + return std::string{to_string_view(behavior)}; + }); final_config.runtime_id = user_config.runtime_id; final_config.root_session_id = user_config.root_session_id; diff --git a/test/test_tracer_config.cpp b/test/test_tracer_config.cpp index b6407f72d..b829e1050 100644 --- a/test/test_tracer_config.cpp +++ b/test/test_tracer_config.cpp @@ -1293,6 +1293,86 @@ TRACER_CONFIG_TEST("TracerConfig propagation styles") { } } +TRACER_CONFIG_TEST("TracerConfig propagation behavior extract") { + TracerConfig config; + config.service = "testsvc"; + + const auto behavior_metadata = [](const FinalizedTracerConfig& finalized) { + return finalized.metadata.at(ConfigName::PROPAGATION_BEHAVIOR_EXTRACT); + }; + + SECTION("defaults to continue") { + const auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->propagation_behavior_extract == + PropagationBehaviorExtract::CONTINUE); + + const auto& metadata = behavior_metadata(*finalized); + REQUIRE(metadata.size() == 1); + CHECK(metadata.back().origin == ConfigMetadata::Origin::DEFAULT); + CHECK(metadata.back().value == "continue"); + } + + SECTION("uses programmatic configuration") { + config.propagation_behavior_extract = PropagationBehaviorExtract::RESTART; + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->propagation_behavior_extract == + PropagationBehaviorExtract::RESTART); + + const auto& metadata = behavior_metadata(*finalized); + REQUIRE(metadata.size() == 2); + CHECK(metadata.back().origin == ConfigMetadata::Origin::CODE); + CHECK(metadata.back().value == "restart"); + } + + SECTION("environment configuration overrides programmatic configuration") { + const EnvGuard guard{"DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT", "ignore"}; + config.propagation_behavior_extract = PropagationBehaviorExtract::RESTART; + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->propagation_behavior_extract == + PropagationBehaviorExtract::IGNORE); + + const auto& metadata = behavior_metadata(*finalized); + REQUIRE(metadata.size() == 3); + CHECK(metadata.back().origin == + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + CHECK(metadata.back().value == "ignore"); + } + + SECTION("invalid environment configuration falls through to code") { + const EnvGuard guard{"DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT", "invalid"}; + config.propagation_behavior_extract = PropagationBehaviorExtract::RESTART; + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->propagation_behavior_extract == + PropagationBehaviorExtract::RESTART); + + const auto& metadata = behavior_metadata(*finalized); + REQUIRE(metadata.size() == 2); + CHECK(metadata.back().origin == ConfigMetadata::Origin::CODE); + CHECK(metadata.back().value == "restart"); + } + + SECTION("invalid environment configuration falls through to the default") { + const EnvGuard guard{"DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT", "invalid"}; + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->propagation_behavior_extract == + PropagationBehaviorExtract::CONTINUE); + + const auto& metadata = behavior_metadata(*finalized); + REQUIRE(metadata.size() == 1); + CHECK(metadata.back().origin == ConfigMetadata::Origin::DEFAULT); + CHECK(metadata.back().value == "continue"); + } +} + TRACER_CONFIG_TEST("configure 128-bit trace IDs") { TracerConfig config; config.service = "testsvc"; From 06145d8da5d150272c2c66751b0a570915f91721 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Fri, 10 Jul 2026 16:58:44 +0200 Subject: [PATCH 07/12] full restart link implementation --- src/datadog/tracer.cpp | 11 +++++++---- test/test_tracer.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index a03750b60..80f5ee861 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -27,6 +27,7 @@ #include "root_session_id.h" #include "span_data.h" #include "span_sampler.h" +#include "string_util.h" #include "tags.h" #include "telemetry_metrics.h" #include "trace_sampler.h" @@ -465,17 +466,19 @@ Expected Tracer::extract_span(const DictReader& reader, case PropagationBehaviorExtract::RESTART: { // restart: create a new trace, with a span link to the previous one + std::string context_headers{to_string_view(*first_style_with_trace_id)}; + to_lower(context_headers); auto link_attributes = SpanLinkAttributes{}; link_attributes.emplace("reason", "propagation_behavior_extract"); - link_attributes.emplace("context_headers", "todo"); + link_attributes.emplace("context_headers", std::move(context_headers)); auto tracestate = extracted_contexts[PropagationStyle::W3C].tracestate_full; auto restarted_span = create_span(config); - restarted_span.add_link(SpanLink{span_data->trace_id, span_data->span_id, - tracestate, link_attributes, - nullopt}); // TODO: flags + restarted_span.add_link( + SpanLink{span_data->trace_id, span_data->parent_id, tracestate, + link_attributes, nullopt}); // TODO: flags return restarted_span; } default: diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 72231115f..8838abebb 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1544,6 +1544,46 @@ TEST_TRACER("span extraction") { } } +TEST_TRACER("restart extraction links to the extracted context") { + TracerConfig config; + config.service = "testsvc"; + config.propagation_behavior_extract = PropagationBehaviorExtract::RESTART; + const auto collector = std::make_shared(); + config.collector = collector; + config.logger = std::make_shared(); + + const auto finalized_config = finalize_config(config); + REQUIRE(finalized_config); + Tracer tracer{*finalized_config}; + + const std::unordered_map headers{ + {"x-datadog-trace-id", "1"}, + {"x-datadog-parent-id", "1"}, + {"x-datadog-sampling-priority", "2"}, + {"x-datadog-tags", "_dd.p.tid=1111111111111111"}, + {"traceparent", "00-11111111111111110000000000000001-0000000000000001-01"}, + }; + + { + MockDictReader reader{headers}; + const auto span = tracer.extract_span(reader); + REQUIRE(span); + REQUIRE_FALSE(span->parent_id()); + } + + REQUIRE(collector->span_count() == 1); + const auto& span = collector->first_span(); + REQUIRE(span.span_links.size() == 1); + const auto& link = span.span_links.front(); + REQUIRE(link.trace_id.low == 1); + REQUIRE(link.trace_id.high == 0x1111111111111111); + REQUIRE(link.span_id == 1); + REQUIRE(link.attributes == SpanLinkAttributes{ + {"reason", "propagation_behavior_extract"}, + {"context_headers", "datadog"}, + }); +} + TEST_TRACER("baggage usage") { TracerConfig config; config.logger = std::make_shared(); From 201958b0778217211c3f3fba20a026b5a214fe11 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Fri, 10 Jul 2026 17:15:37 +0200 Subject: [PATCH 08/12] on_extract_headers: store with synthetic ids to avoid collisions (same as rust parametric) --- test/system-tests/request_handler.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/system-tests/request_handler.cpp b/test/system-tests/request_handler.cpp index 8960912b2..359aae0f9 100644 --- a/test/system-tests/request_handler.cpp +++ b/test/system-tests/request_handler.cpp @@ -1,5 +1,7 @@ #include "request_handler.h" +#include + #include #include #include @@ -15,6 +17,16 @@ namespace { +// Return an opaque key for an extracted context without a remote parent span +// (for example, in propagation restart mode). C++-generated span IDs have +// their most significant bit cleared, so setting it prevents collisions with +// IDs in active_spans_. +std::uint64_t next_synthetic_span_id() { + static std::atomic counter{0}; + return (std::uint64_t{1} << 63) | + counter.fetch_add(1, std::memory_order_relaxed); +} + std::string get_agent_url_from_traces_url(std::string traces_url) { // Strip the API path from the traces URL to get the agent URL // This API path is defined in src/datadog/datadog_agent.cpp. @@ -487,11 +499,11 @@ void RequestHandler::on_extract_headers(const httplib::Request& req, } const auto upstream_id = span->parent_id().value_or(0); - const auto response_body = nlohmann::json{{"span_id", upstream_id}}; - // 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 = span->parent_id().value_or(span->id()); + // Keep a remote parent span ID when extraction has one. Otherwise, return a + // synthetic key so a restart context can still be used as a parent_id. + const auto handle = span->parent_id().value_or(next_synthetic_span_id()); + const auto response_body = nlohmann::json{{"span_id", handle}}; link_contexts_[handle] = make_link_context(span, http_headers, upstream_id); tracing_context_[handle] = std::move(*http_headers); From 14a1732eebdde281875c79177eec683d1978c60e Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Mon, 13 Jul 2026 11:15:16 +0200 Subject: [PATCH 09/12] span link flags field --- src/datadog/tracer.cpp | 8 +++++++- test/test_tracer.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 80f5ee861..a357ac34a 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -474,11 +474,17 @@ Expected Tracer::extract_span(const DictReader& reader, auto tracestate = extracted_contexts[PropagationStyle::W3C].tracestate_full; + auto w3c_sampling_priority = + extracted_contexts[PropagationStyle::W3C].sampling_priority; + Optional flags = + w3c_sampling_priority + ? Optional(*w3c_sampling_priority > 0 ? 1u : 0u) + : nullopt; auto restarted_span = create_span(config); restarted_span.add_link( SpanLink{span_data->trace_id, span_data->parent_id, tracestate, - link_attributes, nullopt}); // TODO: flags + link_attributes, flags}); return restarted_span; } default: diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 8838abebb..776be81fb 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1582,6 +1582,52 @@ TEST_TRACER("restart extraction links to the extracted context") { {"reason", "propagation_behavior_extract"}, {"context_headers", "datadog"}, }); + REQUIRE(link.flags == Optional(1u)); +} + +TEST_TRACER("restart extraction link preserves traceparent sampled flag") { + TracerConfig config; + config.service = "testsvc"; + config.propagation_behavior_extract = PropagationBehaviorExtract::RESTART; + const auto collector = std::make_shared(); + config.collector = collector; + config.logger = std::make_shared(); + + const auto finalized_config = finalize_config(config); + REQUIRE(finalized_config); + Tracer tracer{*finalized_config}; + + SECTION("sampled traceparent yields flags=1") { + const std::unordered_map headers{ + {"traceparent", + "00-11111111111111110000000000000001-0000000000000001-01"}, + }; + { + MockDictReader reader{headers}; + const auto span = tracer.extract_span(reader); + REQUIRE(span); + } + + REQUIRE(collector->span_count() == 1); + const auto& link = collector->first_span().span_links.front(); + REQUIRE(link.flags == Optional(1u)); + } + + SECTION("unsampled traceparent yields flags=0") { + const std::unordered_map headers{ + {"traceparent", + "00-11111111111111110000000000000001-0000000000000001-00"}, + }; + { + MockDictReader reader{headers}; + const auto span = tracer.extract_span(reader); + REQUIRE(span); + } + + REQUIRE(collector->span_count() == 1); + const auto& link = collector->first_span().span_links.front(); + REQUIRE(link.flags == Optional(0u)); + } } TEST_TRACER("baggage usage") { From 25a7fab55b6e0f41575af25ddf967c960e2e7423 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Mon, 13 Jul 2026 14:08:47 +0200 Subject: [PATCH 10/12] flags comve from merged context insted of w3c only, and wrtie tracestate only if w3c context matches the extracted context --- src/datadog/tracer.cpp | 17 +++++++++++------ test/test_tracer.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index a357ac34a..700af4db5 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -472,13 +472,18 @@ Expected Tracer::extract_span(const DictReader& reader, link_attributes.emplace("reason", "propagation_behavior_extract"); link_attributes.emplace("context_headers", std::move(context_headers)); - auto tracestate = - extracted_contexts[PropagationStyle::W3C].tracestate_full; - auto w3c_sampling_priority = - extracted_contexts[PropagationStyle::W3C].sampling_priority; + Optional tracestate; + if (const auto w3c = extracted_contexts.find(PropagationStyle::W3C); + w3c != extracted_contexts.end() && + w3c->second.trace_id == span_data->trace_id) { + tracestate = w3c->second.tracestate_full; + } + Optional flags = - w3c_sampling_priority - ? Optional(*w3c_sampling_priority > 0 ? 1u : 0u) + merged_context.sampling_priority + ? Optional(*merged_context.sampling_priority > 0 + ? 1u + : 0u) : nullopt; auto restarted_span = create_span(config); diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 776be81fb..a90a765c6 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1630,6 +1630,45 @@ TEST_TRACER("restart extraction link preserves traceparent sampled flag") { } } +TEST_TRACER( + "restart extraction link uses metadata from the selected context") { + TracerConfig config; + config.service = "testsvc"; + config.propagation_behavior_extract = PropagationBehaviorExtract::RESTART; + const auto collector = std::make_shared(); + config.collector = collector; + config.logger = std::make_shared(); + + const auto finalized_config = finalize_config(config); + REQUIRE(finalized_config); + Tracer tracer{*finalized_config}; + + // Datadog is the default first extraction style. The W3C context is + // intentionally unrelated and must not contribute metadata to the link. + const std::unordered_map headers{ + {"x-datadog-trace-id", "1"}, + {"x-datadog-parent-id", "1"}, + {"x-datadog-sampling-priority", "2"}, + {"traceparent", + "00-00000000000000000000000000000002-0000000000000002-00"}, + {"tracestate", "vendor=unrelated"}, + }; + + { + MockDictReader reader{headers}; + const auto span = tracer.extract_span(reader); + REQUIRE(span); + } + + REQUIRE(collector->span_count() == 1); + const auto& link = collector->first_span().span_links.front(); + REQUIRE(link.trace_id.low == 1); + REQUIRE(link.trace_id.high == 0); + REQUIRE(link.span_id == 1); + REQUIRE(link.tracestate == nullopt); + REQUIRE(link.flags == Optional(1u)); +} + TEST_TRACER("baggage usage") { TracerConfig config; config.logger = std::make_shared(); From f61aece208ca887e327004bb8ab91acade7648f8 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Mon, 13 Jul 2026 14:24:51 +0200 Subject: [PATCH 11/12] format --- src/datadog/tracer.cpp | 11 +++++------ test/system-tests/request_handler.cpp | 3 +-- test/test_tracer.cpp | 6 +++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 700af4db5..1bb5e750f 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -481,15 +481,14 @@ Expected Tracer::extract_span(const DictReader& reader, Optional flags = merged_context.sampling_priority - ? Optional(*merged_context.sampling_priority > 0 - ? 1u - : 0u) + ? Optional( + *merged_context.sampling_priority > 0 ? 1u : 0u) : nullopt; auto restarted_span = create_span(config); - restarted_span.add_link( - SpanLink{span_data->trace_id, span_data->parent_id, tracestate, - link_attributes, flags}); + restarted_span.add_link(SpanLink{span_data->trace_id, + span_data->parent_id, tracestate, + link_attributes, flags}); return restarted_span; } default: diff --git a/test/system-tests/request_handler.cpp b/test/system-tests/request_handler.cpp index 359aae0f9..3cc61a353 100644 --- a/test/system-tests/request_handler.cpp +++ b/test/system-tests/request_handler.cpp @@ -1,7 +1,5 @@ #include "request_handler.h" -#include - #include #include #include @@ -10,6 +8,7 @@ #include #include +#include #include #include "httplib.h" diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index a90a765c6..49c934f51 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1561,7 +1561,8 @@ TEST_TRACER("restart extraction links to the extracted context") { {"x-datadog-parent-id", "1"}, {"x-datadog-sampling-priority", "2"}, {"x-datadog-tags", "_dd.p.tid=1111111111111111"}, - {"traceparent", "00-11111111111111110000000000000001-0000000000000001-01"}, + {"traceparent", + "00-11111111111111110000000000000001-0000000000000001-01"}, }; { @@ -1630,8 +1631,7 @@ TEST_TRACER("restart extraction link preserves traceparent sampled flag") { } } -TEST_TRACER( - "restart extraction link uses metadata from the selected context") { +TEST_TRACER("restart extraction link uses metadata from the selected context") { TracerConfig config; config.service = "testsvc"; config.propagation_behavior_extract = PropagationBehaviorExtract::RESTART; From 16eb8a745603b1d9bb6fa8ec8eb147d6dfd9e795 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Mon, 13 Jul 2026 14:44:15 +0200 Subject: [PATCH 12/12] rename IGNORE to IGNORE_ because windows exists --- include/datadog/propagation_behavior_extract.h | 3 ++- src/datadog/propagation_behavior_extract.cpp | 4 ++-- src/datadog/tracer.cpp | 4 ++-- test/test_tracer_config.cpp | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/include/datadog/propagation_behavior_extract.h b/include/datadog/propagation_behavior_extract.h index 3305d96b8..521683c27 100644 --- a/include/datadog/propagation_behavior_extract.h +++ b/include/datadog/propagation_behavior_extract.h @@ -18,7 +18,8 @@ enum class PropagationBehaviorExtract { // Reference previous trace through a span-link RESTART, // Discard entirely incoming context - IGNORE, + // (IGNORE without underscore breaks windows builds) + IGNORE_, }; StringView to_string_view(PropagationBehaviorExtract behavior); diff --git a/src/datadog/propagation_behavior_extract.cpp b/src/datadog/propagation_behavior_extract.cpp index b0d3059d6..939fe2707 100644 --- a/src/datadog/propagation_behavior_extract.cpp +++ b/src/datadog/propagation_behavior_extract.cpp @@ -14,7 +14,7 @@ StringView to_string_view(PropagationBehaviorExtract behavior) { return "continue"; case PropagationBehaviorExtract::RESTART: return "restart"; - case PropagationBehaviorExtract::IGNORE: + case PropagationBehaviorExtract::IGNORE_: return "ignore"; default: std::abort(); @@ -35,7 +35,7 @@ Optional parse_propagation_behavior_extract( } else if (token == "restart") { return PropagationBehaviorExtract::RESTART; } else if (token == "ignore") { - return PropagationBehaviorExtract::IGNORE; + return PropagationBehaviorExtract::IGNORE_; } return nullopt; } diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 1bb5e750f..477f1cbf2 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -88,7 +88,7 @@ Tracer::Tracer(const FinalizedTracerConfig& config, collector_ = agent; } - if (propagation_behavior_extract_ != PropagationBehaviorExtract::IGNORE) { + if (propagation_behavior_extract_ != PropagationBehaviorExtract::IGNORE_) { for (const auto style : extraction_styles_) { if (style == PropagationStyle::BAGGAGE) { baggage_extraction_enabled_ = true; @@ -234,7 +234,7 @@ Expected Tracer::extract_span(const DictReader& reader) { Expected Tracer::extract_span(const DictReader& reader, const SpanConfig& config) { // ignore: Discard incoming context, new span with new sampling decision - if (propagation_behavior_extract_ == PropagationBehaviorExtract::IGNORE) { + if (propagation_behavior_extract_ == PropagationBehaviorExtract::IGNORE_) { return Error{ Error::NO_SPAN_TO_EXTRACT, "Ignoring context extraction (propagation_behavior_extract=ignore)"}; diff --git a/test/test_tracer_config.cpp b/test/test_tracer_config.cpp index b829e1050..4a9c0abbe 100644 --- a/test/test_tracer_config.cpp +++ b/test/test_tracer_config.cpp @@ -1334,7 +1334,7 @@ TRACER_CONFIG_TEST("TracerConfig propagation behavior extract") { const auto finalized = finalize_config(config); REQUIRE(finalized); CHECK(finalized->propagation_behavior_extract == - PropagationBehaviorExtract::IGNORE); + PropagationBehaviorExtract::IGNORE_); const auto& metadata = behavior_metadata(*finalized); REQUIRE(metadata.size() == 3);