Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:38:30.704Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

filter.rs

843 lines · 29.6 KB · rust
1use collections::HashMap;
2use std::collections::VecDeque;
3use std::sync::{
4    OnceLock, RwLock,
5    atomic::{AtomicU8, Ordering},
6};
7
8use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, ScopeAlloc, ScopeRef, env_config, private};
9
10use log;
11
12static ENV_FILTER: OnceLock<env_config::EnvFilter> = OnceLock::new();
13static SCOPE_MAP: RwLock<ScopeMap> = RwLock::new(ScopeMap::empty());
14
15pub const LEVEL_ENABLED_MAX_DEFAULT: log::LevelFilter = log::LevelFilter::Info;
16/// The maximum log level of verbosity that is enabled by default.
17/// All messages more verbose than this level will be discarded
18/// by default unless specially configured.
19///
20/// This is used instead of the `log::max_level` as we need to tell the `log`
21/// crate that the max level is everything, so that we can dynamically enable
22/// logs that are more verbose than this level without the `log` crate throwing
23/// them away before we see them
24static LEVEL_ENABLED_MAX_STATIC: AtomicU8 = AtomicU8::new(LEVEL_ENABLED_MAX_DEFAULT as u8);
25
26/// A cache of the true maximum log level that _could_ be printed. This is based
27/// on the maximally verbose level that is configured by the user, and is used
28/// to filter out logs more verbose than any configured level.
29///
30/// E.g. if `LEVEL_ENABLED_MAX_STATIC `is 'info' but a user has configured some
31/// scope to print at a `debug` level, then this will be `debug`, and all
32/// `trace` logs will be discarded.
33/// Therefore, it should always be `>= LEVEL_ENABLED_MAX_STATIC`
34// PERF: this doesn't need to be an atomic, we don't actually care about race conditions here
35pub static LEVEL_ENABLED_MAX_CONFIG: AtomicU8 = AtomicU8::new(LEVEL_ENABLED_MAX_DEFAULT as u8);
36
37const DEFAULT_FILTERS: &[(&str, log::LevelFilter)] = &[
38    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
39    ("zbus", log::LevelFilter::Warn),
40    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))]
41    ("naga::back::spv::writer", log::LevelFilter::Warn),
42    // usvg prints a lot of warnings on rendering an SVG with partial errors, which
43    // can happen a lot with the SVG preview
44    ("usvg::parser", log::LevelFilter::Error),
45    ("pet", log::LevelFilter::Warn),
46];
47
48pub fn init_env_filter(filter: env_config::EnvFilter) {
49    if let Some(level_max) = filter.level_global {
50        LEVEL_ENABLED_MAX_STATIC.store(level_max as u8, Ordering::Release)
51    }
52    if ENV_FILTER.set(filter).is_err() {
53        panic!("Environment filter cannot be initialized twice");
54    }
55}
56
57pub fn is_possibly_enabled_level(level: log::Level) -> bool {
58    level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Acquire)
59}
60
61pub fn is_scope_enabled(
62    scope: &ScopeRef<'_>,
63    module_path: Option<&str>,
64    level: log::Level,
65) -> bool {
66    // TODO: is_always_allowed_level that checks against LEVEL_ENABLED_MIN_CONFIG
67    if !is_possibly_enabled_level(level) {
68        // [FAST PATH]
69        // if the message is above the maximum enabled log level
70        // (where error < warn < info etc) then disable without checking
71        // scope map
72        return false;
73    }
74    let is_enabled_by_default = level as u8 <= LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire);
75    let global_scope_map = SCOPE_MAP.read().unwrap_or_else(|err| {
76        SCOPE_MAP.clear_poison();
77        err.into_inner()
78    });
79
80    if global_scope_map.is_empty() {
81        // if no scopes are enabled, return false because it's not <= LEVEL_ENABLED_MAX_STATIC
82        return is_enabled_by_default;
83    }
84    let enabled_status = global_scope_map.is_enabled(scope, module_path, level);
85    match enabled_status {
86        EnabledStatus::NotConfigured => is_enabled_by_default,
87        EnabledStatus::Enabled => true,
88        EnabledStatus::Disabled => false,
89    }
90}
91
92pub fn refresh_from_settings(settings: &HashMap<String, String>) {
93    let env_config = ENV_FILTER.get();
94    let map_new = ScopeMap::new_from_settings_and_env(settings, env_config, DEFAULT_FILTERS);
95    let mut level_enabled_max = LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire);
96    for entry in &map_new.entries {
97        if let Some(level) = entry.enabled {
98            level_enabled_max = level_enabled_max.max(level as u8);
99        }
100    }
101    LEVEL_ENABLED_MAX_CONFIG.store(level_enabled_max, Ordering::Release);
102
103    {
104        let mut global_map = SCOPE_MAP.write().unwrap_or_else(|err| {
105            SCOPE_MAP.clear_poison();
106            err.into_inner()
107        });
108        *global_map = map_new;
109    }
110    log::trace!("Log configuration updated");
111}
112
113fn level_filter_from_str(level_str: &str) -> Option<log::LevelFilter> {
114    use log::LevelFilter::*;
115    let level = match level_str.to_ascii_lowercase().as_str() {
116        "" => Trace,
117        "trace" => Trace,
118        "debug" => Debug,
119        "info" => Info,
120        "warn" => Warn,
121        "error" => Error,
122        "off" => Off,
123        "disable" | "no" | "none" | "disabled" => {
124            crate::warn!(
125                "Invalid log level \"{level_str}\", to disable logging set to \"off\". Defaulting to \"off\"."
126            );
127            Off
128        }
129        _ => {
130            crate::warn!("Invalid log level \"{level_str}\", ignoring");
131            return None;
132        }
133    };
134    Some(level)
135}
136
137fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> {
138    let mut scope_buf = [""; SCOPE_DEPTH_MAX];
139    let mut index = 0;
140    let mut scope_iter = scope_str.split(SCOPE_STRING_SEP_STR);
141    while index < SCOPE_DEPTH_MAX {
142        let Some(scope) = scope_iter.next() else {
143            break;
144        };
145        if scope.is_empty() {
146            continue;
147        }
148        scope_buf[index] = scope;
149        index += 1;
150    }
151    if index == 0 {
152        return None;
153    }
154    if scope_iter.next().is_some() {
155        crate::warn!(
156            "Invalid scope key, too many nested scopes: '{scope_str}'. Max depth is {SCOPE_DEPTH_MAX}",
157        );
158        return None;
159    }
160    let scope = scope_buf.map(|s| s.to_string());
161    Some(scope)
162}
163
164#[derive(Debug, PartialEq, Eq)]
165pub struct ScopeMap {
166    entries: Vec<ScopeMapEntry>,
167    modules: Vec<(String, log::LevelFilter)>,
168    root_count: usize,
169}
170
171#[derive(Debug, PartialEq, Eq)]
172pub struct ScopeMapEntry {
173    scope: String,
174    enabled: Option<log::LevelFilter>,
175    descendants: std::ops::Range<usize>,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum EnabledStatus {
180    Enabled,
181    Disabled,
182    NotConfigured,
183}
184
185impl ScopeMap {
186    pub fn new_from_settings_and_env(
187        items_input_map: &HashMap<String, String>,
188        env_config: Option<&env_config::EnvFilter>,
189        default_filters: &[(&str, log::LevelFilter)],
190    ) -> Self {
191        let mut items = Vec::<(ScopeAlloc, log::LevelFilter)>::with_capacity(
192            items_input_map.len()
193                + env_config.map_or(0, |c| c.directive_names.len())
194                + default_filters.len(),
195        );
196        let mut modules = Vec::with_capacity(4);
197
198        let env_filters = env_config.iter().flat_map(|env_filter| {
199            env_filter
200                .directive_names
201                .iter()
202                .zip(env_filter.directive_levels.iter())
203                .map(|(scope_str, level_filter)| (scope_str.as_str(), *level_filter))
204        });
205
206        let new_filters = items_input_map.iter().filter_map(|(scope_str, level_str)| {
207            let level_filter = level_filter_from_str(level_str)?;
208            Some((scope_str.as_str(), level_filter))
209        });
210
211        let all_filters = default_filters
212            .iter()
213            .cloned()
214            .chain(env_filters)
215            .chain(new_filters);
216
217        for (scope_str, level_filter) in all_filters {
218            if scope_str.contains("::") {
219                if let Some(idx) = modules.iter().position(|(module, _)| module == scope_str) {
220                    modules[idx].1 = level_filter;
221                } else {
222                    modules.push((scope_str.to_string(), level_filter));
223                }
224                continue;
225            }
226            let Some(scope) = scope_alloc_from_scope_str(scope_str) else {
227                continue;
228            };
229            if let Some(idx) = items
230                .iter()
231                .position(|(scope_existing, _)| scope_existing == &scope)
232            {
233                items[idx].1 = level_filter;
234            } else {
235                items.push((scope, level_filter));
236            }
237        }
238
239        items.sort_by(|a, b| a.0.cmp(&b.0));
240        modules.sort_by(|(a_name, _), (b_name, _)| a_name.cmp(b_name));
241
242        let mut this = Self {
243            entries: Vec::with_capacity(items.len() * SCOPE_DEPTH_MAX),
244            modules,
245            root_count: 0,
246        };
247
248        let items_count = items.len();
249
250        struct ProcessQueueEntry {
251            parent_index: usize,
252            depth: usize,
253            items_range: std::ops::Range<usize>,
254        }
255        let mut process_queue = VecDeque::new();
256        process_queue.push_back(ProcessQueueEntry {
257            parent_index: usize::MAX,
258            depth: 0,
259            items_range: 0..items_count,
260        });
261
262        let empty_range = 0..0;
263
264        while let Some(process_entry) = process_queue.pop_front() {
265            let ProcessQueueEntry {
266                items_range,
267                depth,
268                parent_index,
269            } = process_entry;
270            let mut cursor = items_range.start;
271            let res_entries_start = this.entries.len();
272            while cursor < items_range.end {
273                let sub_items_start = cursor;
274                cursor += 1;
275                let scope_name = &items[sub_items_start].0[depth];
276                while cursor < items_range.end && &items[cursor].0[depth] == scope_name {
277                    cursor += 1;
278                }
279                let sub_items_end = cursor;
280                if scope_name.is_empty() {
281                    assert_eq!(sub_items_start + 1, sub_items_end);
282                    assert_ne!(depth, 0);
283                    assert_ne!(parent_index, usize::MAX);
284                    assert!(this.entries[parent_index].enabled.is_none());
285                    this.entries[parent_index].enabled = Some(items[sub_items_start].1);
286                    continue;
287                }
288                let is_valid_scope = !scope_name.is_empty();
289                let is_last = depth + 1 == SCOPE_DEPTH_MAX || !is_valid_scope;
290                let mut enabled = None;
291                if is_last {
292                    assert_eq!(
293                        sub_items_start + 1,
294                        sub_items_end,
295                        "Expected one item: got: {:?}",
296                        &items[items_range]
297                    );
298                    enabled = Some(items[sub_items_start].1);
299                } else {
300                    let entry_index = this.entries.len();
301                    process_queue.push_back(ProcessQueueEntry {
302                        items_range: sub_items_start..sub_items_end,
303                        parent_index: entry_index,
304                        depth: depth + 1,
305                    });
306                }
307                this.entries.push(ScopeMapEntry {
308                    scope: scope_name.to_owned(),
309                    enabled,
310                    descendants: empty_range.clone(),
311                });
312            }
313            let res_entries_end = this.entries.len();
314            if parent_index != usize::MAX {
315                this.entries[parent_index].descendants = res_entries_start..res_entries_end;
316            } else {
317                this.root_count = res_entries_end;
318            }
319        }
320
321        this
322    }
323
324    pub fn is_empty(&self) -> bool {
325        self.entries.is_empty() && self.modules.is_empty()
326    }
327
328    pub fn is_enabled<S>(
329        &self,
330        scope: &[S; SCOPE_DEPTH_MAX],
331        module_path: Option<&str>,
332        level: log::Level,
333    ) -> EnabledStatus
334    where
335        S: AsRef<str>,
336    {
337        fn search<S>(map: &ScopeMap, scope: &[S; SCOPE_DEPTH_MAX]) -> Option<log::LevelFilter>
338        where
339            S: AsRef<str>,
340        {
341            let mut enabled = None;
342            let mut cur_range = &map.entries[0..map.root_count];
343            let mut depth = 0;
344            'search: while !cur_range.is_empty()
345                && depth < SCOPE_DEPTH_MAX
346                && scope[depth].as_ref() != ""
347            {
348                for entry in cur_range {
349                    if entry.scope == scope[depth].as_ref() {
350                        enabled = entry.enabled.or(enabled);
351                        cur_range = &map.entries[entry.descendants.clone()];
352                        depth += 1;
353                        continue 'search;
354                    }
355                }
356                break 'search;
357            }
358            enabled
359        }
360
361        let mut enabled = search(self, scope);
362
363        if let Some(module_path) = module_path {
364            let scope_is_empty = scope[0].as_ref().is_empty();
365
366            if enabled.is_none() && scope_is_empty {
367                let crate_name = private::extract_crate_name_from_module_path(module_path);
368                let mut crate_name_scope = [""; SCOPE_DEPTH_MAX];
369                crate_name_scope[0] = crate_name;
370                enabled = search(self, &crate_name_scope);
371            }
372
373            if !self.modules.is_empty() {
374                let crate_name = private::extract_crate_name_from_module_path(module_path);
375                let is_scope_just_crate_name =
376                    scope[0].as_ref() == crate_name && scope[1].as_ref() == "";
377                if enabled.is_none() || is_scope_just_crate_name {
378                    for (module, filter) in &self.modules {
379                        if module == module_path {
380                            enabled.replace(*filter);
381                            break;
382                        }
383                    }
384                }
385            }
386        }
387
388        if let Some(enabled_filter) = enabled {
389            if level <= enabled_filter {
390                return EnabledStatus::Enabled;
391            }
392            return EnabledStatus::Disabled;
393        }
394        EnabledStatus::NotConfigured
395    }
396
397    const fn empty() -> ScopeMap {
398        ScopeMap {
399            entries: vec![],
400            modules: vec![],
401            root_count: 0,
402        }
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use log::LevelFilter;
409
410    use crate::Scope;
411    use crate::private::scope_new;
412
413    use super::*;
414
415    fn scope_map_from_keys(kv: &[(&str, &str)]) -> ScopeMap {
416        let hash_map: HashMap<String, String> = kv
417            .iter()
418            .map(|(k, v)| (k.to_string(), v.to_string()))
419            .collect();
420        ScopeMap::new_from_settings_and_env(&hash_map, None, &[])
421    }
422
423    #[test]
424    fn test_initialization() {
425        let map = scope_map_from_keys(&[("a.b.c.d", "trace")]);
426        assert_eq!(map.root_count, 1);
427        assert_eq!(map.entries.len(), 4);
428
429        let map = scope_map_from_keys(&[]);
430        assert_eq!(map.root_count, 0);
431        assert_eq!(map.entries.len(), 0);
432
433        let map = scope_map_from_keys(&[("", "trace")]);
434        assert_eq!(map.root_count, 0);
435        assert_eq!(map.entries.len(), 0);
436
437        let map = scope_map_from_keys(&[("foo..bar", "trace")]);
438        assert_eq!(map.root_count, 1);
439        assert_eq!(map.entries.len(), 2);
440
441        let map = scope_map_from_keys(&[
442            ("a.b.c.d", "trace"),
443            ("e.f.g.h", "debug"),
444            ("i.j.k.l", "info"),
445            ("m.n.o.p", "warn"),
446            ("q.r.s.t", "error"),
447        ]);
448        assert_eq!(map.root_count, 5);
449        assert_eq!(map.entries.len(), 20);
450        assert_eq!(map.entries[0].scope, "a");
451        assert_eq!(map.entries[1].scope, "e");
452        assert_eq!(map.entries[2].scope, "i");
453        assert_eq!(map.entries[3].scope, "m");
454        assert_eq!(map.entries[4].scope, "q");
455    }
456
457    fn scope_from_scope_str(scope_str: &'static str) -> Scope {
458        let mut scope_buf = [""; SCOPE_DEPTH_MAX];
459        let mut index = 0;
460        let mut scope_iter = scope_str.split(SCOPE_STRING_SEP_STR);
461        while index < SCOPE_DEPTH_MAX {
462            let Some(scope) = scope_iter.next() else {
463                break;
464            };
465            if scope.is_empty() {
466                continue;
467            }
468            scope_buf[index] = scope;
469            index += 1;
470        }
471        assert_ne!(index, 0);
472        assert!(scope_iter.next().is_none());
473        scope_buf
474    }
475
476    #[test]
477    fn test_is_enabled() {
478        let map = scope_map_from_keys(&[
479            ("a.b.c.d", "trace"),
480            ("e.f.g.h", "debug"),
481            ("i.j.k.l", "info"),
482            ("m.n.o.p", "warn"),
483            ("q.r.s.t", "error"),
484        ]);
485        use log::Level;
486        assert_eq!(
487            map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Trace),
488            EnabledStatus::Enabled
489        );
490        assert_eq!(
491            map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Debug),
492            EnabledStatus::Enabled
493        );
494
495        assert_eq!(
496            map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Debug),
497            EnabledStatus::Enabled
498        );
499        assert_eq!(
500            map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Info),
501            EnabledStatus::Enabled
502        );
503        assert_eq!(
504            map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Trace),
505            EnabledStatus::Disabled
506        );
507
508        assert_eq!(
509            map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Info),
510            EnabledStatus::Enabled
511        );
512        assert_eq!(
513            map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Warn),
514            EnabledStatus::Enabled
515        );
516        assert_eq!(
517            map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Debug),
518            EnabledStatus::Disabled
519        );
520
521        assert_eq!(
522            map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Warn),
523            EnabledStatus::Enabled
524        );
525        assert_eq!(
526            map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Error),
527            EnabledStatus::Enabled
528        );
529        assert_eq!(
530            map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Info),
531            EnabledStatus::Disabled
532        );
533
534        assert_eq!(
535            map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Error),
536            EnabledStatus::Enabled
537        );
538        assert_eq!(
539            map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Warn),
540            EnabledStatus::Disabled
541        );
542    }
543
544    #[test]
545    fn test_is_enabled_module() {
546        let mut map = scope_map_from_keys(&[("a", "trace")]);
547        map.modules = [("a::b::c", "trace"), ("a::b::d", "debug")]
548            .map(|(k, v)| (k.to_string(), v.parse().unwrap()))
549            .to_vec();
550        use log::Level;
551        assert_eq!(
552            map.is_enabled(
553                &scope_from_scope_str("__unused__"),
554                Some("a::b::c"),
555                Level::Trace
556            ),
557            EnabledStatus::Enabled
558        );
559        assert_eq!(
560            map.is_enabled(
561                &scope_from_scope_str("__unused__"),
562                Some("a::b::d"),
563                Level::Debug
564            ),
565            EnabledStatus::Enabled
566        );
567        assert_eq!(
568            map.is_enabled(
569                &scope_from_scope_str("__unused__"),
570                Some("a::b::d"),
571                Level::Trace,
572            ),
573            EnabledStatus::Disabled
574        );
575        assert_eq!(
576            map.is_enabled(
577                &scope_from_scope_str("__unused__"),
578                Some("a::e"),
579                Level::Info
580            ),
581            EnabledStatus::NotConfigured
582        );
583        // when scope is just crate name, more specific module path overrides it
584        assert_eq!(
585            map.is_enabled(&scope_from_scope_str("a"), Some("a::b::d"), Level::Trace),
586            EnabledStatus::Disabled,
587        );
588        // but when it is scoped, the scope overrides the module path
589        assert_eq!(
590            map.is_enabled(
591                &scope_from_scope_str("a.scope"),
592                Some("a::b::d"),
593                Level::Trace
594            ),
595            EnabledStatus::Enabled,
596        );
597    }
598
599    fn scope_map_from_keys_and_env(kv: &[(&str, &str)], env: &env_config::EnvFilter) -> ScopeMap {
600        let hash_map: HashMap<String, String> = kv
601            .iter()
602            .map(|(k, v)| (k.to_string(), v.to_string()))
603            .collect();
604        ScopeMap::new_from_settings_and_env(&hash_map, Some(env), &[])
605    }
606
607    #[test]
608    fn test_initialization_with_env() {
609        let env_filter = env_config::parse("a.b=debug,u=error").unwrap();
610        let map = scope_map_from_keys_and_env(&[], &env_filter);
611        assert_eq!(map.root_count, 2);
612        assert_eq!(map.entries.len(), 3);
613        assert_eq!(
614            map.is_enabled(&scope_new(&["a"]), None, log::Level::Debug),
615            EnabledStatus::NotConfigured
616        );
617        assert_eq!(
618            map.is_enabled(&scope_new(&["a", "b"]), None, log::Level::Debug),
619            EnabledStatus::Enabled
620        );
621        assert_eq!(
622            map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace),
623            EnabledStatus::Disabled
624        );
625
626        let env_filter = env_config::parse("a.b=debug,e.f.g.h=trace,u=error").unwrap();
627        let map = scope_map_from_keys_and_env(
628            &[
629                ("a.b.c.d", "trace"),
630                ("e.f.g.h", "debug"),
631                ("i.j.k.l", "info"),
632                ("m.n.o.p", "warn"),
633                ("q.r.s.t", "error"),
634            ],
635            &env_filter,
636        );
637        assert_eq!(map.root_count, 6);
638        assert_eq!(map.entries.len(), 21);
639        assert_eq!(map.entries[0].scope, "a");
640        assert_eq!(map.entries[1].scope, "e");
641        assert_eq!(map.entries[2].scope, "i");
642        assert_eq!(map.entries[3].scope, "m");
643        assert_eq!(map.entries[4].scope, "q");
644        assert_eq!(map.entries[5].scope, "u");
645        assert_eq!(
646            map.is_enabled(&scope_new(&["a", "b", "c", "d"]), None, log::Level::Trace),
647            EnabledStatus::Enabled
648        );
649        assert_eq!(
650            map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace),
651            EnabledStatus::Disabled
652        );
653        assert_eq!(
654            map.is_enabled(&scope_new(&["u", "v"]), None, log::Level::Warn),
655            EnabledStatus::Disabled
656        );
657        // settings override env
658        assert_eq!(
659            map.is_enabled(&scope_new(&["e", "f", "g", "h"]), None, log::Level::Trace),
660            EnabledStatus::Disabled,
661        );
662    }
663
664    fn scope_map_from_all(
665        kv: &[(&str, &str)],
666        env: &env_config::EnvFilter,
667        default_filters: &[(&str, log::LevelFilter)],
668    ) -> ScopeMap {
669        let hash_map: HashMap<String, String> = kv
670            .iter()
671            .map(|(k, v)| (k.to_string(), v.to_string()))
672            .collect();
673        ScopeMap::new_from_settings_and_env(&hash_map, Some(env), default_filters)
674    }
675
676    #[test]
677    fn precedence() {
678        // Test precedence: kv > env > default
679
680        // Default filters - these should be overridden by env and kv when they overlap
681        let default_filters = &[
682            ("a.b.c", log::LevelFilter::Debug), // Should be overridden by env
683            ("p.q.r", log::LevelFilter::Info),  // Should be overridden by kv
684            ("x.y.z", log::LevelFilter::Warn),  // Not overridden
685            ("crate::module::default", log::LevelFilter::Error), // Module in default
686            ("crate::module::user", log::LevelFilter::Off), // Module disabled in default
687        ];
688
689        // Environment filters - these should override default but be overridden by kv
690        let env_filter =
691            env_config::parse("a.b.c=trace,p.q=debug,m.n.o=error,crate::module::env=debug")
692                .unwrap();
693
694        // Key-value filters (highest precedence) - these should override everything
695        let kv_filters = &[
696            ("p.q.r", "trace"),              // Overrides default
697            ("m.n.o", "warn"),               // Overrides env
698            ("j.k.l", "info"),               // New filter
699            ("crate::module::env", "trace"), // Overrides env for module
700            ("crate::module::kv", "trace"),  // New module filter
701        ];
702
703        let map = scope_map_from_all(kv_filters, &env_filter, default_filters);
704
705        // Test scope precedence
706        use log::Level;
707
708        // KV overrides all for scopes
709        assert_eq!(
710            map.is_enabled(&scope_from_scope_str("p.q.r"), None, Level::Trace),
711            EnabledStatus::Enabled,
712            "KV should override default filters for scopes"
713        );
714        assert_eq!(
715            map.is_enabled(&scope_from_scope_str("m.n.o"), None, Level::Warn),
716            EnabledStatus::Enabled,
717            "KV should override env filters for scopes"
718        );
719        assert_eq!(
720            map.is_enabled(&scope_from_scope_str("m.n.o"), None, Level::Debug),
721            EnabledStatus::Disabled,
722            "KV correctly limits log level"
723        );
724
725        // ENV overrides default but not KV for scopes
726        assert_eq!(
727            map.is_enabled(&scope_from_scope_str("a.b.c"), None, Level::Trace),
728            EnabledStatus::Enabled,
729            "ENV should override default filters for scopes"
730        );
731
732        // Default is used when no override exists for scopes
733        assert_eq!(
734            map.is_enabled(&scope_from_scope_str("x.y.z"), None, Level::Warn),
735            EnabledStatus::Enabled,
736            "Default filters should work when not overridden"
737        );
738        assert_eq!(
739            map.is_enabled(&scope_from_scope_str("x.y.z"), None, Level::Info),
740            EnabledStatus::Disabled,
741            "Default filters correctly limit log level"
742        );
743
744        // KV overrides all for modules
745        assert_eq!(
746            map.is_enabled(&scope_new(&[""]), Some("crate::module::env"), Level::Trace),
747            EnabledStatus::Enabled,
748            "KV should override env filters for modules"
749        );
750        assert_eq!(
751            map.is_enabled(&scope_new(&[""]), Some("crate::module::kv"), Level::Trace),
752            EnabledStatus::Enabled,
753            "KV module filters should work"
754        );
755
756        // ENV overrides default for modules
757        assert_eq!(
758            map.is_enabled(&scope_new(&[""]), Some("crate::module::env"), Level::Debug),
759            EnabledStatus::Enabled,
760            "ENV should override default for modules"
761        );
762
763        // Default is used when no override exists for modules
764        assert_eq!(
765            map.is_enabled(
766                &scope_new(&[""]),
767                Some("crate::module::default"),
768                Level::Error
769            ),
770            EnabledStatus::Enabled,
771            "Default filters should work for modules"
772        );
773        assert_eq!(
774            map.is_enabled(
775                &scope_new(&[""]),
776                Some("crate::module::default"),
777                Level::Warn
778            ),
779            EnabledStatus::Disabled,
780            "Default filters correctly limit log level for modules"
781        );
782
783        assert_eq!(
784            map.is_enabled(&scope_new(&[""]), Some("crate::module::user"), Level::Error),
785            EnabledStatus::Disabled,
786            "Module turned off in default filters is not enabled"
787        );
788
789        assert_eq!(
790            map.is_enabled(
791                &scope_new(&["crate"]),
792                Some("crate::module::user"),
793                Level::Error
794            ),
795            EnabledStatus::Disabled,
796            "Module turned off in default filters is not enabled, even with crate name as scope"
797        );
798
799        // Test non-conflicting but similar paths
800
801        // Test that "a.b" and "a.b.c" don't conflict (different depth)
802        assert_eq!(
803            map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Trace),
804            EnabledStatus::Enabled,
805            "Scope a.b.c should inherit from a.b env filter"
806        );
807        assert_eq!(
808            map.is_enabled(&scope_from_scope_str("a.b.c"), None, Level::Trace),
809            EnabledStatus::Enabled,
810            "Scope a.b.c.d should use env filter level (trace)"
811        );
812
813        // Test that similar module paths don't conflict
814        assert_eq!(
815            map.is_enabled(&scope_new(&[""]), Some("crate::module"), Level::Error),
816            EnabledStatus::NotConfigured,
817            "Module crate::module should not be affected by crate::module::default filter"
818        );
819        assert_eq!(
820            map.is_enabled(
821                &scope_new(&[""]),
822                Some("crate::module::default::sub"),
823                Level::Error
824            ),
825            EnabledStatus::NotConfigured,
826            "Module crate::module::default::sub should not be affected by crate::module::default filter"
827        );
828    }
829
830    #[test]
831    fn default_filter_crate() {
832        let default_filters = &[("crate", LevelFilter::Off)];
833        let map = scope_map_from_all(&[], &env_config::parse("").unwrap(), default_filters);
834
835        use log::Level;
836        assert_eq!(
837            map.is_enabled(&scope_new(&[""]), Some("crate::submodule"), Level::Error),
838            EnabledStatus::Disabled,
839            "crate::submodule should be disabled by disabling `crate` filter"
840        );
841    }
842}
843
Served at tenant.openagents/omega Member data and write actions are omitted.