diff --git a/Cargo.lock b/Cargo.lock index 983e58a1c2..182ca8dc1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18287,6 +18287,7 @@ dependencies = [ "workspace-hack", "zed_actions", "zeta", + "zlog", "zlog_settings", ] @@ -18599,6 +18600,7 @@ dependencies = [ name = "zlog" version = "0.1.0" dependencies = [ + "anyhow", "log", "workspace-hack", ] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 1c4e6a4b18..6c920d07d8 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -138,6 +138,7 @@ welcome.workspace = true workspace.workspace = true zed_actions.workspace = true zeta.workspace = true +zlog.workspace = true zlog_settings.workspace = true workspace-hack.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b18a8a7c62..f087f73194 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -195,6 +195,7 @@ fn main() { return; } + zlog::init_from_env(); if stdout_is_a_pty() { init_stdout_logger(); } else { diff --git a/crates/zlog/Cargo.toml b/crates/zlog/Cargo.toml index 7fcede9988..b64b72633d 100644 --- a/crates/zlog/Cargo.toml +++ b/crates/zlog/Cargo.toml @@ -17,3 +17,4 @@ default = [] [dependencies] log.workspace = true workspace-hack.workspace = true +anyhow.workspace = true diff --git a/crates/zlog/src/env_config.rs b/crates/zlog/src/env_config.rs new file mode 100644 index 0000000000..481be37d95 --- /dev/null +++ b/crates/zlog/src/env_config.rs @@ -0,0 +1,126 @@ +use anyhow::{Result, anyhow}; + +pub struct EnvFilter { + pub level_global: Option, + pub directive_names: Vec, + pub directive_levels: Vec, +} + +pub fn parse(filter: &str) -> Result { + let mut max_level = None; + let mut directive_names = Vec::new(); + let mut directive_levels = Vec::new(); + + for directive in filter.split(',') { + match directive.split_once('=') { + Some((name, level)) => { + if level.contains('=') { + return Err(anyhow!("Invalid directive: {}", directive)); + } + let level = parse_level(level.trim())?; + directive_names.push(name.to_string()); + directive_levels.push(level); + } + None => { + let Ok(level) = parse_level(directive.trim()) else { + directive_names.push(directive.trim().to_string()); + directive_levels.push(log::LevelFilter::max() /* Enable all levels */); + continue; + }; + if max_level.is_some() { + return Err(anyhow!("Cannot set multiple max levels")); + } + max_level.replace(level); + } + }; + } + + Ok(EnvFilter { + level_global: max_level, + directive_names, + directive_levels, + }) +} + +fn parse_level(level: &str) -> Result { + if level.eq_ignore_ascii_case("TRACE") { + return Ok(log::LevelFilter::Trace); + } + if level.eq_ignore_ascii_case("DEBUG") { + return Ok(log::LevelFilter::Debug); + } + if level.eq_ignore_ascii_case("INFO") { + return Ok(log::LevelFilter::Info); + } + if level.eq_ignore_ascii_case("WARN") { + return Ok(log::LevelFilter::Warn); + } + if level.eq_ignore_ascii_case("ERROR") { + return Ok(log::LevelFilter::Error); + } + if level.eq_ignore_ascii_case("OFF") || level.eq_ignore_ascii_case("NONE") { + return Ok(log::LevelFilter::Off); + } + Err(anyhow!("Invalid level: {}", level)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn global_level() { + let input = "info"; + let filter = parse(input).unwrap(); + + assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); + assert!(filter.directive_names.is_empty()); + assert!(filter.directive_levels.is_empty()); + } + + #[test] + fn directive_level() { + let input = "my_module=debug"; + let filter = parse(input).unwrap(); + + assert_eq!(filter.level_global, None); + assert_eq!(filter.directive_names, vec!["my_module".to_string()]); + assert_eq!(filter.directive_levels, vec![log::LevelFilter::Debug]); + } + + #[test] + fn global_level_and_directive_level() { + let input = "info,my_module=debug"; + let filter = parse(input).unwrap(); + + assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); + assert_eq!(filter.directive_names, vec!["my_module".to_string()]); + assert_eq!(filter.directive_levels, vec![log::LevelFilter::Debug]); + } + + #[test] + fn global_level_and_bare_module() { + let input = "info,my_module"; + let filter = parse(input).unwrap(); + + assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); + assert_eq!(filter.directive_names, vec!["my_module".to_string()]); + assert_eq!(filter.directive_levels, vec![log::LevelFilter::max()]); + } + + #[test] + fn err_when_multiple_max_levels() { + let input = "info,warn"; + let result = parse(input); + + assert!(result.is_err()); + } + + #[test] + fn err_when_invalid_level() { + let input = "my_module=foobar"; + let result = parse(input); + + assert!(result.is_err()); + } +} diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index 56705d7f7b..b953409223 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -1,8 +1,26 @@ //! # logger pub use log as log_impl; +mod env_config; + pub const SCOPE_DEPTH_MAX: usize = 4; +pub fn init_from_env() { + let Ok(env_config) = std::env::var("ZED_LOG").or_else(|_| std::env::var("RUST_LOG")) else { + return; + }; + match env_config::parse(&env_config) { + Ok(filter) => { + scope_map::init_env_filter(filter); + scope_map::refresh(); + // TODO: set max level once removing `env_logger` and `simple_log` crates + } + Err(err) => { + eprintln!("Failed to parse log filter: {}", err); + } + } +} + /// because we are currently just wrapping the `log` crate in `zlog`, /// we need to work around the fact that the `log` crate only provides a /// single global level filter. In order to have more precise control until @@ -275,17 +293,23 @@ pub mod scope_map { collections::{HashMap, VecDeque}, hash::{DefaultHasher, Hasher}, sync::{ - RwLock, + OnceLock, RwLock, atomic::{AtomicU64, Ordering}, }, usize, }; use super::*; - + static ENV_FILTER: OnceLock = OnceLock::new(); static SCOPE_MAP: RwLock> = RwLock::new(None); static SCOPE_MAP_HASH: AtomicU64 = AtomicU64::new(0); + pub fn init_env_filter(filter: env_config::EnvFilter) { + if ENV_FILTER.set(filter).is_err() { + panic!("Environment filter cannot be initialized twice"); + } + } + pub fn is_scope_enabled(scope: &Scope, level: log_impl::Level) -> (bool, log_impl::Level) { let level_min = min_printed_log_level(level); if level <= level_min { @@ -340,13 +364,18 @@ pub mod scope_map { return hasher.finish(); } - pub fn refresh(settings: &HashMap) { + pub(crate) fn refresh() { + refresh_from_settings(&HashMap::default()); + } + + pub fn refresh_from_settings(settings: &HashMap) { let hash_old = SCOPE_MAP_HASH.load(Ordering::Acquire); let hash_new = hash_scope_map_settings(settings); if hash_old == hash_new && hash_old != 0 { return; } - let map_new = ScopeMap::new_from_settings(settings); + let env_config = ENV_FILTER.get(); + let map_new = ScopeMap::new_from_settings_and_env(settings, env_config); if let Ok(_) = SCOPE_MAP_HASH.compare_exchange( hash_old, @@ -430,15 +459,41 @@ pub mod scope_map { } impl ScopeMap { - pub fn new_from_settings(items_input_map: &HashMap) -> Self { - let mut items = items_input_map - .into_iter() - .filter_map(|(scope_str, level_str)| { - let scope = scope_alloc_from_scope_str(&scope_str)?; - let level = level_from_level_str(&level_str)?; - return Some((scope, level)); - }) - .collect::>(); + pub fn new_from_settings_and_env( + items_input_map: &HashMap, + env_config: Option<&env_config::EnvFilter>, + ) -> Self { + let mut items = Vec::with_capacity( + items_input_map.len() + env_config.map_or(0, |c| c.directive_names.len()), + ); + if let Some(env_filter) = env_config { + // TODO: parse on load instead of every reload + items.extend( + env_filter + .directive_names + .iter() + .zip(env_filter.directive_levels.iter()) + .filter_map(|(scope, level_filter)| { + if items_input_map.get(scope).is_some() { + return None; + } + let scope = scope_alloc_from_scope_str(scope)?; + // TODO: use level filters instead of scopes in scope map + let level = level_filter.to_level()?; + + Some((scope, level)) + }), + ); + } + items.extend( + items_input_map + .into_iter() + .filter_map(|(scope_str, level_str)| { + let scope = scope_alloc_from_scope_str(&scope_str)?; + let level = level_from_level_str(&level_str)?; + return Some((scope, level)); + }), + ); items.sort_by(|a, b| a.0.cmp(&b.0)); @@ -491,7 +546,12 @@ pub mod scope_map { let is_last = depth + 1 == SCOPE_DEPTH_MAX || !is_valid_scope; let mut enabled = None; if is_last { - assert_eq!(sub_items_start + 1, sub_items_end); + assert_eq!( + sub_items_start + 1, + sub_items_end, + "Expected one item: got: {:?}", + &items[items_range.clone()] + ); enabled = Some(items[sub_items_start].1); } else { let entry_index = this.entries.len(); @@ -562,6 +622,8 @@ pub mod scope_map { #[cfg(test)] mod tests { + use crate::private::scope_new; + use super::*; fn scope_map_from_keys(kv: &[(&str, &str)]) -> ScopeMap { @@ -569,7 +631,7 @@ pub mod scope_map { .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); - ScopeMap::new_from_settings(&hash_map) + ScopeMap::new_from_settings_and_env(&hash_map, None) } #[test] @@ -692,6 +754,74 @@ pub mod scope_map { EnabledStatus::Disabled ); } + + fn scope_map_from_keys_and_env( + kv: &[(&str, &str)], + env: &env_config::EnvFilter, + ) -> ScopeMap { + let hash_map: HashMap = kv + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + ScopeMap::new_from_settings_and_env(&hash_map, Some(env)) + } + + #[test] + fn test_initialization_with_env() { + let env_filter = env_config::parse("a.b=debug,u=error").unwrap(); + let map = scope_map_from_keys_and_env(&[], &env_filter); + assert_eq!(map.root_count, 2); + assert_eq!(map.entries.len(), 3); + assert_eq!( + map.is_enabled(&scope_new(&["a"]), log_impl::Level::Debug), + EnabledStatus::NotConfigured + ); + assert_eq!( + map.is_enabled(&scope_new(&["a", "b"]), log_impl::Level::Debug), + EnabledStatus::Enabled + ); + assert_eq!( + map.is_enabled(&scope_new(&["a", "b", "c"]), log_impl::Level::Trace), + EnabledStatus::Disabled + ); + + let env_filter = env_config::parse("a.b=debug,e.f.g.h=trace,u=error").unwrap(); + let map = scope_map_from_keys_and_env( + &[ + ("a.b.c.d", "trace"), + ("e.f.g.h", "debug"), + ("i.j.k.l", "info"), + ("m.n.o.p", "warn"), + ("q.r.s.t", "error"), + ], + &env_filter, + ); + assert_eq!(map.root_count, 6); + assert_eq!(map.entries.len(), 21); + assert_eq!(map.entries[0].scope, "a"); + assert_eq!(map.entries[1].scope, "e"); + assert_eq!(map.entries[2].scope, "i"); + assert_eq!(map.entries[3].scope, "m"); + assert_eq!(map.entries[4].scope, "q"); + assert_eq!(map.entries[5].scope, "u"); + assert_eq!( + map.is_enabled(&scope_new(&["a", "b", "c", "d"]), log_impl::Level::Trace), + EnabledStatus::Enabled + ); + assert_eq!( + map.is_enabled(&scope_new(&["a", "b", "c"]), log_impl::Level::Trace), + EnabledStatus::Disabled + ); + assert_eq!( + map.is_enabled(&scope_new(&["u", "v"]), log_impl::Level::Warn), + EnabledStatus::Disabled + ); + // settings override env + assert_eq!( + map.is_enabled(&scope_new(&["e", "f", "g", "h"]), log_impl::Level::Trace), + EnabledStatus::Disabled, + ); + } } } diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index 2553a8874e..36e01dab24 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -10,7 +10,7 @@ pub fn init(cx: &mut App) { cx.observe_global::(|cx| { let zlog_settings = ZlogSettings::get_global(cx); - zlog::scope_map::refresh(&zlog_settings.scopes); + zlog::scope_map::refresh_from_settings(&zlog_settings.scopes); }) .detach(); }