use crate::observability::config::{LogFilterConfig, LogFilterOverrideConfig};
use crate::observability::filter::SharedOrderedFilter;
use anyhow::{bail, Result};
#[derive(Clone, Debug)]
pub struct FilterReloadHandle {
enabled: bool,
scope: &'static str,
filter: SharedOrderedFilter,
}
impl FilterReloadHandle {
pub(crate) fn new(enabled: bool, scope: &'static str, filter: SharedOrderedFilter) -> Self {
Self {
enabled,
scope,
filter,
}
}
pub fn reload_default_level(&self, level: &str) -> Result<()> {
self.ensure_enabled()?;
self.filter.reload_default_level(level)
}
pub fn reload_overrides(&self, rules: Vec<LogFilterOverrideConfig>) -> Result<()> {
self.ensure_enabled()?;
self.filter.reload_overrides(rules)
}
pub fn set_override_enabled(&self, name: &str, enabled: bool) -> Result<()> {
self.ensure_enabled()?;
self.filter.set_override_enabled(name, enabled)
}
pub fn reload_filter(&self, config: LogFilterConfig) -> Result<()> {
self.ensure_enabled()?;
self.filter.reload_filter(config)
}
pub(crate) fn reload_filter_unchecked(&self, config: LogFilterConfig) -> Result<()> {
self.filter.reload_filter(config)
}
pub fn current_filter_config(&self) -> LogFilterConfig {
self.filter.current_config()
}
fn ensure_enabled(&self) -> Result<()> {
if !self.enabled {
bail!(
"dynamic observability reload is disabled for `{}`",
self.scope
);
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct ObservabilityReloadHandle {
local: FilterReloadHandle,
remote: Option<FilterReloadHandle>,
trace: Option<FilterReloadHandle>,
remote_uses_local_filter: bool,
}
impl ObservabilityReloadHandle {
pub(crate) fn new(
local: FilterReloadHandle,
remote: Option<FilterReloadHandle>,
trace: Option<FilterReloadHandle>,
remote_uses_local_filter: bool,
) -> Self {
Self {
local,
remote,
trace,
remote_uses_local_filter,
}
}
pub fn local(&self) -> &FilterReloadHandle {
&self.local
}
pub fn remote(&self) -> Result<&FilterReloadHandle> {
self.remote
.as_ref()
.ok_or_else(|| anyhow::anyhow!("remote log filter is not initialized"))
}
pub fn trace(&self) -> Result<&FilterReloadHandle> {
self.trace
.as_ref()
.ok_or_else(|| anyhow::anyhow!("trace filter is not initialized"))
}
pub(crate) fn remote_uses_local_filter(&self) -> bool {
self.remote_uses_local_filter
}
pub fn reload_default_level(&self, level: &str) -> Result<()> {
self.local.reload_default_level(level)
}
pub fn reload_overrides(&self, rules: Vec<LogFilterOverrideConfig>) -> Result<()> {
self.local.reload_overrides(rules)
}
pub fn set_override_enabled(&self, name: &str, enabled: bool) -> Result<()> {
self.local.set_override_enabled(name, enabled)
}
pub fn reload_filter(&self, config: LogFilterConfig) -> Result<()> {
self.local.reload_filter(config)
}
pub fn current_filter_config(&self) -> LogFilterConfig {
self.local.current_filter_config()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scoped_reload_updates_only_selected_filter() {
let local = reload_scope("local", "info");
let remote = reload_scope("remote", "off");
let trace = reload_scope("trace", "warn");
let reload = ObservabilityReloadHandle::new(local, Some(remote), Some(trace), false);
reload
.remote()
.unwrap()
.reload_default_level("debug")
.unwrap();
reload
.trace()
.unwrap()
.reload_default_level("trace")
.unwrap();
assert_eq!(reload.local().current_filter_config().default_level, "info");
assert_eq!(
reload
.remote()
.unwrap()
.current_filter_config()
.default_level,
"debug"
);
assert_eq!(
reload
.trace()
.unwrap()
.current_filter_config()
.default_level,
"trace"
);
}
#[test]
fn missing_remote_and_trace_scopes_return_errors() {
let reload =
ObservabilityReloadHandle::new(reload_scope("local", "info"), None, None, false);
assert!(reload
.remote()
.unwrap_err()
.to_string()
.contains("remote log filter is not initialized"));
assert!(reload
.trace()
.unwrap_err()
.to_string()
.contains("trace filter is not initialized"));
}
#[test]
fn disabled_reload_rejects_scope_updates() {
let filter = SharedOrderedFilter::new(filter_config("info")).unwrap();
let scope = FilterReloadHandle::new(false, "trace", filter);
let error = scope.reload_default_level("debug").unwrap_err();
assert!(error
.to_string()
.contains("dynamic observability reload is disabled for `trace`"));
}
fn reload_scope(scope: &'static str, level: &str) -> FilterReloadHandle {
let filter = SharedOrderedFilter::new(filter_config(level)).unwrap();
FilterReloadHandle::new(true, scope, filter)
}
fn filter_config(level: &str) -> LogFilterConfig {
LogFilterConfig {
default_level: level.to_string(),
overrides: Vec::new(),
}
}
}