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.tag,
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) -> bool {
217        let tags = Self::parse_tags(klog_str);
218        self.tags.iter().any(|f| tags.iter().any(|t| t.contains(f)))
219    }
220
221    /// Returns true if the given `LogsData` matches the moniker string.
222    fn matches_filter_by_moniker_string(filter_string: &str, log: &LogsData) -> bool {
223        let Ok(filter_moniker) = ExtendedMoniker::from_str(filter_string) else {
224            return false;
225        };
226        filter_moniker == log.moniker
227    }
228
229    /// Returns true if the given `LogsData` matches the filter criteria.
230    fn match_filters_to_log_data(&self, data: &LogsData) -> bool {
231        let min_severity = self
232            .interest_selectors
233            .iter()
234            .filter(|s| data.moniker.matches_component_selector(&s.selector).unwrap_or(false))
235            .filter_map(|selector| selector.interest.min_severity)
236            .min()
237            .unwrap_or_else(|| self.min_severity.into());
238        if data.metadata.severity < min_severity {
239            return false;
240        }
241
242        if let Some(pid) = self.pid {
243            if data.pid() != Some(pid) {
244                return false;
245            }
246        }
247
248        if let Some(tid) = self.tid {
249            if data.tid() != Some(tid) {
250                return false;
251            }
252        }
253
254        if !self.moniker_filters.matched_monikers.is_empty()
255            && !self
256                .moniker_filters
257                .matched_monikers
258                .iter()
259                .any(|f| Self::matches_filter_by_moniker_string(f, data))
260        {
261            return false;
262        }
263
264        let msg = data.msg().unwrap_or("");
265
266        if !self.filters.is_empty()
267            && !self
268                .filters
269                .iter()
270                .any(|f| Self::matches_filter_string(f, msg, data, self.case_sensitive))
271        {
272            return false;
273        }
274
275        if self
276            .excludes
277            .iter()
278            .any(|f| Self::matches_filter_string(f, msg, data, self.case_sensitive))
279        {
280            return false;
281        }
282
283        if !self.tags.is_empty()
284            && !self.tags.iter().any(|query_tag| {
285                let has_tag = data.tags().map(|t| t.contains(query_tag)).unwrap_or(false);
286                let moniker_has_tag = moniker_contains_in_last_segment(&data.moniker, query_tag);
287                has_tag || moniker_has_tag
288            })
289        {
290            if data.moniker == *KLOG_MONIKER {
291                return self.match_synthetic_klog_tags(data.msg().unwrap_or(""));
292            }
293            return false;
294        }
295
296        if self.exclude_tags.iter().any(|excluded_tag| {
297            let has_tag = data.tags().map(|tag| tag.contains(excluded_tag)).unwrap_or(false);
298            let moniker_has_tag = moniker_contains_in_last_segment(&data.moniker, excluded_tag);
299            has_tag || moniker_has_tag
300        }) {
301            return false;
302        }
303
304        true
305    }
306}
307
308fn moniker_contains_in_last_segment(moniker: &ExtendedMoniker, query_tag: &str) -> bool {
309    match moniker {
310        ExtendedMoniker::ComponentInstance(moniker) => moniker
311            .path()
312            .last()
313            .map(|segment| segment.to_string().contains(query_tag))
314            .unwrap_or(false),
315        ExtendedMoniker::ComponentManager => {
316            EXTENDED_MONIKER_COMPONENT_MANAGER_STR.contains(query_tag)
317        }
318    }
319}
320
321#[cfg(test)]
322mod test {
323    use diagnostics_data::{ExtendedMoniker, Timestamp};
324    use selectors::parse_log_interest_selector;
325
326    use crate::log_socket_stream::OneOrMany;
327    use crate::{DumpCommand, LogSubCommand};
328
329    use super::*;
330
331    fn empty_dump_command() -> LogCommand {
332        LogCommand {
333            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
334            ..LogCommand::default()
335        }
336    }
337
338    fn make_log_entry(log_data: LogData) -> LogEntry {
339        LogEntry { data: log_data }
340    }
341
342    #[fuchsia::test]
343    async fn test_criteria_tag_filter_filters_moniker() {
344        let cmd = LogCommand { tag: vec!["testcomponent".to_string()], ..empty_dump_command() };
345        let criteria = LogFilterCriteria::from(cmd);
346
347        assert!(criteria.matches(&make_log_entry(
348            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
349                timestamp: Timestamp::from_nanos(0),
350                component_url: Some("".into()),
351                moniker: "my/testcomponent".try_into().unwrap(),
352                severity: diagnostics_data::Severity::Error,
353            })
354            .set_message("included")
355            .add_tag("tag1")
356            .add_tag("tag2")
357            .build()
358            .into()
359        )));
360    }
361
362    #[fuchsia::test]
363    async fn test_criteria_exclude_tag_filters_moniker() {
364        let cmd =
365            LogCommand { exclude_tags: vec!["testcomponent".to_string()], ..empty_dump_command() };
366        let criteria = LogFilterCriteria::from(cmd);
367        assert!(!criteria.matches(&make_log_entry(
368            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
369                timestamp: Timestamp::from_nanos(0),
370                component_url: Some("".into()),
371                moniker: "my/testcomponent".try_into().unwrap(),
372                severity: diagnostics_data::Severity::Error,
373            })
374            .set_message("excluded")
375            .add_tag("tag1")
376            .add_tag("tag2")
377            .build()
378            .into()
379        )));
380    }
381
382    #[fuchsia::test]
383    async fn test_criteria_tag_filter() {
384        let cmd = LogCommand {
385            tag: vec!["tag1".to_string()],
386            exclude_tags: vec!["tag3".to_string()],
387            ..empty_dump_command()
388        };
389        let criteria = LogFilterCriteria::from(cmd);
390
391        assert!(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: ExtendedMoniker::ComponentManager,
396                severity: diagnostics_data::Severity::Error,
397            })
398            .set_message("included")
399            .add_tag("tag1")
400            .add_tag("tag2")
401            .build()
402            .into()
403        )));
404
405        assert!(!criteria.matches(&make_log_entry(
406            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
407                timestamp: Timestamp::from_nanos(0),
408                component_url: Some("".into()),
409                moniker: ExtendedMoniker::ComponentManager,
410                severity: diagnostics_data::Severity::Error,
411            })
412            .set_message("included")
413            .add_tag("tag2")
414            .build()
415            .into()
416        )));
417        assert!(!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("tag3")
427            .build()
428            .into()
429        )));
430    }
431
432    #[fuchsia::test]
433    async fn test_per_component_severity() {
434        let cmd = LogCommand {
435            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
436            set_severity: vec![OneOrMany::One(
437                parse_log_interest_selector("test_selector#DEBUG").unwrap(),
438            )],
439            ..LogCommand::default()
440        };
441        let expectations = [
442            ("test_selector", diagnostics_data::Severity::Debug, true),
443            ("other_selector", diagnostics_data::Severity::Debug, false),
444            ("other_selector", diagnostics_data::Severity::Info, true),
445        ];
446        let criteria = LogFilterCriteria::from(cmd);
447        assert_eq!(criteria.min_severity, Severity::Info);
448        for (moniker, severity, is_included) in expectations {
449            let entry = make_log_entry(
450                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
451                    timestamp: Timestamp::from_nanos(0),
452                    component_url: Some("".into()),
453                    moniker: moniker.try_into().unwrap(),
454                    severity,
455                })
456                .set_message("message")
457                .add_tag("tag1")
458                .add_tag("tag2")
459                .build()
460                .into(),
461            );
462            assert_eq!(criteria.matches(&entry), is_included);
463        }
464    }
465
466    #[fuchsia::test]
467    async fn test_per_component_severity_uses_min_match() {
468        let severities = [
469            diagnostics_data::Severity::Info,
470            diagnostics_data::Severity::Trace,
471            diagnostics_data::Severity::Debug,
472        ];
473
474        let cmd = LogCommand {
475            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
476            set_severity: vec![
477                OneOrMany::One(parse_log_interest_selector("test_selector#INFO").unwrap()),
478                OneOrMany::One(parse_log_interest_selector("test_selector#TRACE").unwrap()),
479                OneOrMany::One(parse_log_interest_selector("test_selector#DEBUG").unwrap()),
480            ],
481            ..LogCommand::default()
482        };
483        let criteria = LogFilterCriteria::from(cmd);
484
485        for severity in severities {
486            let entry = make_log_entry(
487                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
488                    timestamp: Timestamp::from_nanos(0),
489                    component_url: Some("".into()),
490                    moniker: "test_selector".try_into().unwrap(),
491                    severity,
492                })
493                .set_message("message")
494                .add_tag("tag1")
495                .add_tag("tag2")
496                .build()
497                .into(),
498            );
499            assert!(criteria.matches(&entry));
500        }
501    }
502
503    #[fuchsia::test]
504    async fn test_criteria_tag_filter_legacy() {
505        let cmd = LogCommand {
506            tag: vec!["tag1".to_string()],
507            exclude_tags: vec!["tag3".to_string()],
508            ..empty_dump_command()
509        };
510        let criteria = LogFilterCriteria::from(cmd);
511
512        assert!(criteria.matches(&make_log_entry(
513            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
514                timestamp: Timestamp::from_nanos(0),
515                component_url: Some("".into()),
516                moniker: ExtendedMoniker::ComponentManager,
517                severity: diagnostics_data::Severity::Error,
518            })
519            .set_message("included")
520            .add_tag("tag1")
521            .add_tag("tag2")
522            .build()
523            .into()
524        )));
525
526        assert!(!criteria.matches(&make_log_entry(
527            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
528                timestamp: Timestamp::from_nanos(0),
529                component_url: Some("".into()),
530                moniker: ExtendedMoniker::ComponentManager,
531                severity: diagnostics_data::Severity::Error,
532            })
533            .set_message("included")
534            .add_tag("tag2")
535            .build()
536            .into()
537        )));
538        assert!(!criteria.matches(&make_log_entry(
539            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
540                timestamp: Timestamp::from_nanos(0),
541                component_url: Some("".into()),
542                moniker: ExtendedMoniker::ComponentManager,
543                severity: diagnostics_data::Severity::Error,
544            })
545            .set_message("included")
546            .add_tag("tag1")
547            .add_tag("tag3")
548            .build()
549            .into()
550        )));
551    }
552
553    #[fuchsia::test]
554    async fn test_severity_filter_with_debug() {
555        let mut cmd = empty_dump_command();
556        cmd.severity = Severity::Trace;
557        let criteria = LogFilterCriteria::from(cmd);
558
559        assert!(criteria.matches(&make_log_entry(
560            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
561                timestamp: Timestamp::from_nanos(0),
562                component_url: Some("".into()),
563                moniker: "included/moniker".try_into().unwrap(),
564                severity: diagnostics_data::Severity::Error,
565            })
566            .set_message("included message")
567            .build()
568            .into()
569        )));
570        assert!(criteria.matches(&make_log_entry(
571            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
572                timestamp: Timestamp::from_nanos(0),
573                component_url: Some("".into()),
574                moniker: "included/moniker".try_into().unwrap(),
575                severity: diagnostics_data::Severity::Info,
576            })
577            .set_message("different message")
578            .build()
579            .into()
580        )));
581        assert!(criteria.matches(&make_log_entry(
582            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
583                timestamp: Timestamp::from_nanos(0),
584                component_url: Some("".into()),
585                moniker: "other/moniker".try_into().unwrap(),
586                severity: diagnostics_data::Severity::Debug,
587            })
588            .set_message("included message")
589            .build()
590            .into()
591        )));
592    }
593
594    #[fuchsia::test]
595    async fn test_pid_filter() {
596        let mut cmd = empty_dump_command();
597        cmd.pid = Some(123);
598        let criteria = LogFilterCriteria::from(cmd);
599
600        assert!(criteria.matches(&make_log_entry(
601            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
602                timestamp: Timestamp::from_nanos(0),
603                component_url: Some("".into()),
604                moniker: "included/moniker".try_into().unwrap(),
605                severity: diagnostics_data::Severity::Error,
606            })
607            .set_message("included message")
608            .set_pid(123)
609            .build()
610            .into()
611        )));
612        assert!(!criteria.matches(&make_log_entry(
613            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
614                timestamp: Timestamp::from_nanos(0),
615                component_url: Some("".into()),
616                moniker: "included/moniker".try_into().unwrap(),
617                severity: diagnostics_data::Severity::Error,
618            })
619            .set_message("included message")
620            .set_pid(456)
621            .build()
622            .into()
623        )));
624    }
625
626    struct FakeInstanceGetter;
627    #[async_trait::async_trait(?Send)]
628    impl InstanceGetter for FakeInstanceGetter {
629        async fn get_monikers_from_query(
630            &self,
631            query: &str,
632        ) -> Result<Vec<moniker::Moniker>, LogError> {
633            Ok(vec![moniker::Moniker::try_from(query).unwrap()])
634        }
635    }
636
637    #[fuchsia::test]
638    async fn test_criteria_component_filter() {
639        let cmd = LogCommand {
640            component: vec!["/core/network/netstack".to_string()],
641            ..empty_dump_command()
642        };
643
644        let mut criteria = LogFilterCriteria::from(cmd);
645        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
646
647        assert!(!criteria.matches(&make_log_entry(
648            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
649                timestamp: Timestamp::from_nanos(0),
650                component_url: Some("".into()),
651                moniker: "bootstrap/archivist".try_into().unwrap(),
652                severity: diagnostics_data::Severity::Error,
653            })
654            .set_message("excluded")
655            .build()
656            .into()
657        )));
658
659        assert!(criteria.matches(&make_log_entry(
660            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
661                timestamp: Timestamp::from_nanos(0),
662                component_url: Some("".into()),
663                moniker: "core/network/netstack".try_into().unwrap(),
664                severity: diagnostics_data::Severity::Error,
665            })
666            .set_message("included")
667            .build()
668            .into()
669        )));
670
671        assert!(!criteria.matches(&make_log_entry(
672            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
673                timestamp: Timestamp::from_nanos(0),
674                component_url: Some("".into()),
675                moniker: "core/network/dhcp".try_into().unwrap(),
676                severity: diagnostics_data::Severity::Error,
677            })
678            .set_message("included")
679            .build()
680            .into()
681        )));
682    }
683
684    #[fuchsia::test]
685    async fn test_tid_filter() {
686        let mut cmd = empty_dump_command();
687        cmd.tid = Some(123);
688        let criteria = LogFilterCriteria::from(cmd);
689
690        assert!(criteria.matches(&make_log_entry(
691            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
692                timestamp: Timestamp::from_nanos(0),
693                component_url: Some("".into()),
694                moniker: "included/moniker".try_into().unwrap(),
695                severity: diagnostics_data::Severity::Error,
696            })
697            .set_message("included message")
698            .set_tid(123)
699            .build()
700            .into()
701        )));
702        assert!(!criteria.matches(&make_log_entry(
703            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
704                timestamp: Timestamp::from_nanos(0),
705                component_url: Some("".into()),
706                moniker: "included/moniker".try_into().unwrap(),
707                severity: diagnostics_data::Severity::Error,
708            })
709            .set_message("included message")
710            .set_tid(456)
711            .build()
712            .into()
713        )));
714    }
715
716    #[fuchsia::test]
717    async fn test_setter_functions() {
718        let mut filter = LogFilterCriteria::default();
719        filter.set_min_severity(Severity::Error);
720        assert_eq!(filter.min_severity, Severity::Error);
721        filter.set_tags(["tag1"]);
722        assert_eq!(filter.tags, ["tag1"]);
723        filter.set_exclude_tags(["tag2"]);
724        assert_eq!(filter.exclude_tags, ["tag2"]);
725    }
726
727    #[fuchsia::test]
728    async fn test_criteria_moniker_message_and_severity_matches() {
729        let cmd = LogCommand {
730            filter: vec!["included".to_string()],
731            exclude: vec!["not this".to_string()],
732            severity: Severity::Error,
733            ..empty_dump_command()
734        };
735        let criteria = LogFilterCriteria::from(cmd);
736
737        assert!(criteria.matches(&make_log_entry(
738            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
739                timestamp: Timestamp::from_nanos(0),
740                component_url: Some("".into()),
741                moniker: "included/moniker".try_into().unwrap(),
742                severity: diagnostics_data::Severity::Error,
743            })
744            .set_message("included message")
745            .build()
746            .into()
747        )));
748        assert!(criteria.matches(&make_log_entry(
749            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
750                timestamp: Timestamp::from_nanos(0),
751                component_url: Some("".into()),
752                moniker: "included/moniker".try_into().unwrap(),
753                severity: diagnostics_data::Severity::Fatal,
754            })
755            .set_message("included message")
756            .build()
757            .into()
758        )));
759        assert!(criteria.matches(&make_log_entry(
760            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
761                timestamp: Timestamp::from_nanos(0),
762                component_url: Some("".into()),
763                // Include a "/" prefix on the moniker to test filter permissiveness.
764                moniker: "included/moniker".try_into().unwrap(),
765                severity: diagnostics_data::Severity::Fatal,
766            })
767            .set_message("included message")
768            .build()
769            .into()
770        )));
771        assert!(!criteria.matches(&make_log_entry(
772            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
773                timestamp: Timestamp::from_nanos(0),
774                component_url: Some("".into()),
775                moniker: "not/this/moniker".try_into().unwrap(),
776                severity: diagnostics_data::Severity::Error,
777            })
778            .set_message("different message")
779            .build()
780            .into()
781        )));
782        assert!(!criteria.matches(&make_log_entry(
783            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
784                timestamp: Timestamp::from_nanos(0),
785                component_url: Some("".into()),
786                moniker: "included/moniker".try_into().unwrap(),
787                severity: diagnostics_data::Severity::Warn,
788            })
789            .set_message("included message")
790            .build()
791            .into()
792        )));
793        assert!(!criteria.matches(&make_log_entry(
794            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
795                timestamp: Timestamp::from_nanos(0),
796                component_url: Some("".into()),
797                moniker: "other/moniker".try_into().unwrap(),
798                severity: diagnostics_data::Severity::Error,
799            })
800            .set_message("not this message")
801            .build()
802            .into()
803        )));
804        assert!(!criteria.matches(&make_log_entry(
805            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
806                timestamp: Timestamp::from_nanos(0),
807                component_url: Some("".into()),
808                moniker: "included/moniker".try_into().unwrap(),
809                severity: diagnostics_data::Severity::Error,
810            })
811            .set_message("not this message")
812            .build()
813            .into()
814        )));
815    }
816
817    #[fuchsia::test]
818    async fn test_criteria_klog_only() {
819        let cmd = LogCommand { tag: vec!["component_manager".into()], ..empty_dump_command() };
820        let criteria = LogFilterCriteria::from(cmd);
821
822        assert!(criteria.matches(&make_log_entry(
823            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
824                timestamp: Timestamp::from_nanos(0),
825                component_url: Some("".into()),
826                moniker: "klog".try_into().unwrap(),
827                severity: diagnostics_data::Severity::Error,
828            })
829            .set_message("[component_manager] included message")
830            .build()
831            .into()
832        )));
833        assert!(!criteria.matches(&make_log_entry(
834            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
835                timestamp: Timestamp::from_nanos(0),
836                component_url: Some("".into()),
837                moniker: "klog".try_into().unwrap(),
838                severity: diagnostics_data::Severity::Error,
839            })
840            .set_message("excluded message[component_manager]")
841            .build()
842            .into()
843        )));
844        assert!(criteria.matches(&make_log_entry(
845            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
846                timestamp: Timestamp::from_nanos(0),
847                component_url: Some("".into()),
848                moniker: "klog".try_into().unwrap(),
849                severity: diagnostics_data::Severity::Error,
850            })
851            .set_message("[tag0][component_manager] included message")
852            .build()
853            .into()
854        )));
855        assert!(!criteria.matches(&make_log_entry(
856            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
857                timestamp: Timestamp::from_nanos(0),
858                component_url: Some("".into()),
859                moniker: "klog".try_into().unwrap(),
860                severity: diagnostics_data::Severity::Error,
861            })
862            .set_message("[other] excluded message")
863            .build()
864            .into()
865        )));
866        assert!(!criteria.matches(&make_log_entry(
867            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
868                timestamp: Timestamp::from_nanos(0),
869                component_url: Some("".into()),
870                moniker: "klog".try_into().unwrap(),
871                severity: diagnostics_data::Severity::Error,
872            })
873            .set_message("no tags, excluded")
874            .build()
875            .into()
876        )));
877        assert!(!criteria.matches(&make_log_entry(
878            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
879                timestamp: Timestamp::from_nanos(0),
880                component_url: Some("".into()),
881                moniker: "other/moniker".try_into().unwrap(),
882                severity: diagnostics_data::Severity::Error,
883            })
884            .set_message("[component_manager] excluded message")
885            .build()
886            .into()
887        )));
888    }
889
890    #[fuchsia::test]
891    async fn test_criteria_klog_tag_hack() {
892        let cmd = LogCommand { kernel: true, ..empty_dump_command() };
893        let mut criteria = LogFilterCriteria::from(cmd);
894
895        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
896
897        assert!(criteria.matches(&make_log_entry(
898            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
899                timestamp: Timestamp::from_nanos(0),
900                component_url: Some("".into()),
901                moniker: "klog".try_into().unwrap(),
902                severity: diagnostics_data::Severity::Error,
903            })
904            .set_message("included message")
905            .build()
906            .into()
907        )));
908        assert!(!criteria.matches(&make_log_entry(
909            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
910                timestamp: Timestamp::from_nanos(0),
911                component_url: Some("".into()),
912                moniker: "other/moniker".try_into().unwrap(),
913                severity: diagnostics_data::Severity::Error,
914            })
915            .set_message("included message")
916            .build()
917            .into()
918        )));
919    }
920
921    #[test]
922    fn filter_fiters_filename() {
923        let cmd = LogCommand { filter: vec!["sometestfile".into()], ..empty_dump_command() };
924        let criteria = LogFilterCriteria::from(cmd);
925
926        assert!(criteria.matches(&make_log_entry(
927            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
928                timestamp: Timestamp::from_nanos(0),
929                component_url: Some("".into()),
930                moniker: "core/last_segment".try_into().unwrap(),
931                severity: diagnostics_data::Severity::Error,
932            })
933            .set_file("sometestfile")
934            .set_message("hello world")
935            .build()
936            .into()
937        )));
938    }
939
940    #[fuchsia::test]
941    async fn test_empty_criteria() {
942        let cmd = empty_dump_command();
943        let criteria = LogFilterCriteria::from(cmd);
944
945        assert!(criteria.matches(&make_log_entry(
946            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
947                timestamp: Timestamp::from_nanos(0),
948                component_url: Some("".into()),
949                moniker: "included/moniker".try_into().unwrap(),
950                severity: diagnostics_data::Severity::Error,
951            })
952            .set_message("included message")
953            .build()
954            .into()
955        )));
956        assert!(criteria.matches(&make_log_entry(
957            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
958                timestamp: Timestamp::from_nanos(0),
959                component_url: Some("".into()),
960                moniker: "included/moniker".try_into().unwrap(),
961                severity: diagnostics_data::Severity::Info,
962            })
963            .set_message("different message")
964            .build()
965            .into()
966        )));
967        assert!(!criteria.matches(&make_log_entry(
968            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
969                timestamp: Timestamp::from_nanos(0),
970                component_url: Some("".into()),
971                moniker: "other/moniker".try_into().unwrap(),
972                severity: diagnostics_data::Severity::Debug,
973            })
974            .set_message("included message")
975            .build()
976            .into()
977        )));
978
979        let entry = make_log_entry(
980            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
981                timestamp: Timestamp::from_nanos(0),
982                component_url: Some("".into()),
983                moniker: "other/moniker".try_into().unwrap(),
984                severity: diagnostics_data::Severity::Debug,
985            })
986            .set_message("included message")
987            .build()
988            .into(),
989        );
990
991        assert!(!criteria.matches(&entry));
992    }
993
994    #[test]
995    fn filter_fiters_case_sensitivity() {
996        // Case-insensitive by default
997        let cmd = LogCommand { filter: vec!["sometestfile".into()], ..empty_dump_command() };
998        let criteria = LogFilterCriteria::from(cmd);
999
1000        let entry_0 = make_log_entry(
1001            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1002                timestamp: Timestamp::from_nanos(0),
1003                component_url: Some("".into()),
1004                moniker: "core/last_segment".try_into().unwrap(),
1005                severity: diagnostics_data::Severity::Error,
1006            })
1007            .set_file("sometestfile")
1008            .set_message("hello world")
1009            .build()
1010            .into(),
1011        );
1012
1013        let entry_1 = make_log_entry(
1014            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1015                timestamp: Timestamp::from_nanos(0),
1016                component_url: Some("".into()),
1017                moniker: "core/last_segment".try_into().unwrap(),
1018                severity: diagnostics_data::Severity::Error,
1019            })
1020            .set_file("someTESTfile")
1021            .set_message("hello world")
1022            .build()
1023            .into(),
1024        );
1025        assert!(criteria.matches(&entry_0));
1026        assert!(criteria.matches(&entry_1));
1027
1028        // Case-sensitive
1029        let cmd = LogCommand {
1030            filter: vec!["sometestfile".into()],
1031            case_sensitive: true,
1032            ..empty_dump_command()
1033        };
1034        let criteria = LogFilterCriteria::from(cmd);
1035
1036        assert!(criteria.matches(&entry_0));
1037        assert!(!criteria.matches(&entry_1));
1038    }
1039
1040    #[test]
1041    fn tag_matches_moniker_last_segment() {
1042        // When the tags are empty, the last segment of the moniker is treated as the tag.
1043        let cmd = LogCommand { tag: vec!["last_segment".to_string()], ..empty_dump_command() };
1044        let criteria = LogFilterCriteria::from(cmd);
1045
1046        assert!(criteria.matches(&make_log_entry(
1047            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1048                timestamp: Timestamp::from_nanos(0),
1049                component_url: Some("".into()),
1050                moniker: "core/last_segment".try_into().unwrap(),
1051                severity: diagnostics_data::Severity::Error,
1052            })
1053            .set_message("hello world")
1054            .build()
1055            .into()
1056        )));
1057    }
1058}