log_command/
filter.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::log_formatter::{LogData, LogEntry};
6use crate::{InstanceGetter, LogCommand, LogError};
7use diagnostics_data::{LogsData, Severity};
8use fidl_fuchsia_diagnostics::LogInterestSelector;
9use moniker::{ExtendedMoniker, EXTENDED_MONIKER_COMPONENT_MANAGER_STR};
10use selectors::SelectorExt;
11use std::borrow::Cow;
12use std::str::FromStr;
13use std::sync::LazyLock;
14use zx_types::zx_koid_t;
15
16static KLOG: &str = "klog";
17static KLOG_MONIKER: LazyLock<ExtendedMoniker> =
18    LazyLock::new(|| ExtendedMoniker::try_from(KLOG).unwrap());
19
20struct MonikerFilters {
21    queries: Vec<String>,
22    matched_monikers: Vec<String>,
23}
24
25impl MonikerFilters {
26    fn new(queries: Vec<String>) -> Self {
27        Self { queries, matched_monikers: vec![] }
28    }
29
30    async fn expand_monikers(&mut self, getter: &impl InstanceGetter) -> Result<(), LogError> {
31        self.matched_monikers = vec![];
32        self.matched_monikers.reserve(self.queries.len());
33        for query in &self.queries {
34            if query == KLOG {
35                self.matched_monikers.push(query.clone());
36                continue;
37            }
38
39            let mut instances = getter.get_monikers_from_query(query).await?;
40            if instances.len() > 1 {
41                return Err(LogError::too_many_fuzzy_matches(
42                    instances.into_iter().map(|i| i.to_string()),
43                ));
44            }
45            match instances.pop() {
46                Some(instance) => self.matched_monikers.push(instance.to_string()),
47                None => return Err(LogError::SearchParameterNotFound(query.to_string())),
48            }
49        }
50
51        Ok(())
52    }
53}
54
55/// A struct that holds the criteria for filtering logs.
56pub struct LogFilterCriteria {
57    /// The minimum severity of logs to include.
58    min_severity: Severity,
59    /// Filter by string.
60    filters: Vec<String>,
61    /// Monikers to include in logs.
62    moniker_filters: MonikerFilters,
63    /// Exclude by string.
64    excludes: Vec<String>,
65    /// The tags to include.
66    tags: Vec<String>,
67    /// The tags to exclude.
68    exclude_tags: Vec<String>,
69    /// Filter by PID
70    pid: Option<zx_koid_t>,
71    /// Filter by TID
72    tid: Option<zx_koid_t>,
73    /// Log interest selectors used to filter severity on a per-component basis
74    /// Overrides min_severity for components matching the selector.
75    /// In the event of an ambiguous match, the lowest severity is used.
76    interest_selectors: Vec<LogInterestSelector>,
77    /// True if case sensitive, false otherwise
78    case_sensitive: bool,
79}
80
81impl Default for LogFilterCriteria {
82    fn default() -> Self {
83        Self {
84            min_severity: Severity::Info,
85            filters: vec![],
86            excludes: vec![],
87            tags: vec![],
88            moniker_filters: MonikerFilters::new(vec![]),
89            exclude_tags: vec![],
90            pid: None,
91            tid: None,
92            case_sensitive: false,
93            interest_selectors: vec![],
94        }
95    }
96}
97
98// Convert a string to lowercase if needed for case insensitive comparisons.
99// If case_sensitive is false, the conversion is performed.
100fn convert_to_lowercase_if_needed<'a>(input: &'a str, case_sensitive: bool) -> Cow<'a, str> {
101    if case_sensitive {
102        Cow::Borrowed(input)
103    } else {
104        Cow::Owned(input.to_lowercase())
105    }
106}
107
108impl From<LogCommand> for LogFilterCriteria {
109    fn from(mut cmd: LogCommand) -> Self {
110        Self {
111            min_severity: cmd.severity,
112            filters: cmd.filter,
113            tags: cmd
114                .tag
115                .into_iter()
116                .map(|value| convert_to_lowercase_if_needed(&value, cmd.case_sensitive).to_string())
117                .collect(),
118            excludes: cmd.exclude,
119            moniker_filters: if cmd.kernel {
120                cmd.component.push(KLOG.to_string());
121                MonikerFilters::new(cmd.component)
122            } else {
123                MonikerFilters::new(cmd.component)
124            },
125            exclude_tags: cmd.exclude_tags,
126            pid: cmd.pid,
127            case_sensitive: cmd.case_sensitive,
128            tid: cmd.tid,
129            interest_selectors: cmd.set_severity.into_iter().flatten().collect(),
130        }
131    }
132}
133
134impl LogFilterCriteria {
135    /// Sets the minimum severity of logs to include.
136    pub fn set_min_severity(&mut self, severity: Severity) {
137        self.min_severity = severity;
138    }
139
140    pub async fn expand_monikers(&mut self, getter: &impl InstanceGetter) -> Result<(), LogError> {
141        self.moniker_filters.expand_monikers(getter).await
142    }
143
144    /// Sets the tags to include.
145    pub fn set_tags<I, S>(&mut self, tags: I)
146    where
147        I: IntoIterator<Item = S>,
148        S: Into<String>,
149    {
150        self.tags = tags.into_iter().map(|value| value.into()).collect();
151    }
152
153    /// Sets the tags to exclude.
154    pub fn set_exclude_tags<I, S>(&mut self, tags: I)
155    where
156        I: IntoIterator<Item = S>,
157        S: Into<String>,
158    {
159        self.exclude_tags = tags.into_iter().map(|value| value.into()).collect();
160    }
161
162    /// Returns true if the given `LogEntry` matches the filter criteria.
163    pub fn matches(&self, entry: &LogEntry) -> bool {
164        match entry {
165            LogEntry { data: LogData::TargetLog(data), .. } => self.match_filters_to_log_data(data),
166        }
167    }
168
169    /// Returns true if the given 'LogsData' matches the filter string by
170    /// message, moniker, or component URL.
171    fn matches_filter_string(
172        filter_string: &str,
173        message: &str,
174        log: &LogsData,
175        case_sensitive: bool,
176    ) -> bool {
177        // Convert strings to lower-case if needed
178        let filter_string = convert_to_lowercase_if_needed(filter_string, case_sensitive);
179        let message = convert_to_lowercase_if_needed(message, case_sensitive);
180        let file_path =
181            log.file_path().map(|value| convert_to_lowercase_if_needed(value, case_sensitive));
182        let component_url = log
183            .metadata
184            .component_url
185            .as_ref()
186            .map(|value| convert_to_lowercase_if_needed(value.as_str(), case_sensitive));
187        let moniker_str = log.moniker.to_string();
188        let moniker = convert_to_lowercase_if_needed(&moniker_str, case_sensitive);
189
190        message.contains(&*filter_string)
191            || file_path.is_some_and(|s| s.contains(&*filter_string))
192            || component_url.as_ref().is_some_and(|s| s.contains(&*filter_string))
193            || moniker.contains(&*filter_string)
194    }
195
196    // TODO(b/303315896): If/when debuglog is structured remove this.
197    fn parse_tags(value: &str) -> Vec<&str> {
198        let mut tags = Vec::new();
199        let mut current = value;
200        if !current.starts_with('[') {
201            return tags;
202        }
203        loop {
204            match current.find('[') {
205                Some(opening_index) => {
206                    current = &current[opening_index + 1..];
207                }
208                None => return tags,
209            }
210            match current.find(']') {
211                Some(closing_index) => {
212                    tags.push(&current[..closing_index]);
213                    current = &current[closing_index + 1..];
214                }
215                None => return tags,
216            }
217        }
218    }
219
220    fn match_synthetic_klog_tags(&self, klog_str: &str, case_sensitive: bool) -> bool {
221        let tags = Self::parse_tags(klog_str)
222            .into_iter()
223            .map(|value| convert_to_lowercase_if_needed(value, case_sensitive))
224            .collect::<Vec<_>>();
225        self.tags.iter().any(|f| {
226            tags.iter().any(|t| convert_to_lowercase_if_needed(t, case_sensitive).contains(f))
227        })
228    }
229
230    /// Returns true if the given `LogsData` matches the moniker string.
231    fn matches_filter_by_moniker_string(filter_string: &str, log: &LogsData) -> bool {
232        let Ok(filter_moniker) = ExtendedMoniker::from_str(filter_string) else {
233            return false;
234        };
235        filter_moniker == log.moniker
236    }
237
238    /// Returns true if the given `LogsData` matches the filter criteria.
239    fn match_filters_to_log_data(&self, data: &LogsData) -> bool {
240        let min_severity = self
241            .interest_selectors
242            .iter()
243            .filter(|s| data.moniker.matches_component_selector(&s.selector).unwrap_or(false))
244            .filter_map(|selector| selector.interest.min_severity)
245            .min()
246            .unwrap_or_else(|| self.min_severity.into());
247        if data.metadata.severity < min_severity {
248            return false;
249        }
250
251        if let Some(pid) = self.pid {
252            if data.pid() != Some(pid) {
253                return false;
254            }
255        }
256
257        if let Some(tid) = self.tid {
258            if data.tid() != Some(tid) {
259                return false;
260            }
261        }
262
263        if !self.moniker_filters.matched_monikers.is_empty()
264            && !self
265                .moniker_filters
266                .matched_monikers
267                .iter()
268                .any(|f| Self::matches_filter_by_moniker_string(f, data))
269        {
270            return false;
271        }
272
273        let msg = data.msg().unwrap_or("");
274
275        if !self.filters.is_empty()
276            && !self
277                .filters
278                .iter()
279                .any(|f| Self::matches_filter_string(f, msg, data, self.case_sensitive))
280        {
281            return false;
282        }
283
284        if self
285            .excludes
286            .iter()
287            .any(|f| Self::matches_filter_string(f, msg, data, self.case_sensitive))
288        {
289            return false;
290        }
291        if !self.tags.is_empty()
292            && !self.tags.iter().any(|query_tag| {
293                let has_tag = data
294                    .tags()
295                    .map(|t| {
296                        t.iter().any(|value| {
297                            convert_to_lowercase_if_needed(value, self.case_sensitive) == *query_tag
298                        })
299                    })
300                    .unwrap_or(false);
301                let moniker_has_tag =
302                    moniker_contains_in_last_segment(&data.moniker, query_tag, self.case_sensitive);
303                has_tag || moniker_has_tag
304            })
305        {
306            if data.moniker == *KLOG_MONIKER {
307                return self
308                    .match_synthetic_klog_tags(data.msg().unwrap_or(""), self.case_sensitive);
309            }
310            return false;
311        }
312
313        if self.exclude_tags.iter().any(|excluded_tag| {
314            let has_tag = data.tags().map(|tag| tag.contains(excluded_tag)).unwrap_or(false);
315            let moniker_has_tag =
316                moniker_contains_in_last_segment(&data.moniker, excluded_tag, self.case_sensitive);
317            has_tag || moniker_has_tag
318        }) {
319            return false;
320        }
321
322        true
323    }
324}
325
326fn moniker_contains_in_last_segment(
327    moniker: &ExtendedMoniker,
328    query_tag: &str,
329    case_sensitive: bool,
330) -> bool {
331    let query_tag = convert_to_lowercase_if_needed(query_tag, case_sensitive);
332    match moniker {
333        ExtendedMoniker::ComponentInstance(moniker) => moniker
334            .leaf()
335            .map(|segment| {
336                convert_to_lowercase_if_needed(segment.as_ref(), case_sensitive)
337                    .contains(&*query_tag)
338            })
339            .unwrap_or(false),
340        ExtendedMoniker::ComponentManager => {
341            EXTENDED_MONIKER_COMPONENT_MANAGER_STR.contains(&*query_tag)
342        }
343    }
344}
345
346#[cfg(test)]
347mod test {
348    use diagnostics_data::{ExtendedMoniker, Timestamp};
349    use selectors::parse_log_interest_selector;
350
351    use crate::log_socket_stream::OneOrMany;
352    use crate::{DumpCommand, LogSubCommand};
353
354    use super::*;
355
356    fn empty_dump_command() -> LogCommand {
357        LogCommand {
358            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
359            ..LogCommand::default()
360        }
361    }
362
363    fn make_log_entry(log_data: LogData) -> LogEntry {
364        LogEntry { data: log_data }
365    }
366
367    #[fuchsia::test]
368    async fn test_criteria_tag_filter_filters_moniker() {
369        let cmd = LogCommand { tag: vec!["testcomponent".to_string()], ..empty_dump_command() };
370        let criteria = LogFilterCriteria::from(cmd);
371
372        assert!(criteria.matches(&make_log_entry(
373            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
374                timestamp: Timestamp::from_nanos(0),
375                component_url: Some("".into()),
376                moniker: "my/testcomponent".try_into().unwrap(),
377                severity: diagnostics_data::Severity::Error,
378            })
379            .set_message("included")
380            .add_tag("tag1")
381            .add_tag("tag2")
382            .build()
383            .into()
384        )));
385    }
386
387    #[fuchsia::test]
388    async fn test_criteria_exclude_tag_filters_moniker() {
389        let cmd =
390            LogCommand { exclude_tags: vec!["testcomponent".to_string()], ..empty_dump_command() };
391        let criteria = LogFilterCriteria::from(cmd);
392        assert!(!criteria.matches(&make_log_entry(
393            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
394                timestamp: Timestamp::from_nanos(0),
395                component_url: Some("".into()),
396                moniker: "my/testcomponent".try_into().unwrap(),
397                severity: diagnostics_data::Severity::Error,
398            })
399            .set_message("excluded")
400            .add_tag("tag1")
401            .add_tag("tag2")
402            .build()
403            .into()
404        )));
405    }
406
407    #[fuchsia::test]
408    async fn test_criteria_tag_filter() {
409        let cmd = LogCommand {
410            tag: vec!["tag1".to_string()],
411            exclude_tags: vec!["tag3".to_string()],
412            ..empty_dump_command()
413        };
414        let criteria = LogFilterCriteria::from(cmd);
415
416        assert!(criteria.matches(&make_log_entry(
417            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
418                timestamp: Timestamp::from_nanos(0),
419                component_url: Some("".into()),
420                moniker: ExtendedMoniker::ComponentManager,
421                severity: diagnostics_data::Severity::Error,
422            })
423            .set_message("included")
424            .add_tag("tag1")
425            .add_tag("tag2")
426            .build()
427            .into()
428        )));
429
430        assert!(!criteria.matches(&make_log_entry(
431            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
432                timestamp: Timestamp::from_nanos(0),
433                component_url: Some("".into()),
434                moniker: ExtendedMoniker::ComponentManager,
435                severity: diagnostics_data::Severity::Error,
436            })
437            .set_message("included")
438            .add_tag("tag2")
439            .build()
440            .into()
441        )));
442        assert!(!criteria.matches(&make_log_entry(
443            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
444                timestamp: Timestamp::from_nanos(0),
445                component_url: Some("".into()),
446                moniker: ExtendedMoniker::ComponentManager,
447                severity: diagnostics_data::Severity::Error,
448            })
449            .set_message("included")
450            .add_tag("tag1")
451            .add_tag("tag3")
452            .build()
453            .into()
454        )));
455    }
456
457    #[fuchsia::test]
458    async fn test_per_component_severity() {
459        let cmd = LogCommand {
460            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
461            set_severity: vec![OneOrMany::One(
462                parse_log_interest_selector("test_selector#DEBUG").unwrap(),
463            )],
464            ..LogCommand::default()
465        };
466        let expectations = [
467            ("test_selector", diagnostics_data::Severity::Debug, true),
468            ("other_selector", diagnostics_data::Severity::Debug, false),
469            ("other_selector", diagnostics_data::Severity::Info, true),
470        ];
471        let criteria = LogFilterCriteria::from(cmd);
472        assert_eq!(criteria.min_severity, Severity::Info);
473        for (moniker, severity, is_included) in expectations {
474            let entry = make_log_entry(
475                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
476                    timestamp: Timestamp::from_nanos(0),
477                    component_url: Some("".into()),
478                    moniker: moniker.try_into().unwrap(),
479                    severity,
480                })
481                .set_message("message")
482                .add_tag("tag1")
483                .add_tag("tag2")
484                .build()
485                .into(),
486            );
487            assert_eq!(criteria.matches(&entry), is_included);
488        }
489    }
490
491    #[fuchsia::test]
492    async fn test_per_component_severity_uses_min_match() {
493        let severities = [
494            diagnostics_data::Severity::Info,
495            diagnostics_data::Severity::Trace,
496            diagnostics_data::Severity::Debug,
497        ];
498
499        let cmd = LogCommand {
500            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
501            set_severity: vec![
502                OneOrMany::One(parse_log_interest_selector("test_selector#INFO").unwrap()),
503                OneOrMany::One(parse_log_interest_selector("test_selector#TRACE").unwrap()),
504                OneOrMany::One(parse_log_interest_selector("test_selector#DEBUG").unwrap()),
505            ],
506            ..LogCommand::default()
507        };
508        let criteria = LogFilterCriteria::from(cmd);
509
510        for severity in severities {
511            let entry = make_log_entry(
512                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
513                    timestamp: Timestamp::from_nanos(0),
514                    component_url: Some("".into()),
515                    moniker: "test_selector".try_into().unwrap(),
516                    severity,
517                })
518                .set_message("message")
519                .add_tag("tag1")
520                .add_tag("tag2")
521                .build()
522                .into(),
523            );
524            assert!(criteria.matches(&entry));
525        }
526    }
527
528    #[fuchsia::test]
529    async fn test_criteria_tag_filter_legacy() {
530        let cmd = LogCommand {
531            tag: vec!["tag1".to_string()],
532            exclude_tags: vec!["tag3".to_string()],
533            ..empty_dump_command()
534        };
535        let criteria = LogFilterCriteria::from(cmd);
536
537        assert!(criteria.matches(&make_log_entry(
538            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
539                timestamp: Timestamp::from_nanos(0),
540                component_url: Some("".into()),
541                moniker: ExtendedMoniker::ComponentManager,
542                severity: diagnostics_data::Severity::Error,
543            })
544            .set_message("included")
545            .add_tag("tag1")
546            .add_tag("tag2")
547            .build()
548            .into()
549        )));
550
551        assert!(!criteria.matches(&make_log_entry(
552            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
553                timestamp: Timestamp::from_nanos(0),
554                component_url: Some("".into()),
555                moniker: ExtendedMoniker::ComponentManager,
556                severity: diagnostics_data::Severity::Error,
557            })
558            .set_message("included")
559            .add_tag("tag2")
560            .build()
561            .into()
562        )));
563        assert!(!criteria.matches(&make_log_entry(
564            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
565                timestamp: Timestamp::from_nanos(0),
566                component_url: Some("".into()),
567                moniker: ExtendedMoniker::ComponentManager,
568                severity: diagnostics_data::Severity::Error,
569            })
570            .set_message("included")
571            .add_tag("tag1")
572            .add_tag("tag3")
573            .build()
574            .into()
575        )));
576    }
577
578    #[fuchsia::test]
579    async fn test_severity_filter_with_debug() {
580        let mut cmd = empty_dump_command();
581        cmd.severity = Severity::Trace;
582        let criteria = LogFilterCriteria::from(cmd);
583
584        assert!(criteria.matches(&make_log_entry(
585            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
586                timestamp: Timestamp::from_nanos(0),
587                component_url: Some("".into()),
588                moniker: "included/moniker".try_into().unwrap(),
589                severity: diagnostics_data::Severity::Error,
590            })
591            .set_message("included message")
592            .build()
593            .into()
594        )));
595        assert!(criteria.matches(&make_log_entry(
596            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
597                timestamp: Timestamp::from_nanos(0),
598                component_url: Some("".into()),
599                moniker: "included/moniker".try_into().unwrap(),
600                severity: diagnostics_data::Severity::Info,
601            })
602            .set_message("different message")
603            .build()
604            .into()
605        )));
606        assert!(criteria.matches(&make_log_entry(
607            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
608                timestamp: Timestamp::from_nanos(0),
609                component_url: Some("".into()),
610                moniker: "other/moniker".try_into().unwrap(),
611                severity: diagnostics_data::Severity::Debug,
612            })
613            .set_message("included message")
614            .build()
615            .into()
616        )));
617    }
618
619    #[fuchsia::test]
620    async fn test_pid_filter() {
621        let mut cmd = empty_dump_command();
622        cmd.pid = Some(123);
623        let criteria = LogFilterCriteria::from(cmd);
624
625        assert!(criteria.matches(&make_log_entry(
626            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
627                timestamp: Timestamp::from_nanos(0),
628                component_url: Some("".into()),
629                moniker: "included/moniker".try_into().unwrap(),
630                severity: diagnostics_data::Severity::Error,
631            })
632            .set_message("included message")
633            .set_pid(123)
634            .build()
635            .into()
636        )));
637        assert!(!criteria.matches(&make_log_entry(
638            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
639                timestamp: Timestamp::from_nanos(0),
640                component_url: Some("".into()),
641                moniker: "included/moniker".try_into().unwrap(),
642                severity: diagnostics_data::Severity::Error,
643            })
644            .set_message("included message")
645            .set_pid(456)
646            .build()
647            .into()
648        )));
649    }
650
651    struct FakeInstanceGetter;
652    #[async_trait::async_trait(?Send)]
653    impl InstanceGetter for FakeInstanceGetter {
654        async fn get_monikers_from_query(
655            &self,
656            query: &str,
657        ) -> Result<Vec<moniker::Moniker>, LogError> {
658            Ok(vec![moniker::Moniker::try_from(query).unwrap()])
659        }
660    }
661
662    #[fuchsia::test]
663    async fn test_criteria_component_filter() {
664        let cmd = LogCommand {
665            component: vec!["/core/network/netstack".to_string()],
666            ..empty_dump_command()
667        };
668
669        let mut criteria = LogFilterCriteria::from(cmd);
670        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
671
672        assert!(!criteria.matches(&make_log_entry(
673            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
674                timestamp: Timestamp::from_nanos(0),
675                component_url: Some("".into()),
676                moniker: "bootstrap/archivist".try_into().unwrap(),
677                severity: diagnostics_data::Severity::Error,
678            })
679            .set_message("excluded")
680            .build()
681            .into()
682        )));
683
684        assert!(criteria.matches(&make_log_entry(
685            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
686                timestamp: Timestamp::from_nanos(0),
687                component_url: Some("".into()),
688                moniker: "core/network/netstack".try_into().unwrap(),
689                severity: diagnostics_data::Severity::Error,
690            })
691            .set_message("included")
692            .build()
693            .into()
694        )));
695
696        assert!(!criteria.matches(&make_log_entry(
697            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
698                timestamp: Timestamp::from_nanos(0),
699                component_url: Some("".into()),
700                moniker: "core/network/dhcp".try_into().unwrap(),
701                severity: diagnostics_data::Severity::Error,
702            })
703            .set_message("included")
704            .build()
705            .into()
706        )));
707    }
708
709    #[fuchsia::test]
710    async fn test_tid_filter() {
711        let mut cmd = empty_dump_command();
712        cmd.tid = Some(123);
713        let criteria = LogFilterCriteria::from(cmd);
714
715        assert!(criteria.matches(&make_log_entry(
716            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
717                timestamp: Timestamp::from_nanos(0),
718                component_url: Some("".into()),
719                moniker: "included/moniker".try_into().unwrap(),
720                severity: diagnostics_data::Severity::Error,
721            })
722            .set_message("included message")
723            .set_tid(123)
724            .build()
725            .into()
726        )));
727        assert!(!criteria.matches(&make_log_entry(
728            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
729                timestamp: Timestamp::from_nanos(0),
730                component_url: Some("".into()),
731                moniker: "included/moniker".try_into().unwrap(),
732                severity: diagnostics_data::Severity::Error,
733            })
734            .set_message("included message")
735            .set_tid(456)
736            .build()
737            .into()
738        )));
739    }
740
741    #[fuchsia::test]
742    async fn test_setter_functions() {
743        let mut filter = LogFilterCriteria::default();
744        filter.set_min_severity(Severity::Error);
745        assert_eq!(filter.min_severity, Severity::Error);
746        filter.set_tags(["tag1"]);
747        assert_eq!(filter.tags, ["tag1"]);
748        filter.set_exclude_tags(["tag2"]);
749        assert_eq!(filter.exclude_tags, ["tag2"]);
750    }
751
752    #[fuchsia::test]
753    async fn test_criteria_moniker_message_and_severity_matches() {
754        let cmd = LogCommand {
755            filter: vec!["included".to_string()],
756            exclude: vec!["not this".to_string()],
757            severity: Severity::Error,
758            ..empty_dump_command()
759        };
760        let criteria = LogFilterCriteria::from(cmd);
761
762        assert!(criteria.matches(&make_log_entry(
763            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
764                timestamp: Timestamp::from_nanos(0),
765                component_url: Some("".into()),
766                moniker: "included/moniker".try_into().unwrap(),
767                severity: diagnostics_data::Severity::Error,
768            })
769            .set_message("included message")
770            .build()
771            .into()
772        )));
773        assert!(criteria.matches(&make_log_entry(
774            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
775                timestamp: Timestamp::from_nanos(0),
776                component_url: Some("".into()),
777                moniker: "included/moniker".try_into().unwrap(),
778                severity: diagnostics_data::Severity::Fatal,
779            })
780            .set_message("included message")
781            .build()
782            .into()
783        )));
784        assert!(criteria.matches(&make_log_entry(
785            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
786                timestamp: Timestamp::from_nanos(0),
787                component_url: Some("".into()),
788                // Include a "/" prefix on the moniker to test filter permissiveness.
789                moniker: "included/moniker".try_into().unwrap(),
790                severity: diagnostics_data::Severity::Fatal,
791            })
792            .set_message("included message")
793            .build()
794            .into()
795        )));
796        assert!(!criteria.matches(&make_log_entry(
797            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
798                timestamp: Timestamp::from_nanos(0),
799                component_url: Some("".into()),
800                moniker: "not/this/moniker".try_into().unwrap(),
801                severity: diagnostics_data::Severity::Error,
802            })
803            .set_message("different message")
804            .build()
805            .into()
806        )));
807        assert!(!criteria.matches(&make_log_entry(
808            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
809                timestamp: Timestamp::from_nanos(0),
810                component_url: Some("".into()),
811                moniker: "included/moniker".try_into().unwrap(),
812                severity: diagnostics_data::Severity::Warn,
813            })
814            .set_message("included message")
815            .build()
816            .into()
817        )));
818        assert!(!criteria.matches(&make_log_entry(
819            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
820                timestamp: Timestamp::from_nanos(0),
821                component_url: Some("".into()),
822                moniker: "other/moniker".try_into().unwrap(),
823                severity: diagnostics_data::Severity::Error,
824            })
825            .set_message("not this message")
826            .build()
827            .into()
828        )));
829        assert!(!criteria.matches(&make_log_entry(
830            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
831                timestamp: Timestamp::from_nanos(0),
832                component_url: Some("".into()),
833                moniker: "included/moniker".try_into().unwrap(),
834                severity: diagnostics_data::Severity::Error,
835            })
836            .set_message("not this message")
837            .build()
838            .into()
839        )));
840    }
841
842    #[fuchsia::test]
843    async fn test_criteria_klog_only() {
844        let cmd = LogCommand { tag: vec!["component_manager".into()], ..empty_dump_command() };
845        let criteria = LogFilterCriteria::from(cmd);
846
847        assert!(criteria.matches(&make_log_entry(
848            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
849                timestamp: Timestamp::from_nanos(0),
850                component_url: Some("".into()),
851                moniker: "klog".try_into().unwrap(),
852                severity: diagnostics_data::Severity::Error,
853            })
854            .set_message("[component_manager] included message")
855            .build()
856            .into()
857        )));
858        assert!(!criteria.matches(&make_log_entry(
859            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
860                timestamp: Timestamp::from_nanos(0),
861                component_url: Some("".into()),
862                moniker: "klog".try_into().unwrap(),
863                severity: diagnostics_data::Severity::Error,
864            })
865            .set_message("excluded message[component_manager]")
866            .build()
867            .into()
868        )));
869        assert!(criteria.matches(&make_log_entry(
870            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
871                timestamp: Timestamp::from_nanos(0),
872                component_url: Some("".into()),
873                moniker: "klog".try_into().unwrap(),
874                severity: diagnostics_data::Severity::Error,
875            })
876            .set_message("[tag0][component_manager] included message")
877            .build()
878            .into()
879        )));
880        assert!(!criteria.matches(&make_log_entry(
881            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
882                timestamp: Timestamp::from_nanos(0),
883                component_url: Some("".into()),
884                moniker: "klog".try_into().unwrap(),
885                severity: diagnostics_data::Severity::Error,
886            })
887            .set_message("[other] excluded message")
888            .build()
889            .into()
890        )));
891        assert!(!criteria.matches(&make_log_entry(
892            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
893                timestamp: Timestamp::from_nanos(0),
894                component_url: Some("".into()),
895                moniker: "klog".try_into().unwrap(),
896                severity: diagnostics_data::Severity::Error,
897            })
898            .set_message("no tags, excluded")
899            .build()
900            .into()
901        )));
902        assert!(!criteria.matches(&make_log_entry(
903            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
904                timestamp: Timestamp::from_nanos(0),
905                component_url: Some("".into()),
906                moniker: "other/moniker".try_into().unwrap(),
907                severity: diagnostics_data::Severity::Error,
908            })
909            .set_message("[component_manager] excluded message")
910            .build()
911            .into()
912        )));
913    }
914
915    #[fuchsia::test]
916    async fn test_criteria_klog_tag_hack() {
917        let cmd = LogCommand { kernel: true, ..empty_dump_command() };
918        let mut criteria = LogFilterCriteria::from(cmd);
919
920        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
921
922        assert!(criteria.matches(&make_log_entry(
923            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
924                timestamp: Timestamp::from_nanos(0),
925                component_url: Some("".into()),
926                moniker: "klog".try_into().unwrap(),
927                severity: diagnostics_data::Severity::Error,
928            })
929            .set_message("included message")
930            .build()
931            .into()
932        )));
933        assert!(!criteria.matches(&make_log_entry(
934            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
935                timestamp: Timestamp::from_nanos(0),
936                component_url: Some("".into()),
937                moniker: "other/moniker".try_into().unwrap(),
938                severity: diagnostics_data::Severity::Error,
939            })
940            .set_message("included message")
941            .build()
942            .into()
943        )));
944    }
945
946    #[test]
947    fn filter_fiters_filename() {
948        let cmd = LogCommand { filter: vec!["sometestfile".into()], ..empty_dump_command() };
949        let criteria = LogFilterCriteria::from(cmd);
950
951        assert!(criteria.matches(&make_log_entry(
952            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
953                timestamp: Timestamp::from_nanos(0),
954                component_url: Some("".into()),
955                moniker: "core/last_segment".try_into().unwrap(),
956                severity: diagnostics_data::Severity::Error,
957            })
958            .set_file("sometestfile")
959            .set_message("hello world")
960            .build()
961            .into()
962        )));
963    }
964
965    #[fuchsia::test]
966    async fn test_empty_criteria() {
967        let cmd = empty_dump_command();
968        let criteria = LogFilterCriteria::from(cmd);
969
970        assert!(criteria.matches(&make_log_entry(
971            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
972                timestamp: Timestamp::from_nanos(0),
973                component_url: Some("".into()),
974                moniker: "included/moniker".try_into().unwrap(),
975                severity: diagnostics_data::Severity::Error,
976            })
977            .set_message("included message")
978            .build()
979            .into()
980        )));
981        assert!(criteria.matches(&make_log_entry(
982            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
983                timestamp: Timestamp::from_nanos(0),
984                component_url: Some("".into()),
985                moniker: "included/moniker".try_into().unwrap(),
986                severity: diagnostics_data::Severity::Info,
987            })
988            .set_message("different message")
989            .build()
990            .into()
991        )));
992        assert!(!criteria.matches(&make_log_entry(
993            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
994                timestamp: Timestamp::from_nanos(0),
995                component_url: Some("".into()),
996                moniker: "other/moniker".try_into().unwrap(),
997                severity: diagnostics_data::Severity::Debug,
998            })
999            .set_message("included message")
1000            .build()
1001            .into()
1002        )));
1003
1004        let entry = make_log_entry(
1005            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1006                timestamp: Timestamp::from_nanos(0),
1007                component_url: Some("".into()),
1008                moniker: "other/moniker".try_into().unwrap(),
1009                severity: diagnostics_data::Severity::Debug,
1010            })
1011            .set_message("included message")
1012            .build()
1013            .into(),
1014        );
1015
1016        assert!(!criteria.matches(&entry));
1017    }
1018
1019    #[test]
1020    fn filter_fiters_case_sensitivity() {
1021        // Case-insensitive by default
1022        let cmd = LogCommand { filter: vec!["sometestfile".into()], ..empty_dump_command() };
1023        let criteria = LogFilterCriteria::from(cmd);
1024
1025        let entry_0 = make_log_entry(
1026            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1027                timestamp: Timestamp::from_nanos(0),
1028                component_url: Some("".into()),
1029                moniker: "core/last_segment".try_into().unwrap(),
1030                severity: diagnostics_data::Severity::Error,
1031            })
1032            .set_file("sometestfile")
1033            .set_message("hello world")
1034            .build()
1035            .into(),
1036        );
1037
1038        let entry_1 = make_log_entry(
1039            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1040                timestamp: Timestamp::from_nanos(0),
1041                component_url: Some("".into()),
1042                moniker: "core/last_segment".try_into().unwrap(),
1043                severity: diagnostics_data::Severity::Error,
1044            })
1045            .set_file("someTESTfile")
1046            .set_message("hello world")
1047            .build()
1048            .into(),
1049        );
1050        assert!(criteria.matches(&entry_0));
1051        assert!(criteria.matches(&entry_1));
1052
1053        // Case-sensitive
1054        let cmd = LogCommand {
1055            filter: vec!["sometestfile".into()],
1056            case_sensitive: true,
1057            ..empty_dump_command()
1058        };
1059        let criteria = LogFilterCriteria::from(cmd);
1060
1061        assert!(criteria.matches(&entry_0));
1062        assert!(!criteria.matches(&entry_1));
1063    }
1064
1065    #[test]
1066    fn filter_fiters_case_sensitivity_for_tags() {
1067        // Case-insensitive by default
1068        let cmd = LogCommand { tag: vec!["someTAG".into()], ..empty_dump_command() };
1069        let criteria = LogFilterCriteria::from(cmd);
1070
1071        let entry_0 = make_log_entry(
1072            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1073                timestamp: Timestamp::from_nanos(0),
1074                component_url: Some("".into()),
1075                moniker: "core/last_segment".try_into().unwrap(),
1076                severity: diagnostics_data::Severity::Error,
1077            })
1078            .add_tag("someTAG")
1079            .set_message("hello world")
1080            .build()
1081            .into(),
1082        );
1083
1084        let entry_1 = make_log_entry(
1085            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1086                timestamp: Timestamp::from_nanos(0),
1087                component_url: Some("".into()),
1088                moniker: "core/last_segment".try_into().unwrap(),
1089                severity: diagnostics_data::Severity::Error,
1090            })
1091            .add_tag("SomeTaG")
1092            .set_message("hello world")
1093            .build()
1094            .into(),
1095        );
1096        assert!(criteria.matches(&entry_0));
1097        assert!(criteria.matches(&entry_1));
1098
1099        // Case-sensitive
1100        let cmd = LogCommand {
1101            tag: vec!["someTAG".into()],
1102            case_sensitive: true,
1103            ..empty_dump_command()
1104        };
1105        let criteria = LogFilterCriteria::from(cmd);
1106
1107        assert!(criteria.matches(&entry_0));
1108        assert!(!criteria.matches(&entry_1));
1109    }
1110
1111    #[test]
1112    fn filter_fiters_case_sensitivity_for_tags_including_moniker() {
1113        // Case-insensitive by default
1114        let cmd = LogCommand { tag: vec!["someTAG".into()], ..empty_dump_command() };
1115        let criteria = LogFilterCriteria::from(cmd);
1116
1117        let entry_0 = make_log_entry(
1118            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1119                timestamp: Timestamp::from_nanos(0),
1120                component_url: Some("".into()),
1121                moniker: "core/someTAG".try_into().unwrap(),
1122                severity: diagnostics_data::Severity::Error,
1123            })
1124            .set_message("hello world")
1125            .build()
1126            .into(),
1127        );
1128
1129        let entry_1 = make_log_entry(
1130            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1131                timestamp: Timestamp::from_nanos(0),
1132                component_url: Some("".into()),
1133                moniker: "core/SomeTaG".try_into().unwrap(),
1134                severity: diagnostics_data::Severity::Error,
1135            })
1136            .set_message("hello world")
1137            .build()
1138            .into(),
1139        );
1140        assert!(criteria.matches(&entry_0));
1141        assert!(criteria.matches(&entry_1));
1142
1143        // Case-sensitive
1144        let cmd = LogCommand {
1145            tag: vec!["someTAG".into()],
1146            case_sensitive: true,
1147            ..empty_dump_command()
1148        };
1149        let criteria = LogFilterCriteria::from(cmd);
1150
1151        assert!(criteria.matches(&entry_0));
1152        assert!(!criteria.matches(&entry_1));
1153    }
1154
1155    #[test]
1156    fn tag_matches_moniker_last_segment() {
1157        // When the tags are empty, the last segment of the moniker is treated as the tag.
1158        let cmd = LogCommand { tag: vec!["last_segment".to_string()], ..empty_dump_command() };
1159        let criteria = LogFilterCriteria::from(cmd);
1160
1161        assert!(criteria.matches(&make_log_entry(
1162            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1163                timestamp: Timestamp::from_nanos(0),
1164                component_url: Some("".into()),
1165                moniker: "core/last_segment".try_into().unwrap(),
1166                severity: diagnostics_data::Severity::Error,
1167            })
1168            .set_message("hello world")
1169            .build()
1170            .into()
1171        )));
1172    }
1173}