From f2d4835c08a3dd2b2dc231f5e5a63b7109f9cf11 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Fri, 21 Aug 2026 12:04:53 +0200 Subject: [PATCH 1/6] cleanup(config): move globset to paths configuration This provides a unified place for the paths globset to be held, from the previous approach of having `Bpf` and `HostScanner` build and hold their own, reducing code duplication and ensuring consistency between the two components a bit better. The new approach requires `Bpf` to take a read lock on the paths configuration for each event it processes, this should be fine since updating the paths value would be the case that might cause contention and it should not be done very often during regular operation. --- fact/src/bpf/mod.rs | 39 ++++------- fact/src/config/mod.rs | 111 +++++++++++++++++++++++------- fact/src/config/reloader/mod.rs | 41 ++++++----- fact/src/config/reloader/tests.rs | 24 +++---- fact/src/config/tests.rs | 63 ++++++++++++----- fact/src/host_scanner.rs | 61 ++++++---------- fact/src/lib.rs | 2 +- 7 files changed, 202 insertions(+), 139 deletions(-) diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index 815e2431..b88784fe 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -1,4 +1,4 @@ -use std::{io, path::PathBuf}; +use std::io; use anyhow::{Context, bail}; use aya::{ @@ -7,7 +7,6 @@ use aya::{ programs::{Program, lsm::LsmLink}, }; use checks::Checks; -use globset::{Glob, GlobSet, GlobSetBuilder}; use libc::c_char; use log::{error, info, warn}; use tokio::{ @@ -16,7 +15,12 @@ use tokio::{ task::JoinSet, }; -use crate::{config::BpfConfig, event::Event, host_info, metrics::EventCounter}; +use crate::{ + config::{BpfConfig, PathsConfig}, + event::Event, + host_info, + metrics::EventCounter, +}; use fact_ebpf::{LPM_SIZE_MAX, event_t, inode_key_t, inode_value_t, metrics_t, path_prefix_t}; @@ -30,11 +34,9 @@ pub struct Bpf { tx: mpsc::Sender, - paths_config: watch::Receiver>, + paths_config: watch::Receiver, paths_lpm_map: LpmTrie, - paths_globset: GlobSet, - links: Vec, running: watch::Receiver, @@ -43,7 +45,7 @@ pub struct Bpf { impl Bpf { pub fn new( - paths_config: watch::Receiver>, + paths_config: watch::Receiver, bpf_config: &BpfConfig, running: watch::Receiver, metrics: EventCounter, @@ -66,7 +68,6 @@ impl Bpf { checks, tx, paths_config, - paths_globset: GlobSet::empty(), paths_lpm_map, links: Vec::new(), running, @@ -176,10 +177,9 @@ impl Bpf { } fn load_paths(&mut self) -> anyhow::Result<()> { - if self.paths_config.borrow().is_empty() { + if self.paths_config.borrow().patterns().is_empty() { self.detach_progs(); self.cleanup_lpm_map(&[])?; - self.paths_globset = GlobSet::empty(); return Ok(()); } @@ -190,22 +190,13 @@ impl Bpf { // Add the new prefixes let new_paths = { let paths_config = self.paths_config.borrow(); - let mut new_paths = Vec::with_capacity(paths_config.len()); - let mut builder = GlobSetBuilder::new(); - for p in paths_config.iter() { - let Some(glob_str) = p.to_str() else { - bail!("failed to convert path {} to string", p.display()); - }; - - builder.add( - Glob::new(glob_str).with_context(|| format!("invalid glob {}", glob_str))?, - ); - + let patterns = paths_config.patterns(); + let mut new_paths = Vec::with_capacity(patterns.len()); + for p in patterns { let prefix = path_prefix_t::try_from(p)?; self.paths_lpm_map.insert(&prefix.into(), 0, 0)?; new_paths.push(prefix); } - self.paths_globset = builder.build()?; new_paths }; @@ -318,7 +309,7 @@ impl Bpf { // so we let the event go into HostScanner and make the // decision there. if !event.is_monitored_by_parent() && - event.is_ignored(&self.paths_globset) { + event.is_ignored(&self.paths_config.borrow().globset) { self.metrics.ignored(); continue; } @@ -395,7 +386,7 @@ mod bpf_tests { let mut config = FactConfig::default(); config.set_paths(paths); let bpf_config = config.bpf.clone(); - let reloader = Reloader::from(config); + let reloader = Reloader::try_from(config).unwrap(); let metrics = Metrics::new(); let (run_tx, run_rx) = watch::channel(true); let (bpf, mut rx) = Bpf::new( diff --git a/fact/src/config/mod.rs b/fact/src/config/mod.rs index 1542f86a..3a062ef6 100644 --- a/fact/src/config/mod.rs +++ b/fact/src/config/mod.rs @@ -10,6 +10,7 @@ use std::{ use anyhow::{Context, bail}; use clap::Parser; +use globset::{Glob, GlobSet}; use log::info; use yaml_rust2::{Yaml, YamlLoader, yaml}; @@ -33,7 +34,7 @@ fn yaml_to_duration_secs(v: &Yaml) -> Option { #[derive(Debug, Default, PartialEq, Clone)] pub struct FactConfig { - paths: Option>, + paths: PathsConfig, pub grpc: GrpcConfig, pub otel: OTelConfig, pub endpoint: EndpointConfig, @@ -82,10 +83,7 @@ impl FactConfig { } pub fn update(&mut self, from: &FactConfig) { - if let Some(paths) = from.paths.as_deref() { - self.paths = Some(paths.to_owned()); - } - + self.paths.update(&from.paths); self.grpc.update(&from.grpc); self.otel.update(&from.otel); self.endpoint.update(&from.endpoint); @@ -116,10 +114,6 @@ impl FactConfig { } } - pub fn paths(&self) -> &[PathBuf] { - self.paths.as_ref().map(|v| v.as_ref()).unwrap_or(&[]) - } - pub fn skip_pre_flight(&self) -> bool { self.skip_pre_flight.unwrap_or(false) } @@ -146,7 +140,7 @@ impl FactConfig { #[cfg(test)] pub fn set_paths(&mut self, paths: Vec) { - self.paths = Some(paths); + self.paths = paths.try_into().expect("Invalid paths"); } } @@ -188,21 +182,10 @@ impl TryFrom> for FactConfig { match k { "paths" if v.is_array() => { - let paths = v - .as_vec() - .unwrap() - .iter() - .map(|p| { - let Some(p) = p.as_str() else { - bail!("Path has invalid type: {p:?}"); - }; - Ok(PathBuf::from(p)) - }) - .collect::>()?; - config.paths = Some(paths); + config.paths = v.as_vec().unwrap().try_into()?; } "paths" if v.is_null() => { - config.paths = Some(Vec::new()); + config.paths = PathsConfig::empty(); } "grpc" if v.is_hash() => { let grpc = v.as_hash().unwrap(); @@ -271,6 +254,83 @@ impl TryFrom> for FactConfig { } } +#[derive(Debug, Default, Clone)] +pub struct PathsConfig { + patterns: Option>, + pub globset: GlobSet, +} + +impl PathsConfig { + const fn empty() -> Self { + PathsConfig { + patterns: Some(Vec::new()), + globset: GlobSet::empty(), + } + } + + fn globset_build<'a>(patterns: impl Iterator) -> anyhow::Result { + let mut builder = GlobSet::builder(); + for p in patterns { + let Some(p) = p.to_str() else { + bail!("paths item has invalid UTF-8: {}", p.display()); + }; + let p = Glob::new(p).with_context(|| format!("invalid glob {}", p))?; + builder.add(p); + } + + builder.build().map_err(anyhow::Error::from) + } + + fn update(&mut self, other: &Self) { + if other.patterns.is_some() { + self.patterns = other.patterns.clone(); + self.globset = other.globset.clone(); + } + } + + pub fn patterns(&self) -> &[PathBuf] { + self.patterns.as_deref().unwrap_or(&[]) + } +} + +impl PartialEq for PathsConfig { + fn eq(&self, other: &Self) -> bool { + self.patterns() == other.patterns() + } +} + +impl TryFrom<&yaml::Array> for PathsConfig { + type Error = anyhow::Error; + + fn try_from(value: &yaml::Array) -> Result { + let paths = value + .iter() + .map(|p| match p.as_str() { + Some(p) => Ok(p.into()), + None => bail!("paths field has invalid type: {p:?}"), + }) + .collect::, _>>()?; + let globset = PathsConfig::globset_build(paths.iter())?; + + Ok(PathsConfig { + patterns: Some(paths), + globset, + }) + } +} + +impl TryFrom> for PathsConfig { + type Error = anyhow::Error; + + fn try_from(paths: Vec) -> Result { + let globset = PathsConfig::globset_build(paths.iter())?; + Ok(PathsConfig { + patterns: Some(paths), + globset, + }) + } +} + #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct EndpointConfig { address: Option, @@ -915,7 +975,10 @@ pub struct FactCli { impl FactCli { fn into_config(self) -> FactConfig { FactConfig { - paths: self.paths, + paths: self + .paths + .map(|patterns| patterns.try_into().expect("Invalid paths configuration")) + .unwrap_or_default(), grpc: GrpcConfig { url: self.url, certs: self.certs, diff --git a/fact/src/config/reloader/mod.rs b/fact/src/config/reloader/mod.rs index cb99b898..78e60742 100644 --- a/fact/src/config/reloader/mod.rs +++ b/fact/src/config/reloader/mod.rs @@ -8,7 +8,7 @@ use tokio::{ time::interval, }; -use crate::config::OTelConfig; +use crate::config::{OTelConfig, PathsConfig}; use super::{CONFIG_FILES, EndpointConfig, FactConfig, GrpcConfig}; @@ -18,7 +18,7 @@ pub struct Reloader { endpoint: watch::Sender, grpc: watch::Sender, otel: watch::Sender, - paths: watch::Sender>, + paths: watch::Sender, files: HashMap<&'static str, (i64, i64)>, scan_interval: watch::Sender, rate_limit: watch::Sender, @@ -80,7 +80,7 @@ impl Reloader { /// Subscribe to get notifications when paths configuration is /// changed. - pub fn paths(&self) -> watch::Receiver> { + pub fn paths(&self) -> watch::Receiver { self.paths.subscribe() } @@ -147,17 +147,6 @@ impl Reloader { /// Propagate configuration changes to all subscribers that need it fn send_updates(&self, new: FactConfig) { - self.paths.send_if_modified(|old| { - let new = new.paths(); - if *old != new { - debug!("Sending new paths configuration..."); - *old = new.to_vec(); - true - } else { - false - } - }); - self.scan_interval.send_if_modified(|old| { let new = new.scan_interval(); if *old != new { @@ -188,9 +177,20 @@ impl Reloader { endpoint, grpc, otel, + paths, .. } = new; + self.paths.send_if_modified(|old| { + if *old != paths { + debug!("Sending new paths configuration..."); + *old = paths; + true + } else { + false + } + }); + self.endpoint.send_if_modified(|old| { if *old != endpoint { debug!("Sending new endpoint configuration..."); @@ -242,8 +242,10 @@ impl Reloader { } } -impl From for Reloader { - fn from(config: FactConfig) -> Self { +impl TryFrom for Reloader { + type Error = anyhow::Error; + + fn try_from(config: FactConfig) -> Result { let files = CONFIG_FILES .iter() .filter_map(|path| { @@ -265,7 +267,6 @@ impl From for Reloader { .collect(); let enabled = config.hotreload(); - let (paths, _) = watch::channel(config.paths().to_vec()); let (scan_interval, _) = watch::channel(config.scan_interval()); let (rate_limit, _) = watch::channel(config.rate_limit()); @@ -273,15 +274,17 @@ impl From for Reloader { endpoint, grpc, otel, + paths, .. } = config; let (endpoint, _) = watch::channel(endpoint); let (grpc, _) = watch::channel(grpc); let (otel, _) = watch::channel(otel); + let (paths, _) = watch::channel(paths); let trigger = Arc::new(Notify::new()); - Reloader { + Ok(Reloader { enabled, endpoint, grpc, @@ -291,6 +294,6 @@ impl From for Reloader { rate_limit, files, trigger, - } + }) } } diff --git a/fact/src/config/reloader/tests.rs b/fact/src/config/reloader/tests.rs index a5e128b5..ab2d673e 100644 --- a/fact/src/config/reloader/tests.rs +++ b/fact/src/config/reloader/tests.rs @@ -32,7 +32,7 @@ macro_rules! generate_test { ($testname:ident, $channel:ident, $old:expr, $new:expr, $expected:expr) => { #[test] fn $testname() { - let reloader = Reloader::from($old); + let reloader = Reloader::try_from($old).unwrap(); let channel = reloader.$channel(); reloader.send_updates($new); @@ -57,7 +57,7 @@ generate_paths_test! { test_reloader_paths_from_default_to_empty, FactConfig::default(), FactConfig { - paths: Some(vec![]), + paths: PathsConfig::default(), ..Default::default() }, None @@ -65,23 +65,23 @@ generate_paths_test! { generate_paths_test! { test_reloader_paths_config_change, FactConfig { - paths: Some(vec!["/home".into()]), + paths: vec!["/home".into()].try_into().unwrap(), ..Default::default() }, FactConfig { - paths: Some(vec!["/etc".into()]), + paths: vec!["/etc".into()].try_into().unwrap(), ..Default::default() }, - Some(vec![PathBuf::from("/etc")]) + Some(vec![PathBuf::from("/etc")].try_into().unwrap()) } generate_paths_test! { test_reloader_paths_no_config_change, FactConfig { - paths: Some(vec!["/home".into()]), + paths: vec!["/home".into()].try_into().unwrap(), ..Default::default() }, FactConfig { - paths: Some(vec!["/home".into()]), + paths: vec!["/home".into()].try_into().unwrap(), scan_interval: Some(Duration::from_secs(10)), ..Default::default() }, @@ -156,7 +156,7 @@ generate_scan_interval_test! { }, FactConfig { scan_interval: Some(Duration::from_secs(60)), - paths: Some(vec!["/etc".into()]), + paths: vec!["/etc".into()].try_into().unwrap(), ..Default::default() }, None @@ -230,7 +230,7 @@ generate_rate_limit_test! { }, FactConfig { rate_limit: Some(1000), - paths: Some(vec!["/etc".into()]), + paths: vec!["/etc".into()].try_into().unwrap(), ..Default::default() }, None @@ -266,7 +266,7 @@ generate_endpoint_test! { health_check: Some(true), introspection: Some(true), }, - paths: Some(vec!["/etc".into()]), + paths: vec!["/etc".into()].try_into().unwrap(), ..Default::default() }, None @@ -546,7 +546,7 @@ generate_grpc_test! { retries_max: Some(GRPC_BACKOFF_RETRIES_NEW), } }, - paths: Some(vec!["/etc".into()]), + paths: vec!["/etc".into()].try_into().unwrap(), ..Default::default() }, None @@ -1412,7 +1412,7 @@ generate_otel_test! { otel: OTelConfig { endpoint: Some(OTEL_ENDPOINT_NEW.into()), }, - paths: Some(vec!["/etc".into()]), + paths: vec!["/etc".into()].try_into().unwrap(), ..Default::default() }, None diff --git a/fact/src/config/tests.rs b/fact/src/config/tests.rs index 596ca711..b7d3031e 100644 --- a/fact/src/config/tests.rs +++ b/fact/src/config/tests.rs @@ -12,14 +12,16 @@ fn parsing() { ( "paths:", FactConfig { - paths: Some(Vec::new()), + paths: PathsConfig::default(), ..Default::default() }, ), ( "paths: [/etc, /bin]", FactConfig { - paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/bin")]), + paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + .try_into() + .unwrap(), ..Default::default() }, ), @@ -509,7 +511,7 @@ fn parsing() { replay: /some/path.jsonl "#, FactConfig { - paths: Some(vec![PathBuf::from("/etc")]), + paths: vec![PathBuf::from("/etc")].try_into().unwrap(), grpc: GrpcConfig { url: Some(String::from("https://svc.sensor.stackrox:9090")), certs: Some(PathBuf::from("/etc/stackrox/certs")), @@ -594,7 +596,7 @@ paths: ("- something", "Wrong configuration type"), ("true: something", "key is not string: Boolean(true)"), ("4: something", "key is not string: Integer(4)"), - ("paths: [4]", "Path has invalid type: Integer(4)"), + ("paths: [4]", "paths field has invalid type: Integer(4)"), ( "grpc: true", "Invalid field 'grpc' with value: Boolean(true)", @@ -985,7 +987,7 @@ fn update() { "paths:", FactConfig::default(), FactConfig { - paths: Some(Vec::new()), + paths: PathsConfig::default(), ..Default::default() }, ), @@ -993,40 +995,61 @@ fn update() { "paths: [/etc, /bin]", FactConfig::default(), FactConfig { - paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/bin")]), + paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + .try_into() + .unwrap(), ..Default::default() }, ), ( "paths: [/bin]", FactConfig { - paths: Some(vec![PathBuf::from("/etc")]), + paths: vec![PathBuf::from("/etc")].try_into().unwrap(), ..Default::default() }, FactConfig { - paths: Some(vec![PathBuf::from("/bin")]), + paths: vec![PathBuf::from("/bin")].try_into().unwrap(), ..Default::default() }, ), ( "paths:", FactConfig { - paths: Some(vec![PathBuf::from("/etc")]), + paths: vec![PathBuf::from("/etc")].try_into().unwrap(), ..Default::default() }, FactConfig { - paths: Some(Vec::new()), + paths: PathsConfig::default(), ..Default::default() }, ), ( "paths: [/etc, /bin]", FactConfig { - paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/bin")]), + paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + .try_into() + .unwrap(), ..Default::default() }, FactConfig { - paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/bin")]), + paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + .try_into() + .unwrap(), + ..Default::default() + }, + ), + ( + "", + FactConfig { + paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + .try_into() + .unwrap(), + ..Default::default() + }, + FactConfig { + paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + .try_into() + .unwrap(), ..Default::default() }, ), @@ -1936,7 +1959,9 @@ fn update() { rate_limit: 1000 "#, FactConfig { - paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/bin")]), + paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + .try_into() + .unwrap(), grpc: GrpcConfig { url: Some(String::from("http://localhost")), certs: Some(PathBuf::from("/etc/certs")), @@ -1976,7 +2001,7 @@ fn update() { replay: None, }, FactConfig { - paths: Some(vec![PathBuf::from("/etc")]), + paths: vec![PathBuf::from("/etc")].try_into().unwrap(), grpc: GrpcConfig { url: Some(String::from("https://svc.sensor.stackrox:9090")), certs: Some(PathBuf::from("/etc/stackrox/certs")), @@ -2038,8 +2063,8 @@ fn update() { #[test] fn defaults() { let config = FactConfig::default(); - let default_paths: &[PathBuf] = &[]; - assert_eq!(config.paths(), default_paths); + assert!(config.paths.patterns().is_empty()); + assert!(config.paths.globset.is_empty()); assert_eq!(config.grpc.url(), None); assert_eq!(config.grpc.certs(), None); assert_eq!( @@ -2223,7 +2248,9 @@ fn env_vars() { value: "/etc:/var/log", }, FactConfig { - paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/var/log")]), + paths: vec![PathBuf::from("/etc"), PathBuf::from("/var/log")] + .try_into() + .unwrap(), ..Default::default() }, ), @@ -2580,7 +2607,7 @@ fn env_vars_override_yaml() { }, "paths:\n- /etc", FactConfig { - paths: Some(vec![PathBuf::from("/var/log")]), + paths: vec![PathBuf::from("/var/log")].try_into().unwrap(), ..Default::default() }, ), diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index ccebbc33..d38f63ef 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -36,7 +36,6 @@ use aya::{ sys::SyscallError, }; use fact_ebpf::{inode_key_t, inode_value_t, monitored_t}; -use globset::{Glob, GlobSet, GlobSetBuilder}; use log::{debug, info, warn}; use serde::{Serialize, ser::SerializeMap}; use tokio::{ @@ -47,6 +46,7 @@ use tokio::{ use crate::{ bpf::Bpf, + config::PathsConfig, event::Event, host_info::{self, remove_host_mount}, metrics::host_scanner::{HostScannerLabels, HostScannerMetrics, ScanLabels}, @@ -112,7 +112,7 @@ pub struct HostScanner { kernel_inode_map: RefCell>, inode_map: RefCell, - paths: watch::Receiver>, + paths: watch::Receiver, scan_interval: watch::Receiver, rx: mpsc::Receiver, @@ -120,16 +120,13 @@ pub struct HostScanner { introspection: mpsc::Receiver, metrics: HostScannerMetrics, - - paths_globset: GlobSet, - paths_patterns: Vec, } impl HostScanner { pub fn new( bpf: &mut Bpf, rx: mpsc::Receiver, - paths: watch::Receiver>, + paths: watch::Receiver, scan_interval: watch::Receiver, metrics: HostScannerMetrics, introspection: mpsc::Receiver, @@ -147,8 +144,6 @@ impl HostScanner { tx, introspection, metrics, - paths_globset: GlobSet::empty(), - paths_patterns: Vec::new(), }; host_scanner.reload_paths_config()?; @@ -159,38 +154,18 @@ impl HostScanner { Ok((host_scanner, output)) } - fn reload_paths_config(&mut self) -> anyhow::Result<()> { - let paths = self.paths.borrow(); - let mut builder = GlobSetBuilder::new(); - let mut patterns = Vec::with_capacity(paths.len()); - - for p in paths.iter() { - patterns.push(host_info::prepend_host_mount(p)); - - let Some(glob_str) = p.to_str() else { - bail!("failed to convert path {} to string", p.display()); - }; - - builder.add(Glob::new(glob_str).with_context(|| format!("invalid glob {}", glob_str))?); - } - - self.paths_globset = builder.build()?; - self.paths_patterns = patterns; - - Ok(()) - } - fn scan(&self) -> anyhow::Result<()> { info!("Host scan started"); let start = Instant::now(); self.metrics.scan_inc(ScanLabels::Scans); + let paths = self.paths.borrow(); // Cleanup any items that are either: // * Not configured to be monitored anymore. // * Are configured to be monitored but no longer are found in // the file system. self.inode_map.borrow_mut().retain(|inode, path| { - if self.paths_globset.is_match(&path) && host_info::prepend_host_mount(path).exists() { + if paths.globset.is_match(&path) && host_info::prepend_host_mount(path).exists() { true } else { let _ = self.kernel_inode_map.borrow_mut().remove(inode); @@ -199,8 +174,9 @@ impl HostScanner { } }); - for path in &self.paths_patterns { - self.scan_inner(path)?; + for pattern in paths.patterns() { + let path = host_info::prepend_host_mount(pattern); + self.scan_inner(&path)?; } let duration = start.elapsed(); self.metrics.scan_duration.observe(duration.as_secs_f64()); @@ -292,8 +268,10 @@ impl HostScanner { /// matches the supplied path. fn scan_partial(&self, path: &Path) -> anyhow::Result<()> { let start = Instant::now(); + let paths = self.paths.borrow(); let scan_prefix_patterns = - self.paths_patterns + paths + .patterns() .iter() .enumerate() .filter_map(|(i, pattern)| { @@ -304,15 +282,16 @@ impl HostScanner { .starts_with(path.to_str()?) .then_some(i) }); - let scan_glob_index = self.paths_globset.matches(path); + let scan_glob_index = paths.globset.matches(path); // De-duplicate the indexes let scan_set = scan_prefix_patterns .chain(scan_glob_index.iter().copied()) .collect::>(); - for pattern in scan_set.iter().map(|index| &self.paths_patterns[*index]) { - self.scan_inner(pattern)?; + for pattern in scan_set.iter().map(|index| &paths.patterns()[*index]) { + let pattern = host_info::prepend_host_mount(pattern); + self.scan_inner(&pattern)?; } self.metrics @@ -494,7 +473,7 @@ You can increase this limit with: unreachable!("Rename event did not have an old host path"); }; - if self.paths_globset.is_match(&new_host_path) { + if self.paths.borrow().globset.is_match(&new_host_path) { // New path needs to be tracked. // Move all entries for the old host path to the new one for path in inode_map.values_mut() { @@ -591,11 +570,12 @@ You can increase this limit with: /// the host paths for matches in events that are monitored by /// parent. fn event_is_ignored(&self, event: &Event) -> bool { - event.is_ignored(&self.paths_globset) - && !self.paths_globset.is_match(event.get_host_path()) + let paths = self.paths.borrow(); + event.is_ignored(&paths.globset) + && !paths.globset.is_match(event.get_host_path()) && event .get_old_host_path() - .is_none_or(|path| !self.paths_globset.is_match(path)) + .is_none_or(|path| !paths.globset.is_match(path)) } pub fn start(mut self, task_set: &mut JoinSet>) { @@ -709,7 +689,6 @@ You can increase this limit with: } _ = scan_trigger.notified() => self.scan()?, _ = self.paths.changed() => { - self.reload_paths_config()?; self.scan()?; } } diff --git a/fact/src/lib.rs b/fact/src/lib.rs index 2c0d335a..69279e71 100644 --- a/fact/src/lib.rs +++ b/fact/src/lib.rs @@ -120,7 +120,7 @@ pub async fn run(config: FactConfig) -> anyhow::Result<()> { let metrics_userspace = Metrics::new(); let mut task_set = JoinSet::new(); - let reloader = config::reloader::Reloader::from(config); + let reloader = config::reloader::Reloader::try_from(config)?; let config_trigger = reloader.get_trigger(); let setup_args = SetupArgs { From a939f96c6dc042e6950a0bc421802c644691e2e0 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Mon, 24 Aug 2026 12:11:38 +0200 Subject: [PATCH 2/6] fix: skip globset field when dumping configuration to logs This is done by not printing the field as part of the Debug trait. --- fact/src/config/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/fact/src/config/mod.rs b/fact/src/config/mod.rs index 3a062ef6..d49ab41d 100644 --- a/fact/src/config/mod.rs +++ b/fact/src/config/mod.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + fmt::Debug, fs::read_to_string, net::SocketAddr, path::{Path, PathBuf}, @@ -254,7 +255,7 @@ impl TryFrom> for FactConfig { } } -#[derive(Debug, Default, Clone)] +#[derive(Default, Clone)] pub struct PathsConfig { patterns: Option>, pub globset: GlobSet, @@ -299,6 +300,15 @@ impl PartialEq for PathsConfig { } } +impl Debug for PathsConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PathsConfig") + .field("patterns", &self.patterns) + // Skip globset field for cleaner logs + .finish() + } +} + impl TryFrom<&yaml::Array> for PathsConfig { type Error = anyhow::Error; From 62184945b35679f7137ae6a797ec312b99a1405c Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Mon, 31 Aug 2026 12:07:47 +0200 Subject: [PATCH 3/6] cleanup(config): Reloader from FactConfig cannot fail --- fact/src/bpf/mod.rs | 2 +- fact/src/config/reloader/mod.rs | 10 ++++------ fact/src/config/reloader/tests.rs | 2 +- fact/src/lib.rs | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index b88784fe..c3790446 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -386,7 +386,7 @@ mod bpf_tests { let mut config = FactConfig::default(); config.set_paths(paths); let bpf_config = config.bpf.clone(); - let reloader = Reloader::try_from(config).unwrap(); + let reloader = Reloader::from(config); let metrics = Metrics::new(); let (run_tx, run_rx) = watch::channel(true); let (bpf, mut rx) = Bpf::new( diff --git a/fact/src/config/reloader/mod.rs b/fact/src/config/reloader/mod.rs index 78e60742..62731f16 100644 --- a/fact/src/config/reloader/mod.rs +++ b/fact/src/config/reloader/mod.rs @@ -242,10 +242,8 @@ impl Reloader { } } -impl TryFrom for Reloader { - type Error = anyhow::Error; - - fn try_from(config: FactConfig) -> Result { +impl From for Reloader { + fn from(config: FactConfig) -> Self { let files = CONFIG_FILES .iter() .filter_map(|path| { @@ -284,7 +282,7 @@ impl TryFrom for Reloader { let (paths, _) = watch::channel(paths); let trigger = Arc::new(Notify::new()); - Ok(Reloader { + Reloader { enabled, endpoint, grpc, @@ -294,6 +292,6 @@ impl TryFrom for Reloader { rate_limit, files, trigger, - }) + } } } diff --git a/fact/src/config/reloader/tests.rs b/fact/src/config/reloader/tests.rs index ab2d673e..f3109b8b 100644 --- a/fact/src/config/reloader/tests.rs +++ b/fact/src/config/reloader/tests.rs @@ -32,7 +32,7 @@ macro_rules! generate_test { ($testname:ident, $channel:ident, $old:expr, $new:expr, $expected:expr) => { #[test] fn $testname() { - let reloader = Reloader::try_from($old).unwrap(); + let reloader = Reloader::from($old); let channel = reloader.$channel(); reloader.send_updates($new); diff --git a/fact/src/lib.rs b/fact/src/lib.rs index 69279e71..2c0d335a 100644 --- a/fact/src/lib.rs +++ b/fact/src/lib.rs @@ -120,7 +120,7 @@ pub async fn run(config: FactConfig) -> anyhow::Result<()> { let metrics_userspace = Metrics::new(); let mut task_set = JoinSet::new(); - let reloader = config::reloader::Reloader::try_from(config)?; + let reloader = config::reloader::Reloader::from(config); let config_trigger = reloader.get_trigger(); let setup_args = SetupArgs { From 343bd8383faa900b4cc7a9898b7f86320dc5f03e Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Mon, 31 Aug 2026 16:52:06 +0200 Subject: [PATCH 4/6] fix: HostScanner does not have reload_paths_config method anymore --- fact/src/host_scanner.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index d38f63ef..871b843d 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -135,7 +135,7 @@ impl HostScanner { let inode_map = RefCell::new(InodeMap::new()); let (tx, output) = mpsc::channel(100); - let mut host_scanner = HostScanner { + let host_scanner = HostScanner { kernel_inode_map, inode_map, paths, @@ -146,8 +146,6 @@ impl HostScanner { metrics, }; - host_scanner.reload_paths_config()?; - // Run an initial scan to fill in the inode map host_scanner.scan()?; From 763475b8d576175833d8133ad20fd04a2d52cdd2 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Tue, 1 Sep 2026 15:18:41 +0200 Subject: [PATCH 5/6] Store patterns with host path prepended This avoids some allocations when doing scanning, since the patterns can be used directly. The downside is the BPF side needs to remove the host path when populating the LPM TRIE map, but removing it is done with no additional allocations. --- fact-ebpf/src/lib.rs | 6 +++--- fact/src/bpf/mod.rs | 6 +++--- fact/src/config/mod.rs | 30 ++++++++++++++++++-------- fact/src/config/reloader/tests.rs | 20 ++++++++--------- fact/src/config/tests.rs | 36 +++++++++++++++++++------------ fact/src/host_scanner.rs | 6 ++---- 6 files changed, 61 insertions(+), 43 deletions(-) diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index 0d52fb2e..07e20cbc 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -1,6 +1,6 @@ #![allow(dead_code, non_camel_case_types)] -use std::{error::Error, ffi::c_char, fmt::Display, hash::Hash, path::PathBuf}; +use std::{error::Error, ffi::c_char, fmt::Display, hash::Hash, path::Path}; use aya::{maps::lpm_trie, Pod}; use libc::memcpy; @@ -25,10 +25,10 @@ impl Display for PathPrefixError { } } -impl TryFrom<&PathBuf> for path_prefix_t { +impl TryFrom<&Path> for path_prefix_t { type Error = PathPrefixError; - fn try_from(value: &PathBuf) -> Result { + fn try_from(value: &Path) -> Result { let Some(filename) = value.to_str() else { return Err(PathPrefixError { prefix: value.display().to_string(), diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index c3790446..e141dbdd 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -192,7 +192,7 @@ impl Bpf { let paths_config = self.paths_config.borrow(); let patterns = paths_config.patterns(); let mut new_paths = Vec::with_capacity(patterns.len()); - for p in patterns { + for p in patterns.iter().map(|p| host_info::remove_host_mount(p)) { let prefix = path_prefix_t::try_from(p)?; self.paths_lpm_map.insert(&prefix.into(), 0, 0)?; new_paths.push(prefix); @@ -382,9 +382,9 @@ mod bpf_tests { let monitored_path = env!("CARGO_MANIFEST_DIR"); let monitored_path = PathBuf::from(monitored_path); - let paths = vec![PathBuf::from(format!("{}/**/*", monitored_path.display()))]; + let paths = [PathBuf::from(format!("{}/**/*", monitored_path.display()))]; let mut config = FactConfig::default(); - config.set_paths(paths); + config.set_paths(&paths); let bpf_config = config.bpf.clone(); let reloader = Reloader::from(config); let metrics = Metrics::new(); diff --git a/fact/src/config/mod.rs b/fact/src/config/mod.rs index d49ab41d..68b78346 100644 --- a/fact/src/config/mod.rs +++ b/fact/src/config/mod.rs @@ -15,6 +15,8 @@ use globset::{Glob, GlobSet}; use log::info; use yaml_rust2::{Yaml, YamlLoader, yaml}; +use crate::host_info; + pub mod reloader; #[cfg(test)] mod tests; @@ -140,7 +142,7 @@ impl FactConfig { } #[cfg(test)] - pub fn set_paths(&mut self, paths: Vec) { + pub fn set_paths(&mut self, paths: &[PathBuf]) { self.paths = paths.try_into().expect("Invalid paths"); } } @@ -269,7 +271,7 @@ impl PathsConfig { } } - fn globset_build<'a>(patterns: impl Iterator) -> anyhow::Result { + fn globset_build<'a>(patterns: impl Iterator) -> anyhow::Result { let mut builder = GlobSet::builder(); for p in patterns { let Some(p) = p.to_str() else { @@ -316,11 +318,12 @@ impl TryFrom<&yaml::Array> for PathsConfig { let paths = value .iter() .map(|p| match p.as_str() { - Some(p) => Ok(p.into()), + Some(p) => Ok(host_info::prepend_host_mount(Path::new(p))), None => bail!("paths field has invalid type: {p:?}"), }) .collect::, _>>()?; - let globset = PathsConfig::globset_build(paths.iter())?; + let globset = + PathsConfig::globset_build(paths.iter().map(|p| host_info::remove_host_mount(p)))?; Ok(PathsConfig { patterns: Some(paths), @@ -329,13 +332,17 @@ impl TryFrom<&yaml::Array> for PathsConfig { } } -impl TryFrom> for PathsConfig { +impl TryFrom<&[PathBuf]> for PathsConfig { type Error = anyhow::Error; - fn try_from(paths: Vec) -> Result { - let globset = PathsConfig::globset_build(paths.iter())?; + fn try_from(paths: &[PathBuf]) -> Result { + let globset = PathsConfig::globset_build(paths.iter().map(|p| p.as_path()))?; + let patterns = paths + .iter() + .map(|p| host_info::prepend_host_mount(p)) + .collect(); Ok(PathsConfig { - patterns: Some(paths), + patterns: Some(patterns), globset, }) } @@ -987,7 +994,12 @@ impl FactCli { FactConfig { paths: self .paths - .map(|patterns| patterns.try_into().expect("Invalid paths configuration")) + .map(|patterns| { + patterns + .as_slice() + .try_into() + .expect("Invalid paths configuration") + }) .unwrap_or_default(), grpc: GrpcConfig { url: self.url, diff --git a/fact/src/config/reloader/tests.rs b/fact/src/config/reloader/tests.rs index f3109b8b..5fd3bce9 100644 --- a/fact/src/config/reloader/tests.rs +++ b/fact/src/config/reloader/tests.rs @@ -65,23 +65,23 @@ generate_paths_test! { generate_paths_test! { test_reloader_paths_config_change, FactConfig { - paths: vec!["/home".into()].try_into().unwrap(), + paths: ["/home".into()].as_slice().try_into().unwrap(), ..Default::default() }, FactConfig { - paths: vec!["/etc".into()].try_into().unwrap(), + paths: ["/etc".into()].as_slice().try_into().unwrap(), ..Default::default() }, - Some(vec![PathBuf::from("/etc")].try_into().unwrap()) + Some([PathBuf::from("/etc")].as_slice().try_into().unwrap()) } generate_paths_test! { test_reloader_paths_no_config_change, FactConfig { - paths: vec!["/home".into()].try_into().unwrap(), + paths: ["/home".into()].as_slice().try_into().unwrap(), ..Default::default() }, FactConfig { - paths: vec!["/home".into()].try_into().unwrap(), + paths: ["/home".into()].as_slice().try_into().unwrap(), scan_interval: Some(Duration::from_secs(10)), ..Default::default() }, @@ -156,7 +156,7 @@ generate_scan_interval_test! { }, FactConfig { scan_interval: Some(Duration::from_secs(60)), - paths: vec!["/etc".into()].try_into().unwrap(), + paths: ["/etc".into()].as_slice().try_into().unwrap(), ..Default::default() }, None @@ -230,7 +230,7 @@ generate_rate_limit_test! { }, FactConfig { rate_limit: Some(1000), - paths: vec!["/etc".into()].try_into().unwrap(), + paths: ["/etc".into()].as_slice().try_into().unwrap(), ..Default::default() }, None @@ -266,7 +266,7 @@ generate_endpoint_test! { health_check: Some(true), introspection: Some(true), }, - paths: vec!["/etc".into()].try_into().unwrap(), + paths: ["/etc".into()].as_slice().try_into().unwrap(), ..Default::default() }, None @@ -546,7 +546,7 @@ generate_grpc_test! { retries_max: Some(GRPC_BACKOFF_RETRIES_NEW), } }, - paths: vec!["/etc".into()].try_into().unwrap(), + paths: ["/etc".into()].as_slice().try_into().unwrap(), ..Default::default() }, None @@ -1412,7 +1412,7 @@ generate_otel_test! { otel: OTelConfig { endpoint: Some(OTEL_ENDPOINT_NEW.into()), }, - paths: vec!["/etc".into()].try_into().unwrap(), + paths: ["/etc".into()].as_slice().try_into().unwrap(), ..Default::default() }, None diff --git a/fact/src/config/tests.rs b/fact/src/config/tests.rs index b7d3031e..8c141b44 100644 --- a/fact/src/config/tests.rs +++ b/fact/src/config/tests.rs @@ -19,7 +19,8 @@ fn parsing() { ( "paths: [/etc, /bin]", FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + paths: [PathBuf::from("/etc"), PathBuf::from("/bin")] + .as_slice() .try_into() .unwrap(), ..Default::default() @@ -511,7 +512,7 @@ fn parsing() { replay: /some/path.jsonl "#, FactConfig { - paths: vec![PathBuf::from("/etc")].try_into().unwrap(), + paths: [PathBuf::from("/etc")].as_slice().try_into().unwrap(), grpc: GrpcConfig { url: Some(String::from("https://svc.sensor.stackrox:9090")), certs: Some(PathBuf::from("/etc/stackrox/certs")), @@ -995,7 +996,8 @@ fn update() { "paths: [/etc, /bin]", FactConfig::default(), FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + paths: [PathBuf::from("/etc"), PathBuf::from("/bin")] + .as_slice() .try_into() .unwrap(), ..Default::default() @@ -1004,18 +1006,18 @@ fn update() { ( "paths: [/bin]", FactConfig { - paths: vec![PathBuf::from("/etc")].try_into().unwrap(), + paths: [PathBuf::from("/etc")].as_slice().try_into().unwrap(), ..Default::default() }, FactConfig { - paths: vec![PathBuf::from("/bin")].try_into().unwrap(), + paths: [PathBuf::from("/bin")].as_slice().try_into().unwrap(), ..Default::default() }, ), ( "paths:", FactConfig { - paths: vec![PathBuf::from("/etc")].try_into().unwrap(), + paths: [PathBuf::from("/etc")].as_slice().try_into().unwrap(), ..Default::default() }, FactConfig { @@ -1026,13 +1028,15 @@ fn update() { ( "paths: [/etc, /bin]", FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + paths: [PathBuf::from("/etc"), PathBuf::from("/bin")] + .as_slice() .try_into() .unwrap(), ..Default::default() }, FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + paths: [PathBuf::from("/etc"), PathBuf::from("/bin")] + .as_slice() .try_into() .unwrap(), ..Default::default() @@ -1041,13 +1045,15 @@ fn update() { ( "", FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + paths: [PathBuf::from("/etc"), PathBuf::from("/bin")] + .as_slice() .try_into() .unwrap(), ..Default::default() }, FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + paths: [PathBuf::from("/etc"), PathBuf::from("/bin")] + .as_slice() .try_into() .unwrap(), ..Default::default() @@ -1959,7 +1965,8 @@ fn update() { rate_limit: 1000 "#, FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/bin")] + paths: [PathBuf::from("/etc"), PathBuf::from("/bin")] + .as_slice() .try_into() .unwrap(), grpc: GrpcConfig { @@ -2001,7 +2008,7 @@ fn update() { replay: None, }, FactConfig { - paths: vec![PathBuf::from("/etc")].try_into().unwrap(), + paths: [PathBuf::from("/etc")].as_slice().try_into().unwrap(), grpc: GrpcConfig { url: Some(String::from("https://svc.sensor.stackrox:9090")), certs: Some(PathBuf::from("/etc/stackrox/certs")), @@ -2248,7 +2255,8 @@ fn env_vars() { value: "/etc:/var/log", }, FactConfig { - paths: vec![PathBuf::from("/etc"), PathBuf::from("/var/log")] + paths: [PathBuf::from("/etc"), PathBuf::from("/var/log")] + .as_slice() .try_into() .unwrap(), ..Default::default() @@ -2607,7 +2615,7 @@ fn env_vars_override_yaml() { }, "paths:\n- /etc", FactConfig { - paths: vec![PathBuf::from("/var/log")].try_into().unwrap(), + paths: [PathBuf::from("/var/log")].as_slice().try_into().unwrap(), ..Default::default() }, ), diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 871b843d..c8c72eb2 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -173,8 +173,7 @@ impl HostScanner { }); for pattern in paths.patterns() { - let path = host_info::prepend_host_mount(pattern); - self.scan_inner(&path)?; + self.scan_inner(pattern)?; } let duration = start.elapsed(); self.metrics.scan_duration.observe(duration.as_secs_f64()); @@ -288,8 +287,7 @@ impl HostScanner { .collect::>(); for pattern in scan_set.iter().map(|index| &paths.patterns()[*index]) { - let pattern = host_info::prepend_host_mount(pattern); - self.scan_inner(&pattern)?; + self.scan_inner(pattern)?; } self.metrics From 0533ccb8dd3f88971ceda3eba5d0a83ba52f50f5 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Tue, 1 Sep 2026 15:46:18 +0200 Subject: [PATCH 6/6] cleanup: serialize configuration as JSON Serde provides an easy way to change how a sequence is serialized, in our case it allows us to store PathsConfig with patterns having the host mount and serialize it without it. This also dumps the configuration in a format that should be a bit more common for users (JSON rather than Rust debug). --- fact/src/config/mod.rs | 55 +++++++++++++++++++++------------ fact/src/config/reloader/mod.rs | 5 ++- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/fact/src/config/mod.rs b/fact/src/config/mod.rs index 68b78346..917ba937 100644 --- a/fact/src/config/mod.rs +++ b/fact/src/config/mod.rs @@ -12,7 +12,8 @@ use std::{ use anyhow::{Context, bail}; use clap::Parser; use globset::{Glob, GlobSet}; -use log::info; +use log::{info, warn}; +use serde::{Serialize, Serializer, ser::SerializeSeq}; use yaml_rust2::{Yaml, YamlLoader, yaml}; use crate::host_info; @@ -35,7 +36,7 @@ fn yaml_to_duration_secs(v: &Yaml) -> Option { .map(Duration::from_secs_f64) } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct FactConfig { paths: PathsConfig, pub grpc: GrpcConfig, @@ -53,7 +54,10 @@ pub struct FactConfig { impl FactConfig { pub fn new() -> anyhow::Result { let config = FactConfig::build()?; - info!("{config:#?}"); + match serde_json::to_string_pretty(&config) { + Ok(c) => info!("Configuration: {c}"), + Err(e) => warn!("Failed to serialize configuration: {e:?}"), + } Ok(config) } @@ -257,9 +261,31 @@ impl TryFrom> for FactConfig { } } -#[derive(Default, Clone)] +fn serialize_without_host_mount( + patterns: &Option>, + serializer: S, +) -> Result +where + S: Serializer, +{ + let state = match patterns { + Some(patterns) => { + let mut state = serializer.serialize_seq(Some(patterns.len()))?; + for pattern in patterns.iter() { + state.serialize_element(host_info::remove_host_mount(pattern))?; + } + state + } + None => serializer.serialize_seq(None)?, + }; + state.end() +} + +#[derive(Debug, Default, Clone, Serialize)] pub struct PathsConfig { + #[serde(serialize_with = "serialize_without_host_mount")] patterns: Option>, + #[serde(skip)] pub globset: GlobSet, } @@ -302,15 +328,6 @@ impl PartialEq for PathsConfig { } } -impl Debug for PathsConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PathsConfig") - .field("patterns", &self.patterns) - // Skip globset field for cleaner logs - .finish() - } -} - impl TryFrom<&yaml::Array> for PathsConfig { type Error = anyhow::Error; @@ -348,7 +365,7 @@ impl TryFrom<&[PathBuf]> for PathsConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize)] pub struct EndpointConfig { address: Option, expose_metrics: Option, @@ -434,7 +451,7 @@ impl TryFrom<&yaml::Hash> for EndpointConfig { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct BackoffConfig { initial: Option, max: Option, @@ -534,7 +551,7 @@ impl TryFrom<&yaml::Hash> for BackoffConfig { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct GrpcConfig { url: Option, certs: Option, @@ -600,7 +617,7 @@ impl TryFrom<&yaml::Hash> for GrpcConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize)] pub struct OTelConfig { endpoint: Option, } @@ -642,7 +659,7 @@ impl TryFrom<&yaml::Hash> for OTelConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize)] pub struct BpfConfig { ringbuf_size: Option, inodes_max: Option, @@ -756,7 +773,7 @@ impl TryFrom<&yaml::Hash> for BpfConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize)] pub struct BpfProgConfig { pub enabled: Option, } diff --git a/fact/src/config/reloader/mod.rs b/fact/src/config/reloader/mod.rs index 62731f16..dce34ca3 100644 --- a/fact/src/config/reloader/mod.rs +++ b/fact/src/config/reloader/mod.rs @@ -236,7 +236,10 @@ impl Reloader { return; } }; - info!("Updated configuration: {new:#?}"); + match serde_json::to_string_pretty(&new) { + Ok(c) => info!("Updated configuration: {c}"), + Err(e) => warn!("Failed to serialize configuration: {e:?}"), + } self.send_updates(new); }