)
# What does this PR do?
Fixes adaptive-sampling Remote Config in `datadog-opentelemetry`, rebuilt on top of PR #154. Also adds `DD_TRACE_SAMPLE_RATE` support.
Before this PR, four things were broken:
1. **`tracing_sampling_rate` from RC was ignored.** The handler only acted on `tracing_sampling_rules`; a rate-only payload installed nothing.
2. **RC list-shape `tags` were rejected.** RC encodes `tags` as `[{"key": "env", "value_glob": "prod"}]`, but `libdd_sampling::SamplingRuleConfig::tags` only accepted the map shape, so the parse errored and the whole update was dropped.
3. **Env `DD_TRACE_SAMPLING_RULES` were wiped on every RC update.** `update_sampling_rules_from_remote` does a full override, so any RC change replaced env rules, even when RC only sent a global rate.
4. **`DD_TRACE_SAMPLE_RATE` had no effect.** No binding existed.
# Motivation
End-to-end adaptive sampling didn't work.
# What changed
**Composition.** `ApmTracingHandler::process_config` now follows the multi-source precedence model:
| env rules | env `DD_TRACE_SAMPLE_RATE` | RC `tracing_sampling_rules` | RC `tracing_sampling_rate` | Effective rule chain |
|---|---|---|---|---|
| present | any | absent / null | absent / null | `env_rules` |
| present | unset | absent / null | present | `env_rules + catch_all(rc_rate)` |
| present | set | absent / null | absent / null | `env_rules + catch_all(env_rate)` |
| present | set | absent / null | present | `env_rules + catch_all(rc_rate)` |
| any | any | non-empty array | absent / null | `rc_rules + catch_all(env_rate)` if env_rate set |
| any | any | non-empty array | present | `rc_rules + catch_all(rc_rate)` |
The synthetic catch-all uses libdatadog's default provenance, mapping to DM `-3` (LOCAL_USER). See the "legacy behavior" comment in [test_trace_sampling_rules_override_rate](https://github.com/DataDog/system-tests/blob/main/tests/parametric/test_dynamic_configuration.py#L872).
**`DD_TRACE_SAMPLE_RATE`.** New `Config::trace_sample_rate(): Option<f64>`. When set, the sampler installs an implicit catch-all so `DD_TRACE_RATE_LIMIT` applies. Unset means no catch-all (libdatadog's no-rule path samples at 100%).
**Tag normalization.** RC encodes `tags` as the list shape `[{"key", "value_glob"}]`. This is parsed natively by `libdd-sampling` ≥ 2.1.0 (DataDog/libdatadog#2033), so this PR bumps `libdd-sampling` 1.0.0 → 2.1.0 (pulling `libdd-common` → 4.2.0) and no in-tracer normalization is needed. An earlier revision carried a `normalize_rc_tags` shim for this; it has been removed now that the upstream release is available. Regression coverage: `test_handler_rc_rules_with_list_tags_applied` (list-shape tags apply, tags preserved as a map) and `test_handler_malformed_tags_rejects_update` (malformed list entries still rejected wholesale, not silently broadened).
**Fail-closed behavior.** When libdatadog rejects an update (malformed tags, out-of-range rate), `process_config` returns `Err` so the RC dispatcher reports `apply_state=3` and the prior policy survives. Out-of-range RC `tracing_sampling_rate` (outside `[0.0, 1.0]`) and non-numeric values are rejected up-front.
**Env/code rate validation.** `DD_TRACE_SAMPLE_RATE` (and the programmatic `set_trace_sample_rate`) get the same range check: only finite values in `[0.0, 1.0]` are honored; out-of-range values are logged and treated as unset rather than installed as a catch-all rule that libdatadog would clamp (negative ⇒ drop all, > 1.0 ⇒ keep all).
**Target check.** A config's `service_target` is honored before applying: a payload whose specific (non-`*`) `service`/`env` doesn't match this tracer — primary service or an advertised extra service, compared case-insensitively — is ignored, so a mistargeted RC delivery can never change this service's sampling. Mirrors dd-trace-py/go.
# Additional Notes
- Four parametric tests unblocked by this PR. Companion PR DataDog/system-tests#7007.
- Coordinated with @iunanua's PR #222 (libdatadog RC client wiring). #227 lands first; #222 rebases on top.
Co-authored-by: brian.marks <brian.marks@datadoghq.com>
What does this PR do?
Makes
libdd_sampling::SamplingRuleConfig::tagsaccept two wire shapes:{"env": "prod"}(current).[{"key": "env", "value_glob": "prod"}](new).Internally both normalize to
HashMap<String, String>. List entries missingkeyorvalue_globproduce a deserialization error. We don't silently drop bad entries because doing so could broaden a tag-constrained sampling rule.Motivation
Remote Config delivers
tracing_sampling_rulesentries with tags in the list shape. The previous#[serde(default)]ontagsonly accepted the map shape, so every tracer adoptinglibdd-samplinghas to normalize at the tracer ↔ libdatadog boundary. dd-trace-rs ships anormalize_rc_tagshelper for this. With this change, that workaround is no longer needed in any tracer.Additional Notes
tagsfield type staysHashMap<String, String>; only the deserializer is broader.serde::Deserializer::deserialize_any, which is safe for self-describing formats (JSON). Non-self-describing formats (bincode/postcard) would fail at deserialization time with a clear serde error.normalize_rc_tagsworkaround once this lands in a libdatadog release.How to test the change?
Four unit tests in
libdd-sampling/src/sampling_rule_config.rs:test_sampling_rule_config_tags_accepts_map_shape(regression).test_sampling_rule_config_tags_accepts_rc_list_shape.test_sampling_rule_config_tags_list_with_malformed_entry_rejects.test_sampling_rule_config_tags_absent_defaults_to_empty.Run:
cargo test -p libdd-sampling sampling_rule_config_tags.Integration verified locally against DataDog/dd-trace-rs#227: pointing dd-trace-rs at this branch and removing its
normalize_rc_tagsworkaround keeps all 352 dd-trace-rs tests green, including the handler-level tag tests that now traverse the upstream deserializer end-to-end.