Skip to main content

datadog_agent_runtime/facade/
mod.rs

1//! A staged facade that drives the whole startup sequence of a remote agent.
2//!
3//! [`RemoteAgent`] walks through the stages in [`stage`], each of which hands control back to the binary at the point
4//! where it has work of its own to do:
5//!
6//! 1. [`RemoteAgent::start`] registers with the Datadog Agent, waits for the initial configuration, and reloads logging
7//!    to match it. In the [`Started`] stage, the binary can check its configuration before anything else starts.
8//! 2. [`RemoteAgent::initialize`] creates the registries and the environment provider, which opens the connections for
9//!    the workload provider. In the [`Initialized`] stage, the binary builds its topology and its control plane
10//!    supervisor, and adds the supervisor from [`RemoteAgent::create_remote_agent_supervisor`] to its control plane.
11//!    The binary can also subscribe to Remote Configuration products through [`RemoteAgent::remote_config_client`].
12//! 3. [`RemoteAgent::run`] creates the internal supervisor around the binary's control plane, validates the memory
13//!    bounds of everything registered so far, and runs the supervision tree until the process is asked to shut down.
14//!
15//! The [crate documentation](crate#supervision-tree) shows the resulting supervision tree.
16
17use std::{
18    future::Future,
19    sync::Arc,
20    time::{Duration, Instant},
21};
22
23use datadog_agent_commons::ipc::config::RemoteAgentClientConfiguration;
24use datadog_agent_remote_config::RemoteConfigurationClient;
25use saluki_app::{
26    accounting::initialize_memory_bounds, bootstrap::BootstrapGuard, metrics::emit_startup_metrics,
27    util::wait_for_shutdown_signal,
28};
29use saluki_core::{
30    accounting::ComponentRegistry,
31    health::HealthRegistry,
32    observability::metrics::RemapperRule,
33    runtime::{self, state::ResourceRegistry, RestartMode, RestartStrategy, Supervisor, SupervisorError},
34    topology::TopologyBlueprint,
35};
36use saluki_error::{ErrorContext as _, GenericError};
37use tracing::{info, warn};
38
39use crate::{
40    control_plane::ConfigUpdatesWorker,
41    env::RemoteAgentEnvironmentProvider,
42    logging::LoggingTranslator,
43    remote_agent::{RemoteAgentBootstrap, StatusSectionProvider},
44};
45
46mod config;
47pub use self::config::{AuthoritativeConfiguration, LocalConfiguration, RemoteAgentSpecification};
48
49mod remote_config;
50
51pub mod stage;
52pub use self::stage::{Initialized, Started, Unstarted};
53use self::stage::{InitializedData, Stage, StartedData, UnstartedData};
54
55/// The remote agent startup sequence.
56///
57/// `R` is the binary's [`RemoteAgentSpecification`], and `S` is the current [stage], which determines the methods
58/// available.
59pub struct RemoteAgent<R, S = Unstarted>
60where
61    R: RemoteAgentSpecification,
62    S: Stage,
63{
64    inner: S::Data<R>,
65}
66
67impl<R> RemoteAgent<R, Unstarted>
68where
69    R: RemoteAgentSpecification,
70{
71    /// Creates a new `RemoteAgent` from its local configuration.
72    ///
73    /// `translator` translates the authoritative logging settings once they're known.
74    pub fn new(local: R::LocalConfiguration, translator: LoggingTranslator) -> Self {
75        Self {
76            inner: UnstartedData {
77                local,
78                translator,
79                start_time: Instant::now(),
80            },
81        }
82    }
83
84    /// Sets the time the process started, which is used to report how long the topology took to become healthy.
85    ///
86    /// Defaults to the time the `RemoteAgent` was created.
87    pub fn with_start_time(mut self, start_time: Instant) -> Self {
88        self.inner.start_time = start_time;
89        self
90    }
91
92    /// Selects the configuration authority and reloads logging to match it.
93    ///
94    /// In connected mode, this registers with the Datadog Agent and waits for its initial configuration. In standalone
95    /// mode, the local configuration becomes authoritative.
96    ///
97    /// `bootstrap_supervisor` is the supervisor returned by the process bootstrap, which runs as the first child of the
98    /// root supervisor.
99    ///
100    /// # Errors
101    ///
102    /// If registration fails, or the configuration can't be loaded, an error is returned.
103    pub async fn start(
104        self, bootstrap_guard: &mut BootstrapGuard, bootstrap_supervisor: Supervisor,
105    ) -> Result<RemoteAgent<R, Started>, GenericError> {
106        let UnstartedData {
107            local,
108            translator,
109            start_time,
110        } = self.inner;
111        let app_details = saluki_metadata::get_app_details();
112        info!(
113            version = app_details.version().raw(),
114            git_hash = app_details.git_hash(),
115            target_arch = app_details.target_arch(),
116            build_time = app_details.build_time(),
117            process_id = std::process::id(),
118            "{} starting...",
119            app_details.full_name()
120        );
121
122        // Use local sources for decisions required before the Agent configuration stream is available.
123        let standalone = local.is_standalone();
124
125        // Resolve the authoritative configuration. Connected mode registers as a remote agent and takes the Core
126        // Agent's config stream as authority; standalone mode treats the local sources as authoritative. Both block
127        // until the first configuration is received and validated, failing the boot if it cannot be -- the strict
128        // startup gate.
129        let (config, ra_bootstrap, config_updates) = if standalone {
130            let config = local
131                .into_standalone()
132                .await
133                .error_context("Failed to load configuration.")?;
134            (config, None, None)
135        } else {
136            // Blocks until the Core Agent acknowledges registration.
137            let client_config = local.client_configuration()?;
138            let ra_bootstrap = async {
139                let secure_api_listen_address = local.advertised_secure_api_address()?;
140                RemoteAgentBootstrap::new(&client_config, &secure_api_listen_address).await
141            }
142            .await
143            .error_context("Failed to bootstrap remote agent state.")?;
144
145            // The configuration system owns the config stream. `ra_bootstrap` is kept to build the remote agent workers
146            // in `create_remote_agent_supervisor`.
147            let config_stream = ra_bootstrap.create_config_stream();
148
149            info!("Waiting for initial configuration from Datadog Agent...");
150            let (config, config_updates) = local
151                .connect(config_stream)
152                .await
153                .error_context("Failed to load initial configuration from the Datadog Agent.")?;
154            info!("Initial configuration received.");
155
156            (config, Some(ra_bootstrap), Some(config_updates))
157        };
158
159        // Connected mode may have replaced the bootstrap-phase settings with the Agent's authoritative config, so
160        // reload logging to match. Standalone resolves the same local sources seen at bootstrap, making a reload
161        // redundant.
162        if !standalone {
163            match translator.translate(config.logging()) {
164                Ok(logging_config) => {
165                    if let Err(e) = bootstrap_guard.logging_mut().reload(logging_config).await {
166                        warn!(
167                            error = %e,
168                            "Failed to reload logging from Agent configuration; continuing with bootstrap logging settings."
169                        );
170                    }
171                }
172                Err(e) => warn!(
173                    error = %e,
174                    "Failed to translate logging configuration from Agent; continuing with bootstrap logging settings."
175                ),
176            }
177        }
178
179        Ok(RemoteAgent {
180            inner: StartedData {
181                config,
182                ra_bootstrap,
183                config_updates,
184                bootstrap_supervisor,
185                start_time,
186            },
187        })
188    }
189}
190
191impl<R> RemoteAgent<R, Started>
192where
193    R: RemoteAgentSpecification,
194{
195    /// Returns the authoritative configuration.
196    pub fn config(&self) -> &R::AuthoritativeConfiguration {
197        &self.inner.config
198    }
199
200    /// Returns `true` if the remote agent runs without the Datadog Agent.
201    pub fn is_standalone(&self) -> bool {
202        self.inner.config.is_standalone()
203    }
204
205    /// Creates the registries and the environment provider.
206    ///
207    /// In connected mode, the environment provider connects to the Datadog Agent for the hostname, autodiscovery, and
208    /// workload metadata. In standalone mode, it reports the configured hostname, and nothing else.
209    ///
210    /// # Errors
211    ///
212    /// If the environment provider can't be created, an error is returned.
213    pub async fn initialize(self) -> Result<RemoteAgent<R, Initialized>, GenericError> {
214        // Create the root supervisor up front so that the binary's control plane can serve a handle to the whole
215        // supervision tree. Nothing is added to it until the rest of the tree is built.
216        //
217        // The name can't be a bare `root`: it would collide with the built-in `root` resource group.
218        let root_supervisor_name = format!("{}-root", saluki_metadata::get_app_details().identifier());
219        let root_restart_strategy = RestartStrategy::new(RestartMode::OneForOne, 0, Duration::from_secs(5));
220        let root_supervisor = Supervisor::new(root_supervisor_name)?.with_restart_strategy(root_restart_strategy);
221
222        // Set up all of the building blocks for building topologies and launching internal processes.
223        let component_registry = ComponentRegistry::default();
224        let health_registry = HealthRegistry::new();
225        let resource_registry = ResourceRegistry::new();
226        let (env_provider, env_supervisor) = RemoteAgentEnvironmentProvider::from_authoritative(
227            &self.inner.config,
228            &component_registry,
229            &health_registry,
230        )
231        .await?;
232
233        Ok(RemoteAgent {
234            inner: InitializedData {
235                started: self.inner,
236                root_supervisor,
237                component_registry,
238                health_registry,
239                resource_registry,
240                env_provider,
241                env_supervisor,
242                remote_config: None,
243            },
244        })
245    }
246}
247
248impl<R> RemoteAgent<R, Initialized>
249where
250    R: RemoteAgentSpecification,
251{
252    /// Returns the authoritative configuration.
253    pub fn config(&self) -> &R::AuthoritativeConfiguration {
254        &self.inner.started.config
255    }
256
257    /// Returns `true` if the remote agent runs without the Datadog Agent.
258    pub fn is_standalone(&self) -> bool {
259        self.inner.started.config.is_standalone()
260    }
261
262    /// Returns the environment provider.
263    pub fn env_provider(&self) -> &RemoteAgentEnvironmentProvider {
264        &self.inner.env_provider
265    }
266
267    /// Returns the component registry, which the topology registers its components and memory bounds with.
268    pub fn component_registry(&self) -> &ComponentRegistry {
269        &self.inner.component_registry
270    }
271
272    /// Returns the health registry.
273    pub fn health_registry(&self) -> &HealthRegistry {
274        &self.inner.health_registry
275    }
276
277    /// Returns the configuration for connecting to the Datadog Agent, or `None` in standalone mode.
278    pub fn client_configuration(&self) -> Option<&RemoteAgentClientConfiguration> {
279        self.inner.env_provider.client_configuration()
280    }
281
282    /// Returns the Remote Configuration client, connecting to the Datadog Agent on the first call.
283    ///
284    /// The polling worker runs under the internal supervisor once the remote agent runs, so subscriptions made before
285    /// then are all included in its first poll. Binaries should only call this when they have products to subscribe
286    /// to, since connecting starts the worker polling.
287    ///
288    /// Returns `None` in standalone mode, or if Remote Configuration is turned off.
289    ///
290    /// # Errors
291    ///
292    /// If the client can't connect to the Datadog Agent, an error is returned.
293    pub async fn remote_config_client(&mut self) -> Result<Option<RemoteConfigurationClient>, GenericError> {
294        if let Some((client, _)) = &self.inner.remote_config {
295            return Ok(Some(client.clone()));
296        }
297
298        let Some(client_config) = self.inner.env_provider.client_configuration() else {
299            return Ok(None);
300        };
301        if !self.inner.started.config.remote_config_enabled() {
302            return Ok(None);
303        }
304
305        let (client, worker) = remote_config::connect(client_config).await?;
306        self.inner.remote_config = Some((client.clone(), worker));
307        Ok(Some(client))
308    }
309
310    /// Creates the supervisor for the parts of the control plane that only the facade can build.
311    ///
312    /// The supervisor is named `remote-agent`, and must be added to the binary's control plane supervisor. It serves the
313    /// supervision tree on the privileged API endpoint and, in connected mode, runs the remote agent workers from
314    /// [`RemoteAgentBootstrap`]: it serves the status, flare, and telemetry services, and reports diagnostic events to
315    /// the Datadog Agent.
316    ///
317    /// `status_sections` and `telemetry_rules` are passed to [`RemoteAgentBootstrap::create_services_worker`].
318    ///
319    /// # Errors
320    ///
321    /// If the supervisor can't be created, an error is returned.
322    pub fn create_remote_agent_supervisor(
323        &self, status_sections: Vec<Arc<dyn StatusSectionProvider>>, telemetry_rules: Vec<RemapperRule>,
324    ) -> Result<Supervisor, GenericError> {
325        let mut supervisor = Supervisor::new("remote-agent")?.with_restart_strategy(RestartStrategy::one_to_one());
326
327        supervisor.add_worker(self.inner.root_supervisor.tree_handle().worker());
328        if let Some(ra_bootstrap) = &self.inner.started.ra_bootstrap {
329            supervisor.add_worker(ra_bootstrap.create_dataspace_anchor());
330            supervisor.add_worker(ra_bootstrap.create_event_reporter());
331            supervisor.add_worker(ra_bootstrap.create_services_worker(status_sections, telemetry_rules));
332        }
333
334        Ok(supervisor)
335    }
336
337    /// Runs the remote agent until the process receives a shutdown signal.
338    ///
339    /// See [`run_until`](Self::run_until) for the parameters.
340    ///
341    /// # Errors
342    ///
343    /// If the remote agent can't be assembled, or the supervision tree fails, an error is returned.
344    pub async fn run(self, blueprint: TopologyBlueprint, control_plane: Supervisor) -> Result<(), GenericError> {
345        self.run_until(blueprint, control_plane, wait_for_shutdown_signal())
346            .await
347    }
348
349    /// Runs the remote agent until `shutdown` resolves.
350    ///
351    /// This creates the internal supervisor around `control_plane`, validates the memory bounds, and then runs the
352    /// supervision tree with `blueprint` as the topology.
353    ///
354    /// `control_plane` is the binary's control plane supervisor, which must include the supervisor from
355    /// [`create_remote_agent_supervisor`](Self::create_remote_agent_supervisor). The memory bounds cover every
356    /// component registered with the component registry, so `blueprint` must be fully built.
357    ///
358    /// A shutdown in which some components had to be stopped forcefully, after exceeding their shutdown timeout, is
359    /// logged as a warning rather than returned as an error, since the process did stop.
360    ///
361    /// # Errors
362    ///
363    /// If the internal supervisor can't be created, the memory bounds can't be validated, or the supervision tree
364    /// fails, an error is returned.
365    pub async fn run_until<F>(
366        self, mut blueprint: TopologyBlueprint, control_plane: Supervisor, shutdown: F,
367    ) -> Result<(), GenericError>
368    where
369        F: Future<Output = ()> + Send + 'static,
370    {
371        let InitializedData {
372            started,
373            mut root_supervisor,
374            component_registry,
375            health_registry,
376            resource_registry,
377            env_provider,
378            env_supervisor,
379            remote_config,
380        } = self.inner;
381        let StartedData {
382            config,
383            ra_bootstrap: _,
384            config_updates,
385            bootstrap_supervisor,
386            start_time,
387        } = started;
388        let full_name = saluki_metadata::get_app_details().full_name();
389
390        root_supervisor.add_worker(bootstrap_supervisor);
391
392        // Create the internal supervisor which drives the control plane and internal observability.
393        //
394        // The internal supervisor runs in ambient mode (the caller's runtime). The default restart strategy applies to
395        // its children as units.
396        let mut internal_supervisor =
397            Supervisor::new("internal-sup").error_context("Failed to create internal supervisor.")?;
398        internal_supervisor.add_worker(control_plane);
399        internal_supervisor.add_worker(resource_registry.worker());
400
401        if let Some(env_supervisor) = env_supervisor {
402            internal_supervisor.add_worker(env_supervisor);
403        }
404
405        if let Some(config_updates) = config_updates {
406            internal_supervisor.add_worker(ConfigUpdatesWorker::new(config_updates));
407        }
408
409        // The worker shares the client's subscriptions, so dropping the client here ends none of them.
410        if let Some((_, worker)) = remote_config {
411            internal_supervisor.add_worker(remote_config::remote_config_child(worker)?);
412        }
413
414        root_supervisor.add_worker(internal_supervisor);
415
416        // Run memory bounds validation to ensure that the topology can launch within the configured memory limit, if
417        // any.
418        let memory_limiter = initialize_memory_bounds(config.memory_bounds(), component_registry.root())?;
419
420        // Configure our topology blueprint with all of the relevant registries and ambient capabilities, which
421        // ultimately get used when creating components, and sometimes exposed to them at runtime.
422        blueprint
423            .with_health_registry(health_registry.clone())
424            .with_memory_limiter(memory_limiter)
425            .with_resource_registry(resource_registry.clone())
426            .with_environment_readiness(env_provider.wait_for_ready());
427
428        // Spawn a simple task to report when the topology is healthy, which we have to do here before spawning
429        // the topology as it requires altering the blueprint state to attach itself to the ready signal.
430        spawn_topology_healthy_check(&mut blueprint, start_time);
431
432        root_supervisor.add_worker(runtime::supervisable(blueprint).needed().build());
433
434        info!("{full_name} running.");
435        match root_supervisor.run_with_shutdown(shutdown).await {
436            Ok(()) => {
437                info!("{full_name} shut down successfully.");
438                Ok(())
439            }
440            // The shutdown ran to completion, but one or more components ignored graceful shutdown and had to be
441            // forcefully aborted after exceeding the shutdown timeout. Surface that plainly instead of reporting
442            // success -- but don't treat it as a fatal error: the process did stop, and mapping it through the error
443            // path would mislabel it as a boot or setup failure.
444            Err(SupervisorError::ShutdownTimedOut { aborted }) => {
445                warn!(
446                    aborted,
447                    "{full_name} shut down uncleanly: {aborted} component(s) had to be forcefully stopped after \
448                     exceeding the shutdown timeout. Check the preceding warnings for which components were affected."
449                );
450                Ok(())
451            }
452            Err(e) => Err(e.into()),
453        }
454    }
455}
456
457fn spawn_topology_healthy_check(blueprint: &mut TopologyBlueprint, started: Instant) {
458    let topology_ready = blueprint.topology_ready();
459    tokio::spawn(async move {
460        // If the topology is torn down before it ever becomes ready (for example, shutdown during startup), `wait`
461        // returns `false` and we skip reporting readiness.
462        if topology_ready.wait().await {
463            info!(
464                topology_ready_ms = started.elapsed().as_millis(),
465                "Topology healthy. Waiting for interrupt..."
466            );
467
468            emit_startup_metrics();
469        }
470    });
471}