Skip to main content

saluki_components/config_registry/
mod.rs

1//! Configuration key registry.
2//!
3//! A programmatic registry of all recognized configuration keys. Each entry describes the key
4//! purely from the configuration system's perspective: its canonical YAML path, the environment
5//! variables that map to it, the shape of its value, and which internal config structs consume it.
6//!
7//! This registry is intentionally free of Rust field names and struct internals—it models the
8//! configuration surface as an operator would see it, and can be used at runtime to detect
9//! unknown or unsupported keys in a loaded configuration file.
10//!
11//! ## User Guide
12//!
13//! The config registry tests enforce uniqueness and completeness constraints similar to
14//! primary-key and unique constraints in a relational database. Every key in the vendored
15//! schema (`core_schema.yaml`) must appear in exactly one of two places: an annotation
16//! ([`SalukiAnnotation`]) or the ignored-keys list (`etc/ignored_keys.yaml`). No key may
17//! appear in both, and no key may appear twice within either list.
18//!
19//! If a test fails, it means one of these constraints was violated. Common scenarios:
20//!
21//! ### Adding a Configuration Key
22//!
23//! When Saluki gains support for a new key, add a [`SalukiAnnotation`] in the appropriate
24//! `config_registry::datadog::*` submodule with [`SupportLevel::Full`] or
25//! [`SupportLevel::Partial`] and a non-empty `used_by` list. If the key was previously in
26//! `etc/ignored_keys.yaml`, remove it from there. If it was previously
27//! [`SupportLevel::Incompatible`], move it from `datadog/unsupported.rs` to the
28//! appropriate submodule and update its support level.
29//!
30//! ### Updating the Vendored Schema
31//!
32//! After updating `vendor/core_schema.yaml`, rebuild to regenerate the schema module
33//! (the `build.rs` script handles this automatically). Any new keys will cause the
34//! `all_schema_entries_are_annotated_or_ignored` test to fail. For each new key, decide:
35//!
36//! - **Irrelevant to ADP**: add to `etc/ignored_keys.yaml` with a reason.
37//! - **Incompatible**: add to `datadog/unsupported.rs` with a [`Severity`] level.
38//! - **Supported**: add to the appropriate `datadog/*.rs` submodule.
39//!
40//! If the update removed keys, the `no_stale_entries` test catches annotations or
41//! ignored entries that reference keys no longer in the schema. Delete the stale entries.
42
43mod classifier;
44pub mod datadog;
45/// Generated schema entries from the vendored Datadog Agent config schema.
46pub mod generated;
47
48pub use classifier::{Classification, ConfigClassifier};
49
50pub(crate) use self::datadog::ALL_ANNOTATIONS;
51#[cfg(any(test, feature = "config-test-support"))]
52pub(crate) use self::datadog::SUPPORTED_ANNOTATIONS;
53#[cfg(test)]
54pub(crate) use self::generated::schema::ALL_SCHEMA_ENTRIES;
55#[cfg(test)]
56pub(crate) use self::generated::schema::IGNORED_ENTRIES;
57
58/// Declares a set of [`SalukiAnnotation`] constants and generates a companion `ALL` slice.
59///
60/// Each entry declares one named `pub const` annotation. The macro also emits:
61///
62/// ```ignore
63/// pub const ALL: &[&SalukiAnnotation] = &[&NAME1, &NAME2, ...];
64/// ```
65///
66/// so that `datadog/mod.rs` can aggregate submodules with a single
67/// `v.extend_from_slice(my_module::ALL)` line rather than listing every constant by name.
68///
69/// # Example
70///
71/// ```ignore
72/// declare_annotations! {
73///     /// Doc comment for PROXY_HTTP.
74///     PROXY_HTTP = SalukiAnnotation { ... };
75///     PROXY_HTTPS = SalukiAnnotation { ... };
76/// }
77/// ```
78#[macro_export]
79macro_rules! declare_annotations {
80    ( $( $(#[$attr:meta])* $name:ident = $val:expr ;)+ ) => {
81        $(
82            $(#[$attr])*
83            pub const $name: $crate::config_registry::SalukiAnnotation = $val;
84        )+
85
86        /// All annotations declared in this module, in declaration order.
87        pub const ALL: &[$crate::config_registry::SalukiAnnotationRef] = &[
88            $( &$name, )+
89        ];
90    };
91}
92
93/// Shared helpers for config smoke tests.
94#[cfg(any(test, feature = "config-test-support"))]
95pub mod test_support;
96
97/// Identifiers for known configuration structs.
98///
99/// Used as values in [`SalukiAnnotation::used_by`] to declare which structs consume a given key.
100/// Adding a new struct here is the first step when registering its configuration keys.
101pub mod structs {
102    /// Identifier for `ProxyConfiguration`.
103    pub const PROXY_CONFIGURATION: &str = "ProxyConfiguration";
104    /// Identifier for `ForwarderConfiguration`.
105    pub const FORWARDER_CONFIGURATION: &str = "ForwarderConfiguration";
106    /// Identifier for `DogStatsDConfiguration`.
107    pub const DOGSTATSD_CONFIGURATION: &str = "DogStatsDConfiguration";
108    /// Identifier for `ContainerdConfiguration`.
109    pub const CONTAINERD_CONFIGURATION: &str = "ContainerdConfiguration";
110    /// Identifier for `OtlpConfiguration`.
111    pub const OTLP_CONFIGURATION: &str = "OtlpConfiguration";
112    /// Identifier for `AggregateConfiguration`.
113    pub const AGGREGATE_CONFIGURATION: &str = "AggregateConfiguration";
114    /// Identifier for `DogStatsDMapperConfiguration`.
115    pub const DOGSTATSD_MAPPER_CONFIGURATION: &str = "DogStatsDMapperConfiguration";
116    /// Identifier for `DogStatsDDebugLogConfiguration`.
117    pub const DOGSTATSD_DEBUG_LOG_CONFIGURATION: &str = "DogStatsDDebugLogConfiguration";
118    /// Identifier for `DogStatsDPrefixFilterConfiguration`.
119    pub const DOGSTATSD_PREFIX_FILTER_CONFIGURATION: &str = "DogStatsDPrefixFilterConfiguration";
120    /// Identifier for `DatadogMetricsConfiguration`.
121    pub const DATADOG_METRICS_CONFIGURATION: &str = "DatadogMetricsConfiguration";
122    /// Identifier for `DatadogTraceConfiguration`.
123    pub const DATADOG_TRACE_CONFIGURATION: &str = "DatadogTraceConfiguration";
124    /// Identifier for `DatadogLogsConfiguration`.
125    pub const DATADOG_LOGS_CONFIGURATION: &str = "DatadogLogsConfiguration";
126    /// Identifier for `DatadogEventsConfiguration`.
127    pub const DATADOG_EVENTS_CONFIGURATION: &str = "DatadogEventsConfiguration";
128    /// Identifier for `DatadogServiceChecksConfiguration`.
129    pub const DATADOG_SERVICE_CHECKS_CONFIGURATION: &str = "DatadogServiceChecksConfiguration";
130    /// Identifier for `DatadogApmStatsEncoderConfiguration`.
131    pub const DATADOG_APM_STATS_ENCODER_CONFIGURATION: &str = "DatadogApmStatsEncoderConfiguration";
132    /// Identifier for `OtlpDecoderConfiguration`.
133    pub const OTLP_DECODER_CONFIGURATION: &str = "OtlpDecoderConfiguration";
134    /// Identifier for `OtlpRelayConfiguration`.
135    pub const OTLP_RELAY_CONFIGURATION: &str = "OtlpRelayConfiguration";
136    /// Identifier for `TraceObfuscationConfiguration`.
137    pub const TRACE_OBFUSCATION_CONFIGURATION: &str = "TraceObfuscationConfiguration";
138    /// Identifier for `RemoteAgentClientConfiguration`.
139    pub const REMOTE_AGENT_CLIENT_CONFIGURATION: &str = "RemoteAgentClientConfiguration";
140    /// Keys read via `get_typed` / `try_get_typed` rather than struct deserialization.
141    pub const GET_TYPED: &str = "get_typed";
142}
143
144/// The ADP pipeline a config key affects.
145///
146/// Primary user-controlled toggles are represented.
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
148pub enum Pipeline {
149    /// DogStatsD metrics pipeline.
150    DogStatsD,
151    /// Agent checks pipeline.
152    Checks,
153    /// OTLP ingestion frontend.
154    Otlp,
155    /// Internal trace processing. Active when OTLP is enabled and proxy/relay mode (which uses the
156    /// core Agent for transport) is off.
157    Traces,
158}
159
160/// Which pipelines a [`SalukiAnnotation`] affects.
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum PipelineAffinity {
163    /// The list of pipelines affected by the `SalukiAnnotation`.
164    ///
165    /// This list must be non-empty, enforced by test.
166    Pipelines(&'static [Pipeline]),
167    /// The `SalukiAnnotation` affects all pipelines or ADP behavior as a whole.
168    CrossCutting,
169}
170
171/// The `Severity` level of a config key that Saluki doesn't support.
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub enum Severity {
174    /// Saluki's incompatibility with the key is considered minor.
175    Low,
176
177    /// Saluki's incompatibility with the key is considered potentially impactful.
178    Medium,
179
180    /// Saluki's incompatibility with the key is considered problematic.
181    High,
182}
183
184/// How well saluki supports a given configuration key.
185///
186/// Used in [`SalukiAnnotation`] to classify each key from saluki's perspective. `Ignored` is
187/// reserved for keys in the schema that have no annotation at all—it must not appear in any
188/// handwritten annotation.
189///
190/// Invariants enforced at test time:
191/// - `Full` and `Partial` require a non-empty `used_by` list.
192/// - `Incompatible` requires an empty `used_by` list.
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194pub enum SupportLevel {
195    /// Fully supported. The key is wired up end-to-end and must be consumed by at least one struct.
196    Full,
197    /// Partially supported. The key is consumed by at least one struct, but support is incomplete.
198    Partial,
199    /// Explicitly incompatible. Saluki doesn't support this key and may not behave as expected in
200    /// its presence; `used_by` must be empty. Support for the key may be added in the future but
201    /// tracking such intent isn't encoded here.
202    Incompatible(Severity),
203    /// Intentionally ignored. The key isn't relevant to Saluki. This assignment must be
204    /// intentionally chosen and specified for a key. We never assume that a key is `Ignored` just
205    /// because we're unaware of it.
206    #[allow(unused)]
207    Ignored,
208    /// Unrecognized. The Saluki codebase is unaware of the existence of this key. It's not in our
209    /// vendored datadog config schema, nor is it annotated.
210    #[allow(unused)]
211    Unrecognized,
212}
213
214/// The shape of a configuration value.
215///
216/// Describes how a value should be parsed from both YAML and environment variables.
217/// `StringList` values are represented as a YAML sequence or a space-separated string in env vars.
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
219pub enum ValueType {
220    /// A boolean (`true` / `false`).
221    Bool,
222    /// A UTF-8 string.
223    String,
224    /// An unsigned integer.
225    Integer,
226    /// A floating-point number.
227    Float,
228    /// A list of strings (YAML sequence or space-separated env var string).
229    StringList,
230}
231
232/// Which schema source of truth defined the `SchemaEntry`
233#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
234#[repr(u8)]
235pub(crate) enum Schema {
236    /// Saluki defined the `SchemaEntry` and the key isn't expected to exist in the vendored Datadog config schema.
237    Saluki,
238    /// The vendored Datadog config schema defines the `SchemaEntry`.
239    Datadog,
240}
241
242/// Schema-derived metadata for a single configuration key.
243///
244/// Generated from the vendored Datadog Agent config schema. Contains only what the schema
245/// knows: the canonical YAML path, declared environment variables, and value type. Saluki-specific
246/// fields (`used_by`, etc.) live in [`SalukiAnnotation`] instead.
247///
248/// Don't construct these manually—they're produced by `cargo xtask gen-config-schema` and
249/// live in `config_registry::generated::schema`.
250#[derive(Debug)]
251pub struct SchemaEntry {
252    /// The source of truth from which this entry was derived.
253    #[allow(dead_code)]
254    pub(crate) schema: Schema,
255
256    /// Canonical dot-separated YAML path for this key (for example, `"proxy.http"`).
257    pub yaml_path: &'static str,
258
259    /// Environment variables that deliver this value, as declared in the schema.
260    ///
261    /// Empty when the schema marks the key `no-env` or lists no env vars. Annotations may
262    /// override this with additional or corrected env vars.
263    pub env_vars: &'static [&'static str],
264
265    /// Shape of the value.
266    pub value_type: ValueType,
267
268    /// JSON-encoded default value from the Agent schema, if present.
269    ///
270    /// Examples: `Some("true")`, `Some("8125")`, `Some("\"datadoghq.com\"")`, `None`.
271    /// Used by smoke tests to auto-detect when a generic test value matches the default
272    /// and flip it so the test actually exercises the field.
273    pub default: Option<&'static str>,
274}
275
276/// Saluki-specific annotation for a single configuration key.
277///
278/// Pairs a [`SchemaEntry`] (generated from the vendored schema) with the metadata that only
279/// saluki knows: which internal config structs consume the key, and any corrections to the
280/// schema's env var list.
281///
282/// These are hand-written constants, one per key saluki cares about, and live in
283/// `config_registry::datadog::*` submodules. They're never overwritten by codegen.
284#[derive(Debug)]
285pub struct SalukiAnnotation {
286    /// The schema entry this annotation enriches.
287    pub schema: &'static SchemaEntry,
288
289    /// How well saluki supports this key.
290    ///
291    /// Must not be [`SupportLevel::Ignored`] or [`SupportLevel::Unrecognized`] which are reserved
292    /// for unannotated keys.
293    pub support_level: SupportLevel,
294
295    /// Additional YAML paths beyond the canonical one in the schema (aliases).
296    ///
297    /// Most keys have no aliases; leave this as `&[]` unless the config system recognises
298    /// the key under more than one dot-separated path.
299    pub additional_yaml_paths: &'static [&'static str],
300
301    /// Overrides the schema's `env_vars` list entirely when `Some`.
302    ///
303    /// Use when the schema marks a key `no-env` but env vars are actually supported, or when
304    /// the schema's list is incorrect or incomplete (for example, the proxy sub-keys).
305    pub env_var_override: Option<&'static [&'static str]>,
306
307    /// Config structs that incorporate this key, as [`structs`] constants.
308    ///
309    /// Must be non-empty for [`SupportLevel::Full`] and [`SupportLevel::Partial`].
310    /// Must be empty for [`SupportLevel::Incompatible`].
311    pub used_by: &'static [&'static str],
312
313    /// Overrides the schema's `value_type` when `Some`.
314    ///
315    /// Use when the schema declares a key as `Float` but the consuming Rust field is `i32` (or
316    /// similarly mismatched), so that smoke tests inject a value the struct can actually parse.
317    pub value_type_override: Option<ValueType>,
318
319    /// Overrides the smoke-test injected value entirely when `Some`.
320    ///
321    /// Must be a valid JSON literal (for example, `Some("[{\"name\":\"test\"}]")`). Use when the field
322    /// requires a structured value that the generic `ValueType`-derived test values can't satisfy
323    /// (for example, JSON-encoded arrays or objects).
324    pub test_json: Option<&'static str>,
325
326    /// Which pipelines this key affects.
327    pub pipeline_affinity: PipelineAffinity,
328}
329
330/// A reference to a [`SalukiAnnotation`], used as the element type of `ALL` slices generated by
331/// [`declare_annotations!`].
332pub type SalukiAnnotationRef = &'static SalukiAnnotation;
333
334impl SalukiAnnotation {
335    /// The canonical YAML path for this key (from the schema).
336    pub fn yaml_path(&self) -> &'static str {
337        self.schema.yaml_path
338    }
339
340    /// All YAML paths for this key: canonical first, then any aliases.
341    pub fn all_yaml_paths(&self) -> impl Iterator<Item = &'static str> {
342        std::iter::once(self.schema.yaml_path).chain(self.additional_yaml_paths.iter().copied())
343    }
344
345    /// Effective env vars: the override list if set, otherwise the schema's list.
346    pub fn effective_env_vars(&self) -> &'static [&'static str] {
347        self.env_var_override.unwrap_or(self.schema.env_vars)
348    }
349
350    /// Shape of the value: override if set, otherwise from the schema.
351    pub fn value_type(&self) -> ValueType {
352        self.value_type_override.unwrap_or(self.schema.value_type)
353    }
354}
355
356/// A fully resolved configuration key, derived from a [`SalukiAnnotation`] at registry init time.
357///
358/// Used for runtime unknown-key detection and anywhere a flattened, owned view of a key is
359/// needed. For test infrastructure, prefer working with [`SalukiAnnotation`] directly.
360#[derive(Debug)]
361pub struct ConfigKey {
362    /// All dot-separated YAML paths that deliver this value.
363    pub yaml_paths: Vec<&'static str>,
364
365    /// All environment variables that deliver this value.
366    pub env_vars: Vec<&'static str>,
367
368    /// Shape of the value.
369    pub value_type: ValueType,
370
371    /// Config structs that incorporate this key, as [`structs`] constants.
372    pub used_by: &'static [&'static str],
373}
374
375impl From<&SalukiAnnotation> for ConfigKey {
376    fn from(a: &SalukiAnnotation) -> Self {
377        ConfigKey {
378            yaml_paths: a.all_yaml_paths().collect(),
379            env_vars: a.effective_env_vars().to_vec(),
380            value_type: a.value_type(),
381            used_by: a.used_by,
382        }
383    }
384}