From db2ed0d56948ced5fba01c5549ed5ec6701587aa Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Mon, 20 Jul 2026 12:25:32 +0200 Subject: [PATCH 1/5] chore: print scan duration --- fact/src/host_scanner.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 273bd5df..cad8ddcb 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -38,6 +38,7 @@ use log::{debug, info, warn}; use tokio::{ sync::{Notify, mpsc, watch}, task::JoinHandle, + time::Instant, }; use crate::{ @@ -112,7 +113,8 @@ impl HostScanner { } fn scan(&self) -> anyhow::Result<()> { - debug!("Host scan started"); + info!("Host scan started"); + let start = Instant::now(); self.metrics.scan_inc(ScanLabels::Scans); let config = self.paths.borrow(); @@ -136,7 +138,11 @@ impl HostScanner { let path = host_info::prepend_host_mount(pattern); self.scan_inner(&path)?; } - debug!("Host scan done"); + let duration = start.elapsed(); + info!( + "Host scan done, took {duration:?}. Inodes tracked: {}", + self.inode_map.borrow().len() + ); Ok(()) } From aaa8df7692ba6558919af06e80c53752c8b38c03 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Tue, 21 Jul 2026 11:17:35 +0200 Subject: [PATCH 2/5] feat(metrics): add scan duration histogram Add a histogram capturing the duration of a host_scanner scan. --- fact/src/host_scanner.rs | 1 + fact/src/metrics/host_scanner.rs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index cad8ddcb..0afc5d6c 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -139,6 +139,7 @@ impl HostScanner { self.scan_inner(&path)?; } let duration = start.elapsed(); + self.metrics.scan_duration.observe(duration.as_secs_f64()); info!( "Host scan done, took {duration:?}. Inodes tracked: {}", self.inode_map.borrow().len() diff --git a/fact/src/metrics/host_scanner.rs b/fact/src/metrics/host_scanner.rs index 81bef60c..bacf2628 100644 --- a/fact/src/metrics/host_scanner.rs +++ b/fact/src/metrics/host_scanner.rs @@ -1,6 +1,6 @@ use prometheus_client::{ encoding::{EncodeLabelSet, EncodeLabelValue}, - metrics::{counter::Counter, family::Family}, + metrics::{counter::Counter, family::Family, histogram::Histogram}, registry::Registry, }; @@ -29,6 +29,7 @@ pub struct ScanEvents { pub struct HostScannerMetrics { pub events: EventCounter, pub scan: Family>, + pub scan_duration: Histogram, } impl HostScannerMetrics { @@ -59,8 +60,15 @@ impl HostScannerMetrics { ] { let _ = scan.get_or_create(&ScanEvents { label }); } + let scan_duration = Histogram::new([ + 0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 120.0, + ]); - HostScannerMetrics { events, scan } + HostScannerMetrics { + events, + scan, + scan_duration, + } } pub(super) fn register(&self, reg: &mut Registry) { @@ -70,6 +78,12 @@ impl HostScannerMetrics { "Counter of events by scans from the host scanner component", self.scan.clone(), ); + + reg.register( + "host_scanner_scan_duration", + "Histogram of scan durations from the host scanner component", + self.scan_duration.clone(), + ); } pub fn scan_inc(&self, label: ScanLabels) { From e2e95572dad190e527fec70b3aa3122ac96c7439 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Mon, 20 Jul 2026 17:01:44 +0200 Subject: [PATCH 3/5] fix(host_scanner): reduce the amount of times stat is called per scan Instead of using `PathBuf::is_file`, `PathBuf::is_dir` and `PathBuf::metadata` individually, each of which produce independent calls to `stat` under the hood, use a single call to `PathBuf::metadata` and propagate it down to the places it is needed. --- fact/src/host_scanner.rs | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 0afc5d6c..fa1ff697 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -20,6 +20,7 @@ use std::{ cell::RefCell, + fs::Metadata, io, os::linux::fs::MetadataExt, path::{Path, PathBuf}, @@ -156,14 +157,28 @@ impl HostScanner { }; for entry in glob::glob(glob_str)? { - let path = entry?; - if path.is_file() { + let path = match entry { + Ok(p) => p, + Err(e) => { + debug!("Glob expansion failed: {e:?}"); + continue; + } + }; + let metadata = match path.metadata() { + Ok(p) => p, + Err(e) => { + debug!("Failed to get metadata for {}: {e:?}", path.display()); + continue; + } + }; + + if metadata.is_file() { self.metrics.scan_inc(ScanLabels::FileScanned); - self.update_entry(path.as_path()) + self.update_entry(path.as_path(), &metadata) .with_context(|| format!("Failed to update entry for {}", path.display()))?; - } else if path.is_dir() { + } else if metadata.is_dir() { self.metrics.scan_inc(ScanLabels::DirectoryScanned); - self.update_entry(path.as_path()) + self.update_entry(path.as_path(), &metadata) .with_context(|| format!("Failed to update entry for {}", path.display()))?; } else { self.metrics.scan_inc(ScanLabels::FsItemIgnored); @@ -172,14 +187,7 @@ impl HostScanner { Ok(()) } - fn update_entry(&self, path: &Path) -> anyhow::Result<()> { - if !path.exists() { - // If path does not exist, we don't have anything to update - self.metrics.scan_inc(ScanLabels::FileRemoved); - return Ok(()); - } - - let metadata = path.metadata()?; + fn update_entry(&self, path: &Path, metadata: &Metadata) -> anyhow::Result<()> { let inode = inode_key_t { inode: metadata.st_ino(), dev: metadata.st_dev(), From e7a0da44818ab9b602622acfcd18ee153e011e7f Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Mon, 20 Jul 2026 17:13:42 +0200 Subject: [PATCH 4/5] fix(host_scanner): skip inserting already tracked inodes Avoid additional syscalls to insert inodes into the kernel BPF inode map if the userspace already has said inode. --- fact/src/host_scanner.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index fa1ff697..f4b098a0 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -202,8 +202,24 @@ impl HostScanner { /// Similar to update_entry except we are are directly using the inode instead of the path. fn update_entry_with_inode(&self, inode: inode_key_t, path: PathBuf) -> anyhow::Result<()> { + let mut inode_map = self.inode_map.borrow_mut(); + match inode_map.get_mut(&inode) { + Some(p) => { + // inode is already tracked. + if path != *p { + *p = path; + self.metrics.scan_inc(ScanLabels::FileUpdated); + } + return Ok(()); + } + None => { + self.metrics.scan_inc(ScanLabels::FileUpdated); + inode_map.insert(inode, path.clone()); + } + }; + match self.kernel_inode_map.borrow_mut().insert(inode, 0, 0) { - Ok(_) => {} + Ok(_) => Ok(()), Err(MapError::SyscallError(SyscallError { io_error, .. })) if io_error.kind() == io::ErrorKind::ArgumentListTooLong => { @@ -215,20 +231,8 @@ You can increase this limit with: * The --inodes-max argument."#, ) } - e => { - return e.with_context(|| { - format!("Failed to insert kernel entry for {}", path.display()) - }); - } + e => e.with_context(|| format!("Failed to insert kernel entry for {}", path.display())), } - - let mut inode_map = self.inode_map.borrow_mut(); - let entry = inode_map.entry(inode).or_default(); - *entry = path; - - self.metrics.scan_inc(ScanLabels::FileUpdated); - - Ok(()) } fn get_host_path(&self, inode: Option<&inode_key_t>) -> Option { From 0a1d04e620c2330384d434c7f6d587b35c386105 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Tue, 21 Jul 2026 14:38:52 +0200 Subject: [PATCH 5/5] chore(host_scanner): add metadata and glob error metrics --- fact/src/host_scanner.rs | 2 ++ fact/src/metrics/host_scanner.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index f4b098a0..b0415a0d 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -161,6 +161,7 @@ impl HostScanner { Ok(p) => p, Err(e) => { debug!("Glob expansion failed: {e:?}"); + self.metrics.scan_inc(ScanLabels::GlobFailed); continue; } }; @@ -168,6 +169,7 @@ impl HostScanner { Ok(p) => p, Err(e) => { debug!("Failed to get metadata for {}: {e:?}", path.display()); + self.metrics.scan_inc(ScanLabels::FsMetadataFailed); continue; } }; diff --git a/fact/src/metrics/host_scanner.rs b/fact/src/metrics/host_scanner.rs index bacf2628..3b106381 100644 --- a/fact/src/metrics/host_scanner.rs +++ b/fact/src/metrics/host_scanner.rs @@ -17,6 +17,8 @@ pub enum ScanLabels { FileRemoved, FileUpdated, FsItemIgnored, + FsMetadataFailed, + GlobFailed, } #[derive(Clone, Hash, Eq, Debug, PartialEq, EncodeLabelSet)] @@ -57,6 +59,8 @@ impl HostScannerMetrics { ScanLabels::FileRemoved, ScanLabels::FileUpdated, ScanLabels::FsItemIgnored, + ScanLabels::FsMetadataFailed, + ScanLabels::GlobFailed, ] { let _ = scan.get_or_create(&ScanEvents { label }); }