agent_data_plane_config/
lib.rs

1//! ADP-native configuration model: the typed target of configuration translation.
2//!
3//! This crate owns the domain-shaped model types that translation produces and that ADP runtime
4//! code consumes: `SalukiConfiguration { control, shared, domains }`, `ControlConfiguration`,
5//! `SharedConfiguration`, `DomainConfiguration` and its per-domain structs.
6//!
7//! It does not embed component config structs (those stay in `saluki-components`, built from this
8//! model). It depends on neither the raw configuration map nor the Datadog source model, so a
9//! consumer can depend on it without inheriting either.
10//!
11//! Every field is plain, source-agnostic data. There are no source key names in identifiers and no
12//! source serde (these structs are serialized for the `/config/runtime` view but never
13//! deserialized from a source language; that is the source adapter's job).
14//!
15//! A field whose meaning depends on whether its value was set explicitly, rather than on the value
16//! alone, is a [`ConfigValue<T>`] instead of a plain `T`.
17
18use std::fmt;
19
20use serde::Serialize;
21
22pub mod control;
23pub mod defaults;
24pub mod domains;
25pub mod live;
26pub mod provenance;
27pub mod shared;
28
29pub use control::{ControlConfiguration, Logging};
30pub use domains::DomainConfiguration;
31pub use live::Live;
32pub use provenance::{ConfigValue, Provenance};
33pub use shared::SharedConfiguration;
34
35/// The complete ADP-native runtime configuration after translation.
36///
37/// Two writers fill it: the Datadog witness `drive` (schema fields) and `seed` (Saluki-only
38/// fields). They write disjoint fields. It is read by the orchestration layer (`control`) and by
39/// components at topology assembly (`shared` and `domains`).
40#[derive(Clone, Debug, Default, PartialEq, Serialize)]
41pub struct SalukiConfiguration {
42    /// Read first: decides which pipelines/topology to build. Orchestration layer only.
43    pub control: ControlConfiguration,
44    /// Cross-cutting values consumed by more than one domain, each with a single home.
45    pub shared: SharedConfiguration,
46    /// Per-domain resolved config, grouped by ownership domain.
47    pub domains: DomainConfiguration,
48}
49
50/// An error produced while translating a `DatadogConfiguration` or `SalukiOnly` value into a
51/// `SalukiConfiguration` value.
52#[derive(Debug)]
53pub struct Error {
54    context: String,
55    error: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
56}
57
58/// A boxed source error that can cross thread boundaries.
59pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
60
61impl Error {
62    /// Create an error that wraps an underlying error.
63    ///
64    /// Displays `{context}: {error}`.
65    pub fn new<S, E>(context: S, error: E) -> Self
66    where
67        S: Into<String>,
68        E: Into<BoxError>,
69    {
70        Self {
71            context: context.into(),
72            error: Some(error.into()),
73        }
74    }
75
76    /// Create an error without an underlying source error.
77    pub fn new_without_source<S>(context: S) -> Self
78    where
79        S: Into<String>,
80    {
81        Self {
82            context: context.into(),
83            error: None,
84        }
85    }
86}
87
88impl fmt::Display for Error {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(f, "{}", self.context)?;
91        if let Some(error) = &self.error {
92            write!(f, ": {error}")?;
93        }
94        Ok(())
95    }
96}
97
98impl std::error::Error for Error {
99    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
100        self.error.as_deref().map(|s| s as &(dyn std::error::Error + 'static))
101    }
102}