diff --git a/easytier/src/common/log.rs b/easytier/src/common/log.rs index 1a78f48a..cfab48d9 100644 --- a/easytier/src/common/log.rs +++ b/easytier/src/common/log.rs @@ -58,6 +58,21 @@ fn parse_env_filter(default_level: Option) -> Result Result { + EnvFilter::builder() + .with_default_directive(level.into()) + .parse("") + .with_context(|| "failed to create static filter") +} + +fn parse_file_filter(level: LevelFilter) -> Result { + if matches!(level, LevelFilter::OFF) { + parse_static_filter(level) + } else { + parse_env_filter(Some(level)) + } +} + fn is_log(meta: &Metadata) -> bool { meta.target() == LOG_TARGET || meta.target().starts_with(&format!("{LOG_TARGET}::")) } @@ -165,14 +180,17 @@ fn file_layers( ) -> anyhow::Result<(Vec, Option)> { let mut layers = Vec::new(); - let level = config.level.map(|s| s.parse().unwrap()); + let level = config + .level + .map(|s| s.parse().unwrap()) + .unwrap_or(LevelFilter::OFF); - if matches!(level, Some(LevelFilter::OFF)) && !reload { + if matches!(level, LevelFilter::OFF) && !reload { return Ok((layers, None)); } let (file_filter, file_filter_reloader) = - tracing_subscriber::reload::Layer::<_, Registry>::new(parse_env_filter(level)?); + tracing_subscriber::reload::Layer::<_, Registry>::new(parse_file_filter(level)?); let layer = |wrapper| { layer() @@ -218,9 +236,7 @@ fn file_layers( // 初始化全局状态 let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(tx.clone())); - if let Some(level) = level { - let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string())); - } + let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string())); std::thread::spawn(move || { while let Ok(lf) = rx.recv() { @@ -232,11 +248,7 @@ fn file_layers( } }; - let mut new_filter = match EnvFilter::builder() - .with_default_directive(parsed_level.into()) - .from_env() - .with_context(|| "failed to create file filter") - { + let mut new_filter = match parse_file_filter(parsed_level) { Ok(filter) => Some(filter), Err(e) => { error!("Failed to build new log filter for {:?}: {:?}", lf, e); @@ -268,6 +280,36 @@ mod tests { use super::*; use crate::common::config::FileLoggerConfig; + const RUST_LOG: &str = "RUST_LOG"; + + struct EnvVarGuard { + key: &'static str, + previous: Option, + } + + impl EnvVarGuard { + fn set(key: &'static str, value: &str) -> Self { + let previous = std::env::var_os(key); + unsafe { std::env::set_var(key, value) }; + Self { key, previous } + } + + fn unset(key: &'static str) -> Self { + let previous = std::env::var_os(key); + unsafe { std::env::remove_var(key) }; + Self { key, previous } + } + } + + impl Drop for EnvVarGuard { + fn drop(&mut self) { + match &self.previous { + Some(value) => unsafe { std::env::set_var(self.key, value) }, + None => unsafe { std::env::remove_var(self.key) }, + } + } + } + #[ctor::ctor] fn init() { let _ = Registry::default() @@ -276,7 +318,147 @@ mod tests { } #[test] + fn default_file_logger_level_is_off_without_reload() { + let (layers, sender) = file_layers(FileLoggerConfig::default(), false).unwrap(); + + assert!(layers.is_empty()); + assert!(sender.is_none()); + } + + #[test] + #[serial_test::serial] + fn default_file_logger_level_filters_info_with_reload() { + let _guard = EnvVarGuard::set(RUST_LOG, "info"); + let temp_dir = tempfile::tempdir().unwrap(); + let log_file_name = "default-off-test.log".to_string(); + let log_path = temp_dir.path().join(&log_file_name); + + let cfg = FileLoggerConfig { + file: Some(log_file_name), + dir: Some(temp_dir.path().to_string_lossy().to_string()), + ..Default::default() + }; + + let (layers, _sender) = file_layers(cfg, true).unwrap(); + let marker = "default-file-logger-off-marker"; + let subscriber = Registry::default().with(layers); + + tracing::subscriber::with_default(subscriber, || { + tracing::info!(target: LOG_TARGET, "{}", marker); + std::thread::sleep(std::time::Duration::from_millis(300)); + }); + + let content = std::fs::read_to_string(&log_path).unwrap_or_default(); + assert!( + !content.contains(marker), + "default file logger level should filter info logs" + ); + } + + #[test] + #[serial_test::serial] + fn file_logger_level_uses_env_filter_when_enabled() { + let _guard = EnvVarGuard::set(RUST_LOG, "debug"); + let temp_dir = tempfile::tempdir().unwrap(); + let log_file_name = "env-filter-test.log".to_string(); + let log_path = temp_dir.path().join(&log_file_name); + + let cfg = FileLoggerConfig { + level: Some(LevelFilter::INFO.to_string()), + file: Some(log_file_name), + dir: Some(temp_dir.path().to_string_lossy().to_string()), + ..Default::default() + }; + + let (layers, _sender) = file_layers(cfg, true).unwrap(); + let marker = "file-logger-env-filter-marker"; + let subscriber = Registry::default().with(layers); + + tracing::subscriber::with_default(subscriber, || { + tracing::debug!(target: LOG_TARGET, "{}", marker); + std::thread::sleep(std::time::Duration::from_millis(300)); + }); + + let content = std::fs::read_to_string(&log_path).unwrap_or_default(); + assert!( + content.contains(marker), + "enabled file logger should use RUST_LOG directives" + ); + } + + #[test] + #[serial_test::serial] + fn file_logger_reload_uses_env_filter_when_enabled() { + let _guard = EnvVarGuard::set(RUST_LOG, "debug"); + let temp_dir = tempfile::tempdir().unwrap(); + let log_file_name = "reload-env-filter-test.log".to_string(); + let log_path = temp_dir.path().join(&log_file_name); + + let cfg = FileLoggerConfig { + file: Some(log_file_name), + dir: Some(temp_dir.path().to_string_lossy().to_string()), + ..Default::default() + }; + + let (layers, sender) = file_layers(cfg, true).unwrap(); + let sender = sender.expect("reload=true should return a sender"); + let marker = "file-logger-reload-env-filter-marker"; + let subscriber = Registry::default().with(layers); + + tracing::subscriber::with_default(subscriber, || { + sender.send(LevelFilter::INFO.to_string()).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(300)); + + tracing::debug!(target: LOG_TARGET, "{}", marker); + std::thread::sleep(std::time::Duration::from_millis(300)); + }); + + let content = std::fs::read_to_string(&log_path).unwrap_or_default(); + assert!( + content.contains(marker), + "file logger enabled by reload should use RUST_LOG directives" + ); + } + + #[test] + #[serial_test::serial] + fn file_logger_reload_off_ignores_env_filter() { + let _guard = EnvVarGuard::set(RUST_LOG, "info"); + let temp_dir = tempfile::tempdir().unwrap(); + let log_file_name = "reload-off-test.log".to_string(); + let log_path = temp_dir.path().join(&log_file_name); + + let cfg = FileLoggerConfig { + level: Some(LevelFilter::INFO.to_string()), + file: Some(log_file_name), + dir: Some(temp_dir.path().to_string_lossy().to_string()), + ..Default::default() + }; + + let (layers, sender) = file_layers(cfg, true).unwrap(); + let sender = sender.expect("reload=true should return a sender"); + let marker = "file-logger-reload-off-marker"; + let subscriber = Registry::default().with(layers); + + tracing::subscriber::with_default(subscriber, || { + sender.send(LevelFilter::OFF.to_string()).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(300)); + + tracing::info!(target: LOG_TARGET, "{}", marker); + std::thread::sleep(std::time::Duration::from_millis(300)); + }); + + let content = std::fs::read_to_string(&log_path).unwrap_or_default(); + assert!( + !content.contains(marker), + "disabled file logger should ignore RUST_LOG directives" + ); + } + + #[test] + #[serial_test::serial] fn test_logger_reload() { + let _guard = EnvVarGuard::unset(RUST_LOG); let temp_dir = tempfile::tempdir().unwrap(); let log_file_name = "reload-test.log".to_string(); let log_path = temp_dir.path().join(&log_file_name);