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