logid_core/logging/filter/
mod.rs

1use std::sync::{Arc, RwLock};
2
3use evident::event::origin::Origin;
4
5use crate::log_id::{LogId, LogLevel};
6
7use super::{event_entry::AddonKind, msg::LogMsg};
8
9mod filter_builders;
10
11pub use filter_builders::*;
12
13#[derive(Default, Debug)]
14pub struct LogFilter {
15    filter: Arc<RwLock<FilterConfig>>,
16}
17
18/// Returns `true` if logid is configured to allow the given level.
19/// Debug and Trace levels must explicitly be allowed by enabling features `log_debugs` or `log_traces`.
20///
21/// Without using this function as early filter return, debug and trace logs would mostly go through all filter steps, which decreases performance.
22#[allow(unreachable_code)]
23#[allow(unused_variables)]
24fn allow_level(level: LogLevel) -> bool {
25    #[cfg(feature = "log_traces")]
26    return true; // Note: No level is below Trace
27
28    #[cfg(feature = "log_debugs")]
29    return level >= LogLevel::Debug;
30
31    level > LogLevel::Debug
32}
33
34impl LogFilter {
35    pub fn new() -> Self {
36        let filter_config = FilterConfig::new(&filter_config());
37
38        LogFilter {
39            filter: Arc::new(RwLock::new(filter_config)),
40        }
41    }
42
43    pub fn set_filter(&self, filter_config: FilterConfig) -> Result<(), FilterError> {
44        match self.filter.write() {
45            Ok(mut locked_filter) => {
46                locked_filter.replace(filter_config);
47            }
48            Err(mut err) => {
49                // lock poisoned, replace inner filter completely
50                **err.get_mut() = filter_config;
51            }
52        }
53
54        Ok(())
55    }
56
57    pub fn allow_addon(&self, id: LogId, origin: &Origin, addon: &AddonKind) -> bool {
58        if !allow_level(id.log_level) {
59            return false;
60        } else if let Ok(addon_filter) = AddonFilter::try_from(addon) {
61            if addon_filter == AddonFilter::Debugs && !allow_level(LogLevel::Debug)
62                || addon_filter == AddonFilter::Traces && !allow_level(LogLevel::Trace)
63            {
64                return false;
65            }
66        }
67
68        match self.filter.read() {
69            Ok(locked_filter) => locked_filter.allow_addon(id, origin, addon),
70            Err(_) => false,
71        }
72    }
73
74    pub fn show_origin_info(&self, id: LogId, origin: &Origin) -> bool {
75        match self.filter.read() {
76            Ok(locked_filter) => locked_filter.show_origin_info(id, origin),
77            Err(_) => false,
78        }
79    }
80
81    pub fn show_id(&self, id: LogId, origin: &Origin) -> bool {
82        match self.filter.read() {
83            Ok(locked_filter) => locked_filter.show_id(id, origin),
84            Err(_) => false,
85        }
86    }
87}
88
89fn filter_config() -> String {
90    if cfg!(feature = "test_filter") {
91        return "trace(all)".to_string();
92    }
93
94    match std::env::var("LOGID_FILTER") {
95        Ok(config) => config,
96        Err(_) => "error".to_string(),
97    }
98}
99
100pub fn set_filter<T>(into_filter: T) -> Result<(), crate::logging::filter::FilterError>
101where
102    T: Into<FilterConfig>,
103{
104    if let Some(filter) = crate::logging::LOGGER.get_filter() {
105        filter.set_filter(into_filter.into())
106    } else {
107        Err(crate::logging::filter::FilterError::SettingFilter)
108    }
109}
110
111impl evident::event::filter::Filter<LogId, LogMsg> for LogFilter {
112    fn allow_entry(&self, entry: &impl evident::event::entry::EventEntry<LogId, LogMsg>) -> bool {
113        if !allow_level(entry.get_event_id().log_level) {
114            return false;
115        }
116
117        match self.filter.read() {
118            Ok(locked_filter) => locked_filter.allow_entry(entry),
119            Err(_) => false,
120        }
121    }
122}
123
124#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
125pub enum AddonFilter {
126    #[default]
127    Id,
128    Origin,
129    Infos,
130    Debugs,
131    Traces,
132    Related,
133    AllAllowed,
134
135    #[cfg(feature = "hint_note")]
136    Hint,
137    #[cfg(feature = "hint_note")]
138    Note,
139
140    #[cfg(feature = "diagnostics")]
141    Diagnostics,
142
143    #[cfg(feature = "payloads")]
144    Payloads,
145}
146
147impl From<&AddonKind> for AddonFilter {
148    fn from(value: &AddonKind) -> Self {
149        match value {
150            AddonKind::Info(_) => AddonFilter::Infos,
151            AddonKind::Debug(_) => AddonFilter::Debugs,
152            AddonKind::Trace(_) => AddonFilter::Traces,
153            AddonKind::Related(_) => AddonFilter::Related,
154
155            #[cfg(feature = "fmt")]
156            AddonKind::FmtInfo(_) => AddonFilter::Infos,
157            #[cfg(feature = "fmt")]
158            AddonKind::FmtDebug(_) => AddonFilter::Debugs,
159            #[cfg(feature = "fmt")]
160            AddonKind::FmtTrace(_) => AddonFilter::Traces,
161
162            #[cfg(feature = "hint_note")]
163            AddonKind::Hint(_) => AddonFilter::Hint,
164            #[cfg(all(feature = "hint_note", feature = "fmt"))]
165            AddonKind::FmtHint(_) => AddonFilter::Hint,
166            #[cfg(feature = "hint_note")]
167            AddonKind::Note(_) => AddonFilter::Note,
168            #[cfg(all(feature = "hint_note", feature = "fmt"))]
169            AddonKind::FmtNote(_) => AddonFilter::Note,
170
171            #[cfg(feature = "diagnostics")]
172            AddonKind::Diagnostic(_) => AddonFilter::Diagnostics,
173            #[cfg(all(feature = "diagnostics", feature = "fmt"))]
174            AddonKind::FmtDiagnostic(_) => AddonFilter::Diagnostics,
175
176            #[cfg(feature = "payloads")]
177            AddonKind::Payload(_) => AddonFilter::Payloads,
178            #[cfg(all(feature = "payloads", feature = "fmt"))]
179            AddonKind::FmtPayload(_) => AddonFilter::Payloads,
180        }
181    }
182}
183
184impl TryFrom<&str> for AddonFilter {
185    type Error = FilterError;
186
187    fn try_from(value: &str) -> Result<Self, Self::Error> {
188        let addon = match value {
189            "id" => AddonFilter::Id,
190            "origin" => AddonFilter::Origin,
191            "infos" => AddonFilter::Infos,
192            "debugs" => AddonFilter::Debugs,
193            "traces" => AddonFilter::Traces,
194            "related" => AddonFilter::Related,
195            "all" => AddonFilter::AllAllowed,
196
197            #[cfg(feature = "hint_note")]
198            "hints" => AddonFilter::Hint,
199            #[cfg(feature = "hint_note")]
200            "notes" => AddonFilter::Note,
201
202            #[cfg(feature = "diagnostics")]
203            "diagnostics" => AddonFilter::Diagnostics,
204
205            #[cfg(feature = "payloads")]
206            "payloads" => AddonFilter::Payloads,
207
208            _ => {
209                return Err(FilterError::ParsingAddons(value.to_string()));
210            }
211        };
212
213        Ok(addon)
214    }
215}
216
217impl IntoIterator for AddonFilter {
218    type Item = Self;
219
220    type IntoIter = std::iter::Once<Self::Item>;
221
222    fn into_iter(self) -> Self::IntoIter {
223        std::iter::once(self)
224    }
225}
226
227#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
228pub struct LogIdFilter {
229    module_path: String,
230    identifier: String,
231}
232
233impl PartialEq<LogId> for LogIdFilter {
234    fn eq(&self, other: &LogId) -> bool {
235        self.module_path == other.module_path && self.identifier == other.identifier
236    }
237}
238
239impl TryFrom<&str> for LogIdFilter {
240    type Error = FilterError;
241
242    fn try_from(value: &str) -> Result<Self, Self::Error> {
243        let parts = value.split("::");
244        let len = parts.clone().count();
245
246        if len < 2 {
247            return Err(FilterError::ParsingLogId(value.to_string()));
248        }
249
250        // Form: <module path (might have more '::' between)>::<identifier>
251
252        let mut module_parts = String::default();
253        let mut identifier = String::default();
254
255        for (i, part) in parts.enumerate() {
256            if part.trim().is_empty() {
257                return Err(FilterError::ParsingLogId(value.to_string()));
258            }
259
260            if i < len - 1 {
261                module_parts.push_str(part);
262                module_parts.push_str("::");
263            } else {
264                identifier.push_str(part);
265            }
266        }
267        let module_path = module_parts
268            .strip_suffix("::")
269            .ok_or(FilterError::ParsingLogId(value.to_string()))?;
270
271        if identifier.is_empty() {
272            return Err(FilterError::ParsingLogId(value.to_string()));
273        }
274
275        Ok(LogIdFilter {
276            module_path: module_path.to_string(),
277            identifier: identifier.to_string(),
278        })
279    }
280}
281
282#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
283pub struct LogIdAddonFilter {
284    log_id: LogIdFilter,
285    allowed_addons: Vec<AddonFilter>,
286}
287
288impl IntoIterator for LogIdAddonFilter {
289    type Item = Self;
290
291    type IntoIter = std::iter::Once<Self>;
292
293    fn into_iter(self) -> Self::IntoIter {
294        std::iter::once(self)
295    }
296}
297
298impl TryFrom<&str> for LogIdAddonFilter {
299    type Error = FilterError;
300
301    fn try_from(value: &str) -> Result<Self, Self::Error> {
302        let mut stripped_id = value.to_string();
303        let addons = get_addons(&mut stripped_id);
304
305        Ok(LogIdAddonFilter {
306            log_id: LogIdFilter::try_from(stripped_id.as_str())?,
307            allowed_addons: addons,
308        })
309    }
310}
311
312#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
313pub struct LogIdModuleFilter {
314    no_general_logging: bool,
315    origin_module_path: String,
316    level: LogLevel,
317    allowed_ids: Vec<LogIdAddonFilter>,
318    allowed_addons: Vec<AddonFilter>,
319}
320
321impl IntoIterator for LogIdModuleFilter {
322    type Item = Self;
323
324    type IntoIter = std::iter::Once<Self>;
325
326    fn into_iter(self) -> Self::IntoIter {
327        std::iter::once(self)
328    }
329}
330
331impl LogIdModuleFilter {
332    pub fn origin_in_module(&self, origin: &Origin) -> bool {
333        origin.module_path.starts_with(&self.origin_module_path)
334    }
335
336    pub fn event_allowed(&self, id: LogId, origin: &Origin) -> bool {
337        if !self.origin_in_module(origin) {
338            return false;
339        }
340
341        (!self.no_general_logging && self.level <= id.log_level)
342            || id_allowed(&self.allowed_ids, id)
343    }
344
345    pub fn addon_allowed(&self, id: LogId, origin: &Origin, addon: &AddonFilter) -> bool {
346        if !self.origin_in_module(origin) {
347            return false;
348        }
349
350        (!self.no_general_logging
351            && self.level <= id.log_level
352            && self.allowed_addons.contains(addon))
353            || addon_allowed(&self.allowed_ids, id, addon)
354    }
355
356    fn try_from(
357        s: &str,
358        ids: Vec<LogIdAddonFilter>,
359        addons: Vec<AddonFilter>,
360    ) -> Result<Self, FilterError> {
361        let mut module_filter = s.split('=');
362        let len = module_filter.clone().count();
363        if len != 1 && len != 2 {
364            return Err(FilterError::ParsingModule(s.to_string()));
365        }
366
367        let module = module_filter
368            .next()
369            .ok_or(FilterError::ParsingModule(s.to_string()))?
370            .trim();
371
372        if module.is_empty() {
373            return Err(FilterError::ParsingModule(s.to_string()));
374        }
375
376        if len == 1 {
377            return Ok(LogIdModuleFilter {
378                no_general_logging: true,
379                origin_module_path: module.to_string(),
380                allowed_ids: ids,
381                allowed_addons: addons,
382                ..Default::default()
383            });
384        }
385
386        let level_part = module_filter
387            .next()
388            .ok_or(FilterError::ParsingModule(s.to_string()))?
389            .trim();
390
391        let level =
392            try_into_log_level(level_part).ok_or(FilterError::ParsingModule(s.to_string()))?;
393
394        Ok(LogIdModuleFilter {
395            no_general_logging: false,
396            origin_module_path: module.to_string(),
397            level,
398            allowed_ids: ids,
399            allowed_addons: addons,
400        })
401    }
402}
403
404#[derive(Default, Debug)]
405pub struct FilterConfig {
406    general_logging_enabled: bool,
407    general_level: LogLevel,
408    general_addons: Vec<AddonFilter>,
409    /// LogIds set with `on[LogId]`
410    allowed_global_ids: Vec<LogIdAddonFilter>,
411    allowed_modules: Vec<LogIdModuleFilter>,
412}
413
414impl FilterConfig {
415    pub fn new(filter: &str) -> Self {
416        if filter.trim().is_empty() || filter.to_lowercase() == "off" {
417            return FilterConfig {
418                general_logging_enabled: false,
419                ..Default::default()
420            };
421        }
422
423        let mut log_filter = FilterConfig {
424            general_logging_enabled: false,
425            general_level: LogLevel::Error,
426            general_addons: Vec::new(),
427            allowed_global_ids: Vec::new(),
428            allowed_modules: Vec::new(),
429        };
430
431        for filter_part in filter.split(',') {
432            let mut stripped_filter_part = filter_part.to_string();
433
434            let mut ids = get_ids(&mut stripped_filter_part);
435
436            if stripped_filter_part.starts_with("on") && !ids.is_empty() {
437                log_filter.allowed_global_ids.append(&mut ids);
438            } else {
439                let addons = get_addons(&mut stripped_filter_part);
440
441                if let Some(general_level) = try_into_log_level(stripped_filter_part.trim()) {
442                    log_filter.general_logging_enabled = true;
443                    log_filter.general_level = general_level;
444                    log_filter.general_addons = addons;
445                } else if let Ok(module_filter) =
446                    LogIdModuleFilter::try_from(&stripped_filter_part, ids, addons)
447                {
448                    log_filter.allowed_modules.push(module_filter);
449                }
450            }
451        }
452
453        log_filter
454    }
455
456    fn replace(&mut self, other: Self) {
457        self.allowed_global_ids = other.allowed_global_ids;
458        self.allowed_modules = other.allowed_modules;
459        self.general_addons = other.general_addons;
460        self.general_level = other.general_level;
461        self.general_logging_enabled = other.general_logging_enabled;
462    }
463
464    pub fn allow_addon(&self, id: LogId, origin: &Origin, addon: &AddonKind) -> bool {
465        let addon_filter = AddonFilter::from(addon);
466
467        if self.general_logging_enabled && self.general_addons.contains(&addon_filter) {
468            return true;
469        }
470
471        addon_allowed(&self.allowed_global_ids, id, &addon_filter)
472            || addon_allowed_in_origin(&self.allowed_modules, id, origin, &addon_filter)
473    }
474
475    pub fn show_origin_info(&self, id: LogId, origin: &Origin) -> bool {
476        let addon_filter = AddonFilter::Origin;
477
478        if self.general_logging_enabled && self.general_addons.contains(&addon_filter) {
479            return true;
480        }
481
482        addon_allowed(&self.allowed_global_ids, id, &addon_filter)
483            || addon_allowed_in_origin(&self.allowed_modules, id, origin, &addon_filter)
484    }
485
486    pub fn show_id(&self, id: LogId, origin: &Origin) -> bool {
487        let addon_filter = AddonFilter::Id;
488
489        if self.general_logging_enabled && self.general_addons.contains(&addon_filter) {
490            return true;
491        }
492
493        addon_allowed(&self.allowed_global_ids, id, &addon_filter)
494            || addon_allowed_in_origin(&self.allowed_modules, id, origin, &addon_filter)
495    }
496
497    pub fn builder(log_level: LogLevel) -> FilterConfigBuilder {
498        FilterConfigBuilder::new(log_level)
499    }
500}
501
502impl evident::event::filter::Filter<LogId, LogMsg> for FilterConfig {
503    fn allow_entry(&self, entry: &impl evident::event::entry::EventEntry<LogId, LogMsg>) -> bool {
504        // Note: event handler creates unique LogIds per handler => filter on origin
505        if entry
506            .get_origin()
507            .module_path
508            .starts_with("logid::event_handler")
509        {
510            return true;
511        }
512
513        // Note: `Trace` starts at `0`
514        if self.general_logging_enabled && self.general_level <= entry.get_event_id().log_level {
515            return true;
516        }
517
518        id_allowed(&self.allowed_global_ids, *entry.get_event_id())
519            || id_allowed_in_origin(
520                &self.allowed_modules,
521                *entry.get_event_id(),
522                entry.get_origin(),
523            )
524    }
525}
526
527impl<I> From<(LogLevel, I)> for FilterConfig
528where
529    I: IntoIterator<Item = AddonFilter>,
530{
531    fn from((level, addons): (LogLevel, I)) -> Self {
532        FilterConfig::builder(level).allowed_addons(addons).build()
533    }
534}
535
536#[derive(Debug, Clone)]
537pub enum FilterError {
538    ParsingLogId(String),
539    ParsingAddons(String),
540    ParsingModule(String),
541    SettingFilter,
542}
543
544impl std::error::Error for FilterError {}
545
546impl std::fmt::Display for FilterError {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        match self {
549            FilterError::ParsingLogId(bad_id) => {
550                write!(f, "Could not parse '{}' as `LogId`.", bad_id)
551            }
552            FilterError::ParsingAddons(bad_addons) => {
553                write!(f, "Could not parse addons '{}'.", bad_addons)
554            }
555            FilterError::ParsingModule(bad_module) => {
556                write!(f, "Could not parse module '{}'.", bad_module)
557            }
558            FilterError::SettingFilter => {
559                write!(f, "Could not set the new filter configuration.")
560            }
561        }
562    }
563}
564
565fn id_allowed(ids: &Vec<LogIdAddonFilter>, id: LogId) -> bool {
566    for allowed_id in ids {
567        if allowed_id.log_id == id {
568            return true;
569        }
570    }
571
572    false
573}
574
575fn id_allowed_in_origin(modules: &Vec<LogIdModuleFilter>, id: LogId, origin: &Origin) -> bool {
576    for module in modules {
577        if module.event_allowed(id, origin) {
578            return true;
579        }
580    }
581
582    false
583}
584
585fn addon_allowed(ids: &Vec<LogIdAddonFilter>, id: LogId, addon: &AddonFilter) -> bool {
586    for allowed_id in ids {
587        if allowed_id.log_id == id && allowed_id.allowed_addons.contains(addon) {
588            return true;
589        }
590    }
591
592    false
593}
594
595fn addon_allowed_in_origin(
596    modules: &Vec<LogIdModuleFilter>,
597    id: LogId,
598    origin: &Origin,
599    addon: &AddonFilter,
600) -> bool {
601    for module in modules {
602        if module.addon_allowed(id, origin, addon) {
603            return true;
604        }
605    }
606
607    false
608}
609
610fn get_addons(s: &mut String) -> Vec<AddonFilter> {
611    let mut addons = Vec::new();
612
613    if let (Some(addon_start), Some(addon_end)) = (s.find('('), s.find(')')) {
614        if addon_start >= addon_end {
615            return addons;
616        }
617
618        if let Some(addons_part) = s.get((addon_start + 1)..addon_end) {
619            for addon_part in addons_part.split('&') {
620                if let Ok(addon) = AddonFilter::try_from(addon_part.trim()) {
621                    if addon == AddonFilter::AllAllowed {
622                        addons.push(AddonFilter::Id);
623                        addons.push(AddonFilter::Origin);
624                        addons.push(AddonFilter::Infos);
625                        addons.push(AddonFilter::Debugs);
626                        addons.push(AddonFilter::Traces);
627                        addons.push(AddonFilter::Related);
628
629                        #[cfg(feature = "hint_note")]
630                        addons.push(AddonFilter::Hint);
631                        #[cfg(feature = "hint_note")]
632                        addons.push(AddonFilter::Note);
633
634                        #[cfg(feature = "diagnostics")]
635                        addons.push(AddonFilter::Diagnostics);
636
637                        #[cfg(feature = "payloads")]
638                        addons.push(AddonFilter::Payloads);
639                    } else {
640                        addons.push(addon);
641                    }
642                }
643            }
644        }
645
646        s.replace_range(addon_start..(addon_end + 1), "");
647    }
648
649    addons
650}
651
652fn get_ids(s: &mut String) -> Vec<LogIdAddonFilter> {
653    let mut ids = Vec::new();
654
655    if let (Some(ids_start), Some(ids_end)) = (s.find('['), s.find(']')) {
656        if ids_start >= ids_end {
657            return ids;
658        }
659
660        if let Some(ids_part) = s.get((ids_start + 1)..ids_end) {
661            for id_part in ids_part.split('|') {
662                if let Ok(id) = LogIdAddonFilter::try_from(id_part.trim()) {
663                    ids.push(id);
664                }
665            }
666        }
667
668        s.replace_range(ids_start..(ids_end + 1), "");
669    }
670
671    ids
672}
673
674fn try_into_log_level(s: &str) -> Option<LogLevel> {
675    match s.to_lowercase().as_str() {
676        "error" => Some(LogLevel::Error),
677        "warn" => Some(LogLevel::Warn),
678        "info" => Some(LogLevel::Info),
679        "debug" => Some(LogLevel::Debug),
680        "trace" | "on" => Some(LogLevel::Trace),
681        _ => None,
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::LogIdFilter;
688
689    #[test]
690    fn valid_log_id_filter() {
691        let log_id_filter = LogIdFilter::try_from("my_crate::my_id").unwrap();
692
693        assert_eq!(
694            log_id_filter.module_path, "my_crate",
695            "Module path extraction was not correct"
696        );
697        assert_eq!(
698            log_id_filter.identifier, "my_id",
699            "Identifier extraction was not correct"
700        );
701    }
702
703    #[test]
704    fn valid_log_id_filter_with_one_submodule() {
705        let log_id_filter = LogIdFilter::try_from("my_crate::my_module::my_id").unwrap();
706
707        assert_eq!(
708            log_id_filter.module_path, "my_crate::my_module",
709            "Module path extraction was not correct"
710        );
711        assert_eq!(
712            log_id_filter.identifier, "my_id",
713            "Identifier extraction was not correct"
714        );
715    }
716
717    #[test]
718    fn valid_log_id_filter_with_submodule() {
719        let log_id_filter =
720            LogIdFilter::try_from("my_crate::my_module::sub_module::my_id").unwrap();
721
722        assert_eq!(
723            log_id_filter.module_path, "my_crate::my_module::sub_module",
724            "Module path extraction was not correct"
725        );
726        assert_eq!(
727            log_id_filter.identifier, "my_id",
728            "Identifier extraction was not correct"
729        );
730    }
731
732    #[test]
733    fn invalid_log_id_filter_with_empty_module_path() {
734        let log_id_filter = LogIdFilter::try_from("::my_id");
735
736        assert!(
737            log_id_filter.is_err(),
738            "Parsing invalid LogIdFilter did not result in error."
739        );
740    }
741
742    #[test]
743    fn invalid_log_id_filter_with_empty_identifier() {
744        let log_id_filter = LogIdFilter::try_from("my_crate::");
745
746        assert!(
747            log_id_filter.is_err(),
748            "Parsing invalid LogIdFilter did not result in error."
749        );
750    }
751}