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