Skip to content

Commit 401e623

Browse files
authored
refactor(remote-config)!: DI via RemoteConfigParsedData trait + ParserRegistry (#1945)
# What does this PR do? The `datadog-remote-config` crate previously owned the full deserialization pipeline for every product via a closed `RemoteConfigData` enum. Adding a new product required editing the RC crate itself, importing product types, and adding enum variants — creating compile-time coupling to `datadog-live-debugger` and `datadog-ffe` via Cargo optional features. This PR flips the dependency: RC defines a typed `RemoteConfigContent` trait (with a `PRODUCT` const and a `parse` method), an opaque `RemoteConfigParsedData` trait-object boundary, and a `ParserRegistry` to compose products. Product crates implement `RemoteConfigContent` on their own types; a blanket `impl<T: RemoteConfigContent> RemoteConfigParsedData for T` lets the registry hold them as `Box<dyn RemoteConfigParsedData>` without knowing the concrete type. ## Follow-up changes on this branch - **Sidecar LiveDebugger parser registration.** Pre-refactor, `datadog-sidecar` enabled the `live-debugger` Cargo feature on `datadog-remote-config`, which made the closed-enum parser handle LiveDebugger payloads. After the DI refactor that feature is gone, so `RemoteConfigManager::new()` now explicitly registers the `LiveDebuggingData` parser on top of `default_registry()` to preserve pre-refactor sidecar behavior. Added a regression test (`test_live_debugger_config_parsed`) that drives a `SERVICE_CONFIGURATION` payload through the SHM round trip and asserts the downcast to `LiveDebuggingData` succeeds. - **`set_extra_services` on the fetchers.** Runtime-discovered extra services live on `ConfigClientState` (the per-client mutable state), not on `Target`. New API: `ConfigClientState::set_extra_services(Vec<String>)`, `SingleFetcher::set_extra_services`, `SingleChangesFetcher::set_extra_services`. `fetch_once` reads from `opaque_state` and forwards via `ClientTracer.extra_services` exactly as before. Replace-semantics: each set fully overrides the previous list. This was kept off `Target` because `Target` is `Hash + Eq + Clone` (used as a map key in `MultiTargetFetcher`) — putting growing dynamic state on it would force costly map rekeying or break the `Hash`/`Eq` contract. ## Adding a new RC product going forward 1. **In `datadog-remote-config`** (still required): add a variant to `RemoteConfigProduct` and its `Display`/`try_parse` arms in `path.rs`. The product taxonomy is still a closed enum. 2. **In the product crate**: implement `RemoteConfigContent` on the product's config type: ```rust impl RemoteConfigContent for MyProductConfig { const PRODUCT: RemoteConfigProduct = RemoteConfigProduct::MyProduct; fn parse(data: &[u8]) -> anyhow::Result<Self> { /* … */ } } ``` 3. **At construction time**: chain `.with::<MyProductConfig>()` onto a `ParserRegistry` (typically `default_registry()`). Only the parsing is fully extensible after this PR; the product enumeration is still closed. Making `RemoteConfigProduct` open (e.g. `Cow<'static, str>` or a sealed trait) would close that gap and is a reasonable follow-up. ## Motivation Drives `datadog-remote-config`'s coupling to product crates from compile-time (Cargo features + closed enum) to runtime (registry injection), so consumers compose the products they care about without forcing the RC crate to know about every product. ## Note Check [this](#1958) other PR with a different approach Co-authored-by: igor.unanua <igor.unanua@datadoghq.com>
1 parent 1612ee9 commit 401e623

20 files changed

Lines changed: 602 additions & 168 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

datadog-ffe/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ publish = false
1111
bench = false
1212

1313
[dependencies]
14+
datadog-remote-config = { path = "../datadog-remote-config", default-features = false, optional = true }
1415
faststr = { version = "0.2.23", default-features = false, features = ["serde"] }
1516
serde = { version = "1.0", default-features = false, features = ["derive", "rc"] }
1617
serde_json = { version = "1.0", default-features = false, features = ["std", "raw_value"] }
@@ -30,6 +31,8 @@ prost = { version = "0.14.1", optional = true }
3031
pyo3 = { version = "0.28", optional = true, default-features = false, features = ["macros"] }
3132

3233
[features]
34+
default = ["remote-config"]
3335
exposure-events = ["dep:lru"]
3436
evaluation-metrics = ["dep:libdd-trace-protobuf", "dep:prost"]
3537
pyo3 = ["dep:pyo3"]
38+
remote-config = ["dep:datadog-remote-config"]

datadog-ffe/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
mod flag_type;
55

6+
#[cfg(feature = "remote-config")]
7+
mod remote_config;
68
pub mod rules_based;
79
#[cfg(any(feature = "exposure-events", feature = "evaluation-metrics"))]
810
pub mod telemetry;

datadog-ffe/src/remote_config.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::rules_based::UniversalFlagConfig;
5+
use datadog_remote_config::{ParseError, RemoteConfigContent, RemoteConfigProduct};
6+
7+
impl RemoteConfigContent for UniversalFlagConfig {
8+
const PRODUCT: RemoteConfigProduct = RemoteConfigProduct::FfeFlags;
9+
10+
fn parse(data: &[u8]) -> Result<Self, ParseError> {
11+
Ok(UniversalFlagConfig::from_json(data.to_vec())?)
12+
}
13+
}

datadog-live-debugger/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ publish = false
77

88
[dependencies]
99
anyhow = "1.0"
10+
datadog-remote-config = { path = "../datadog-remote-config", default-features = false }
1011
libdd-common = { path = "../libdd-common" }
1112
libdd-data-pipeline = { path = "../libdd-data-pipeline" }
1213
http-body-util = "0.1"

datadog-live-debugger/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod probe_defs;
1313

1414
pub mod debugger_defs;
1515
mod redacted_names;
16+
mod remote_config;
1617
pub mod sender;
1718

1819
pub use expr_eval::*;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::probe_defs::LiveDebuggingData;
5+
use datadog_remote_config::{ParseError, RemoteConfigContent, RemoteConfigProduct};
6+
7+
impl RemoteConfigContent for LiveDebuggingData {
8+
const PRODUCT: RemoteConfigProduct = RemoteConfigProduct::LiveDebugger;
9+
10+
fn parse(data: &[u8]) -> Result<Self, ParseError> {
11+
crate::parse_json::parse(&String::from_utf8_lossy(data))
12+
.map_err(|e| ParseError::Custom(e.into()))
13+
}
14+
}

datadog-remote-config/Cargo.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,14 @@ client = [
2121
"time",
2222
"tracing"
2323
]
24-
live-debugger = ["datadog-live-debugger"]
24+
2525
regex-lite = ["libdd-common/regex-lite"]
26-
ffe = ["datadog-ffe"]
2726
test = ["hyper/server", "hyper-util"]
2827

2928
[dependencies]
3029
anyhow = { version = "1.0" }
3130
libdd-common = { path = "../libdd-common"}
3231
libdd-trace-protobuf = { path = "../libdd-trace-protobuf", optional = true }
33-
datadog-live-debugger = { path = "../datadog-live-debugger", optional = true }
34-
datadog-ffe = { path = "../datadog-ffe", optional = true }
3532
hyper = { workspace = true, optional = true, default-features = false }
3633
http-body-util = {version = "0.1", optional = true }
3734
http = { version = "1.1", optional = true }
@@ -47,6 +44,7 @@ tracing = { version = "0.1", default-features = false, optional = true }
4744
serde = "1.0"
4845
serde_json = { version = "1.0", features = ["raw_value"] }
4946
serde_with = "3"
47+
thiserror = "2"
5048

5149
# Test feature
5250
hyper-util = { workspace = true, features = ["service"], optional = true }

datadog-remote-config/examples/remote_config_fetch.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use datadog_remote_config::fetch::{ConfigInvariants, ConfigOptions, SingleChange
55
use datadog_remote_config::file_change_tracker::{Change, FilePath};
66
use datadog_remote_config::file_storage::ParsedFileStorage;
77
use datadog_remote_config::RemoteConfigProduct::ApmTracing;
8-
use datadog_remote_config::{RemoteConfigData, Target};
8+
use datadog_remote_config::{RemoteConfigParsed, Target};
99
use libdd_common::tag::Tag;
1010
use libdd_common::Endpoint;
1111
use std::time::Duration;
@@ -86,12 +86,15 @@ async fn main() {
8686
}
8787
}
8888

89-
fn print_file_contents(contents: &anyhow::Result<RemoteConfigData>) {
89+
fn print_file_contents(contents: &anyhow::Result<Option<RemoteConfigParsed>>) {
9090
// Note: these contents may be large. Do not actually print it fully in a non-dev env.
9191
match contents {
92-
Ok(data) => {
92+
Ok(Some(data)) => {
9393
println!("File contents: {data:?}");
9494
}
95+
Ok(None) => {
96+
println!("Unregistered product, no parsed data");
97+
}
9598
Err(e) => {
9699
println!("Failed parsing file: {e:?}");
97100
}

datadog-remote-config/src/config/dynamic.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use serde::{Deserialize, Serialize};
55
use std::collections::HashMap;
66

7-
#[derive(Debug, Deserialize)]
7+
#[derive(Debug, Clone, Deserialize)]
88
#[cfg_attr(feature = "test", derive(Default, Serialize))]
99
pub struct DynamicConfigTarget {
1010
#[serde(default)]
@@ -13,7 +13,7 @@ pub struct DynamicConfigTarget {
1313
pub env: Option<String>,
1414
}
1515

16-
#[derive(Debug, Deserialize)]
16+
#[derive(Debug, Clone, Deserialize)]
1717
#[cfg_attr(feature = "test", derive(Serialize))]
1818
pub struct DynamicConfigFile {
1919
pub action: String,
@@ -76,7 +76,7 @@ pub struct TracingSamplingRule {
7676
pub sample_rate: f64,
7777
}
7878

79-
#[derive(Debug, Deserialize)]
79+
#[derive(Debug, Clone, Deserialize)]
8080
#[cfg_attr(feature = "test", derive(Default, Serialize))]
8181
pub struct DynamicConfig {
8282
pub(crate) tracing_header_tags: Option<Vec<TracingHeaderTag>>,

0 commit comments

Comments
 (0)