Skip to content

Commit 8bed996

Browse files
committed
chore(config): dogstatsd config through the witness trait
Add the first end-to-end slice of the typed config system. ScopedConfig<T> and DogStatsDConfig land in saluki-component-config; a minimal SalukiConfiguration embeds DogStatsDConfig in agent-data-plane-config; and a Translator implements the generated witness in agent-data-plane-config-system, seeding the Saluki-only base then driving the Datadog source over it. Only DogStatsD-source keys have a native destination so far; the remaining witness methods are temporary no-ops until their destinations land. Trim the new crates' dependencies to what this slice actually uses.
1 parent 98c0cdd commit 8bed996

15 files changed

Lines changed: 1219 additions & 54 deletions

File tree

Cargo.lock

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

lib/agent-data-plane-config-system/Cargo.toml

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,8 @@ workspace = true
1010

1111
[dependencies]
1212
agent-data-plane-config = { workspace = true }
13-
14-
bytesize = { workspace = true }
15-
datadog-agent-commons = { workspace = true }
1613
datadog-agent-config = { workspace = true }
17-
datadog-protos = { workspace = true }
18-
figment = { workspace = true }
19-
futures = { workspace = true }
20-
prost-types = { workspace = true }
21-
saluki-common = { workspace = true }
2214
saluki-component-config = { workspace = true }
23-
saluki-config-tools = { workspace = true }
2415
saluki-context = { workspace = true }
25-
saluki-error = { workspace = true }
26-
saluki-io = { workspace = true }
27-
serde = { workspace = true }
2816
serde_json = { workspace = true }
29-
serde_yaml = { workspace = true }
3017
stringtheory = { workspace = true }
31-
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
32-
tracing = { workspace = true }
33-
34-
[dev-dependencies]
35-
tempfile = { workspace = true }

lib/agent-data-plane-config-system/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@
4646
//! | tools | | config |
4747
//! +----------------+ +----------------+
4848
//! ```
49+
//!
50+
51+
// TODO(visibility): add crate-boundary architectural guard when arch tests are wired up
52+
pub(crate) mod translate;

lib/agent-data-plane-config-system/src/translate/datadog/mod.rs

Lines changed: 334 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//! Translation layer from Datadog config to ADP-native config.
2+
//!
3+
//! This module owns the [`Translator`] -- the [`DatadogConfigConsumer`] implementation that writes
4+
//! witnessed Datadog values directly into the ADP-native structs embedded in a
5+
//! [`SalukiConfiguration`].
6+
//!
7+
//! 1. The Saluki-schema-only seed (`saluki_only.seed()`) produces the base `SalukiConfiguration`
8+
//! with defaults plus Saluki-only fields that cannot arrive from the Datadog agent.
9+
//! 2. The Datadog witness `drive` overlays its config fields via the `consume_<key>` methods.
10+
//!
11+
//! [`DatadogConfigConsumer`]: datadog_agent_config::DatadogConfigConsumer
12+
//! [`SalukiConfiguration`]: agent_data_plane_config::SalukiConfiguration
13+
14+
mod datadog;
15+
16+
use agent_data_plane_config::{SalukiConfiguration, SalukiOnlyConfiguration};
17+
use datadog_agent_config::{drive, DatadogConfiguration, TranslateError};
18+
19+
/// The witness consumer that accumulates a [`SalukiConfiguration`] from a Datadog source parse.
20+
///
21+
/// The accumulator composes the output type [`SalukiConfiguration`], along with accumulated
22+
/// translation errors.
23+
#[allow(dead_code)]
24+
pub(crate) struct Translator {
25+
/// The in-progress native model. Seeded from the Saluki-only base, then overlaid by the drive.
26+
pub(crate) saluki: SalukiConfiguration,
27+
28+
/// Accumulated semantic translation errors. The first is surfaced by `drive`.
29+
errors: Vec<TranslateError>,
30+
}
31+
32+
impl Translator {
33+
/// Creates a translator over a seeded base.
34+
///
35+
/// `base` is the lowest-precedence starting point (typically `saluki_only.seed()`). The Datadog
36+
/// drive overlays its schema fields on top, so the base only usefully carries disjoint
37+
/// Saluki-schema-only fields.
38+
#[allow(dead_code)]
39+
pub(crate) fn new(base: SalukiConfiguration) -> Self {
40+
Self {
41+
saluki: base,
42+
errors: Vec::new(),
43+
}
44+
}
45+
46+
/// Returns the finished native model.
47+
#[allow(dead_code)]
48+
pub(crate) fn finish(self) -> SalukiConfiguration {
49+
self.saluki
50+
}
51+
52+
/// Records a semantic translation error. The first recorded error is surfaced by `drive`.
53+
#[allow(dead_code)]
54+
pub(crate) fn record_error(&mut self, e: TranslateError) {
55+
self.errors.push(e);
56+
}
57+
}
58+
59+
/// Translates a Datadog source parse plus the Saluki-only seed into the ADP-native model.
60+
///
61+
/// This is the single reusable translation path used by both startup and each dynamic update:
62+
/// seed a [`Translator`] from `saluki_only`, drive the Datadog witness over it, and finish.
63+
///
64+
/// # Errors
65+
///
66+
/// Returns the first [`TranslateError`] recorded while consuming a witnessed value (for example, a
67+
/// value that cannot be parsed into its native destination). Callers decide policy: startup bails;
68+
/// a dynamic update is rejected and the last-good config retained.
69+
#[allow(dead_code)]
70+
pub(crate) fn translate(
71+
saluki_only: &SalukiOnlyConfiguration, datadog: &DatadogConfiguration,
72+
) -> Result<SalukiConfiguration, TranslateError> {
73+
let mut t = Translator::new(saluki_only.seed());
74+
drive(datadog, &mut t)?;
75+
Ok(t.finish())
76+
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use saluki_component_config::dogstatsd::SourceConfig;
81+
82+
use super::*;
83+
84+
// Datadog Agent defaults clobber Saluki native defaults by design. Translation is
85+
// clobber-not-merge: a Datadog-schema field's default overwrites the native default even when
86+
// the two differ.
87+
// TODO: we need a mechanism for default alignment #1802
88+
#[test]
89+
fn seed_then_drive_default_succeeds_and_clobbers() {
90+
let saluki_only = SalukiOnlyConfiguration::default();
91+
let datadog = DatadogConfiguration::default();
92+
let config = translate(&saluki_only, &datadog).expect("default translation succeeds");
93+
// The drive ran: the Datadog default port (8125) flows into the native source.
94+
assert_eq!(config.components.dogstatsd.source.port, 8125);
95+
// Clobber, not merge: the Datadog default for `dogstatsd_capture_depth` is 0, and it
96+
// overwrites the native default of 1024 (the component later raises sub-1024 values).
97+
assert_eq!(config.components.dogstatsd.source.capture_depth, 0);
98+
}
99+
100+
#[test]
101+
fn seed_and_drive_write_disjoint_dogstatsd_fields() {
102+
// Saluki-only seeds `allow_context_heap_allocs` (a Saluki-schema-only field); the Datadog
103+
// drive overlays `dogstatsd_port` (a Datadog-schema field). Both survive into one struct.
104+
let mut saluki_only = SalukiOnlyConfiguration::default();
105+
saluki_only.dogstatsd.allow_context_heap_allocs = Some(false);
106+
107+
let datadog = DatadogConfiguration {
108+
dogstatsd_port: 7000,
109+
..Default::default()
110+
};
111+
112+
let config = translate(&saluki_only, &datadog).expect("translation succeeds");
113+
assert!(
114+
!config.components.dogstatsd.source.allow_context_heap_allocations,
115+
"seeded Saluki-only field survives"
116+
);
117+
assert_eq!(
118+
config.components.dogstatsd.source.port, 7000,
119+
"Datadog drive overlays its field"
120+
);
121+
}
122+
123+
#[test]
124+
fn drive_overlays_dogstatsd_source() {
125+
let saluki_only = SalukiOnlyConfiguration::default();
126+
let datadog = DatadogConfiguration {
127+
dogstatsd_buffer_size: 4096,
128+
dogstatsd_non_local_traffic: true,
129+
dogstatsd_tag_cardinality: "high".to_string(),
130+
..Default::default()
131+
};
132+
133+
let config = translate(&saluki_only, &datadog).expect("translation succeeds");
134+
let source: &SourceConfig = &config.components.dogstatsd.source;
135+
assert_eq!(source.buffer_size, 4096);
136+
assert!(source.non_local_traffic);
137+
assert_eq!(
138+
source.origin_enrichment.tag_cardinality,
139+
saluki_context::origin::OriginTagCardinality::High
140+
);
141+
}
142+
143+
#[test]
144+
fn malformed_value_surfaces_as_error() {
145+
let saluki_only = SalukiOnlyConfiguration::default();
146+
let datadog = DatadogConfiguration {
147+
dogstatsd_tag_cardinality: "bogus".to_string(),
148+
..Default::default()
149+
};
150+
assert!(translate(&saluki_only, &datadog).is_err());
151+
}
152+
}

lib/agent-data-plane-config/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ workspace = true
1111
[dependencies]
1212
bytesize = { workspace = true }
1313
saluki-component-config = { workspace = true }
14-
saluki-io = { workspace = true }
1514
serde = { workspace = true }
1615

1716
[dev-dependencies]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//! [`ControlConfiguration`]: pipeline gates, topology-shaping, and data plane decisions.
2+
//!
3+
//! Control config is read by the config system and the data plane, not by components.
4+
5+
/// Pipeline gates and topology-shaping decisions for ADP.
6+
///
7+
/// Read first, before [`ComponentConfiguration`](crate::ComponentConfiguration): it decides which
8+
/// pipelines and topology to build. Consumed by the orchestration layer only.
9+
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
10+
pub struct ControlConfiguration {
11+
/// Whether the data plane runs at all.
12+
pub enabled: bool,
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//! DogStatsD-domain component configuration group.
2+
3+
/// DogStatsD-domain component configuration: source, mapper, aggregate, debug-log, and filter.
4+
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
5+
pub struct Config {
6+
/// DogStatsD source configuration (listeners, parser/decoding options).
7+
pub source: saluki_component_config::dogstatsd::SourceConfig,
8+
}
Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
//! # Configuration model for Saluki components and ADP.
22
//!
3-
//! `SalukiConfiguration` is the type which configures ADP.
3+
//! [`SalukiConfiguration`] is the ADP-native runtime configuration model: the typed output of
4+
//! configuration translation, and the single model that all ADP runtime code consumes. It
5+
//! adds control-level configuration ([`ControlConfiguration`]) to the component groups, which embed
6+
//! the leaf structs from `saluki-component-config` directly.
47
//!
5-
//! ## Responsibilities
8+
//! [`SalukiOnlyConfiguration`] is the Saluki-schema-only source input. Its [`seed`] produces
9+
//! a base `SalukiConfiguration` (defaults plus Saluki-only values) that the Datadog `drive` later
10+
//! overlays its disjoint schema fields onto.
611
//!
7-
//! Model all runtime configuration used by ADP in a single, hierarchical type by adding
8-
//! control-level configuration to what is modeled in `saluki-component-config`.
12+
//! ## Workspace dependency boundaries
913
//!
10-
//! ## Workspace Dependency Boundaries
14+
//! Depends on `saluki-component-config` (embeds its leaf structs). Must not depend on the Datadog
15+
//! source model (`datadog-agent-config`), the raw config map (`saluki-config-tools`), the config
16+
//! system (`agent-data-plane-config-system`), or component implementations (`saluki-components`). It
17+
//! is separate from `saluki-component-config` so components see only their own slice, and separate
18+
//! from the config-system so the model provably cannot import source mechanics.
1119
//!
12-
//! This depends only on `saluki-component-config` and `saluki-io`; it must not depend on the
13-
//! Datadog model or a raw config map. It is separate from `saluki-component-config` so components
14-
//! see only their own slice, and separate from the config-system so the model provably cannot
15-
//! import source mechanics: it does not depend on them.
20+
//! [`seed`]: SalukiOnlyConfiguration::seed
21+
22+
#![deny(missing_docs)]
23+
24+
pub mod control;
25+
pub mod dogstatsd;
26+
pub mod model;
27+
pub mod saluki_only;
28+
29+
pub use self::control::ControlConfiguration;
30+
pub use self::model::{ComponentConfiguration, SalukiConfiguration};
31+
pub use self::saluki_only::SalukiOnlyConfiguration;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! The ADP-native runtime configuration model: [`SalukiConfiguration`] and its component groups.
2+
//!
3+
//! [`SalukiConfiguration`] is the single output of configuration translation. It has two top-level
4+
//! groups: [`ControlConfiguration`] (pipeline gates and topology shaping and
5+
//! [`ComponentConfiguration`] (the per-domain component groups, each read by its owning component).
6+
//!
7+
//! Every group wrapper embeds the `saluki-component-config` leaf structs directly. The translator
8+
//! writes witnessed values into these embedded leaf structs.
9+
//!
10+
11+
// TODO: Only the DogStatsD source is modeled; component groups land with rapid strangler-fig PRs.
12+
13+
use crate::control::ControlConfiguration;
14+
use crate::dogstatsd;
15+
16+
/// The complete ADP-native runtime configuration after translation.
17+
///
18+
/// First, configuration not appearing the Datadog config model, [`SalukiOnlyConfiguration`], is
19+
/// used as a seed (since it cannot come from Datadog config). This is done with
20+
/// [`SalukiOnlyConfiguration::seed`](crate::SalukiOnlyConfiguration::seed).
21+
///
22+
/// Then that seeded configuration is mutated by everything found in `DatadogConfiguration` by
23+
/// driving it through the witness trait.
24+
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
25+
pub struct SalukiConfiguration {
26+
/// Pipeline gates, topology-shaping decisions and data plane configuration. Consumed by the
27+
/// orchestration layers (config-system and the topology builder).
28+
pub control: ControlConfiguration,
29+
30+
/// Per-domain component configuration groups. Each component receives its own slice from here.
31+
pub components: ComponentConfiguration,
32+
}
33+
34+
/// The per-domain component configuration groups.
35+
///
36+
/// Each field is a group wrapper that embeds the `saluki-component-config` leaf structs for one
37+
/// ownership domain. A component is handed only its own leaf slice (for example,
38+
/// `&components.dogstatsd.source`), never the whole `ComponentConfiguration` or
39+
/// [`SalukiConfiguration`].
40+
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
41+
pub struct ComponentConfiguration {
42+
/// DogStatsD source, mapper, aggregate, debug-log, and filter configuration.
43+
pub dogstatsd: dogstatsd::Config,
44+
}

0 commit comments

Comments
 (0)