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}