Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ possible include a PR number for easier tracking.

## Next

* feat(endpoints): add inodes introspection endpoint (#1273)
* feat: add --replay mode for JSONL event replay without eBPF (#1010)
* feat(config): configurable loaded BPF programs (#1086)
* feat(output): add basic opentelemetry output (#971)
Expand Down
26 changes: 26 additions & 0 deletions fact/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ pub struct EndpointConfig {
address: Option<SocketAddr>,
expose_metrics: Option<bool>,
health_check: Option<bool>,
introspection: Option<bool>,
}

impl EndpointConfig {
Expand All @@ -291,6 +292,10 @@ impl EndpointConfig {
if let Some(health_check) = from.health_check {
self.health_check = Some(health_check);
}

if let Some(introspection) = from.introspection {
self.introspection = Some(introspection);
}
}

pub fn address(&self) -> SocketAddr {
Expand All @@ -305,6 +310,10 @@ impl EndpointConfig {
pub fn health_check(&self) -> bool {
self.health_check.unwrap_or(false)
}

pub fn introspection(&self) -> bool {
self.introspection.unwrap_or(false)
}
}

impl TryFrom<&yaml::Hash> for EndpointConfig {
Expand Down Expand Up @@ -340,6 +349,12 @@ impl TryFrom<&yaml::Hash> for EndpointConfig {
};
endpoint.health_check = Some(hc);
}
"introspection" => {
let Some(i) = v.as_bool() else {
bail!("endpoint.introspection field has incorrect type: {v:?}");
};
endpoint.introspection = Some(i);
}
name => bail!("Invalid field 'endpoint.{name}' with value: {v:?}"),
}
}
Expand Down Expand Up @@ -792,6 +807,16 @@ pub struct FactCli {
#[arg(long, overrides_with = "health_check", hide(true))]
no_health_check: bool,

/// Whether the introspection endpoints should be enabled
#[arg(
long,
overrides_with("no_introspection"),
env = "FACT_ENDPOINT_INTROSPECTION"
)]
introspection: bool,
#[arg(long, overrides_with = "introspection", hide(true))]
no_introspection: bool,

/// Whether to perform a pre flight check
#[arg(
long,
Expand Down Expand Up @@ -880,6 +905,7 @@ impl FactCli {
address: self.address,
expose_metrics: resolve_bool_arg(self.expose_metrics, self.no_expose_metrics),
health_check: resolve_bool_arg(self.health_check, self.no_health_check),
introspection: resolve_bool_arg(self.introspection, self.no_introspection),
},
bpf: BpfConfig {
ringbuf_size: self.ringbuf_size,
Expand Down
277 changes: 260 additions & 17 deletions fact/src/config/reloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,26 +145,39 @@ impl Reloader {
res
}

/// Recreate the configuration and notify of changes to any
/// subscribers.
fn reload(&mut self) {
if !self.update_cache() {
return;
}

let new = match FactConfig::build() {
Ok(config) => config,
Err(e) => {
warn!("Configuration reloading failed: {e}");
return;
}
};
info!("Updated configuration: {new:#?}");
/// Compare endpoint configurations and figure out if reloading is
/// needed.
///
/// EndpointConfig has non-reloadable fields, its PartialEq
/// implementation still has all fields so unit tests can check
/// properly.
///
/// The first argument is destructured here so the compiler
/// complains when new fields are added, making sure we fix this
/// with things that need to be reloaded.
fn endpoint_should_reload(
&EndpointConfig {
address: l_address,
expose_metrics: l_expose_metrics,
health_check: l_health_check,
introspection: _,
}: &EndpointConfig,
right: &EndpointConfig,
) -> bool {
l_address != right.address
|| l_expose_metrics != right.expose_metrics
|| l_health_check != right.health_check
}

/// Propagate configuration changes to all subscribers that need it
fn send_updates(&self, new: &FactConfig) {
self.endpoint.send_if_modified(|old| {
if *old != new.endpoint {
if Reloader::endpoint_should_reload(old, &new.endpoint) {
debug!("Sending new endpoint configuration...");
*old = new.endpoint.clone();
*old = EndpointConfig {
introspection: old.introspection,
..new.endpoint
};
true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
false
Expand Down Expand Up @@ -227,6 +240,25 @@ impl Reloader {
if self.config.hotreload() != new.hotreload() {
warn!("Changes to the hotreload field only take effect on startup");
}
}

/// Recreate the configuration and notify of changes to any
/// subscribers.
fn reload(&mut self) {
if !self.update_cache() {
return;
}

let new = match FactConfig::build() {
Ok(config) => config,
Err(e) => {
warn!("Configuration reloading failed: {e}");
return;
}
};
info!("Updated configuration: {new:#?}");

self.send_updates(&new);

self.config = new;
}
Expand Down Expand Up @@ -274,3 +306,214 @@ impl From<FactConfig> for Reloader {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_reloading_endpoint() {
let tests = [
(FactConfig::default(), FactConfig::default(), None),
(
FactConfig::default(),
FactConfig {
endpoint: EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
..Default::default()
},
..Default::default()
},
Some(EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
..Default::default()
}),
),
(
FactConfig {
endpoint: EndpointConfig {
address: Some(([0, 0, 0, 0], 9090).into()),
..Default::default()
},
..Default::default()
},
FactConfig {
endpoint: EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
..Default::default()
},
..Default::default()
},
Some(EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
..Default::default()
}),
),
(
FactConfig::default(),
FactConfig {
endpoint: EndpointConfig {
expose_metrics: Some(true),
..Default::default()
},
..Default::default()
},
Some(EndpointConfig {
expose_metrics: Some(true),
..Default::default()
}),
),
(
FactConfig {
endpoint: EndpointConfig {
expose_metrics: Some(true),
..Default::default()
},
..Default::default()
},
FactConfig {
endpoint: EndpointConfig {
expose_metrics: Some(false),
..Default::default()
},
..Default::default()
},
Some(EndpointConfig {
expose_metrics: Some(false),
..Default::default()
}),
),
(
FactConfig::default(),
FactConfig {
endpoint: EndpointConfig {
health_check: Some(true),
..Default::default()
},
..Default::default()
},
Some(EndpointConfig {
health_check: Some(true),
..Default::default()
}),
),
(
FactConfig {
endpoint: EndpointConfig {
health_check: Some(true),
..Default::default()
},
..Default::default()
},
FactConfig {
endpoint: EndpointConfig {
health_check: Some(false),
..Default::default()
},
..Default::default()
},
Some(EndpointConfig {
health_check: Some(false),
..Default::default()
}),
),
(
FactConfig::default(),
FactConfig {
endpoint: EndpointConfig {
introspection: Some(true),
..Default::default()
},
..Default::default()
},
None,
),
(
FactConfig {
endpoint: EndpointConfig {
introspection: Some(true),
..Default::default()
},
..Default::default()
},
FactConfig {
endpoint: EndpointConfig {
introspection: Some(false),
..Default::default()
},
..Default::default()
},
None,
),
(
FactConfig::default(),
FactConfig {
endpoint: EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
introspection: Some(true),
expose_metrics: Some(true),
health_check: Some(true),
},
..Default::default()
},
Some(EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
introspection: None,
expose_metrics: Some(true),
health_check: Some(true),
}),
),
(
FactConfig {
endpoint: EndpointConfig {
address: Some(([0, 0, 0, 0], 9090).into()),
introspection: Some(true),
expose_metrics: Some(true),
health_check: Some(true),
},
..Default::default()
},
FactConfig {
endpoint: EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
introspection: Some(false),
expose_metrics: Some(false),
health_check: Some(false),
},
..Default::default()
},
Some(EndpointConfig {
address: Some(([127, 0, 0, 1], 8080).into()),
introspection: Some(true),
expose_metrics: Some(false),
health_check: Some(false),
}),
),
];

for (old, new, expected) in tests {
let reloader = Reloader::from(old);
let endpoint = reloader.endpoint();
let assert_has_changed = |has_changed: bool| {
assert!(
has_changed,
"\ninput: {:?}\nnew: {:?}",
reloader.config().endpoint,
new.endpoint
);
};

reloader.send_updates(&new);

match expected {
Some(expected) => {
assert_has_changed(endpoint.has_changed().unwrap());
assert_eq!(*endpoint.borrow(), expected);
}
None => {
assert_has_changed(!endpoint.has_changed().unwrap());
}
}
}
}
}
Loading
Loading