saluki_env/workload/collectors/
containerd.rs1use std::{path::PathBuf, time::Duration};
2
3use async_stream::stream;
4use async_trait::async_trait;
5use containerd_protos::services::namespaces::v1::Namespace;
6use futures::{stream::select_all, Stream, StreamExt as _};
7use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
8use saluki_core::health::Health;
9use saluki_error::GenericError;
10use saluki_metrics::{static_metrics, Counter};
11use stringtheory::interning::{GenericMapInterner, Interner as _};
12use tokio::{select, sync::mpsc, time::sleep};
13use tracing::{debug, error, warn};
14
15use super::MetadataCollector;
16use crate::workload::{
17 entity::EntityId,
18 helpers::containerd::{
19 events::{ContainerdEvent, ContainerdTopic},
20 ContainerdClient, ContainerdConfiguration,
21 },
22 metadata::MetadataOperation,
23};
24
25static CONTAINERD_WATCH_EVENTS: &[ContainerdTopic] = &[ContainerdTopic::TaskStarted, ContainerdTopic::TaskDeleted];
26
27#[static_metrics(prefix = containerd_metadata_collector, labels(namespace))]
28#[derive(Clone)]
29struct Telemetry {
30 rpc_errors_total: Counter,
31 intern_failed_total: Counter,
32 events_task_started_total: Counter,
33 events_task_deleted_total: Counter,
34}
35
36pub struct ContainerdMetadataCollector {
38 client: ContainerdClient,
39 watched_namespaces: Vec<Namespace>,
40 tag_interner: GenericMapInterner,
41 health: Health,
42}
43
44impl ContainerdMetadataCollector {
45 pub async fn new(
54 socket_path: Option<PathBuf>, containerd_config: &ContainerdConfiguration, health: Health,
55 tag_interner: GenericMapInterner,
56 ) -> Result<Self, GenericError> {
57 let client = ContainerdClient::new(socket_path, containerd_config).await?;
58 let watched_namespaces = client.list_namespaces().await?;
59
60 Ok(Self {
61 client,
62 watched_namespaces,
63 tag_interner,
64 health,
65 })
66 }
67}
68
69#[async_trait]
70impl MetadataCollector for ContainerdMetadataCollector {
71 fn name(&self) -> &'static str {
72 "containerd"
73 }
74
75 async fn watch(&mut self, operations_tx: &mut mpsc::Sender<MetadataOperation>) -> Result<(), GenericError> {
76 self.health.mark_ready();
77
78 let watchers = self
81 .watched_namespaces
82 .iter()
83 .map(|ns| NamespaceWatcher::new(self.client.clone(), ns.clone(), self.tag_interner.clone()).watch());
84
85 let mut operations_stream = select_all(watchers);
86
87 loop {
88 select! {
89 _ = self.health.live() => {},
90 maybe_operation = operations_stream.next() => match maybe_operation {
91 Some(operation) => {
92 operations_tx.send(operation).await?;
93 },
94 None => break,
95 },
96 }
97 }
98
99 self.health.mark_not_ready();
100
101 Ok(())
102 }
103}
104
105impl MemoryBounds for ContainerdMetadataCollector {
106 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
107 builder
110 .firm()
111 .with_fixed_amount("self struct", std::mem::size_of::<Self>());
112 }
113}
114
115struct NamespaceWatcher {
116 namespace: Namespace,
117 client: ContainerdClient,
118 tag_interner: GenericMapInterner,
119 telemetry: Telemetry,
120}
121
122impl NamespaceWatcher {
123 fn new(client: ContainerdClient, namespace: Namespace, tag_interner: GenericMapInterner) -> Self {
124 let telemetry = Telemetry::new(&namespace.name);
125 Self {
126 client,
127 namespace,
128 tag_interner,
129 telemetry,
130 }
131 }
132
133 async fn process_event(&self, event: ContainerdEvent) -> Option<MetadataOperation> {
134 match event {
135 ContainerdEvent::TaskStarted { id, pid } => {
136 self.telemetry.events_task_started_total().increment(1);
137 let pid_entity_id = EntityId::ContainerPid(pid);
138 let container_entity_id = EntityId::Container(id);
139 Some(MetadataOperation::add_alias(pid_entity_id, container_entity_id))
140 }
141 ContainerdEvent::TaskDeleted { pid, .. } => {
142 self.telemetry.events_task_deleted_total().increment(1);
143 Some(MetadataOperation::delete(EntityId::ContainerPid(pid)))
144 }
145 }
146 }
147
148 async fn build_initial_metadata_operations(&self) -> Option<Vec<MetadataOperation>> {
149 let mut operations = Vec::new();
150
151 let containers = match self.client.list_containers(&self.namespace).await {
153 Ok(containers) => containers,
154 Err(e) => {
155 self.telemetry.rpc_errors_total().increment(1);
156 error!(namespace = self.namespace.name, error = %e, "Error listing containers.");
157 return None;
158 }
159 };
160
161 for container in containers {
162 let pids = match self
163 .client
164 .list_pids_for_container(&self.namespace, container.id.clone())
165 .await
166 {
167 Ok(pids) => pids,
168 Err(e) => {
169 if let Some(status) = e.as_response_error() {
170 if status.code() == tonic::Code::NotFound {
171 continue;
174 }
175 }
176
177 self.telemetry.rpc_errors_total().increment(1);
178 error!(namespace = self.namespace.name, container_id = container.id, error = %e, "Error getting PIDs for container.");
179 continue;
180 }
181 };
182
183 for pid in pids {
184 let pid_entity_id = EntityId::ContainerPid(pid);
185
186 match self.tag_interner.try_intern(container.id.as_str()) {
187 Some(container_id) => {
188 let container_entity_id = EntityId::Container(container_id.into());
189 operations.push(MetadataOperation::add_alias(pid_entity_id, container_entity_id));
190 }
191 None => {
192 self.telemetry.intern_failed_total().increment(1);
193 warn!(
194 namespace = self.namespace.name,
195 container_id = container.id,
196 container_task_pid = pid,
197 "Failed to intern container ID. Container ID/task PID link will not be created."
198 );
199 }
200 }
201 }
202 }
203
204 Some(operations)
205 }
206
207 fn watch(self) -> impl Stream<Item = MetadataOperation> + Unpin {
208 debug!(
209 namespace = self.namespace.name,
210 "Starting containerd namespace watcher."
211 );
212
213 Box::pin(stream! {
216 if let Some(initial_operations) = self.build_initial_metadata_operations().await {
219 for operation in initial_operations {
220 yield operation;
221 }
222 }
223
224 loop {
226 let mut event_stream = match self.client.watch_events(CONTAINERD_WATCH_EVENTS, &self.namespace).await {
229 Ok(stream) => stream,
230 Err(e) => {
231 self.telemetry.rpc_errors_total().increment(1);
232 error!(namespace = self.namespace.name, error = %e, "Error watching container events.");
233
234 sleep(Duration::from_secs(1)).await;
235 continue;
236 },
237 };
238
239 while let Some(event_result) = event_stream.next().await {
240 let event = match event_result {
241 Ok(event) => event,
242 Err(e) => {
243 error!(namespace = self.namespace.name, error = %e, "Error watching container events.");
244 continue;
245 },
246 };
247
248 if let Some(operation) = self.process_event(event).await {
249 yield operation;
250 }
251 }
252 }
253 })
254 }
255}