Skip to main content

datadog_agent_runtime/env/workload/
mod.rs

1//! A workload provider based on the Datadog Agent's remote tagger and workloadmeta APIs.
2
3use std::{future::Future, num::NonZeroUsize, time::Duration};
4
5use datadog_agent_commons::ipc::config::RemoteAgentClientConfiguration;
6use saluki_core::{
7    accounting::{ComponentRegistry, MemoryBounds, MemoryBoundsBuilder},
8    data_model::{
9        origin::{OriginTagCardinality, RawOrigin},
10        tags::SharedTagSet,
11    },
12    health::{Health, HealthRegistry},
13    runtime::{RestartStrategy, Supervisor},
14    support::SubsystemIdentifier,
15};
16#[cfg(unix)]
17use saluki_env::features::Feature;
18#[cfg(target_os = "linux")]
19use saluki_env::workload::{collectors::CgroupsMetadataCollector, CgroupsConfiguration};
20#[cfg(unix)]
21use saluki_env::workload::{collectors::ContainerdMetadataCollector, ContainerdConfiguration};
22use saluki_env::{
23    features::FeatureDetector,
24    workload::{
25        aggregator::MetadataAggregator,
26        collectors::MetadataCollectorWorker,
27        entity::EntityId,
28        origin::{OriginResolver, ResolvedOrigin},
29        stores::{ExternalDataStore, TagStore, TagStoreQuerier},
30        OnDemandPIDResolver,
31    },
32    CaptureEntityResolver, WorkloadProvider,
33};
34use saluki_error::{generic_error, GenericError};
35use stringtheory::interning::GenericMapInterner;
36
37mod api;
38use self::api::RemoteAgentWorkloadAPIWorker;
39use crate::env::{root_provider_id, WorkloadSettings};
40
41mod collectors;
42use self::collectors::{RemoteAgentTaggerMetadataCollector, RemoteAgentWorkloadMetadataCollector};
43
44// TODO: Make these configurable.
45
46// SAFETY: The value is demonstrably not zero.
47const DEFAULT_TAG_STORE_ENTITY_LIMIT: NonZeroUsize = NonZeroUsize::new(2000).unwrap();
48
49// SAFETY: The value is demonstrably not zero.
50const DEFAULT_EXTERNAL_DATA_STORE_ENTITY_LIMIT: NonZeroUsize = NonZeroUsize::new(2000).unwrap();
51
52/// Datadog Agent-based workload provider.
53///
54/// This provider is based primarily on the remote tagger API exposed by the Datadog Agent, which handles the bulk of
55/// the work by collecting and aggregating tags for container entities. This remote tagger API operates in a streaming
56/// fashion, which the provider uses to stream update operations to the tag store.
57///
58/// Additionally, two collectors are optionally used: a `containerd` collector and a `cgroups` collector. The
59/// `containerd` collector will, if containerd is running, be used to collect metadata that allows mapping container
60/// PIDs (UDS-based Origin Detection) to container IDs. The `cgroups` collector will collect metadata about the current
61/// set of cgroups v1/v2 controllers, tracking any controllers which appear related to containers and storing a mapping
62/// of controller inodes to container IDs.
63///
64/// These additional collectors are necessary to bridge the gap from container PID and cgroup controller inode, as the
65/// remote tagger API doesn't stream us these mappings itself and only deals with resolved container IDs.
66#[derive(Clone)]
67pub struct RemoteAgentWorkloadProvider {
68    tags_querier: TagStoreQuerier,
69    origin_resolver: OriginResolver,
70    on_demand_pid_resolver: OnDemandPIDResolver,
71}
72
73impl RemoteAgentWorkloadProvider {
74    /// Creates a provider and the [`Supervisor`] that drives its collectors.
75    ///
76    /// # Errors
77    ///
78    /// If there is an issue creating the underlying metadata collectors, an error is returned.
79    pub async fn new(
80        settings: &WorkloadSettings, client_config: &RemoteAgentClientConfiguration,
81        component_registry: &ComponentRegistry, health_registry: &HealthRegistry,
82    ) -> Result<(Self, Supervisor), GenericError> {
83        let string_interner_size_bytes = settings.string_interner_size_bytes;
84        let workload_provider_id = root_provider_id().child("workload").child("remote_agent");
85        let mut provider_bounds = component_registry.bounds_builder(&workload_provider_id);
86
87        let string_interner = GenericMapInterner::new(string_interner_size_bytes);
88
89        provider_bounds
90            .subcomponent("string_interner")
91            .firm()
92            .with_fixed_amount("string interner", string_interner_size_bytes.get());
93
94        // Construct our metadata aggregator and any relevant metadata collectors based on the detected features we've
95        // been given.
96        let aggregator_id = workload_provider_id.clone().child("aggregator");
97        let aggregator_health = health_registry
98            .register_component(&aggregator_id)
99            .ok_or_else(|| generic_error!("Component '{aggregator_id}' already registered in health registry."))?;
100        let (mut aggregator, operations_tx) = MetadataAggregator::new(aggregator_health);
101
102        let collectors_root = workload_provider_id.child("collectors");
103        let mut collector_bounds = provider_bounds.subcomponent("collectors");
104        let mut collector_workers: Vec<MetadataCollectorWorker> = Vec::new();
105
106        let containerd_socket_path = settings.containerd_socket_path.clone();
107        let container_proc_root = settings.proc_root.clone();
108        let container_cgroup_root = settings.cgroup_root.clone();
109
110        let feature_detector = FeatureDetector::automatic(containerd_socket_path.clone());
111
112        // Add the containerd collector if the feature is available.
113        #[cfg(unix)]
114        if feature_detector.is_feature_available(Feature::Containerd) {
115            let containerd_config = ContainerdConfiguration {
116                connection_timeout: settings.containerd_connection_timeout,
117                query_timeout: settings.containerd_query_timeout,
118            };
119            let cri_collector = build_collector(
120                &collectors_root,
121                "containerd",
122                health_registry,
123                &mut collector_bounds,
124                |health| {
125                    ContainerdMetadataCollector::new(
126                        containerd_socket_path.clone(),
127                        &containerd_config,
128                        health,
129                        string_interner.clone(),
130                    )
131                },
132            )
133            .await?;
134
135            collector_workers.push(MetadataCollectorWorker::new(cri_collector, operations_tx.clone()));
136        }
137
138        // Add the cgroups collector if the feature if we're on Linux.
139        #[cfg(target_os = "linux")]
140        {
141            let cgroups_config = CgroupsConfiguration::new(
142                container_proc_root.clone(),
143                container_cgroup_root.clone(),
144                &feature_detector,
145            );
146            let cgroups_collector = build_collector(
147                &collectors_root,
148                "cgroups",
149                health_registry,
150                &mut collector_bounds,
151                |health| CgroupsMetadataCollector::new(&cgroups_config, health, string_interner.clone()),
152            )
153            .await?;
154
155            collector_workers.push(MetadataCollectorWorker::new(cgroups_collector, operations_tx.clone()));
156        }
157
158        // Finally, add the Remote Agent collectors: one for the tagger, and one for workloadmeta.
159        let ra_tags_collector = build_collector(
160            &collectors_root,
161            "remote_agent_tags",
162            health_registry,
163            &mut collector_bounds,
164            |health| RemoteAgentTaggerMetadataCollector::new(client_config, health, string_interner.clone()),
165        )
166        .await?;
167
168        collector_workers.push(MetadataCollectorWorker::new(ra_tags_collector, operations_tx.clone()));
169
170        let ra_wmeta_collector = build_collector(
171            &collectors_root,
172            "remote_agent_wmeta",
173            health_registry,
174            &mut collector_bounds,
175            |health| RemoteAgentWorkloadMetadataCollector::new(client_config, health, string_interner.clone()),
176        )
177        .await?;
178
179        collector_workers.push(MetadataCollectorWorker::new(ra_wmeta_collector, operations_tx));
180
181        // Create and attach the various metadata stores.
182        let tag_store = TagStore::with_entity_limit(DEFAULT_TAG_STORE_ENTITY_LIMIT);
183        let tags_querier = tag_store.querier();
184
185        aggregator.add_store(tag_store);
186
187        let external_data_store = ExternalDataStore::with_entity_limit(DEFAULT_EXTERNAL_DATA_STORE_ENTITY_LIMIT);
188        let eds_resolver = external_data_store.resolver();
189
190        aggregator.add_store(external_data_store);
191
192        let on_demand_pid_resolver = OnDemandPIDResolver::new(
193            container_proc_root,
194            container_cgroup_root,
195            &feature_detector,
196            string_interner,
197        )?;
198        let origin_resolver = OriginResolver::new(eds_resolver.clone());
199
200        // With the aggregator configured, update the memory bounds before handing it off to the supervisor.
201        provider_bounds.with_subcomponent("aggregator", &aggregator);
202
203        let api_worker = RemoteAgentWorkloadAPIWorker::from_state(tags_querier.clone(), eds_resolver.clone());
204
205        // Build the workload supervisor.
206        let mut supervisor = Supervisor::new("workload")?
207            .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(5, Duration::from_secs(30)));
208        supervisor.add_worker(aggregator);
209        for worker in collector_workers {
210            supervisor.add_worker(worker);
211        }
212        supervisor.add_worker(api_worker);
213
214        let provider = Self {
215            tags_querier,
216            origin_resolver,
217            on_demand_pid_resolver,
218        };
219
220        Ok((provider, supervisor))
221    }
222}
223
224impl WorkloadProvider for RemoteAgentWorkloadProvider {
225    fn get_tags_for_entity(&self, entity_id: &EntityId, cardinality: OriginTagCardinality) -> Option<SharedTagSet> {
226        // Query the tag store for the tags associated with the given entity ID.
227        match self.tags_querier.get_entity_tags(entity_id, cardinality) {
228            Some(tags) => Some(tags),
229            None => {
230                // If no tags came back, check if the entity ID is a PID. If it is, we can try to resolve it to a
231                // container ID first before trying again.
232                if let EntityId::ContainerPid(pid) = entity_id {
233                    if let Some(container_id) = self.on_demand_pid_resolver.resolve(*pid) {
234                        // If we successfully resolved the PID to a container ID, try again.
235                        return self.tags_querier.get_entity_tags(&container_id, cardinality);
236                    }
237                }
238
239                None
240            }
241        }
242    }
243
244    fn get_self_container_tags(&self) -> Option<SharedTagSet> {
245        let self_container_entity = self.on_demand_pid_resolver.resolve_self_container()?;
246        self.tags_querier
247            .get_entity_tags(&self_container_entity, OriginTagCardinality::Low)
248    }
249
250    fn get_resolved_origin(&self, origin: RawOrigin<'_>) -> Option<ResolvedOrigin> {
251        self.origin_resolver.get_resolved_origin(origin)
252    }
253}
254
255impl CaptureEntityResolver for RemoteAgentWorkloadProvider {
256    fn resolve_container_entity_for_live_pid(&self, process_id: u32) -> Option<EntityId> {
257        self.on_demand_pid_resolver.resolve(process_id)
258    }
259}
260
261async fn build_collector<F, Fut, O>(
262    collectors_root: &SubsystemIdentifier, collector_name: &str, health_registry: &HealthRegistry,
263    bounds_builder: &mut MemoryBoundsBuilder<'_>, build: F,
264) -> Result<O, GenericError>
265where
266    F: FnOnce(Health) -> Fut,
267    Fut: Future<Output = Result<O, GenericError>>,
268    O: MemoryBounds,
269{
270    let collector_id = collectors_root.clone().child(collector_name);
271    let health = health_registry
272        .register_component(&collector_id)
273        .ok_or_else(|| generic_error!("Component '{collector_id}' already registered in health registry."))?;
274    let collector = build(health).await?;
275    bounds_builder.with_subcomponent(collector_name, &collector);
276
277    Ok(collector)
278}