diff --git a/include/datadog/collector.h b/include/datadog/collector.h index 96f9e64d..4c8dbe75 100644 --- a/include/datadog/collector.h +++ b/include/datadog/collector.h @@ -19,7 +19,7 @@ namespace datadog { namespace tracing { struct SpanData; -class TraceSampler; +class ErasedTraceSampler; class Collector { public: @@ -29,7 +29,7 @@ class Collector { // occurs. virtual Expected send( std::vector>&& spans, - const std::shared_ptr& response_handler) = 0; + const std::shared_ptr& response_handler) = 0; // Return a JSON representation of this object's configuration. The JSON // representation is an object with the following properties: diff --git a/include/datadog/config.h b/include/datadog/config.h index df017e54..3a26c78d 100644 --- a/include/datadog/config.h +++ b/include/datadog/config.h @@ -27,6 +27,7 @@ enum class ConfigName : char { SPAN_SAMPLING_RULES, TRACE_BAGGAGE_MAX_BYTES, TRACE_BAGGAGE_MAX_ITEMS, + APM_TRACING_ENABLED, }; // Represents metadata for configuration parameters diff --git a/include/datadog/datadog_agent_config.h b/include/datadog/datadog_agent_config.h index a8a0742b..827a2776 100644 --- a/include/datadog/datadog_agent_config.h +++ b/include/datadog/datadog_agent_config.h @@ -66,6 +66,9 @@ struct DatadogAgentConfig { // How often, in seconds, to query the Datadog Agent for remote configuration // updates. Optional remote_configuration_poll_interval_seconds; + // Whether APM tracing is enabled. This affects whether the + // "Datadog-Client-Computed-Stats: yes" header is sent with trace requests. + Optional apm_tracing_enabled; }; class FinalizedDatadogAgentConfig { @@ -87,6 +90,7 @@ class FinalizedDatadogAgentConfig { std::chrono::steady_clock::duration shutdown_timeout; std::chrono::steady_clock::duration remote_configuration_poll_interval; std::unordered_map metadata; + bool apm_tracing_enabled; // Origin detection Optional admission_controller_uid; diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 65d445f2..680b89af 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -60,6 +60,7 @@ namespace environment { MACRO(DD_INSTRUMENTATION_INSTALL_ID) \ MACRO(DD_INSTRUMENTATION_INSTALL_TYPE) \ MACRO(DD_INSTRUMENTATION_INSTALL_TIME) \ + MACRO(DD_APM_TRACING_ENABLED) \ MACRO(DD_EXTERNAL_ENV) #define WITH_COMMA(ARG) ARG, diff --git a/include/datadog/injection_options.h b/include/datadog/injection_options.h index f6f24b2d..6890794a 100644 --- a/include/datadog/injection_options.h +++ b/include/datadog/injection_options.h @@ -4,10 +4,19 @@ // parameters to `Span::inject` that alter the behavior of trace context // propagation. +#include + +#include "optional.h" + namespace datadog { namespace tracing { -struct InjectionOptions {}; +struct InjectionOptions { + // If DD_APM_TRACING_ENABLED=false and what we're injecting is not an APM + // trace, then the code for the trace source (e.g. 02 for Appsec) can be + // set here. + Optional> trace_source{}; +}; } // namespace tracing } // namespace datadog diff --git a/include/datadog/null_collector.h b/include/datadog/null_collector.h index e541f3db..0a53b660 100644 --- a/include/datadog/null_collector.h +++ b/include/datadog/null_collector.h @@ -11,7 +11,7 @@ namespace tracing { class NullCollector : public Collector { public: Expected send(std::vector>&&, - const std::shared_ptr&) override { + const std::shared_ptr&) override { return {}; } diff --git a/include/datadog/sampling_mechanism.h b/include/datadog/sampling_mechanism.h index 71c99a99..07a65f1c 100644 --- a/include/datadog/sampling_mechanism.h +++ b/include/datadog/sampling_mechanism.h @@ -49,7 +49,7 @@ enum class SamplingMechanism { // The sampling decision was made explicitly by the user, who set a sampling // priority. MANUAL = 4, - // Reserved for future use. + // Trace was kept because of AppSec event. APP_SEC = 5, // Reserved for future use. REMOTE_RATE_USER_DEFINED = 6, diff --git a/include/datadog/trace_segment.h b/include/datadog/trace_segment.h index 4db61f85..1bad7127 100644 --- a/include/datadog/trace_segment.h +++ b/include/datadog/trace_segment.h @@ -53,7 +53,7 @@ class Logger; struct SpanData; struct SpanDefaults; class SpanSampler; -class TraceSampler; +class ErasedTraceSampler; class ConfigManager; class TraceSegment { @@ -61,7 +61,8 @@ class TraceSegment { std::shared_ptr logger_; std::shared_ptr collector_; - std::shared_ptr trace_sampler_; + std::shared_ptr trace_sampler_; + bool apm_tracing_enabled_; std::shared_ptr span_sampler_; std::shared_ptr defaults_; @@ -83,7 +84,8 @@ class TraceSegment { public: TraceSegment(const std::shared_ptr& logger, const std::shared_ptr& collector, - const std::shared_ptr& trace_sampler, + std::shared_ptr trace_sampler, + bool apm_tracing_enabled, const std::shared_ptr& span_sampler, const std::shared_ptr& defaults, const std::shared_ptr& config_manager, diff --git a/include/datadog/tracer.h b/include/datadog/tracer.h index 5c183795..45511c08 100644 --- a/include/datadog/tracer.h +++ b/include/datadog/tracer.h @@ -29,7 +29,7 @@ namespace tracing { class ConfigManager; class DictReader; struct SpanConfig; -class TraceSampler; +class ErasedTraceSampler; class SpanSampler; class IDGenerator; class InMemoryFile; @@ -39,6 +39,8 @@ class Tracer { RuntimeID runtime_id_; TracerSignature signature_; std::shared_ptr config_manager_; + std::shared_ptr + apm_tracing_disabled_sampler_; // empty if enabled std::shared_ptr collector_; std::shared_ptr span_sampler_; std::shared_ptr generator_; @@ -104,6 +106,10 @@ class Tracer { // same JSON object that was logged when this Tracer was created. std::string config() const; + bool is_apm_tracing_enabled() const { + return apm_tracing_disabled_sampler_ == nullptr; + } + private: void store_config(); }; diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index ab8608d0..6b50a2f7 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -28,6 +28,7 @@ class Collector; class Logger; class SpanSampler; class TraceSampler; +class ErasedTraceSampler; struct TracerConfig { // Set the service name. @@ -80,6 +81,16 @@ struct TracerConfig { // `telemetry/configuration.h` By default, the telemetry module is enabled. telemetry::Configuration telemetry; + // `apm_tracing_enabled` indicates whether APM traces and APM trace metrics + // are enabled. If `false`, APM-specific traces are dropped and APM trace + // metrics are not computed. This allows other products (e.g., AppSec) to + // operate independently. + // This is distinct from `report_traces`, which controls whether any traces + // are sent at all. + // `apm_tracing_enabled` is overridden by the `DD_APM_TRACING_ENABLED` + // environment variable. Defaults to `true`. + Optional apm_tracing_enabled; + // `trace_sampler` configures trace sampling. Trace sampling determines which // traces are sent to Datadog. See `trace_sampler_config.h`. TraceSamplerConfig trace_sampler; @@ -197,6 +208,7 @@ class FinalizedTracerConfig final { std::shared_ptr logger; bool log_on_startup; bool generate_128bit_trace_ids; + bool apm_tracing_enabled; Optional runtime_id; Clock clock; std::string integration_name; diff --git a/src/datadog/config_manager.cpp b/src/datadog/config_manager.cpp index 36b054e5..712ec935 100644 --- a/src/datadog/config_manager.cpp +++ b/src/datadog/config_manager.cpp @@ -130,6 +130,8 @@ ConfigManager::ConfigManager(const FinalizedTracerConfig& config) default_metadata_(config.metadata), trace_sampler_( std::make_shared(config.trace_sampler, clock_)), + erased_trace_sampler_( + std::make_shared(trace_sampler_)), rules_(config.trace_sampler.rules), span_defaults_(std::make_shared(config.defaults)), report_traces_(config.report_traces) {} @@ -163,9 +165,9 @@ void ConfigManager::on_revert(const Configuration&) { telemetry::capture_configuration_change(config_metadata); } -std::shared_ptr ConfigManager::trace_sampler() { +std::shared_ptr ConfigManager::trace_sampler() { std::lock_guard lock(mutex_); - return trace_sampler_; + return erased_trace_sampler_; } std::shared_ptr ConfigManager::span_defaults() { diff --git a/src/datadog/config_manager.h b/src/datadog/config_manager.h index a19824a4..e6a6025d 100644 --- a/src/datadog/config_manager.h +++ b/src/datadog/config_manager.h @@ -70,6 +70,9 @@ class ConfigManager : public remote_config::Listener { std::unordered_map default_metadata_; std::shared_ptr trace_sampler_; + // wraps trace_sampler_ and should be changed if trace_sampler_ is changed + // Could be created every time, but that would be a waste + std::shared_ptr erased_trace_sampler_; std::vector rules_; DynamicConfig> span_defaults_; @@ -93,7 +96,7 @@ class ConfigManager : public remote_config::Listener { void on_post_process() override{}; // Return the `TraceSampler` consistent with the most recent configuration. - std::shared_ptr trace_sampler(); + std::shared_ptr trace_sampler(); // Return the `SpanDefaults` consistent with the most recent configuration. std::shared_ptr span_defaults(); diff --git a/src/datadog/datadog_agent.cpp b/src/datadog/datadog_agent.cpp index 6e788ae7..4378010e 100644 --- a/src/datadog/datadog_agent.cpp +++ b/src/datadog/datadog_agent.cpp @@ -157,7 +157,8 @@ DatadogAgent::DatadogAgent( flush_interval_(config.flush_interval), request_timeout_(config.request_timeout), shutdown_timeout_(config.shutdown_timeout), - remote_config_(tracer_signature, rc_listeners, logger) { + remote_config_(tracer_signature, rc_listeners, logger), + apm_tracing_enabled_(config.apm_tracing_enabled) { assert(logger_); // Set HTTP headers @@ -211,7 +212,7 @@ DatadogAgent::~DatadogAgent() { Expected DatadogAgent::send( std::vector>&& spans, - const std::shared_ptr& response_handler) { + const std::shared_ptr& response_handler) { std::lock_guard lock(mutex_); trace_chunks_.push_back(TraceChunk{std::move(spans), response_handler}); return nullopt; @@ -272,7 +273,7 @@ void DatadogAgent::flush() { // One HTTP request to the Agent could possibly involve trace chunks from // multiple tracers, and thus multiple trace samplers might need to have // their rates updated. Unlikely, but possible. - std::unordered_set> response_handlers; + std::unordered_set> response_handlers; for (auto& chunk : trace_chunks) { response_handlers.insert(std::move(chunk.response_handler)); } @@ -284,6 +285,9 @@ void DatadogAgent::flush() { for (const auto& [key, value] : headers_) { writer.set(key, value); } + if (!apm_tracing_enabled_) { + writer.set("Datadog-Client-Computed-Stats", "yes"); + } }; // This is the callback for the HTTP response. It's invoked diff --git a/src/datadog/datadog_agent.h b/src/datadog/datadog_agent.h index fe016c46..701bf122 100644 --- a/src/datadog/datadog_agent.h +++ b/src/datadog/datadog_agent.h @@ -21,17 +21,17 @@ namespace datadog { namespace tracing { +class ErasedTraceSampler; class FinalizedDatadogAgentConfig; class Logger; struct SpanData; -class TraceSampler; struct TracerSignature; class DatadogAgent : public Collector { public: struct TraceChunk { std::vector> spans; - std::shared_ptr response_handler; + std::shared_ptr response_handler; }; private: @@ -51,6 +51,7 @@ class DatadogAgent : public Collector { remote_config::Manager remote_config_; std::unordered_map headers_; + bool apm_tracing_enabled_; void flush(); @@ -63,7 +64,7 @@ class DatadogAgent : public Collector { Expected send( std::vector>&& spans, - const std::shared_ptr& response_handler) override; + const std::shared_ptr& response_handler) override; void get_and_apply_remote_configuration_updates(); diff --git a/src/datadog/datadog_agent_config.cpp b/src/datadog/datadog_agent_config.cpp index 9ffe6217..f0134a1c 100644 --- a/src/datadog/datadog_agent_config.cpp +++ b/src/datadog/datadog_agent_config.cpp @@ -31,6 +31,10 @@ Expected load_datadog_agent_env_config() { env_config.remote_configuration_poll_interval_seconds = *res; } + if (auto apm_enabled_env = lookup(environment::DD_APM_TRACING_ENABLED)) { + env_config.apm_tracing_enabled = !falsy(*apm_enabled_env); + } + auto env_host = lookup(environment::DD_AGENT_HOST); auto env_port = lookup(environment::DD_TRACE_AGENT_PORT); @@ -135,6 +139,9 @@ Expected finalize_config( value_or(env_config->remote_configuration_enabled, user_config.remote_configuration_enabled, true); + result.apm_tracing_enabled = value_or(env_config->apm_tracing_enabled, + user_config.apm_tracing_enabled, true); + const auto [origin, url] = pick(env_config->url, user_config.url, "http://localhost:8126"); auto parsed_url = HTTPClient::URL::parse(url); diff --git a/src/datadog/extraction_util.cpp b/src/datadog/extraction_util.cpp index 0b4965fa..dfeed0bc 100644 --- a/src/datadog/extraction_util.cpp +++ b/src/datadog/extraction_util.cpp @@ -34,7 +34,7 @@ void handle_trace_tags(StringView trace_tags, ExtractedData& result, } for (auto& [key, value] : *maybe_trace_tags) { - if (!starts_with(key, "_dd.p.")) { + if (!starts_with(key, "_dd.p.") && key != "_dd.p.ts") { continue; } diff --git a/src/datadog/tags.cpp b/src/datadog/tags.cpp index 32e1d4d5..63d86f86 100644 --- a/src/datadog/tags.cpp +++ b/src/datadog/tags.cpp @@ -31,6 +31,8 @@ const std::string language = "language"; const std::string runtime_id = "runtime-id"; const std::string sampling_decider = "_dd.is_sampling_decider"; const std::string w3c_parent_id = "_dd.parent_id"; +const std::string trace_source = "_dd.p.ts"; +const std::string apm_enabled = "_dd.apm.enabled"; } // namespace internal diff --git a/src/datadog/tags.h b/src/datadog/tags.h index 5593b26f..52a1af34 100644 --- a/src/datadog/tags.h +++ b/src/datadog/tags.h @@ -39,6 +39,8 @@ extern const std::string language; extern const std::string runtime_id; extern const std::string sampling_decider; extern const std::string w3c_parent_id; +extern const std::string trace_source; // _dd.p.ts +extern const std::string apm_enabled; // _dd.apm.enabled } // namespace internal // Return whether the specified `tag_name` is reserved for use internal to this diff --git a/src/datadog/telemetry/telemetry_impl.cpp b/src/datadog/telemetry/telemetry_impl.cpp index 71cd6405..4838d689 100644 --- a/src/datadog/telemetry/telemetry_impl.cpp +++ b/src/datadog/telemetry/telemetry_impl.cpp @@ -107,6 +107,8 @@ std::string to_string(datadog::tracing::ConfigName name) { return "trace_baggage_max_bytes"; case ConfigName::TRACE_BAGGAGE_MAX_ITEMS: return "trace_baggage_max_items"; + case ConfigName::APM_TRACING_ENABLED: + return "apm_tracing_enabled"; } std::abort(); diff --git a/src/datadog/trace_sampler.cpp b/src/datadog/trace_sampler.cpp index d6f2d8bd..a672620c 100644 --- a/src/datadog/trace_sampler.cpp +++ b/src/datadog/trace_sampler.cpp @@ -12,6 +12,7 @@ #include "json_serializer.h" #include "sampling_util.h" #include "span_data.h" +#include "tags.h" namespace datadog { namespace tracing { @@ -124,5 +125,97 @@ nlohmann::json TraceSampler::config_json() const { }); } +SamplingDecision ApmDisabledTraceSampler::decide(const SpanData& span_data) { + SamplingDecision decision; + decision.origin = SamplingDecision::Origin::LOCAL; + + auto now = clock_(); + uint64_t num_allowed; + if (span_data.tags.find(tags::internal::trace_source) != + span_data.tags.end()) { + decision.mechanism = static_cast(SamplingMechanism::APP_SEC); + decision.priority = static_cast(SamplingPriority::USER_KEEP); + last_kept_.store(now.wall, std::memory_order_relaxed); + num_allowed = num_allowed_.fetch_add(1, std::memory_order_relaxed) + 1; + } else { + auto last_kept = last_kept_.load(std::memory_order_relaxed); + if (now.wall - last_kept >= INTERVAL) { + if (last_kept_.compare_exchange_strong(last_kept, now.wall)) { + decision.priority = static_cast(SamplingPriority::USER_KEEP); + num_allowed = num_allowed_.fetch_add(1, std::memory_order_relaxed) + 1; + } else { + // another thread got to it first + decision.priority = static_cast(SamplingPriority::USER_DROP); + num_allowed = num_allowed_.load(std::memory_order_relaxed); + } + } else { + decision.priority = static_cast(SamplingPriority::USER_DROP); + num_allowed = num_allowed_.load(std::memory_order_relaxed); + } + } + + auto num_asked = num_asked_.fetch_add(1, std::memory_order_relaxed) + 1; + decision.limiter_max_per_second = ALLOWED_PER_SECOND; + double effective_rate = static_cast(num_allowed) / num_asked; + if (effective_rate > 1.0) { + // can happen due to the relaxed atomic operations + effective_rate = 1.0; + } + decision.limiter_effective_rate = Rate::from(effective_rate).value(); + + return decision; +} + +void ApmDisabledTraceSampler::handle_collector_response( + const CollectorResponse&) { + // do nothing +} + +nlohmann::json ApmDisabledTraceSampler::config_json() const { + return nlohmann::json::object({ + {"max_per_second", ALLOWED_PER_SECOND}, + }); +} + +template +struct ErasedTraceSampler::Model : Concept { + Model(Ptr&& samplerImpl) : impl_(std::move(samplerImpl)) {} + + SamplingDecision decide(const SpanData& span_data) override { + return impl_->decide(span_data); + } + + void handle_collector_response(const CollectorResponse& response) override { + impl_->handle_collector_response(response); + } + + nlohmann::json config_json() const override { return impl_->config_json(); } + + private: + Ptr impl_; +}; + +template +ErasedTraceSampler::ErasedTraceSampler(Ptr samplerImpl) { + impl_ = std::make_unique>(std::move(samplerImpl)); +} + +template ErasedTraceSampler::ErasedTraceSampler(std::shared_ptr); +template ErasedTraceSampler::ErasedTraceSampler( + std::unique_ptr); + +SamplingDecision ErasedTraceSampler::decide(const SpanData& span_data) { + return impl_->decide(span_data); +} + +void ErasedTraceSampler::handle_collector_response( + const CollectorResponse& response) { + impl_->handle_collector_response(response); +} + +nlohmann::json ErasedTraceSampler::config_json() const { + return impl_->config_json(); +} + } // namespace tracing } // namespace datadog diff --git a/src/datadog/trace_sampler.h b/src/datadog/trace_sampler.h index 034e6f31..dc936471 100644 --- a/src/datadog/trace_sampler.h +++ b/src/datadog/trace_sampler.h @@ -88,6 +88,7 @@ #include #include +#include #include #include #include @@ -127,5 +128,50 @@ class TraceSampler { nlohmann::json config_json() const; }; +class ApmDisabledTraceSampler { + public: + ApmDisabledTraceSampler(const Clock& clock) : clock_(clock) {} + + SamplingDecision decide(const SpanData& span_data); + void handle_collector_response(const CollectorResponse& response); + nlohmann::json config_json() const; + + private: + static constexpr auto ALLOWED_PER_SECOND = 1.0 / 60.0; + // allow a bit more than the declared ALLOWED_PER_SECOND rate + static constexpr auto INTERVAL = std::chrono::seconds(50); + + // the Limiter is not used, it's difficult to test reliably + Clock clock_; + std::atomic last_kept_{}; + std::atomic num_allowed_{0}; + std::atomic num_asked_{0}; +}; + +/* Erases the actual type implementing the decide function */ +class ErasedTraceSampler { + public: + template + ErasedTraceSampler(Ptr samplerImpl); + + SamplingDecision decide(const SpanData& span_data); + void handle_collector_response(const CollectorResponse& response); + nlohmann::json config_json() const; + + private: + struct Concept { + virtual ~Concept() = default; + virtual SamplingDecision decide(const SpanData& span_data) = 0; + virtual void handle_collector_response( + const CollectorResponse& response) = 0; + virtual nlohmann::json config_json() const = 0; + }; + + template + struct Model; + + std::unique_ptr impl_; +}; + } // namespace tracing } // namespace datadog diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index b7f74c0f..55b8ab07 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -5,11 +5,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -83,7 +85,7 @@ void inject_trace_tags( TraceSegment::TraceSegment( const std::shared_ptr& logger, const std::shared_ptr& collector, - const std::shared_ptr& trace_sampler, + std::shared_ptr trace_sampler, bool apm_tracing_enabled, const std::shared_ptr& span_sampler, const std::shared_ptr& defaults, const std::shared_ptr& config_manager, @@ -98,7 +100,8 @@ TraceSegment::TraceSegment( std::unique_ptr local_root) : logger_(logger), collector_(collector), - trace_sampler_(trace_sampler), + trace_sampler_(std::move(trace_sampler)), + apm_tracing_enabled_(apm_tracing_enabled), span_sampler_(span_sampler), defaults_(defaults), runtime_id_(runtime_id), @@ -231,6 +234,13 @@ void TraceSegment::span_finished() { local_root.tags[tags::internal::sampling_decider] = "0"; } + // RFC seems to only mandate that this be set if the trace is kept. + // However, system-tests expect this to be always be set. + // Add it all the time; can't hurt + if (!apm_tracing_enabled_) { + local_root.numeric_tags[tags::internal::apm_enabled] = 0; + } + // Some tags are repeated on all spans. for (const auto& span_ptr : spans_) { SpanData& span = *span_ptr; @@ -279,7 +289,23 @@ void TraceSegment::make_sampling_decision_if_null() { return; } - const SpanData& local_root = *spans_.front(); + SpanData& local_root = *spans_.front(); + + // ApmDisabledTraceSampler needs to know the value of _dd.p.ts. Copy it here + // so that the value is accessible in the span passed to decide(). + // The value may have been set because it was propagated from upstream or from + // a WAF run we already did + if (!apm_tracing_enabled_) { + auto it = std::find_if( + trace_tags_.begin(), trace_tags_.end(), + [](const auto& entry) { return entry.first == "_dd.p.ts"; }); + if (it != trace_tags_.end()) { + local_root.tags.insert_or_assign(it->first, it->second); + // don't move the strings or erase the entry from trace_tags_: + // besides filling meta, it may still be in the tags propagation header + } + } + sampling_decision_ = trace_sampler_->decide(local_root); update_decision_maker_trace_tag(); @@ -317,13 +343,20 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span) { } bool TraceSegment::inject(DictWriter& writer, const SpanData& span, - const InjectionOptions&) { + const InjectionOptions& inj_opts) { // If the only injection style is `NONE`, then don't do anything. if (injection_styles_.size() == 1 && injection_styles_[0] == PropagationStyle::NONE) { return false; } + if (inj_opts.trace_source) { + std::string trace_source = std::string(inj_opts.trace_source->begin(), + inj_opts.trace_source->end()); + trace_tags_.emplace_back(tags::internal::trace_source, + std::move(trace_source)); + } + // The sampling priority can change (it can be overridden on another thread), // and trace tags might change when that happens ("_dd.p.dm"). // So, we lock here, make a sampling decision if necessary, and then copy the @@ -338,6 +371,23 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span, trace_tags = trace_tags_; } + // with APM tracing disabled, stop propagation unless we must keep the trace + if (!apm_tracing_enabled_) { + if (sampling_priority != 2) { + writer.set("x-datadog-trace-id", {}); + writer.set("x-datadog-parent-id", {}); + writer.set("x-datadog-sampling-priority", {}); + writer.set("x-datadog-origin", {}); + writer.set("x-datadog-tags", {}); + writer.set("x-b3-traceid", {}); + writer.set("x-b3-spanid", {}); + writer.set("x-b3-sampled", {}); + writer.set("traceparent", {}); + writer.set("tracestate", {}); + return true; + } + } + for (const auto style : injection_styles_) { switch (style) { case PropagationStyle::DATADOG: diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index e0f34a1e..26aa1274 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -48,6 +48,11 @@ Tracer::Tracer(const FinalizedTracerConfig& config, signature_{runtime_id_, config.defaults.service, config.defaults.environment}, config_manager_(std::make_shared(config)), + apm_tracing_disabled_sampler_( + config.apm_tracing_enabled + ? nullptr + : std::make_shared( + std::make_unique(config.clock))), collector_(/* see constructor body */), span_sampler_( std::make_shared(config.span_sampler, config.clock)), @@ -180,14 +185,22 @@ Span Tracer::create_span(const SpanConfig& config) { hex_padded(span_data->trace_id.high)); } + std::shared_ptr trace_sampler; + if (is_apm_tracing_enabled()) { + trace_sampler = config_manager_->trace_sampler(); + } else { + trace_sampler = apm_tracing_disabled_sampler_; + } + const auto span_data_ptr = span_data.get(); telemetry::counter::increment(metrics::tracer::trace_segments_created, {"new_continued:new"}); const auto segment = std::make_shared( - logger_, collector_, config_manager_->trace_sampler(), span_sampler_, - defaults, config_manager_, runtime_id_, injection_styles_, hostname_, - nullopt /* origin */, tags_header_max_size_, std::move(trace_tags), - nullopt /* sampling_decision */, nullopt /* additional_w3c_tracestate */, + logger_, collector_, std::move(trace_sampler), is_apm_tracing_enabled(), + span_sampler_, defaults, config_manager_, runtime_id_, injection_styles_, + hostname_, nullopt /* origin */, tags_header_max_size_, + std::move(trace_tags), nullopt /* sampling_decision */, + nullopt /* additional_w3c_tracestate */, nullopt /* additional_datadog_w3c_tracestate*/, std::move(span_data)); Span span{span_data_ptr, segment, [generator = generator_]() { return generator->span_id(); }, @@ -378,7 +391,7 @@ Expected Tracer::extract_span(const DictReader& reader, } Optional sampling_decision; - if (merged_context.sampling_priority) { + if (merged_context.sampling_priority && is_apm_tracing_enabled()) { SamplingDecision decision; decision.priority = *merged_context.sampling_priority; // `decision.mechanism` is null. We might be able to infer it once we @@ -388,15 +401,22 @@ Expected Tracer::extract_span(const DictReader& reader, sampling_decision = decision; } + std::shared_ptr trace_sampler; + if (is_apm_tracing_enabled()) { + trace_sampler = config_manager_->trace_sampler(); + } else { + trace_sampler = apm_tracing_disabled_sampler_; + } + 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), + logger_, collector_, std::move(trace_sampler), is_apm_tracing_enabled(), + 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)); diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index 629cd130..78b20341 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "datadog_agent.h" @@ -14,6 +15,7 @@ #include "parse_util.h" #include "platform_util.h" #include "string_util.h" +#include "tags.h" #include "threaded_event_scheduler.h" namespace datadog { @@ -128,6 +130,10 @@ Expected load_tracer_env_config(Logger &logger) { env_cfg.generate_128bit_trace_ids = !falsy(*enabled_env); } + if (auto apm_enabled_env = lookup(environment::DD_APM_TRACING_ENABLED)) { + env_cfg.apm_tracing_enabled = !falsy(*apm_enabled_env); + } + // Baggage if (auto baggage_items_env = lookup(environment::DD_TRACE_BAGGAGE_MAX_ITEMS)) { @@ -345,6 +351,13 @@ Expected finalize_config(const TracerConfig &user_config, final_config.metadata[ConfigName::REPORT_TRACES] = ConfigMetadata( ConfigName::REPORT_TRACES, to_string(final_config.report_traces), origin); + // APM Tracing Enabled + std::tie(origin, final_config.apm_tracing_enabled) = pick( + env_config->apm_tracing_enabled, user_config.apm_tracing_enabled, true); + final_config.metadata[ConfigName::APM_TRACING_ENABLED] = + ConfigMetadata(ConfigName::APM_TRACING_ENABLED, + to_string(final_config.apm_tracing_enabled), origin); + // Report hostname final_config.report_hostname = value_or(env_config->report_hostname, user_config.report_hostname, false); diff --git a/src/datadog/tracer_telemetry.cpp b/src/datadog/tracer_telemetry.cpp new file mode 100644 index 00000000..e69de29b diff --git a/test/mocks/collectors.h b/test/mocks/collectors.h index c2f8768a..91043f62 100644 --- a/test/mocks/collectors.h +++ b/test/mocks/collectors.h @@ -22,7 +22,7 @@ struct MockCollector : public Collector { std::vector>> chunks; Expected send(std::vector>&& spans, - const std::shared_ptr&) override { + const std::shared_ptr&) override { chunks.emplace_back(std::move(spans)); return {}; } @@ -51,7 +51,7 @@ struct MockCollectorWithResponse : public MockCollector { Expected send( std::vector>&& spans, - const std::shared_ptr& response_handler) override { + const std::shared_ptr& response_handler) override { MockCollector::send(std::move(spans), response_handler); response_handler->handle_collector_response(response); return {}; @@ -64,7 +64,7 @@ struct PriorityCountingCollector : public Collector { std::map sampling_priority_count; Expected send(std::vector>&& spans, - const std::shared_ptr&) override { + const std::shared_ptr&) override { const SpanData& root = root_span(spans); const auto priority = root.numeric_tags.at(tags::internal::sampling_priority); @@ -127,7 +127,7 @@ struct PriorityCountingCollectorWithResponse Expected send( std::vector>&& spans, - const std::shared_ptr& response_handler) override { + const std::shared_ptr& response_handler) override { PriorityCountingCollector::send(std::move(spans), response_handler); REQUIRE(response_handler); response_handler->handle_collector_response(response); @@ -141,7 +141,7 @@ struct FailureCollector : public Collector { Error failure{Error::OTHER, "send(...) failed because I told it to."}; Expected send(std::vector>&&, - const std::shared_ptr&) override { + const std::shared_ptr&) override { return failure; } diff --git a/test/test_datadog_agent.cpp b/test/test_datadog_agent.cpp index d07fb0fb..4aa3fbaf 100644 --- a/test/test_datadog_agent.cpp +++ b/test/test_datadog_agent.cpp @@ -231,3 +231,58 @@ DATADOG_AGENT_TEST("Remote Configuration") { CHECK(logger->error_count() == 1); } } + +TEST_CASE("Datadog-Client-Computed-Stats header", "[datadog_agent]") { + const auto logger = + std::make_shared(std::cerr, MockLogger::ERRORS_ONLY); + const auto event_scheduler = std::make_shared(); + const auto http_client = std::make_shared(); + + TracerConfig config; + config.service = "testsvc"; + config.logger = logger; + config.agent.event_scheduler = event_scheduler; + config.agent.http_client = http_client; + config.telemetry.enabled = false; + + SECTION("header sent when apm_tracing_enabled is false") { + config.agent.apm_tracing_enabled = false; + auto finalized = finalize_config(config); + REQUIRE(finalized); + + { + http_client->response_status = 200; + http_client->response_body << "{}"; + Tracer tracer{*finalized}; + Span span = tracer.create_span(); + } + + http_client->drain(std::chrono::steady_clock::now()); + + // the header is present + auto header_it = http_client->request_headers.items.find( + "Datadog-Client-Computed-Stats"); + REQUIRE(header_it != http_client->request_headers.items.end()); + REQUIRE(header_it->second == "yes"); + } + + SECTION("header not sent when apm_tracing_enabled is true") { + config.agent.apm_tracing_enabled = true; + auto finalized = finalize_config(config); + REQUIRE(finalized); + + { + http_client->response_status = 200; + http_client->response_body << "{}"; + Tracer tracer{*finalized}; + Span span = tracer.create_span(); + } + + http_client->drain(std::chrono::steady_clock::now()); + + // verify trhat the header is not present + auto header_it = http_client->request_headers.items.find( + "Datadog-Client-Computed-Stats"); + REQUIRE(header_it == http_client->request_headers.items.end()); + } +} diff --git a/test/test_span.cpp b/test/test_span.cpp index e12af2a4..9672397f 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -860,3 +860,147 @@ TEST_CASE("128-bit trace ID injection") { REQUIRE(found != writer.items.end()); REQUIRE(found->second == "deadbeefdeadbeefcafebabecafebabe"); } + +TEST_CASE("injection with trace_source option") { + TracerConfig config; + config.service = "testsvc"; + config.collector = std::make_shared(); + config.logger = std::make_shared(); + config.injection_styles = {PropagationStyle::DATADOG}; + + auto finalized_config = finalize_config(config); + REQUIRE(finalized_config); + Tracer tracer{*finalized_config}; + + SECTION("trace_source is not set (default)") { + auto span = tracer.create_span(); + InjectionOptions options; + options.trace_source = std::nullopt; + + MockDictWriter writer; + span.inject(writer, options); + + const auto& headers = writer.items; + // When there is no trace source, there should be no x-datadog-tags + // header or if there is one, it should not contain _dd.p.ts + if (headers.count("x-datadog-tags") > 0) { + const auto decoded_tags = decode_tags(headers.at("x-datadog-tags")); + REQUIRE(decoded_tags); + auto found = + std::find_if(decoded_tags->begin(), decoded_tags->end(), + [](const auto& tag) { return tag.first == "_dd.p.ts"; }); + REQUIRE(found == decoded_tags->end()); + } + } + + SECTION("trace_source is 02 (appsec)") { + auto span = tracer.create_span(); + InjectionOptions options; + options.trace_source = {'0', '2'}; + + MockDictWriter writer; + span.inject(writer, options); + + const auto& headers = writer.items; + REQUIRE(headers.count("x-datadog-tags") == 1); + + const auto decoded_tags = decode_tags(headers.at("x-datadog-tags")); + REQUIRE(decoded_tags); + + // Check that _dd.p.ts=02 is present in the trace tags + bool found_trace_source = false; + for (const auto& [key, value] : *decoded_tags) { + if (key == "_dd.p.ts" && value == "02") { + found_trace_source = true; + break; + } + } + REQUIRE(found_trace_source); + } + + SECTION("trace source is 02 (appsec) with existing trace tags") { + // Extract a span with existing trace tags + const std::unordered_map headers{ + {"x-datadog-trace-id", "123"}, + {"x-datadog-parent-id", "456"}, + {"x-datadog-sampling-priority", "1"}, + {"x-datadog-tags", "_dd.p.existing=value,_dd.p.another=test"}}; + MockDictReader reader{headers}; + auto span = tracer.extract_span(reader); + REQUIRE(span); + + InjectionOptions options; + options.trace_source = {'0', '2'}; + + MockDictWriter writer; + span->inject(writer, options); + + const auto& output_headers = writer.items; + REQUIRE(output_headers.count("x-datadog-tags") == 1); + + const auto decoded_tags = decode_tags(output_headers.at("x-datadog-tags")); + REQUIRE(decoded_tags); + + // Check that _dd.p.ts=02 is present along with existing tags + bool found_trace_source = false; + bool found_existing = false; + bool found_another = false; + + for (const auto& [key, value] : *decoded_tags) { + if (key == "_dd.p.ts" && value == "02") { + found_trace_source = true; + } else if (key == "_dd.p.existing" && value == "value") { + found_existing = true; + } else if (key == "_dd.p.another" && value == "test") { + found_another = true; + } + } + + REQUIRE(found_trace_source); + REQUIRE(found_existing); + REQUIRE(found_another); + } + + SECTION("trace_source with APM tracing disabled") { + // Test the scenario where APM tracing is disabled + TracerConfig apm_disabled_config; + apm_disabled_config.service = "testsvc"; + apm_disabled_config.collector = std::make_shared(); + apm_disabled_config.logger = std::make_shared(); + apm_disabled_config.injection_styles = { + PropagationStyle::DATADOG, PropagationStyle::B3, PropagationStyle::W3C}; + // Disable APM tracing + apm_disabled_config.apm_tracing_enabled = false; + + auto apm_disabled_finalized_config = finalize_config(apm_disabled_config); + REQUIRE(apm_disabled_finalized_config); + Tracer apm_disabled_tracer{*apm_disabled_finalized_config}; + + SECTION( + "sampling priority != 2 (USER-KEEP) and no _dd.p.ts - headers should " + "be cleared") { + auto span = apm_disabled_tracer.create_span(); + span.trace_segment().override_sampling_priority(1); + + InjectionOptions options; + options.trace_source = {'0', '2'}; + + MockDictWriter writer; + span.inject(writer, options); + + const auto& headers = writer.items; + // all headers should be empty when APM tracing is disabled and sampling + // priority != 2 + REQUIRE(headers.at("x-datadog-trace-id").empty()); + REQUIRE(headers.at("x-datadog-parent-id").empty()); + REQUIRE(headers.at("x-datadog-sampling-priority").empty()); + REQUIRE(headers.at("x-datadog-origin").empty()); + REQUIRE(headers.at("x-datadog-tags").empty()); + REQUIRE(headers.at("x-b3-traceid").empty()); + REQUIRE(headers.at("x-b3-spanid").empty()); + REQUIRE(headers.at("x-b3-sampled").empty()); + REQUIRE(headers.at("traceparent").empty()); + REQUIRE(headers.at("tracestate").empty()); + } + } +} diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 48dda9d1..8c994034 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1746,3 +1746,137 @@ TEST_CASE("move semantics") { Tracer tracer2{std::move(tracer1)}; (void)tracer2; } + +TEST_CASE("apm_tracing_enabled behavior") { + TracerConfig base_config; + base_config.service = "testsvc"; + base_config.name = "test.op"; + auto collector = std::make_shared(); + base_config.collector = collector; + base_config.logger = std::make_shared(); + + TimePoint current_time = default_clock(); + auto clock = [¤t_time]() { return current_time; }; + + SECTION( + "APM tracing disabled - span with _dd.p.ts is kept, _dd.apm.enabled tag " + "added") { + TracerConfig config = base_config; + config.apm_tracing_enabled = false; + + auto finalized_config = finalize_config(config, clock); + REQUIRE(finalized_config); + REQUIRE(!finalized_config->apm_tracing_enabled); + Tracer tracer{*finalized_config}; + + SpanConfig span_cfg; + span_cfg.tags[tags::internal::trace_source] = "02"; + { tracer.create_span(span_cfg); } + + REQUIRE(collector->chunks.size() == 1); + REQUIRE(collector->chunks.front().size() == 1); + const datadog::tracing::SpanData& span_data = + *collector->chunks.front().front(); + + REQUIRE(span_data.tags.at("_dd.p.dm") == "-5"); + REQUIRE(span_data.numeric_tags.at(tags::internal::apm_enabled) == 0); + REQUIRE(span_data.numeric_tags.at(tags::internal::sampling_priority) == 2); + } + + SECTION("APM tracing disabled - no _dd.p.ts - rate limited to 1/min") { + TracerConfig config = base_config; + config.apm_tracing_enabled = false; + + auto finalized_config = finalize_config(config, clock); + REQUIRE(finalized_config); + Tracer tracer{*finalized_config}; + { auto root1 = tracer.create_span(); } + REQUIRE(collector->chunks.size() == 1); + REQUIRE(collector->chunks.front().size() == 1); + const datadog::tracing::SpanData& span1_data = + *collector->chunks.front().front(); + CHECK(span1_data.numeric_tags.at(tags::internal::sampling_priority) == 2); + CHECK(span1_data.numeric_tags.at(tags::internal::apm_enabled) == 0); + CHECK(std::stoi(span1_data.tags.at("_dd.p.dm")) < -10); + + collector->chunks.clear(); + + current_time += + std::chrono::seconds(1); // Advance clock a bit, still within 1 min + { auto root2 = tracer.create_span(); } + REQUIRE(collector->chunks.size() == 1); + REQUIRE(collector->chunks.front().size() == 1); + const datadog::tracing::SpanData& span2_data = + *collector->chunks.front().front(); + CHECK(span2_data.numeric_tags.at(tags::internal::sampling_priority) == -1); + CHECK(span2_data.numeric_tags.at(tags::internal::apm_enabled) == 0); + + collector->chunks.clear(); + + current_time += std::chrono::minutes(1) + + std::chrono::seconds(1); // Advance clock past 1 min + { auto root3 = tracer.create_span(); } + REQUIRE(collector->chunks.size() == 1); + REQUIRE(collector->chunks.front().size() == 1); + const auto& span3_data = *collector->chunks.front().front(); + CHECK(span2_data.numeric_tags.at(tags::internal::sampling_priority) == 2); + CHECK(span3_data.numeric_tags.at(tags::internal::apm_enabled) == 0); + } + + SECTION("APM tracing disabled - extracted context behavior") { + TracerConfig config = base_config; + config.apm_tracing_enabled = false; + + auto finalized_config = finalize_config(config, clock); + REQUIRE(finalized_config); + Tracer tracer{*finalized_config}; + + // Case 1: extracted context with priority, but no _dd.p.ts → dropped + // To ensure this, let's make this the *second* span in this disabled state + // for this test section. The first consumes the limiter slot. + { auto dummy_span = tracer.create_span(); } + collector->chunks.clear(); + + const std::unordered_map headers_with_priority{ + {"x-datadog-trace-id", "123"}, + {"x-datadog-parent-id", "456"}, + {"x-datadog-sampling-priority", "2"} // USER_KEEP + }; + MockDictReader reader_with_priority{headers_with_priority}; + { + auto span = tracer.extract_span(reader_with_priority); + REQUIRE(span); + } + REQUIRE(collector->chunks.size() == 1); + REQUIRE(collector->chunks.front().size() == 1); + const SpanData& span1_data = *collector->chunks.front().front(); + // although incoming priority was USER_KEEP, we should still drop it + CHECK(span1_data.numeric_tags.at(tags::internal::sampling_priority) == -1); + CHECK(span1_data.numeric_tags.at(tags::internal::apm_enabled) == 0.); + + collector->chunks.clear(); + + // Case 2: Extracted context with priority AND _dd.p.ts -> Kept by AppSec + // rule + current_time += + std::chrono::minutes(1) + std::chrono::seconds(1); // Refresh limiter + const std::unordered_map + headers_with_priority_and_appsec{ + {"x-datadog-trace-id", "789"}, + {"x-datadog-parent-id", "101"}, + {"x-datadog-sampling-priority", + "-1"}, // USER_DROP, to show _dd.p.ts overrides + {tags::internal::trace_source, "02"}}; + MockDictReader reader_with_priority_and_appsec{ + headers_with_priority_and_appsec}; + { + auto span = tracer.extract_span(reader_with_priority_and_appsec); + REQUIRE(span); + } + REQUIRE(collector->chunks.size() == 1); + REQUIRE(collector->chunks.front().size() == 1); + const SpanData& span2_data = *collector->chunks.front().front(); + CHECK(span2_data.numeric_tags.at(tags::internal::sampling_priority) == 2); + CHECK(span2_data.numeric_tags.at(tags::internal::apm_enabled) == 0); + } +} diff --git a/test/test_tracer_config.cpp b/test/test_tracer_config.cpp index 4fc4b488..21476290 100644 --- a/test/test_tracer_config.cpp +++ b/test/test_tracer_config.cpp @@ -272,6 +272,25 @@ TRACER_CONFIG_TEST("TracerConfig::log_on_startup") { } } +TRACER_CONFIG_TEST("DD_APM_TRACING_ENABLED") { + TracerConfig config; + config.service = "testsvc"; // Required for finalize_config + + SECTION("default is true") { + const EnvGuard guard{"DD_APM_TRACING_ENABLED", ""}; + auto finalized = finalize_config(config); + REQUIRE(finalized); + REQUIRE(finalized->apm_tracing_enabled); + } + + SECTION("can be set to false") { + const EnvGuard guard{"DD_APM_TRACING_ENABLED", "false"}; + auto finalized = finalize_config(config); + REQUIRE(finalized); + REQUIRE(!finalized->apm_tracing_enabled); + } +} + TRACER_CONFIG_TEST("TracerConfig::report_traces") { TracerConfig config; config.service = "testsvc";