@@ -1,6 +1,5 @@
use std::{
collections::{HashMap, VecDeque},
- hash::{DefaultHasher, Hasher},
sync::{
OnceLock, RwLock,
atomic::{AtomicU8, Ordering},
@@ -13,11 +12,7 @@ use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, Scope, ScopeAlloc, env_config
use log;
static ENV_FILTER: OnceLock<env_config::EnvFilter> = OnceLock::new();
-static SCOPE_MAP: RwLock<Option<GlobalScopeMap>> = RwLock::new(None);
-struct GlobalScopeMap {
- map: ScopeMap,
- hash: u64,
-}
+static SCOPE_MAP: RwLock<Option<ScopeMap>> = RwLock::new(None);
const LEVEL_ENABLED_MAX_DEFAULT: log::LevelFilter = log::LevelFilter::Info;
/// The maximum log level of verbosity that is enabled by default.
@@ -54,7 +49,7 @@ pub fn is_possibly_enabled_level(level: log::Level) -> bool {
return level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Relaxed);
}
-pub fn is_scope_enabled(scope: &Scope, level: log::Level) -> bool {
+pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Level) -> bool {
if level <= unsafe { LEVEL_ENABLED_MAX_STATIC } {
// [FAST PATH]
// if the message is at or below the minimum printed log level
@@ -73,7 +68,7 @@ pub fn is_scope_enabled(scope: &Scope, level: log::Level) -> bool {
return err.into_inner();
});
- let Some(GlobalScopeMap { map, .. }) = global_scope_map.as_ref() else {
+ let Some(map) = global_scope_map.as_ref() else {
// on failure, return false because it's not <= LEVEL_ENABLED_MAX_STATIC
return false;
};
@@ -82,7 +77,7 @@ pub fn is_scope_enabled(scope: &Scope, level: log::Level) -> bool {
// if no scopes are enabled, return false because it's not <= LEVEL_ENABLED_MAX_STATIC
return false;
}
- let enabled_status = map.is_enabled(&scope, level);
+ let enabled_status = map.is_enabled(&scope, module_path, level);
return match enabled_status {
// if it isn't configured, then it it's disabled because it's not <= LEVEL_ENABLED_MAX_STATIC
EnabledStatus::NotConfigured => false,
@@ -91,69 +86,46 @@ pub fn is_scope_enabled(scope: &Scope, level: log::Level) -> bool {
};
}
-fn hash_scope_map_settings(map: &HashMap<String, String>) -> u64 {
- let mut hasher = DefaultHasher::new();
- let mut items = map.iter().collect::<Vec<_>>();
- items.sort();
- for (key, value) in items {
- Hasher::write(&mut hasher, key.as_bytes());
- Hasher::write(&mut hasher, value.as_bytes());
- }
- return hasher.finish();
-}
-
pub(crate) fn refresh() {
refresh_from_settings(&HashMap::default());
}
pub fn refresh_from_settings(settings: &HashMap<String, String>) {
- let hash_old = {
- SCOPE_MAP
- .read()
- .unwrap_or_else(|err| {
- SCOPE_MAP.clear_poison();
- err.into_inner()
- })
- .as_ref()
- .map(|scope_map| scope_map.hash)
- };
- let hash_new = hash_scope_map_settings(settings);
- if hash_old == Some(hash_new) {
- return;
- }
let env_config = ENV_FILTER.get();
let map_new = ScopeMap::new_from_settings_and_env(settings, env_config);
let mut level_enabled_max = unsafe { LEVEL_ENABLED_MAX_STATIC };
for entry in &map_new.entries {
if let Some(level) = entry.enabled {
- level_enabled_max = level_enabled_max.max(level.to_level_filter());
+ level_enabled_max = level_enabled_max.max(level);
}
}
LEVEL_ENABLED_MAX_CONFIG.store(level_enabled_max as u8, Ordering::Release);
- let mut global_map = SCOPE_MAP.write().unwrap_or_else(|err| {
- SCOPE_MAP.clear_poison();
- err.into_inner()
- });
- global_map.replace(GlobalScopeMap {
- map: map_new,
- hash: hash_new,
- });
+ {
+ let mut global_map = SCOPE_MAP.write().unwrap_or_else(|err| {
+ SCOPE_MAP.clear_poison();
+ err.into_inner()
+ });
+ global_map.replace(map_new);
+ }
+ log::trace!("Log configuration updated");
}
-fn level_from_level_str(level_str: &String) -> Option<log::Level> {
+fn level_filter_from_str(level_str: &String) -> Option<log::LevelFilter> {
+ use log::LevelFilter::*;
let level = match level_str.to_ascii_lowercase().as_str() {
- "" => log::Level::Trace,
- "trace" => log::Level::Trace,
- "debug" => log::Level::Debug,
- "info" => log::Level::Info,
- "warn" => log::Level::Warn,
- "error" => log::Level::Error,
- "off" | "disable" | "no" | "none" | "disabled" => {
+ "" => Trace,
+ "trace" => Trace,
+ "debug" => Debug,
+ "info" => Info,
+ "warn" => Warn,
+ "error" => Error,
+ "off" => Off,
+ "disable" | "no" | "none" | "disabled" => {
crate::warn!(
- "Invalid log level \"{level_str}\", set to error to disable non-error logging. Defaulting to error"
+ "Invalid log level \"{level_str}\", to disable logging set to \"off\". Defaulting to \"off\"."
);
- log::Level::Error
+ Off
}
_ => {
crate::warn!("Invalid log level \"{level_str}\", ignoring");
@@ -190,14 +162,17 @@ fn scope_alloc_from_scope_str(scope_str: &String) -> Option<ScopeAlloc> {
return Some(scope);
}
+#[derive(PartialEq, Eq)]
pub struct ScopeMap {
entries: Vec<ScopeMapEntry>,
+ modules: Vec<(String, log::LevelFilter)>,
root_count: usize,
}
+#[derive(PartialEq, Eq)]
pub struct ScopeMapEntry {
scope: String,
- enabled: Option<log::Level>,
+ enabled: Option<log::LevelFilter>,
descendants: std::ops::Range<usize>,
}
@@ -216,6 +191,8 @@ impl ScopeMap {
let mut items = Vec::with_capacity(
items_input_map.len() + env_config.map_or(0, |c| c.directive_names.len()),
);
+ let mut modules = Vec::with_capacity(4);
+
if let Some(env_filter) = env_config {
// TODO: parse on load instead of every reload
items.extend(
@@ -223,15 +200,17 @@ impl ScopeMap {
.directive_names
.iter()
.zip(env_filter.directive_levels.iter())
- .filter_map(|(scope, level_filter)| {
- if items_input_map.get(scope).is_some() {
+ .filter_map(|(scope_str, &level_filter)| {
+ if items_input_map.get(scope_str).is_some() {
+ return None;
+ }
+ if scope_str.contains("::") {
+ modules.push((scope_str.clone(), level_filter));
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()?;
+ let scope = scope_alloc_from_scope_str(scope_str)?;
- Some((scope, level))
+ Some((scope, level_filter))
}),
);
}
@@ -239,16 +218,22 @@ impl ScopeMap {
items_input_map
.into_iter()
.filter_map(|(scope_str, level_str)| {
+ if scope_str.contains("::") {
+ modules.push((scope_str.clone(), level_str.parse().ok()?));
+ return None;
+ }
+ let level_filter = level_filter_from_str(&level_str)?;
let scope = scope_alloc_from_scope_str(&scope_str)?;
- let level = level_from_level_str(&level_str)?;
- return Some((scope, level));
+ return Some((scope, level_filter));
}),
);
items.sort_by(|a, b| a.0.cmp(&b.0));
+ modules.sort_by(|(a_name, _), (b_name, _)| a_name.cmp(b_name));
let mut this = Self {
entries: Vec::with_capacity(items.len() * SCOPE_DEPTH_MAX),
+ modules,
root_count: 0,
};
@@ -329,10 +314,15 @@ impl ScopeMap {
}
pub fn is_empty(&self) -> bool {
- self.entries.is_empty()
+ self.entries.is_empty() && self.modules.is_empty()
}
- pub fn is_enabled<S>(&self, scope: &[S; SCOPE_DEPTH_MAX], level: log::Level) -> EnabledStatus
+ pub fn is_enabled<S>(
+ &self,
+ scope: &[S; SCOPE_DEPTH_MAX],
+ module_path: Option<&str>,
+ level: log::Level,
+ ) -> EnabledStatus
where
S: AsRef<str>,
{
@@ -346,7 +336,6 @@ impl ScopeMap {
{
for entry in cur_range {
if entry.scope == scope[depth].as_ref() {
- // note:
enabled = entry.enabled.or(enabled);
cur_range = &self.entries[entry.descendants.clone()];
depth += 1;
@@ -356,13 +345,23 @@ impl ScopeMap {
break 'search;
}
- return enabled.map_or(EnabledStatus::NotConfigured, |level_enabled| {
- if level <= level_enabled {
- EnabledStatus::Enabled
- } else {
- EnabledStatus::Disabled
+ if enabled.is_none() && !self.modules.is_empty() && module_path.is_some() {
+ let module_path = module_path.unwrap();
+ for (module, filter) in &self.modules {
+ if module == module_path {
+ enabled.replace(*filter);
+ break;
+ }
}
- });
+ }
+
+ if let Some(enabled_filter) = enabled {
+ if level <= enabled_filter {
+ return EnabledStatus::Enabled;
+ }
+ return EnabledStatus::Disabled;
+ }
+ return EnabledStatus::NotConfigured;
}
}
@@ -444,61 +443,106 @@ mod tests {
]);
use log::Level;
assert_eq!(
- map.is_enabled(&scope_from_scope_str("a.b.c.d"), Level::Trace),
+ map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Trace),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("a.b.c.d"), Level::Debug),
+ map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Debug),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("e.f.g.h"), Level::Debug),
+ map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Debug),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("e.f.g.h"), Level::Info),
+ map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Info),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("e.f.g.h"), Level::Trace),
+ map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Trace),
EnabledStatus::Disabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("i.j.k.l"), Level::Info),
+ map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Info),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("i.j.k.l"), Level::Warn),
+ map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Warn),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("i.j.k.l"), Level::Debug),
+ map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Debug),
EnabledStatus::Disabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("m.n.o.p"), Level::Warn),
+ map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Warn),
+ EnabledStatus::Enabled
+ );
+ assert_eq!(
+ map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Error),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("m.n.o.p"), Level::Error),
+ map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Info),
+ EnabledStatus::Disabled
+ );
+
+ assert_eq!(
+ map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Error),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("m.n.o.p"), Level::Info),
+ map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Warn),
EnabledStatus::Disabled
);
+ }
+ #[test]
+ fn test_is_enabled_module() {
+ let mut map = scope_map_from_keys(&[("a", "trace")]);
+ map.modules = [("a::b::c", "trace"), ("a::b::d", "debug")]
+ .map(|(k, v)| (k.to_string(), v.parse().unwrap()))
+ .to_vec();
+ use log::Level;
+ assert_eq!(
+ map.is_enabled(
+ &scope_from_scope_str("__unused__"),
+ Some("a::b::c"),
+ Level::Trace
+ ),
+ EnabledStatus::Enabled
+ );
assert_eq!(
- map.is_enabled(&scope_from_scope_str("q.r.s.t"), Level::Error),
+ map.is_enabled(
+ &scope_from_scope_str("__unused__"),
+ Some("a::b::d"),
+ Level::Debug
+ ),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_from_scope_str("q.r.s.t"), Level::Warn),
+ map.is_enabled(
+ &scope_from_scope_str("__unused__"),
+ Some("a::b::d"),
+ Level::Trace,
+ ),
EnabledStatus::Disabled
);
+ assert_eq!(
+ map.is_enabled(
+ &scope_from_scope_str("__unused__"),
+ Some("a::e"),
+ Level::Info
+ ),
+ EnabledStatus::NotConfigured
+ );
+ assert_eq!(
+ map.is_enabled(&scope_from_scope_str("a"), Some("a::b::d"), Level::Trace),
+ EnabledStatus::Enabled,
+ );
}
fn scope_map_from_keys_and_env(kv: &[(&str, &str)], env: &env_config::EnvFilter) -> ScopeMap {
@@ -516,15 +560,15 @@ mod tests {
assert_eq!(map.root_count, 2);
assert_eq!(map.entries.len(), 3);
assert_eq!(
- map.is_enabled(&scope_new(&["a"]), log::Level::Debug),
+ map.is_enabled(&scope_new(&["a"]), None, log::Level::Debug),
EnabledStatus::NotConfigured
);
assert_eq!(
- map.is_enabled(&scope_new(&["a", "b"]), log::Level::Debug),
+ map.is_enabled(&scope_new(&["a", "b"]), None, log::Level::Debug),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_new(&["a", "b", "c"]), log::Level::Trace),
+ map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace),
EnabledStatus::Disabled
);
@@ -548,20 +592,20 @@ mod tests {
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::Level::Trace),
+ map.is_enabled(&scope_new(&["a", "b", "c", "d"]), None, log::Level::Trace),
EnabledStatus::Enabled
);
assert_eq!(
- map.is_enabled(&scope_new(&["a", "b", "c"]), log::Level::Trace),
+ map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace),
EnabledStatus::Disabled
);
assert_eq!(
- map.is_enabled(&scope_new(&["u", "v"]), log::Level::Warn),
+ map.is_enabled(&scope_new(&["u", "v"]), None, log::Level::Warn),
EnabledStatus::Disabled
);
// settings override env
assert_eq!(
- map.is_enabled(&scope_new(&["e", "f", "g", "h"]), log::Level::Trace),
+ map.is_enabled(&scope_new(&["e", "f", "g", "h"]), None, log::Level::Trace),
EnabledStatus::Disabled,
);
}
@@ -43,28 +43,28 @@ impl log::Log for Zlog {
if !self.enabled(record.metadata()) {
return;
}
- let scope = match record.module_path_static() {
+ let (crate_name_scope, module_scope) = match record.module_path_static() {
Some(module_path) => {
- // TODO: better module name -> scope translation
let crate_name = private::extract_crate_name_from_module_path(module_path);
- private::scope_new(&[crate_name])
+ let crate_name_scope = private::scope_new(&[crate_name]);
+ let module_scope = private::scope_new(&[module_path]);
+ (crate_name_scope, module_scope)
}
// TODO: when do we hit this
- None => private::scope_new(&["*unknown*"]),
+ None => (private::scope_new(&[]), private::scope_new(&["*unknown*"])),
};
let level = record.metadata().level();
- if !filter::is_scope_enabled(&scope, level) {
+ if !filter::is_scope_enabled(&crate_name_scope, record.module_path(), level) {
return;
}
sink::submit(sink::Record {
- scope,
+ scope: module_scope,
level,
message: record.args(),
});
}
fn flush(&self) {
- // todo: necessary?
sink::flush();
}
}
@@ -74,7 +74,7 @@ macro_rules! log {
($logger:expr, $level:expr, $($arg:tt)+) => {
let level = $level;
let logger = $logger;
- let enabled = $crate::filter::is_scope_enabled(&logger.scope, level);
+ let enabled = $crate::filter::is_scope_enabled(&logger.scope, Some(module_path!()), level);
if enabled {
$crate::sink::submit($crate::sink::Record {
scope: logger.scope,
@@ -143,7 +143,7 @@ macro_rules! error {
/// However, this is a feature not a bug, as it allows for a more accurate
/// understanding of how long the action actually took to complete, including
/// interruptions, which can help explain why something may have timed out,
-/// why it took longer to complete than it would had the await points resolved
+/// why it took longer to complete than it would have had the await points resolved
/// immediately, etc.
#[macro_export]
macro_rules! time {
@@ -168,15 +168,7 @@ macro_rules! scoped {
if index >= scope.len() {
#[cfg(debug_assertions)]
{
- panic!("Scope overflow trying to add scope {}", name);
- }
- #[cfg(not(debug_assertions))]
- {
- $crate::warn!(
- parent =>
- "Scope overflow trying to add scope {}... ignoring scope",
- name
- );
+ unreachable!("Scope overflow trying to add scope... ignoring scope");
}
}
scope[index] = name;
@@ -208,17 +200,31 @@ macro_rules! crate_name {
pub mod private {
use super::*;
- pub fn extract_crate_name_from_module_path(module_path: &'static str) -> &'static str {
- return module_path
- .split_once("::")
- .map(|(crate_name, _)| crate_name)
- .unwrap_or(module_path);
+ pub const fn extract_crate_name_from_module_path(module_path: &'static str) -> &'static str {
+ let mut i = 0;
+ let mod_path_bytes = module_path.as_bytes();
+ let mut index = mod_path_bytes.len();
+ while i + 1 < mod_path_bytes.len() {
+ if mod_path_bytes[i] == b':' && mod_path_bytes[i + 1] == b':' {
+ index = i;
+ break;
+ }
+ i += 1;
+ }
+ let Some((crate_name, _)) = module_path.split_at_checked(index) else {
+ return module_path;
+ };
+ return crate_name;
}
- pub fn scope_new(scopes: &[&'static str]) -> Scope {
+ pub const fn scope_new(scopes: &[&'static str]) -> Scope {
assert!(scopes.len() <= SCOPE_DEPTH_MAX);
let mut scope = [""; SCOPE_DEPTH_MAX];
- scope[0..scopes.len()].copy_from_slice(scopes);
+ let mut i = 0;
+ while i < scopes.len() {
+ scope[i] = scopes[i];
+ i += 1;
+ }
scope
}
@@ -244,6 +250,31 @@ pub struct Logger {
pub scope: Scope,
}
+impl log::Log for Logger {
+ fn enabled(&self, metadata: &log::Metadata) -> bool {
+ filter::is_possibly_enabled_level(metadata.level())
+ }
+
+ fn log(&self, record: &log::Record) {
+ if !self.enabled(record.metadata()) {
+ return;
+ }
+ let level = record.metadata().level();
+ if !filter::is_scope_enabled(&self.scope, record.module_path(), level) {
+ return;
+ }
+ sink::submit(sink::Record {
+ scope: self.scope,
+ level,
+ message: record.args(),
+ });
+ }
+
+ fn flush(&self) {
+ sink::flush();
+ }
+}
+
pub struct Timer {
pub logger: Logger,
pub start_time: std::time::Instant,
@@ -269,6 +300,7 @@ impl Timer {
done: false,
};
}
+
pub fn warn_if_gt(mut self, warn_limit: std::time::Duration) -> Self {
self.warn_if_longer_than = Some(warn_limit);
return self;
@@ -313,5 +345,21 @@ mod tests {
#[test]
fn test_crate_name() {
assert_eq!(crate_name!(), "zlog");
+ assert_eq!(
+ private::extract_crate_name_from_module_path("my_speedy_⚡️_crate::some_module"),
+ "my_speedy_⚡️_crate"
+ );
+ assert_eq!(
+ private::extract_crate_name_from_module_path("my_speedy_crate_⚡️::some_module"),
+ "my_speedy_crate_⚡️"
+ );
+ assert_eq!(
+ private::extract_crate_name_from_module_path("my_speedy_crate_:⚡️:some_module"),
+ "my_speedy_crate_:⚡️:some_module"
+ );
+ assert_eq!(
+ private::extract_crate_name_from_module_path("my_speedy_crate_::⚡️some_module"),
+ "my_speedy_crate_"
+ );
}
}