diff --git a/Cargo.lock b/Cargo.lock index d0935804..0f00da29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,16 +340,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -957,22 +947,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "tokio", - "tokio-rustls", - "tower-service", -] - [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1257,9 +1231,9 @@ dependencies = [ [[package]] name = "libdd-common" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "933c5940905e9a3c8dd8c0439697b3fb327289cef7858370e6a5f440139f6314" +checksum = "4af81e6953fab2814792b8893a2cee9f16aad1b71cb3f8720b7256f28d6a2d8d" dependencies = [ "anyhow", "bytes", @@ -1273,19 +1247,15 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", "libc", "nix", "pin-project", "regex", - "rustls", - "rustls-native-certs", "serde", "static_assertions", "thiserror 1.0.69", "tokio", - "tokio-rustls", "tower-service", "windows-sys 0.52.0", ] @@ -1368,9 +1338,9 @@ dependencies = [ [[package]] name = "libdd-sampling" -version = "1.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ae8728d9d717eb1b648223c4c1da53f77627fecd6045027ccb758a72815b300" +checksum = "df229b55274db81b8a85a1b9ab14663af070358f39017ab68d26a3aada535db6" dependencies = [ "libdd-common", "lru", @@ -1653,12 +1623,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "opentelemetry" version = "0.31.0" @@ -2181,20 +2145,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rmp" version = "0.8.14" @@ -2259,52 +2209,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -2326,44 +2230,12 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.28" @@ -2564,12 +2436,6 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b3c8667cd96245cbb600b8dec5680a7319edd719c5aa2b5d23c6bff94f39765" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.117" @@ -2618,7 +2484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", - "core-foundation 0.9.4", + "core-foundation", "system-configuration-sys", ] @@ -2771,16 +2637,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.18" @@ -3011,12 +2867,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.8" @@ -3609,12 +3459,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 35baf7fd..83e1c2ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,10 +29,10 @@ libdd-data-pipeline = { version = "5.0.0", features = [ libdd-trace-utils = { version = "5.0.0", default-features = false } libdd-capabilities-impl = { version = "2.0.0", default-features = false } libdd-telemetry = { version = "5.0.0", default-features = false } -libdd-common = { version = "4.1.0", default-features = false } +libdd-common = { version = "4.2.0", default-features = false } libdd-tinybytes = { version = "1.1.1", default-features = false } libdd-library-config = { version = "2.0.0", default-features = false } -libdd-sampling = { version = "1.0.0", default-features = false } +libdd-sampling = { version = "2.1.0", default-features = false } opentelemetry_sdk = { version = "0.31.0", features = [ "trace", "metrics", diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index ebda13cd..ea77aeb9 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -87,7 +87,6 @@ httparse,https://github.com/seanmonstar/httparse,MIT OR Apache-2.0,Sean McArthur httpdate,https://github.com/pyfisch/httpdate,MIT OR Apache-2.0,Pyfisch httpmock,https://github.com/httpmock/httpmock,MIT,Alexander Liesenfeld hyper,https://github.com/hyperium/hyper,MIT,Sean McArthur -hyper-rustls,https://github.com/rustls/hyper-rustls,Apache-2.0 OR ISC OR MIT,The hyper-rustls Authors hyper-timeout,https://github.com/hjr3/hyper-timeout,MIT OR Apache-2.0,Herman J. Radtke III hyper-util,https://github.com/hyperium/hyper-util,MIT,Sean McArthur icu_collections,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers @@ -143,7 +142,6 @@ nu-ansi-term,https://github.com/nushell/nu-ansi-term,MIT,"ogham@bsago.me, Ryan S num-traits,https://github.com/rust-num/num-traits,MIT OR Apache-2.0,The Rust Project Developers once_cell,https://github.com/matklad/once_cell,MIT OR Apache-2.0,Aleksey Kladov oorandom,https://hg.sr.ht/~icefox/oorandom,MIT,Simon Heath -openssl-probe,https://github.com/alexcrichton/openssl-probe,MIT OR Apache-2.0,Alex Crichton opentelemetry,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry,Apache-2.0,The opentelemetry Authors opentelemetry-appender-tracing,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-appender-tracing,Apache-2.0,The opentelemetry-appender-tracing Authors opentelemetry-http,https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http,Apache-2.0,The opentelemetry-http Authors @@ -185,24 +183,16 @@ regex,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Dev regex-automata,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " regex-syntax,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " reqwest,https://github.com/seanmonstar/reqwest,MIT OR Apache-2.0,Sean McArthur -ring,https://github.com/briansmith/ring,Apache-2.0 AND ISC,The ring Authors rmp,https://github.com/3Hren/msgpack-rust,MIT,Evgeny Safronov rmp-serde,https://github.com/3Hren/msgpack-rust,MIT,Evgeny Safronov rmpv,https://github.com/3Hren/msgpack-rust,MIT,Evgeny Safronov rustc_version,https://github.com/djc/rustc-version-rs,MIT OR Apache-2.0,The rustc_version Authors rustc_version_runtime,https://github.com/seppo0010/rustc-version-runtime-rs,MIT,Sebastian Waisbrot rustix,https://github.com/bytecodealliance/rustix,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,"Dan Gohman , Jakub Konka " -rustls,https://github.com/rustls/rustls,Apache-2.0 OR ISC OR MIT,The rustls Authors -rustls-native-certs,https://github.com/rustls/rustls-native-certs,Apache-2.0 OR ISC OR MIT,The rustls-native-certs Authors -rustls-pki-types,https://github.com/rustls/pki-types,MIT OR Apache-2.0,The rustls-pki-types Authors -rustls-webpki,https://github.com/rustls/webpki,ISC,The rustls-webpki Authors rustversion,https://github.com/dtolnay/rustversion,MIT OR Apache-2.0,David Tolnay ryu,https://github.com/dtolnay/ryu,Apache-2.0 OR BSL-1.0,David Tolnay same-file,https://github.com/BurntSushi/same-file,Unlicense OR MIT,Andrew Gallant -schannel,https://github.com/steffengy/schannel-rs,MIT,"Steven Fackler , Steffen Butzer " scopeguard,https://github.com/bluss/scopeguard,MIT OR Apache-2.0,bluss -security-framework,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " -security-framework-sys,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " semver,https://github.com/dtolnay/semver,MIT OR Apache-2.0,David Tolnay serde,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_bytes,https://github.com/serde-rs/bytes,MIT OR Apache-2.0,David Tolnay @@ -224,7 +214,6 @@ socket2,https://github.com/rust-lang/socket2,MIT OR Apache-2.0,"Alex Crichton static_assertions,https://github.com/nvzqz/static-assertions-rs,MIT OR Apache-2.0,Nikolai Vazquez stringmetrics,https://github.com/pluots/stringmetrics,Apache-2.0,Trevor Gross -subtle,https://github.com/dalek-cryptography/subtle,BSD-3-Clause,"Isis Lovecruft , Henry de Valence " syn,https://github.com/dtolnay/syn,MIT OR Apache-2.0,David Tolnay sync_wrapper,https://github.com/Actyx/sync_wrapper,Apache-2.0,Actyx AG synstructure,https://github.com/mystor/synstructure,MIT,Nika Layzell @@ -241,7 +230,6 @@ tinystr,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Devel tinytemplate,https://github.com/bheisler/TinyTemplate,Apache-2.0 OR MIT,Brook Heisler tokio,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-macros,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors -tokio-rustls,https://github.com/rustls/tokio-rustls,MIT OR Apache-2.0,The tokio-rustls Authors tokio-stream,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-util,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tonic,https://github.com/hyperium/tonic,MIT,Lucio Franco @@ -262,7 +250,6 @@ unicode-ident,https://github.com/dtolnay/unicode-ident,(MIT OR Apache-2.0) AND U unicode-width,https://github.com/unicode-rs/unicode-width,MIT OR Apache-2.0,"kwantam , Manish Goregaokar " unicode-xid,https://github.com/unicode-rs/unicode-xid,MIT OR Apache-2.0,"erick.tryzelaar , kwantam , Manish Goregaokar " unsafe-libyaml,https://github.com/dtolnay/unsafe-libyaml,MIT,David Tolnay -untrusted,https://github.com/briansmith/untrusted,ISC,Brian Smith url,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers urlencoding,https://github.com/kornelski/rust_urlencoding,MIT,"Kornel , Bertram Truong " utf8_iter,https://github.com/hsivonen/utf8_iter,Apache-2.0 OR MIT,Henri Sivonen @@ -318,7 +305,6 @@ zerocopy,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"J zerocopy-derive,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " zerofrom,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zerofrom-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar -zeroize,https://github.com/RustCrypto/utils,Apache-2.0 OR MIT,The RustCrypto Project Developers zerotrie,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerovec,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerovec-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar diff --git a/datadog-opentelemetry/src/core/configuration/configuration.rs b/datadog-opentelemetry/src/core/configuration/configuration.rs index be56104b..08a98c46 100644 --- a/datadog-opentelemetry/src/core/configuration/configuration.rs +++ b/datadog-opentelemetry/src/core/configuration/configuration.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use libdd_telemetry::data::Configuration; -use std::collections::{HashSet, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::fmt::Display; use std::ops::Deref; use std::path::Path; @@ -149,6 +149,23 @@ fn parse_temporality(s: String) -> Option 1.0 would keep all of it). Mirrors the +/// range check applied to RC's `tracing_sampling_rate`. +fn validate_trace_sample_rate(rate: f64) -> Option { + if rate.is_finite() && (0.0..=1.0).contains(&rate) { + Some(rate) + } else { + crate::dd_warn!( + "DD_TRACE_SAMPLE_RATE must be in [0.0, 1.0], got {rate}; treating as unset" + ); + None + } +} + enum ConfigItemRef<'a, T> { Ref(&'a T), ArcRef(arc_swap::Guard>>), @@ -458,6 +475,13 @@ impl ConfigItemWithOverride { ConfigItemRef::Ref(self.config_item.value()) } } + + /// Returns the env/code/default value, ignoring any remote-config override. + /// Use this when the caller must compose with RC-delivered values without + /// losing the locally-configured value to the override. + fn local_value(&self) -> ConfigItemRef<'_, T> { + ConfigItemRef::Ref(self.config_item.value()) + } } impl ConfigurationProvider @@ -650,6 +674,25 @@ impl ExtraServicesTracker { } } + /// Returns true if `name` matches any tracked extra service + /// (case-insensitively), checking both the resolved set and the pending + /// queue. Read-only: does not drain the queue. + fn contains_service(&self, name: &str) -> bool { + if let Ok(set) = self.extra_services.lock() { + if set.iter().any(|s| s.eq_ignore_ascii_case(name)) { + return true; + } + } + if let Ok(queue) = self.extra_services_queue.lock() { + if let Some(ref q) = *queue { + if q.iter().any(|s| s.eq_ignore_ascii_case(name)) { + return true; + } + } + } + false + } + fn add_extra_services( &self, services: impl Iterator>, @@ -914,7 +957,7 @@ impl ConfigurationValueProvider for Option, bool, u32, usize, i32, f64, ServiceName, LevelFilter, ParsedSamplingRules); -impl_config_value_provider!(option: String); +impl_config_value_provider!(option: String, f64); #[derive(Clone)] /// Configuration for the Datadog Tracer @@ -972,6 +1015,11 @@ pub struct Config { /// If a rule matches, the trace is sampled with the associated sample rate. trace_sampling_rules: SamplingRulesConfigItem, + /// Global trace sample rate (DD_TRACE_SAMPLE_RATE). `None` means unset + /// (no implicit catch-all; libdatadog's no-rule path samples at 100%). + /// `Some(rate)` installs a catch-all rule so the rate limiter applies. + trace_sample_rate: ConfigItem>, + /// Maximum number of spans to sample per second /// Only applied if trace_sampling_rules are matched trace_rate_limit: ConfigItem, @@ -1178,6 +1226,10 @@ impl Config { // Use the initialized ConfigItem trace_sampling_rules: sampling_rules_item, + trace_sample_rate: cisu.update_parsed_with_transform( + default.trace_sample_rate, + validate_trace_sample_rate, + ), trace_rate_limit: cisu.update_parsed(default.trace_rate_limit), enabled: cisu.update_parsed(default.enabled), @@ -1268,6 +1320,7 @@ impl Config { &self.dogstatsd_agent_port, &self.dogstatsd_agent_url, &self.trace_sampling_rules, + &self.trace_sample_rate, &self.trace_rate_limit, &self.enabled, &self.log_level_filter, @@ -1400,11 +1453,27 @@ impl Config { self.trace_sampling_rules.value() } + /// Returns the locally-configured (env/code/default) trace sampling rules, + /// ignoring any Remote Config override. Used by the RC handler to compose + /// env rules with RC-delivered values without losing them. + pub(crate) fn local_trace_sampling_rules( + &self, + ) -> impl Deref + use<'_> { + self.trace_sampling_rules.local_value() + } + /// Returns the maximum number of traces per second (rate limit). pub fn trace_rate_limit(&self) -> i32 { *self.trace_rate_limit.value() } + /// Returns the configured global trace sample rate (DD_TRACE_SAMPLE_RATE), + /// or `None` if unset. Applied as a catch-all sample rate when no explicit + /// sampling rule matches. + pub fn trace_sample_rate(&self) -> Option { + *self.trace_sample_rate.value() + } + /// Returns whether tracing is enabled. pub fn enabled(&self) -> bool { *self.enabled.value() @@ -1639,17 +1708,47 @@ impl Config { } } - pub(crate) fn clear_remote_sampling_rules(&self, config_id: Option) { - self.trace_sampling_rules.unset_override_value(); - self.trace_sampling_rules.set_config_id(config_id); - - // Fallback rules are locally defined, so "local" provenance is correct - let internal: Vec = self - .trace_sampling_rules() + /// Composes the rules that the sampler should see in the absence of any + /// active Remote Config override: locally-configured rules followed by an + /// implicit catch-all that applies `DD_TRACE_SAMPLE_RATE`. + /// + /// The catch-all is appended only when `DD_TRACE_SAMPLE_RATE` is explicitly + /// set. Its default is unset (`None`), in which case nothing is appended and + /// libdatadog's no-rule fallback samples unmatched spans at 100%. An explicit + /// value — including `1.0` — does install the catch-all, so `DD_TRACE_RATE_LIMIT` + /// applies to otherwise-unmatched spans. The rate is already validated to a + /// finite value in `[0.0, 1.0]` at ingestion; the `is_finite` guard below is + /// belt-and-suspenders. + pub(crate) fn effective_initial_rules(&self) -> Vec { + let mut rules: Vec = self + .local_trace_sampling_rules() .iter() .cloned() .map(Into::into) .collect(); + if let Some(env_rate) = self.trace_sample_rate() { + if env_rate.is_finite() { + rules.push(libdd_sampling::SamplingRuleConfig { + sample_rate: env_rate, + service: None, + name: None, + resource: None, + tags: HashMap::new(), + // "default" is libdatadog's documented default provenance + // (default_provenance() in libdd-sampling) and matches the + // value the RC-rate catch-all path produces via serde omission. + provenance: "default".to_string(), + }); + } + } + rules + } + + pub(crate) fn clear_remote_sampling_rules(&self, config_id: Option) { + self.trace_sampling_rules.unset_override_value(); + self.trace_sampling_rules.set_config_id(config_id); + + let internal = self.effective_initial_rules(); self.remote_config_callbacks .lock() .unwrap() @@ -1695,6 +1794,16 @@ impl Config { self.extra_services_tracker.get_extra_services() } + /// Returns true if an RC `service_target.service` value applies to this + /// tracer: it matches the primary service or any advertised extra service, + /// compared case-insensitively. Used to guard which Remote Config sampling + /// payloads this tracer applies (a config that advertised extra service is + /// legitimately ours; service-name case can differ from the UI). + pub(crate) fn rc_service_target_matches(&self, target_service: &str) -> bool { + target_service.eq_ignore_ascii_case(&self.service()) + || self.extra_services_tracker.contains_service(target_service) + } + /// Check if remote configuration is enabled pub fn remote_config_enabled(&self) -> bool { *self.remote_config_enabled.value() @@ -1762,6 +1871,7 @@ impl std::fmt::Debug for Config { .field("trace_agent_url", &self.trace_agent_url) .field("dogstatsd_agent_url", &self.dogstatsd_agent_url) .field("trace_sampling_rules", &self.trace_sampling_rules) + .field("trace_sample_rate", &self.trace_sample_rate) .field("trace_rate_limit", &self.trace_rate_limit) .field("enabled", &self.enabled) .field("log_level_filter", &self.log_level_filter) @@ -1840,6 +1950,7 @@ fn default_config() -> Config { SupportedConfigurations::DD_TRACE_SAMPLING_RULES, ParsedSamplingRules::default(), // Empty rules by default ), + trace_sample_rate: ConfigItem::new(SupportedConfigurations::DD_TRACE_SAMPLE_RATE, None), trace_rate_limit: ConfigItem::new(SupportedConfigurations::DD_TRACE_RATE_LIMIT, 100), enabled: ConfigItem::new(SupportedConfigurations::DD_TRACE_ENABLED, true), log_level_filter: ConfigItem::new( @@ -2398,6 +2509,21 @@ impl ConfigBuilder { self } + /// Global trace sample rate. Applied as a catch-all sample rate when no + /// explicit sampling rule matches. + /// + /// **Default**: `1.0` + /// + /// Env variable: `DD_TRACE_SAMPLE_RATE` + pub fn set_trace_sample_rate(&mut self, rate: f64) -> &mut Self { + // Only accept finite values in [0.0, 1.0]; an out-of-range value is + // logged and left unset rather than installed as a catch-all rule. + if let Some(rate) = validate_trace_sample_rate(rate) { + self.config.trace_sample_rate.set_code(Some(rate)); + } + self + } + /// A list of propagation styles to use for both extraction and injection. Supported values are /// `datadog` and `tracecontext`. /// @@ -3114,6 +3240,48 @@ mod tests { assert_eq!(received[0].provenance, "local"); } + #[test] + fn test_clear_remote_rules_includes_env_rate_catch_all() { + // When DD_TRACE_SAMPLE_RATE is set and the remote override is cleared, + // the callback must receive [env_rules..., catch_all(env_rate)] so the + // sampler applies the env rate to unmatched spans. + let mut config_builder = Config::builder(); + config_builder.set_trace_sample_rate(0.25); + config_builder.set_trace_sampling_rules(vec![SamplingRuleConfig { + sample_rate: 0.5, + name: Some("env_name".to_string()), + ..SamplingRuleConfig::default() + }]); + let config = config_builder.build(); + + let received = Arc::new(Mutex::new(Vec::::new())); + let clone = received.clone(); + config.set_sampling_rules_callback(move |update| { + let RemoteConfigUpdate::SamplingRules(rules) = update; + *clone.lock().unwrap() = rules.clone(); + }); + + // Install a remote override, then clear it. + config + .update_sampling_rules_from_remote( + r#"[{"sample_rate":0.9,"provenance":"customer"}]"#, + None, + ) + .unwrap(); + config.clear_remote_sampling_rules(None); + + let got = received.lock().unwrap(); + assert_eq!(got.len(), 2, "expected env rule + env catch-all"); + assert_eq!(got[0].sample_rate, 0.5); + assert_eq!(got[0].name.as_deref(), Some("env_name")); + assert_eq!(got[1].sample_rate, 0.25); + assert!(got[1].name.is_none()); + assert!(got[1].service.is_none()); + // env catch-all carries "default" provenance (libdatadog's documented + // default; maps to DM -3 in libdd-sampling). + assert_eq!(got[1].provenance, "default"); + } + #[test] fn test_public_sampling_rule_config_ignores_provenance_in_json() { // The public SamplingRuleConfig should silently ignore a "provenance" field in JSON, @@ -3316,6 +3484,39 @@ mod tests { assert_eq!(config.trace_sampling_rules.get_config_id(), None); } + #[test] + fn test_local_trace_sampling_rules_bypasses_remote_override() { + let config = Config::builder() + .set_trace_sampling_rules(vec![SamplingRuleConfig { + sample_rate: 0.5, + name: Some("env_name".to_string()), + ..SamplingRuleConfig::default() + }]) + .build(); + + // With no RC override, local == public. + assert_eq!(config.local_trace_sampling_rules().len(), 1); + assert_eq!(config.trace_sampling_rules().len(), 1); + + // Set an RC override. + config + .update_sampling_rules_from_remote( + r#"[{"sample_rate":0.9,"service":"svc","provenance":"customer"}]"#, + None, + ) + .unwrap(); + + // The public accessor reflects the override; the local one still + // returns the env-side value. + assert_eq!(config.trace_sampling_rules().len(), 1); + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.9); + + let local = config.local_trace_sampling_rules(); + assert_eq!(local.len(), 1); + assert_eq!(local[0].sample_rate, 0.5); + assert_eq!(local[0].name.as_deref(), Some("env_name")); + } + #[test] fn test_telemetry_config_from_sources() { let mut sources = CompositeSource::new(); @@ -3660,4 +3861,95 @@ mod tests { assert_eq!(config.remote_config_poll_interval(), 0.2); } + + #[test] + fn test_trace_sample_rate_defaults_to_none_when_unset() { + let config = Config::builder().build(); + assert_eq!(config.trace_sample_rate(), None); + } + + #[test] + fn test_trace_sample_rate_parses_from_env() { + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [("DD_TRACE_SAMPLE_RATE", "0.25")], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources).build(); + assert_eq!(config.trace_sample_rate(), Some(0.25)); + } + + #[test] + fn test_explicit_dd_trace_sample_rate_one_installs_catch_all() { + // Codex MEDIUM fix: explicit DD_TRACE_SAMPLE_RATE=1.0 must install a + // catch-all rule so the rate limiter applies. Unset is still distinct + // (no catch-all, libdatadog's 100% fallback). + let mut builder = Config::builder(); + builder.set_trace_sample_rate(1.0); + let config = builder.build(); + + let rules = config.effective_initial_rules(); + assert_eq!(rules.len(), 1, "explicit 1.0 must install a catch-all"); + assert_eq!(rules[0].sample_rate, 1.0); + + // Unset: no catch-all. + let unset_config = Config::builder().build(); + let unset_rules = unset_config.effective_initial_rules(); + assert!(unset_rules.is_empty(), "unset must not install a catch-all"); + } + + #[test] + fn test_out_of_range_dd_trace_sample_rate_env_is_treated_as_unset() { + // Codex fix: a finite-but-out-of-range DD_TRACE_SAMPLE_RATE must be + // rejected at ingestion (treated as unset), not installed as a + // catch-all that libdd-sampling would clamp (negative -> drop all, + // >1.0 -> keep all) while still enabling the rate limiter. + for bad in ["1.5", "-0.5", "2", "inf", "-inf", "nan"] { + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [("DD_TRACE_SAMPLE_RATE", bad)], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources).build(); + assert_eq!( + config.trace_sample_rate(), + None, + "DD_TRACE_SAMPLE_RATE={bad} must be treated as unset" + ); + assert!( + config.effective_initial_rules().is_empty(), + "DD_TRACE_SAMPLE_RATE={bad} must not install a catch-all rule" + ); + } + } + + #[test] + fn test_in_range_boundary_dd_trace_sample_rate_env_accepted() { + // Boundaries 0.0 and 1.0 are valid and must be accepted. + for (val, expected) in [("0.0", 0.0), ("1.0", 1.0)] { + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [("DD_TRACE_SAMPLE_RATE", val)], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources).build(); + assert_eq!(config.trace_sample_rate(), Some(expected)); + } + } + + #[test] + fn test_out_of_range_set_trace_sample_rate_is_ignored() { + // The programmatic (code) setter must apply the same validation as the + // env path: an out-of-range rate is ignored, leaving the rate unset. + let mut builder = Config::builder(); + builder.set_trace_sample_rate(42.0); + let config = builder.build(); + assert_eq!(config.trace_sample_rate(), None); + assert!(config.effective_initial_rules().is_empty()); + + // A valid rate is still accepted. + let mut ok_builder = Config::builder(); + ok_builder.set_trace_sample_rate(0.3); + assert_eq!(ok_builder.build().trace_sample_rate(), Some(0.3)); + } } diff --git a/datadog-opentelemetry/src/core/configuration/remote_config.rs b/datadog-opentelemetry/src/core/configuration/remote_config.rs index f9b8a2b2..923cbd41 100644 --- a/datadog-opentelemetry/src/core/configuration/remote_config.rs +++ b/datadog-opentelemetry/src/core/configuration/remote_config.rs @@ -27,11 +27,19 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); // lowest timeout with struct ClientCapabilities(u64); impl ClientCapabilities { - /// APM_TRACING_SAMPLE_RULES capability bit position + /// APM_TRACING_SAMPLE_RATE — bit 12. Tells the backend the tracer + /// honors RC's `tracing_sampling_rate` (global rate). Without this, + /// Datadog's APM Sampling UI marks the service as "No remotely + /// configurable tracer detected" for rate-only configs even when + /// the tracer-side logic is fully wired up. + const APM_TRACING_SAMPLE_RATE: u64 = 1 << 12; + + /// APM_TRACING_SAMPLE_RULES — bit 29. Tells the backend the tracer + /// honors RC's `tracing_sampling_rules`. const APM_TRACING_SAMPLE_RULES: u64 = 1 << 29; fn new() -> Self { - Self(Self::APM_TRACING_SAMPLE_RULES) + Self(Self::APM_TRACING_SAMPLE_RATE | Self::APM_TRACING_SAMPLE_RULES) } /// Encode capabilities as base64 string @@ -192,6 +200,24 @@ where struct ApmTracingConfig { id: String, lib_config: LibConfig, // lib_config is a required property + /// The service/env this config targets. The backend RC predicate already + /// filters delivery by target, so this is a defense-in-depth guard: a + /// stale or mistargeted payload must never install another service's or + /// env's sampling policy on this tracer. A `*` (or absent) component + /// applies regardless of the tracer's value. Mirrors dd-trace-py/go. + #[serde(default)] + service_target: Option, +} + +/// `service_target` block of an APM_TRACING RC payload (apm-tracing.json). Both +/// fields are optional here so a malformed/partial target degrades to "applies" +/// for the missing component rather than erroring the whole update. +#[derive(Debug, Clone, Deserialize)] +struct ServiceTarget { + #[serde(default)] + service: Option, + #[serde(default)] + env: Option, } #[derive(Debug, Clone, Deserialize)] @@ -202,7 +228,17 @@ struct LibConfig { rename = "tracing_sampling_rules" )] tracing_sampling_rules: Option, - // Add other APM tracing config fields as needed (e.g., tracing_header_tags, etc.) + + /// Global trace sample rate (0.0–1.0) pushed via Remote Config. + /// `None` means the field was absent (no change intended). + /// `Some(Value::Null)` means the field was explicitly `null` (clear the override). + /// `Some(Value::Number)` means a concrete rate was provided. + #[serde( + deserialize_with = "missing_field_and_null_value", + default, + rename = "tracing_sampling_rate" + )] + tracing_sampling_rate: Option, } /// TUF targets metadata @@ -780,41 +816,163 @@ struct ApmTracingHandler; impl ProductHandler for ApmTracingHandler { fn process_config(&self, config_json: &[u8], config: &Arc) -> Result<()> { - // Parse the config to extract sampling rules as raw JSON let tracing_config: ApmTracingConfig = serde_json::from_slice(config_json) .map_err(|e| anyhow::anyhow!("Failed to parse APM tracing config: {}", e))?; - // Extract sampling rules if present - if let Some(rules_value) = tracing_config.lib_config.tracing_sampling_rules { - if !rules_value.is_null() { - // Convert the raw JSON value to string for the config method - let rules_json = serde_json::to_string(&rules_value) - .map_err(|e| anyhow::anyhow!("Failed to serialize sampling rules: {}", e))?; + // Defense-in-depth target check: only apply a config whose service_target + // matches this tracer's service/env. The backend predicate already scopes + // delivery, but a stale or mistargeted payload must never install another + // service's/env's policy. A `*` (or absent) component applies regardless. + // Mismatch => ignore (no sampler mutation), mirroring dd-trace-py. + if let Some(target) = &tracing_config.service_target { + // Only skip a config whose target is specific (non-`*`) AND does not + // apply to this tracer. Service matches the primary or any advertised + // extra service; both service and env are compared case-insensitively + // (the UI warns service-name case can differ). Being lenient here is + // deliberate: the guard must skip only configs that are definitely + // for another service/env, never a valid one for us. + if let Some(svc) = target.service.as_deref() { + if svc != "*" && !config.rc_service_target_matches(svc) { + crate::dd_debug!( + "RemoteConfigClient: ignoring APM_TRACING config targeting service {:?} (not this tracer's service or extra services)", + svc + ); + return Ok(()); + } + } + if let Some(target_env) = target.env.as_deref() { + let tracer_env = config.env().unwrap_or(""); + if target_env != "*" && !target_env.eq_ignore_ascii_case(tracer_env) { + crate::dd_debug!( + "RemoteConfigClient: ignoring APM_TRACING config targeting env {:?} (tracer env is {:?})", + target_env, + tracer_env + ); + return Ok(()); + } + } + } - match config.update_sampling_rules_from_remote(&rules_json, Some(tracing_config.id)) - { - Ok(()) => { - crate::dd_debug!( - "RemoteConfigClient: Applied sampling rules from remote config" - ); + let lib = tracing_config.lib_config; + + let any_field_present = + lib.tracing_sampling_rules.is_some() || lib.tracing_sampling_rate.is_some(); + + // tracing_sampling_rate must be either null (clear) or a JSON number. + // Any other present-but-non-numeric value (string, bool, object) is a + // malformed payload — reject rather than silently treating it as a + // clear, which would wipe an active remote sampling policy. + let rate: Option = match &lib.tracing_sampling_rate { + None | Some(serde_json::Value::Null) => None, + Some(serde_json::Value::Number(n)) => match n.as_f64() { + Some(r) if r.is_finite() && (0.0..=1.0).contains(&r) => Some(r), + Some(r) => { + return Err(anyhow::anyhow!( + "tracing_sampling_rate must be in [0.0, 1.0], got: {}", + r + )); + } + None => { + return Err(anyhow::anyhow!( + "tracing_sampling_rate is not representable as f64" + )); + } + }, + Some(other) => { + return Err(anyhow::anyhow!( + "tracing_sampling_rate must be a JSON number or null, got: {}", + other + )); + } + }; + let rules_value = match lib.tracing_sampling_rules { + Some(v) if !v.is_null() => Some(v), + _ => None, + }; + + match (rules_value, rate) { + (None, None) => { + if any_field_present { + crate::dd_debug!( + "RemoteConfigClient: APM tracing config received with null sampling fields, clearing remote override" + ); + config.clear_remote_sampling_rules(Some(tracing_config.id)); + } else { + crate::dd_debug!( + "RemoteConfigClient: APM tracing config received but no tracing_sampling_rules or tracing_sampling_rate present" + ); + } + } + (rules_opt, rate_opt) => { + // An explicit empty `tracing_sampling_rules: []` is treated the + // same as null/absent: RC has no rules to deliver, so env-side + // rules survive. Operators clear remote rules by sending + // `tracing_sampling_rules: null` (the conventional RC clear); + // an empty array is an unusual edge case and the lenient + // interpretation is safer than wiping env config silently. + let rc_has_explicit_rules = matches!( + rules_opt, + Some(serde_json::Value::Array(ref arr)) if !arr.is_empty() + ); + + let mut rules: Vec = match rules_opt { + Some(serde_json::Value::Array(arr)) => arr, + Some(other) => { + return Err(anyhow::anyhow!( + "tracing_sampling_rules must be a JSON array, got: {}", + other + )); } - Err(e) => { - crate::dd_debug!( - "RemoteConfigClient: Failed to update sampling rules: {}", - e - ); + None => Vec::new(), + }; + + // Multi-source precedence: + // - If RC delivered explicit rules, env rules are replaced. + // - If RC delivered only a rate, env rules survive and apply in front of the + // synthetic catch-all. + if !rc_has_explicit_rules { + let env_rules = config.local_trace_sampling_rules(); + if !env_rules.is_empty() { + let env_json = serde_json::to_value(&*env_rules).map_err(|e| { + anyhow::anyhow!("Failed to serialize env sampling rules: {}", e) + })?; + let serde_json::Value::Array(env_arr) = env_json else { + return Err(anyhow::anyhow!( + "BUG: serialized env sampling rules are not a JSON array" + )); + }; + let mut composed = env_arr; + composed.append(&mut rules); + rules = composed; } } - } else { - crate::dd_debug!( - "RemoteConfigClient: APM tracing config received but tracing_sampling_rules is null" - ); - config.clear_remote_sampling_rules(Some(tracing_config.id)); + + // Effective catch-all rate: RC rate wins; otherwise fall back + // to DD_TRACE_SAMPLE_RATE if it's set (Option distinguishes + // unset from explicit 1.0). + let env_rate = config.trace_sample_rate(); + let catch_all_rate: Option = match rate_opt { + Some(r) => Some(r), + None => env_rate.filter(|r| r.is_finite()), + }; + if let Some(r) = catch_all_rate { + // The global RC rate is a "local-user-like" fallback: it must + // produce DM "-3" (LOCAL_USER), not "-12" (REMOTE_DYNAMIC). + // Omit `provenance`; libdd-sampling deserializes it as + // "default" via its serde default, which maps to DM -3. + rules.push(serde_json::json!({ "sample_rate": r })); + } + + let rules_json = serde_json::to_string(&serde_json::Value::Array(rules)) + .map_err(|e| anyhow::anyhow!("Failed to serialize sampling rules: {}", e))?; + + config + .update_sampling_rules_from_remote(&rules_json, Some(tracing_config.id)) + .map_err(|e| { + anyhow::anyhow!("Failed to update sampling rules from remote: {}", e) + })?; + crate::dd_debug!("RemoteConfigClient: Applied sampling rules from remote config"); } - } else { - crate::dd_debug!( - "RemoteConfigClient: APM tracing config received but no tracing_sampling_rules present" - ); } Ok(()) @@ -887,10 +1045,15 @@ fn extract_product_and_id_from_path(path: &str) -> Option<(String, String)> { #[cfg(test)] mod tests { use super::*; + use crate::core::configuration::SamplingRuleConfig; use pretty_assertions::assert_eq; use proptest::prelude::*; use test_case::test_case; + fn build_config_for_handler() -> Arc { + Arc::new(Config::builder().build()) + } + #[test] fn test_client_capabilities() { let caps = ClientCapabilities::new(); @@ -910,8 +1073,17 @@ mod tests { bytes[offset..].copy_from_slice(&decoded); let value = u64::from_be_bytes(bytes); - // Verify the capability bit is set - assert_eq!(value, ClientCapabilities::APM_TRACING_SAMPLE_RULES); + // Both APM_TRACING_SAMPLE_RATE (bit 12) and APM_TRACING_SAMPLE_RULES + // (bit 29) must be advertised; the backend uses each independently to + // decide which RC config types it will offer in the Datadog UI for + // this service. + let expected = ClientCapabilities::APM_TRACING_SAMPLE_RATE + | ClientCapabilities::APM_TRACING_SAMPLE_RULES; + assert_eq!(value, expected); + assert_eq!( + value & ClientCapabilities::APM_TRACING_SAMPLE_RATE, + ClientCapabilities::APM_TRACING_SAMPLE_RATE + ); assert_eq!( value & ClientCapabilities::APM_TRACING_SAMPLE_RULES, ClientCapabilities::APM_TRACING_SAMPLE_RULES @@ -2121,4 +2293,566 @@ mod tests { assert!(tracing_config.lib_config.tracing_sampling_rules.is_none()); } + + #[test] + fn test_deserialize_tracing_sampling_rate_concrete() { + let config_json = r#"{"id": "42", "lib_config": {"tracing_sampling_rate": 0.25}}"#; + let tracing_config: ApmTracingConfig = serde_json::from_str(config_json).unwrap(); + assert_eq!( + tracing_config.lib_config.tracing_sampling_rate, + Some(serde_json::json!(0.25)) + ); + } + + #[test] + fn test_deserialize_tracing_sampling_rate_null() { + let config_json = r#"{"id": "42", "lib_config": {"tracing_sampling_rate": null}}"#; + let tracing_config: ApmTracingConfig = serde_json::from_str(config_json).unwrap(); + assert_eq!( + tracing_config.lib_config.tracing_sampling_rate, + Some(serde_json::Value::Null) + ); + } + + #[test] + fn test_deserialize_tracing_sampling_rate_missing() { + // Field absent entirely. + let config_json = r#"{"id": "42", "lib_config": {}}"#; + let tracing_config: ApmTracingConfig = serde_json::from_str(config_json).unwrap(); + assert!(tracing_config.lib_config.tracing_sampling_rate.is_none()); + } + + #[test] + fn test_handler_applies_only_rate_as_wildcard_rule() { + // RC sends only tracing_sampling_rate -> handler installs a single + // wildcard rule with the libdd default provenance ("default", DM -3). + let config = build_config_for_handler(); + let payload = br#"{ + "id": "rc-rate-only", + "lib_config": {"tracing_sampling_rate": 0.25} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!( + rules.len(), + 1, + "expected exactly one synthesized wildcard rule" + ); + assert_eq!(rules[0].sample_rate, 0.25); + assert!(rules[0].service.is_none()); + assert!(rules[0].name.is_none()); + assert!(rules[0].resource.is_none()); + assert!(rules[0].tags.is_empty()); + } + + #[test] + fn test_handler_synthetic_rate_rule_uses_default_provenance() { + // The synthetic catch-all built from RC's tracing_sampling_rate must + // produce DM "-3" (LOCAL_USER), not "-12" (REMOTE_DYNAMIC). Assert via + // the callback path: libdatadog converts the JSON to its internal + // SamplingRuleConfig, whose `provenance` must be the libdd default + // ("default"), not "dynamic". + use crate::core::configuration::RemoteConfigUpdate; + let config = build_config_for_handler(); + let received = Arc::new(Mutex::new(Vec::::new())); + let clone = received.clone(); + config.set_sampling_rules_callback(move |update| { + let RemoteConfigUpdate::SamplingRules(rules) = update; + *clone.lock().unwrap() = rules.clone(); + }); + + let payload = br#"{ + "id": "rc-rate-only-provenance", + "lib_config": {"tracing_sampling_rate": 0.25} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + + let got = received.lock().unwrap(); + // The callback receives the full composed chain. The catch-all is the + // last entry; it must carry the libdd default provenance ("default"). + let catch_all = got.last().expect("expected at least one rule"); + assert_eq!(catch_all.sample_rate, 0.25); + assert_eq!( + catch_all.provenance, "default", + "synthetic catch-all must use default provenance" + ); + } + + #[test] + fn test_handler_appends_rate_after_rules() { + // RC sends both rules and a rate -> rate becomes the last (wildcard) rule. + let config = build_config_for_handler(); + let payload = br#"{ + "id": "rc-both", + "lib_config": { + "tracing_sampling_rate": 0.1, + "tracing_sampling_rules": [ + {"sample_rate": 0.9, "service": "auth"} + ] + } + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!(rules.len(), 2); + assert_eq!(rules[0].sample_rate, 0.9); + assert_eq!(rules[0].service.as_deref(), Some("auth")); + assert_eq!(rules[1].sample_rate, 0.1); + assert!(rules[1].service.is_none()); + assert!(rules[1].tags.is_empty()); + } + + #[test] + fn test_handler_null_fields_clear_prior_override() { + // 1. Install rules via RC. + let config = build_config_for_handler(); + let install = br#"{ + "id": "rc-install", + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(install, &config).unwrap(); + assert_eq!(config.trace_sampling_rules().len(), 1); + + // 2. Send explicit null for both fields -> override cleared. + let clear = br#"{ + "id": "rc-clear", + "lib_config": { + "tracing_sampling_rate": null, + "tracing_sampling_rules": null + } + }"#; + ApmTracingHandler.process_config(clear, &config).unwrap(); + // After clearing, trace_sampling_rules() returns the local-config default + // (empty unless DD_TRACE_SAMPLING_RULES is set in the test environment). + assert_eq!(config.trace_sampling_rules().len(), 0); + } + + #[test] + fn test_handler_null_rate_only_clears_prior_override() { + // Explicit null on tracing_sampling_rate (with tracing_sampling_rules absent) + // must clear a prior remote override. + let config = build_config_for_handler(); + let install = br#"{ + "id": "rc-install", + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(install, &config).unwrap(); + assert_eq!(config.trace_sampling_rules().len(), 1); + + let clear = br#"{ + "id": "rc-clear", + "lib_config": {"tracing_sampling_rate": null} + }"#; + ApmTracingHandler.process_config(clear, &config).unwrap(); + assert_eq!(config.trace_sampling_rules().len(), 0); + } + + #[test] + fn test_handler_rc_rules_with_list_tags_applied() { + // RC sends tags as a list-of-objects ([{key, value_glob}]); libdd-sampling + // (>=2.1.0) parses that wire shape natively, so no in-tracer normalization + // is needed. Regression guard: a list-shape tagged rule must apply with its + // tags preserved as a map. + let config = build_config_for_handler(); + let payload = br#"{ + "id": "rc-list-tags", + "lib_config": { + "tracing_sampling_rules": [ + { + "sample_rate": 0.5, + "service": "svc", + "tags": [ + {"key": "env", "value_glob": "prod"}, + {"key": "region", "value_glob": "us-east-1"} + ] + } + ] + } + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!(rules.len(), 1); + assert_eq!(rules[0].sample_rate, 0.5); + assert_eq!(rules[0].service.as_deref(), Some("svc")); + assert_eq!(rules[0].tags.get("env").map(String::as_str), Some("prod")); + assert_eq!( + rules[0].tags.get("region").map(String::as_str), + Some("us-east-1") + ); + } + + #[test] + fn test_handler_malformed_tags_rejects_update() { + // Bug B fail-closed guard: a sampling rule with malformed list-shape + // tags must not be installed in a broadened form. With the prior + // override of sample_rate=0.5 in place, sending a rule with one bad + // tag entry must leave the prior override intact. + let config = build_config_for_handler(); + // 1. Install a working override. + let install = br#"{ + "id": "rc-install", + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(install, &config).unwrap(); + assert_eq!(config.trace_sampling_rules().len(), 1); + + // 2. Send a rule with a malformed tag entry. + let bad = br#"{ + "id": "rc-bad-tags", + "lib_config": { + "tracing_sampling_rules": [ + { + "sample_rate": 0.0, + "service": "svc", + "tags": [ + {"key": "env", "value_glob": "prod"}, + {"key": "region"} + ] + } + ] + } + }"#; + // The libdatadog parse rejects list-shape tags, so process_config + // returns Err (post-Codex HIGH fix). The key invariant is that the + // prior remote override is not overwritten or cleared. + let result = ApmTracingHandler.process_config(bad, &config); + assert!(result.is_err(), "malformed tags must propagate as Err"); + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!(rules.len(), 1, "prior override must remain installed"); + assert_eq!(rules[0].sample_rate, 0.5); + } + + #[test] + fn test_handler_malformed_tags_returns_error_to_dispatcher() { + // After the fix for the Codex HIGH finding: when libdatadog rejects the + // composed JSON (e.g., because the synthetic catch-all rule's tags were + // left in list-shape due to a malformed entry), process_config returns + // Err so the RC dispatcher records apply_state=3 and the bad target is + // not cached. + let config = build_config_for_handler(); + let payload = br#"{ + "id": "rc-bad-tags", + "lib_config": { + "tracing_sampling_rules": [ + { + "sample_rate": 0.5, + "service": "svc", + "tags": [ + {"key": "env", "value_glob": "prod"}, + {"key": "region"} + ] + } + ] + } + }"#; + let result = ApmTracingHandler.process_config(payload, &config); + assert!(result.is_err(), "malformed tags must propagate as Err"); + } + + #[test] + fn test_handler_rejects_negative_rate() { + let config = build_config_for_handler(); + let payload = br#"{ + "id": "rc-neg-rate", + "lib_config": {"tracing_sampling_rate": -0.1} + }"#; + let result = ApmTracingHandler.process_config(payload, &config); + assert!(result.is_err(), "negative rate must be rejected"); + } + + #[test] + fn test_handler_rejects_rate_above_one() { + let config = build_config_for_handler(); + let payload = br#"{ + "id": "rc-high-rate", + "lib_config": {"tracing_sampling_rate": 1.5} + }"#; + let result = ApmTracingHandler.process_config(payload, &config); + assert!(result.is_err(), "rate > 1.0 must be rejected"); + } + + #[test] + fn test_handler_non_numeric_rate_rejects_update() { + // A schema-drifted rate (e.g. string) must be rejected as a malformed + // payload, not silently treated as a clear that would wipe an active + // remote override. + let config = build_config_for_handler(); + let install = br#"{ + "id": "rc-install", + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(install, &config).unwrap(); + assert_eq!(config.trace_sampling_rules().len(), 1); + + let bad = br#"{ + "id": "rc-bad-rate", + "lib_config": {"tracing_sampling_rate": "0.5"} + }"#; + let result = ApmTracingHandler.process_config(bad, &config); + assert!(result.is_err(), "non-numeric rate must be rejected"); + // Prior override survives. + assert_eq!(config.trace_sampling_rules().len(), 1); + assert_eq!(config.trace_sampling_rules()[0].sample_rate, 0.5); + } + + #[test] + fn test_handler_rate_only_preserves_env_rules() { + // When RC delivers only tracing_sampling_rate (no rules), env-configured + // sampling rules must still apply for matching spans. Composed chain: + // [env_rules..., catch_all(rc_rate)]. + let env_rule = SamplingRuleConfig { + sample_rate: 0.55, + name: Some("env_name".to_string()), + ..SamplingRuleConfig::default() + }; + let config = Arc::new( + Config::builder() + .set_trace_sampling_rules(vec![env_rule.clone()]) + .build(), + ); + + let payload = br#"{ + "id": "rc-rate-only-with-env-rules", + "lib_config": {"tracing_sampling_rate": 0.70} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!(rules.len(), 2, "expected env rule + synthetic catch-all"); + assert_eq!(rules[0].sample_rate, 0.55); + assert_eq!(rules[0].name.as_deref(), Some("env_name")); + assert_eq!(rules[1].sample_rate, 0.70); + assert!(rules[1].name.is_none()); + assert!(rules[1].service.is_none()); + assert!(rules[1].resource.is_none()); + assert!(rules[1].tags.is_empty()); + } + + #[test] + fn test_handler_rc_rules_replace_env_rules() { + // Contract: when RC delivers tracing_sampling_rules (with or without a + // rate), env rules are fully replaced. RC rules + catch-all(rc_rate). + let env_rule = SamplingRuleConfig { + sample_rate: 0.55, + name: Some("env_name".to_string()), + ..SamplingRuleConfig::default() + }; + let config = Arc::new( + Config::builder() + .set_trace_sampling_rules(vec![env_rule.clone()]) + .build(), + ); + + let payload = br#"{ + "id": "rc-rules-replace-env", + "lib_config": { + "tracing_sampling_rate": 0.9, + "tracing_sampling_rules": [ + {"sample_rate": 0.8, "service": "svc", "provenance": "customer"} + ] + } + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!( + rules.len(), + 2, + "env rule must be excluded when RC has rules" + ); + assert_eq!(rules[0].sample_rate, 0.8); + assert_eq!(rules[0].service.as_deref(), Some("svc")); + assert_eq!(rules[1].sample_rate, 0.9); + assert!(rules[1].service.is_none()); + assert!(rules.iter().all(|r| r.name.as_deref() != Some("env_name"))); + } + + #[test] + fn test_handler_rc_rules_only_falls_back_to_env_rate_catch_all() { + // RC delivers rules without a rate; DD_TRACE_SAMPLE_RATE is set. The + // composed chain should be [rc_rules..., catch_all(env_rate)] so + // unmatched spans still get sampled at env_rate. + let mut builder = Config::builder(); + builder.set_trace_sample_rate(0.1); + let config = Arc::new(builder.build()); + + let payload = br#"{ + "id": "rc-rules-only-with-env-rate", + "lib_config": { + "tracing_sampling_rules": [ + {"sample_rate": 0.8, "service": "svc", "provenance": "customer"} + ] + } + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!(rules.len(), 2, "expected rc rule + env-rate catch-all"); + assert_eq!(rules[0].sample_rate, 0.8); + assert_eq!(rules[0].service.as_deref(), Some("svc")); + assert_eq!(rules[1].sample_rate, 0.1); + assert!(rules[1].service.is_none()); + } + + #[test] + fn test_handler_empty_rc_rules_array_preserves_env_rules() { + // Contract: an explicit empty `tracing_sampling_rules: []` is treated as + // "RC has no rules" — env rules survive. Operators clear RC rules by + // sending `tracing_sampling_rules: null`. This test locks that behavior. + let env_rule = SamplingRuleConfig { + sample_rate: 0.55, + name: Some("env_name".to_string()), + ..SamplingRuleConfig::default() + }; + let config = Arc::new( + Config::builder() + .set_trace_sampling_rules(vec![env_rule.clone()]) + .build(), + ); + + let payload = br#"{ + "id": "rc-empty-rules", + "lib_config": { + "tracing_sampling_rate": 0.70, + "tracing_sampling_rules": [] + } + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + + let rules = config.trace_sampling_rules().to_vec(); + assert_eq!(rules.len(), 2, "expected env rule + synthetic catch-all"); + assert_eq!(rules[0].sample_rate, 0.55); + assert_eq!(rules[0].name.as_deref(), Some("env_name")); + assert_eq!(rules[1].sample_rate, 0.70); + assert!(rules[1].name.is_none()); + } + + fn build_config_for_handler_with_target(service: &str, env: &str) -> Arc { + let mut builder = Config::builder(); + builder.set_service(service.to_string()); + builder.set_env(env.to_string()); + Arc::new(builder.build()) + } + + #[test] + fn test_handler_service_target_match_applies() { + // A config whose service_target matches the tracer's service/env applies. + let config = build_config_for_handler_with_target("svc-a", "env-a"); + let payload = br#"{ + "id": "rc-target-match", + "service_target": {"service": "svc-a", "env": "env-a"}, + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + assert_eq!( + config.trace_sampling_rules().len(), + 1, + "matching service_target must apply" + ); + } + + #[test] + fn test_handler_service_target_service_mismatch_ignored() { + // Codex fix: a config targeting a DIFFERENT service must never mutate + // this tracer's sampler state. + let config = build_config_for_handler_with_target("svc-a", "env-a"); + let payload = br#"{ + "id": "rc-other-svc", + "service_target": {"service": "svc-b", "env": "env-a"}, + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + assert_eq!( + config.trace_sampling_rules().len(), + 0, + "config for another service must be ignored" + ); + } + + #[test] + fn test_handler_service_target_env_mismatch_ignored() { + let config = build_config_for_handler_with_target("svc-a", "env-a"); + let payload = br#"{ + "id": "rc-other-env", + "service_target": {"service": "svc-a", "env": "env-b"}, + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + assert_eq!( + config.trace_sampling_rules().len(), + 0, + "config for another env must be ignored" + ); + } + + #[test] + fn test_handler_service_target_wildcard_applies() { + // A wildcard ("*") target applies regardless of the tracer's service/env. + let config = build_config_for_handler_with_target("svc-a", "env-a"); + let payload = br#"{ + "id": "rc-wildcard", + "service_target": {"service": "*", "env": "*"}, + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + assert_eq!( + config.trace_sampling_rules().len(), + 1, + "wildcard service_target must apply" + ); + } + + #[test] + fn test_handler_absent_service_target_applies() { + // Absent service_target (e.g. older payloads) must still apply — the + // target check only gates when a specific, non-wildcard target is set. + let config = build_config_for_handler_with_target("svc-a", "env-a"); + let payload = br#"{ + "id": "rc-no-target", + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + assert_eq!( + config.trace_sampling_rules().len(), + 1, + "payload without service_target must apply" + ); + } + + #[test] + fn test_handler_service_target_case_insensitive_applies() { + // service/env case can differ from what the tracer reports (the UI warns + // about this); a case-only difference must still apply, not be skipped. + let config = build_config_for_handler_with_target("svc-a", "env-a"); + let payload = br#"{ + "id": "rc-case", + "service_target": {"service": "SVC-A", "env": "ENV-A"}, + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + assert_eq!( + config.trace_sampling_rules().len(), + 1, + "case-only service/env difference must still apply" + ); + } + + #[test] + fn test_handler_service_target_extra_service_applies() { + // A config targeting an advertised extra service is legitimately ours and + // must apply (the tracer reports extra_services to the backend, which can + // deliver a config scoped to one of them). + let config = build_config_for_handler_with_target("svc-a", "env-a"); + config.add_extra_services(["svc-extra"].into_iter()); + let payload = br#"{ + "id": "rc-extra", + "service_target": {"service": "svc-extra", "env": "*"}, + "lib_config": {"tracing_sampling_rate": 0.5} + }"#; + ApmTracingHandler.process_config(payload, &config).unwrap(); + assert_eq!( + config.trace_sampling_rules().len(), + 1, + "config for an advertised extra service must apply" + ); + } } diff --git a/datadog-opentelemetry/src/core/configuration/supported_configurations.rs b/datadog-opentelemetry/src/core/configuration/supported_configurations.rs index 36cec636..eb99b4cc 100644 --- a/datadog-opentelemetry/src/core/configuration/supported_configurations.rs +++ b/datadog-opentelemetry/src/core/configuration/supported_configurations.rs @@ -34,6 +34,7 @@ pub(crate) enum SupportedConfigurations { DD_TRACE_PROPAGATION_STYLE_EXTRACT, DD_TRACE_PROPAGATION_STYLE_INJECT, DD_TRACE_RATE_LIMIT, + DD_TRACE_SAMPLE_RATE, DD_TRACE_SAMPLING_RULES, DD_TRACE_STATS_COMPUTATION_ENABLED, DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, @@ -121,6 +122,7 @@ impl SupportedConfigurations { "DD_TRACE_PROPAGATION_STYLE_INJECT" } SupportedConfigurations::DD_TRACE_RATE_LIMIT => "DD_TRACE_RATE_LIMIT", + SupportedConfigurations::DD_TRACE_SAMPLE_RATE => "DD_TRACE_SAMPLE_RATE", SupportedConfigurations::DD_TRACE_SAMPLING_RULES => "DD_TRACE_SAMPLING_RULES", SupportedConfigurations::DD_TRACE_STATS_COMPUTATION_ENABLED => { "DD_TRACE_STATS_COMPUTATION_ENABLED" diff --git a/datadog-opentelemetry/src/propagation/baggage.rs b/datadog-opentelemetry/src/propagation/baggage.rs index 99a5d8fb..fa533c15 100644 --- a/datadog-opentelemetry/src/propagation/baggage.rs +++ b/datadog-opentelemetry/src/propagation/baggage.rs @@ -228,7 +228,7 @@ mod tests { assert_eq!(baggage.len(), expected_keys.len(), "header: {header:?}"); for key in expected_keys { assert!( - baggage.get(&Key::new(key)).is_some(), + baggage.get(Key::new(key)).is_some(), "missing key {key} in {header:?}" ); } diff --git a/datadog-opentelemetry/src/sampler.rs b/datadog-opentelemetry/src/sampler.rs index 00c6a69b..ad940258 100644 --- a/datadog-opentelemetry/src/sampler.rs +++ b/datadog-opentelemetry/src/sampler.rs @@ -45,13 +45,7 @@ impl Sampler { // This is an Option to allow benchmarking different parts of sampling trace_registry: Option, ) -> Self { - let internal_configs: Vec = cfg - .trace_sampling_rules() - .iter() - .cloned() - .map(Into::into) - .collect(); - let rules = SamplingRule::from_configs(internal_configs); + let rules = SamplingRule::from_configs(cfg.effective_initial_rules()); let sampler = DatadogSampler::new(rules, cfg.trace_rate_limit()); Self { cfg, diff --git a/instrumentation/Cargo.lock b/instrumentation/Cargo.lock index 75ef16f4..cd57b7fd 100644 --- a/instrumentation/Cargo.lock +++ b/instrumentation/Cargo.lock @@ -244,22 +244,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -777,22 +761,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "tokio", - "tokio-rustls", - "tower-service", -] - [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1113,9 +1081,9 @@ dependencies = [ [[package]] name = "libdd-common" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "933c5940905e9a3c8dd8c0439697b3fb327289cef7858370e6a5f440139f6314" +checksum = "4af81e6953fab2814792b8893a2cee9f16aad1b71cb3f8720b7256f28d6a2d8d" dependencies = [ "anyhow", "bytes", @@ -1129,19 +1097,15 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", "libc", "nix", "pin-project", "regex", - "rustls", - "rustls-native-certs", "serde", "static_assertions", "thiserror 1.0.69", "tokio", - "tokio-rustls", "tower-service", "windows-sys 0.52.0", ] @@ -1224,9 +1188,9 @@ dependencies = [ [[package]] name = "libdd-sampling" -version = "1.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ae8728d9d717eb1b648223c4c1da53f77627fecd6045027ccb758a72815b300" +checksum = "df229b55274db81b8a85a1b9ab14663af070358f39017ab68d26a3aada535db6" dependencies = [ "libdd-common", "lru", @@ -1480,12 +1444,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "opentelemetry" version = "0.31.0" @@ -1869,20 +1827,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rmp" version = "0.8.15" @@ -1943,52 +1887,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -2010,38 +1908,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.28" @@ -2198,12 +2064,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.117" @@ -2340,16 +2200,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.18" @@ -2566,12 +2416,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.8" @@ -3091,12 +2935,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.4" diff --git a/supported-configurations.json b/supported-configurations.json index 27a0d4ea..d6e65133 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -211,6 +211,14 @@ "propertyKeys": ["trace_rate_limit"] } ], + "DD_TRACE_SAMPLE_RATE": [ + { + "version": "B", + "type": "decimal", + "default": null, + "propertyKeys": ["trace_sample_rate"] + } + ], "DD_TRACE_SAMPLING_RULES": [ { "version": "A",