1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::Arc;
4
5use thiserror::Error;
6use tokio::sync::{Mutex, RwLock};
7use tracing::{debug, error, warn};
8
9use crate::MachineStats;
10use crate::MonitordStats;
11
12pub type MachineConnections = HashMap<String, (u32, zbus::Connection)>;
16
17#[derive(Debug, PartialEq)]
19enum CacheAction {
20 Reuse,
22 Replace,
24 Create,
26}
27
28#[derive(Error, Debug)]
29pub enum MonitordMachinesError {
30 #[error("Machines D-Bus error: {0}")]
31 ZbusError(#[from] zbus::Error),
32}
33
34pub fn filter_machines(
35 machines: Vec<crate::dbus::zbus_machines::ListedMachine>,
36 allowlist: &HashSet<String>,
37 blocklist: &HashSet<String>,
38) -> Vec<crate::dbus::zbus_machines::ListedMachine> {
39 machines
40 .into_iter()
41 .filter(|c| c.class == "container")
42 .filter(|c| !blocklist.contains(&c.name))
43 .filter(|c| allowlist.is_empty() || allowlist.contains(&c.name))
44 .collect()
45}
46
47pub async fn get_machines(
48 connection: &zbus::Connection,
49 config: &crate::config::Config,
50) -> Result<HashMap<String, u32>, MonitordMachinesError> {
51 let c = crate::dbus::zbus_machines::ManagerProxy::builder(connection)
52 .cache_properties(zbus::proxy::CacheProperties::No)
53 .build()
54 .await?;
55 let mut results = HashMap::<String, u32>::new();
56
57 let machines = c.list_machines().await?;
58
59 for machine in filter_machines(
60 machines,
61 &config.machines.allowlist,
62 &config.machines.blocklist,
63 ) {
64 let m = c.get_machine(&machine.name).await?;
65 let leader_pid = m.leader().await?;
66 results.insert(machine.name, leader_pid);
67 }
68
69 Ok(results)
70}
71
72fn decide_cache_action(cached_pid: Option<u32>, leader_pid: u32) -> CacheAction {
74 match cached_pid {
75 Some(pid) if pid == leader_pid => CacheAction::Reuse,
76 Some(_) => CacheAction::Replace,
77 None => CacheAction::Create,
78 }
79}
80
81async fn evict_stale_connections(
83 cached_connections: &Mutex<MachineConnections>,
84 current_machines: &HashMap<String, u32>,
85) {
86 let mut cache = cached_connections.lock().await;
87 cache.retain(|name, _| current_machines.contains_key(name));
88}
89
90async fn evict_failed_connection(cached_connections: &Mutex<MachineConnections>, machine: &str) {
92 debug!(
93 "Evicting cached D-Bus connection for {} due to errors",
94 machine
95 );
96 let mut cache = cached_connections.lock().await;
97 cache.remove(machine);
98}
99
100async fn get_or_create_connection(
103 config: &crate::config::Config,
104 cached_connections: &Mutex<MachineConnections>,
105 machine: &str,
106 leader_pid: u32,
107) -> anyhow::Result<zbus::Connection> {
108 {
110 let mut cache = cached_connections.lock().await;
111 match decide_cache_action(cache.get(machine).map(|(pid, _)| *pid), leader_pid) {
112 CacheAction::Reuse => {
113 debug!("Reusing cached D-Bus connection for {}", machine);
114 let (_, conn) = cache.get(machine).unwrap();
115 return Ok(conn.clone());
116 }
117 CacheAction::Replace => {
118 debug!(
119 "Leader PID changed for {}, dropping stale connection",
120 machine
121 );
122 cache.remove(machine);
123 }
124 CacheAction::Create => {}
125 }
126 }
127
128 debug!("Creating new D-Bus connection for {}", machine);
130 let container_address = format!(
131 "unix:path=/proc/{}/root/run/dbus/system_bus_socket",
132 leader_pid
133 );
134 let conn = zbus::connection::Builder::address(container_address.as_str())?
135 .method_timeout(std::time::Duration::from_secs(config.monitord.dbus_timeout))
136 .build()
137 .await?;
138
139 {
141 let mut cache = cached_connections.lock().await;
142 cache.insert(machine.to_string(), (leader_pid, conn.clone()));
143 }
144
145 Ok(conn)
146}
147
148pub async fn update_machines_stats(
149 config: Arc<crate::config::Config>,
150 connection: zbus::Connection,
151 locked_monitord_stats: Arc<RwLock<MonitordStats>>,
152 cached_connections: Arc<Mutex<MachineConnections>>,
153) -> anyhow::Result<()> {
154 let locked_machine_stats: Arc<RwLock<MachineStats>> =
155 Arc::new(RwLock::new(MachineStats::default()));
156
157 let current_machines = get_machines(&connection, &config).await?;
158
159 evict_stale_connections(&cached_connections, ¤t_machines).await;
160
161 for (machine, leader_pid) in current_machines.into_iter() {
162 debug!(
163 "Collecting container: machine: {} leader_pid: {}",
164 machine, leader_pid
165 );
166
167 let sdc = match get_or_create_connection(&config, &cached_connections, &machine, leader_pid)
168 .await
169 {
170 Ok(conn) => conn,
171 Err(e) => {
172 error!("Failed to connect to container {}: {:?}", machine, e);
173 continue;
174 }
175 };
176
177 let mut join_set = tokio::task::JoinSet::new();
178
179 if config.pid1.enabled {
180 join_set.spawn(crate::pid1::update_pid1_stats(
181 leader_pid as i32,
182 locked_machine_stats.clone(),
183 ));
184 }
185
186 if config.networkd.enabled {
187 let config_clone = Arc::clone(&config);
188 let stats_clone = locked_machine_stats.clone();
189 let machine_name = machine.clone();
190 let no_fallback = config_clone.varlink.no_fallback;
191 join_set.spawn(async move {
192 if config_clone
193 .use_varlink(&[config_clone.machines.varlink, config_clone.networkd.varlink])
194 {
195 let socket_path = format!(
196 "/proc/{}/root{}",
197 leader_pid,
198 crate::varlink_networkd::NETWORK_SOCKET_PATH
199 );
200 match crate::varlink_networkd::get_networkd_state(&socket_path).await {
201 Ok(networkd_stats) => {
202 let mut machine_stats = stats_clone.write().await;
203 machine_stats.networkd = networkd_stats;
204 machine_stats.varlink_usage.networkd =
205 Some(crate::CollectorTransport::Varlink);
206 return Ok(());
207 }
208 Err(err) => {
209 crate::varlink_fallback::report_varlink_failure(
210 no_fallback,
211 &format!("container {machine_name} networkd"),
212 "file-based",
213 err,
214 )?;
215 }
216 }
217 }
218 stats_clone.write().await.varlink_usage.networkd =
219 Some(crate::CollectorTransport::Dbus);
220 let container_root = format!("/proc/{leader_pid}/root");
228 let container_sysfs = std::path::PathBuf::from(format!("{container_root}/sys"));
229 let container_links = std::path::PathBuf::from(format!(
230 "{container_root}{}",
231 config_clone.networkd.link_state_dir.display()
232 ));
233 crate::networkd::update_networkd_stats(
234 container_links,
235 None,
236 container_sysfs,
237 None,
238 stats_clone,
239 )
240 .await
241 });
242 }
243
244 let manager_describe = config
248 .use_varlink(&[config.machines.varlink, config.system_state.varlink])
249 .then(|| {
250 crate::varlink_system::shared_describe(format!(
251 "/proc/{}/root{}",
252 leader_pid,
253 crate::varlink_system::MANAGER_SOCKET_PATH
254 ))
255 });
256
257 if config.system_state.enabled {
258 let sdc_clone = sdc.clone();
259 let stats_clone = locked_machine_stats.clone();
260 let machine_name = machine.clone();
261 let no_fallback = config.varlink.no_fallback;
262 let describe = manager_describe.clone();
263 join_set.spawn(async move {
264 if let Some(describe) = describe {
265 match crate::varlink_system::update_system_stats(describe, stats_clone.clone())
266 .await
267 {
268 Ok(()) => {
269 stats_clone.write().await.varlink_usage.system_state =
270 Some(crate::CollectorTransport::Varlink);
271 return Ok(());
272 }
273 Err(err) => {
274 crate::varlink_fallback::report_varlink_failure(
275 no_fallback,
276 &format!("container {machine_name} system state"),
277 "D-Bus",
278 err,
279 )?;
280 }
281 }
282 }
283 stats_clone.write().await.varlink_usage.system_state =
284 Some(crate::CollectorTransport::Dbus);
285 crate::system::update_system_stats(sdc_clone, stats_clone.clone()).await
286 });
287 }
288
289 {
290 let sdc_clone = sdc.clone();
291 let stats_clone = locked_machine_stats.clone();
292 let machine_name = machine.clone();
293 let no_fallback = config.varlink.no_fallback;
294 let describe = manager_describe.clone();
295 join_set.spawn(async move {
296 if let Some(describe) = describe {
297 match crate::varlink_system::update_version(describe, stats_clone.clone()).await
298 {
299 Ok(()) => {
300 stats_clone.write().await.varlink_usage.version =
301 Some(crate::CollectorTransport::Varlink);
302 return Ok(());
303 }
304 Err(err) => {
305 crate::varlink_fallback::report_varlink_failure(
306 no_fallback,
307 &format!("container {machine_name} version"),
308 "D-Bus",
309 err,
310 )?;
311 }
312 }
313 }
314 stats_clone.write().await.varlink_usage.version =
315 Some(crate::CollectorTransport::Dbus);
316 crate::system::update_version(sdc_clone, stats_clone.clone()).await
317 });
318 }
319
320 if config.units.enabled {
321 if config.use_varlink(&[config.machines.varlink, config.units.varlink]) {
322 let config_clone = Arc::clone(&config);
323 let sdc_clone = sdc.clone();
324 let stats_clone = locked_machine_stats.clone();
325 let no_fallback = config_clone.varlink.no_fallback;
326 let machine_name = machine.clone();
327 let container_socket_path = format!(
328 "/proc/{}/root{}",
329 leader_pid,
330 crate::varlink_units::METRICS_SOCKET_PATH
331 );
332 join_set.spawn(async move {
333 match crate::varlink_units::update_unit_stats(
334 Arc::clone(&config_clone),
335 stats_clone.clone(),
336 container_socket_path,
337 )
338 .await
339 {
340 Ok(_timer_names) => {
341 let timer_start = std::time::Instant::now();
349 let timer_result = crate::timer::collect_all_timers_dbus(
350 &sdc_clone,
351 &config_clone,
352 )
353 .await;
354 let timer_elapsed_ms =
355 timer_start.elapsed().as_secs_f64() * 1000.0;
356 match timer_result {
357 Ok(timer_stats) => {
358 let mut ms = stats_clone.write().await;
359 crate::timer::merge_timer_stats(
360 &mut ms.units,
361 timer_stats,
362 timer_elapsed_ms,
363 );
364 }
365 Err(err) => {
366 warn!(
367 "Varlink timer stats (D-Bus fallback) failed for container {}: {:?}",
368 machine_name, err
369 );
370 let mut ms = stats_clone.write().await;
371 crate::timer::record_backfill_duration(
372 &mut ms.units,
373 timer_elapsed_ms,
374 );
375 }
376 }
377 crate::varlink_units::apply_oneshot_dbus_override(
380 &sdc_clone,
381 &stats_clone,
382 &config_clone.units,
383 )
384 .await;
385 let container_root = format!("/proc/{}/root", leader_pid);
386 if config_clone.units.unit_files {
387 let unit_files =
388 crate::units::collect_unit_files_stats(&container_root).await;
389 let mut ms = stats_clone.write().await;
390 ms.units.unit_files = unit_files;
391 }
392 stats_clone.write().await.varlink_usage.units =
393 Some(crate::CollectorTransport::Varlink);
394 Ok(())
395 }
396 Err(err) => {
397 crate::varlink_fallback::report_varlink_failure(
398 no_fallback,
399 &format!("container {machine_name} units"),
400 "D-Bus",
401 err,
402 )?;
403 let container_root = format!("/proc/{}/root", leader_pid);
404 stats_clone.write().await.varlink_usage.units =
408 Some(crate::CollectorTransport::Dbus);
409 crate::units::update_unit_stats(
410 config_clone,
411 sdc_clone,
412 stats_clone,
413 container_root,
414 )
415 .await
416 }
417 }
418 });
419 } else {
420 let container_root = format!("/proc/{}/root", leader_pid);
421 let config_clone = Arc::clone(&config);
422 let sdc_clone = sdc.clone();
423 let stats_clone = locked_machine_stats.clone();
424 join_set.spawn(async move {
425 stats_clone.write().await.varlink_usage.units =
429 Some(crate::CollectorTransport::Dbus);
430 crate::units::update_unit_stats(
431 config_clone,
432 sdc_clone,
433 stats_clone,
434 container_root,
435 )
436 .await
437 });
438 }
439 }
440
441 if config.dbus_stats.enabled {
442 join_set.spawn(crate::dbus_stats::update_machine_dbus_stats(
443 Arc::clone(&config),
444 sdc.clone(),
445 locked_machine_stats.clone(),
446 ));
447 }
448
449 let mut had_error = false;
450 while let Some(res) = join_set.join_next().await {
451 match res {
452 Ok(r) => match r {
453 Ok(_) => (),
454 Err(e) => {
455 had_error = true;
456 error!(
457 "Collection specific failure (container {}): {:?}",
458 machine, e
459 );
460 }
461 },
462 Err(e) => {
463 had_error = true;
464 error!("Join error (container {}): {:?}", machine, e);
465 }
466 }
467 }
468
469 if had_error {
470 evict_failed_connection(&cached_connections, &machine).await;
471 }
472
473 {
474 let mut monitord_stats = locked_monitord_stats.write().await;
475 let machine_stats = locked_machine_stats.read().await;
476 monitord_stats
477 .machines
478 .insert(machine, machine_stats.clone());
479 }
480 }
481
482 Ok(())
483}
484
485#[cfg(test)]
486mod tests {
487 use std::collections::HashSet;
488 use zbus::zvariant::OwnedObjectPath;
489
490 use super::{decide_cache_action, CacheAction};
491
492 #[test]
493 fn test_filter_machines() {
494 let machines = vec![
495 crate::dbus::zbus_machines::ListedMachine {
496 name: "foo".to_string(),
497 class: "container".to_string(),
498 service: "".to_string(),
499 path: OwnedObjectPath::try_from("/sample/object").unwrap(),
500 },
501 crate::dbus::zbus_machines::ListedMachine {
502 name: "bar".to_string(),
503 class: "container".to_string(),
504 service: "".to_string(),
505 path: OwnedObjectPath::try_from("/sample/object").unwrap(),
506 },
507 crate::dbus::zbus_machines::ListedMachine {
508 name: "baz".to_string(),
509 class: "container".to_string(),
510 service: "".to_string(),
511 path: OwnedObjectPath::try_from("/sample/object").unwrap(),
512 },
513 ];
514 let allowlist = HashSet::from(["foo".to_string(), "baz".to_string()]);
515 let blocklist = HashSet::from(["bar".to_string()]);
516
517 let filtered = super::filter_machines(machines, &allowlist, &blocklist);
518
519 assert_eq!(filtered.len(), 2);
520 assert_eq!(filtered[0].name, "foo");
521 assert_eq!(filtered[1].name, "baz");
522 }
523
524 #[test]
525 fn test_decide_cache_action_reuse_on_same_pid() {
526 assert_eq!(decide_cache_action(Some(42), 42), CacheAction::Reuse);
527 }
528
529 #[test]
530 fn test_decide_cache_action_replace_on_pid_change() {
531 assert_eq!(decide_cache_action(Some(42), 99), CacheAction::Replace);
532 }
533
534 #[test]
535 fn test_decide_cache_action_create_on_miss() {
536 assert_eq!(decide_cache_action(None, 42), CacheAction::Create);
537 }
538}