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            .path()
335            .last()
336            .map(|segment| {
337                convert_to_lowercase_if_needed(&segment.to_string(), case_sensitive)
338                    .contains(&*query_tag)
339            })
340            .unwrap_or(false),
341        ExtendedMoniker::ComponentManager => {
342            EXTENDED_MONIKER_COMPONENT_MANAGER_STR.contains(&*query_tag)
343        }
344    }
345}
346
347#[cfg(test)]
348mod test {
349    use diagnostics_data::{ExtendedMoniker, Timestamp};
350    use selectors::parse_log_interest_selector;
351
352    use crate::log_socket_stream::OneOrMany;
353    use crate::{DumpCommand, LogSubCommand};
354
355    use super::*;
356
357    fn empty_dump_command() -> LogCommand {
358        LogCommand {
359            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
360            ..LogCommand::default()
361        }
362    }
363
364    fn make_log_entry(log_data: LogData) -> LogEntry {
365        LogEntry { data: log_data }
366    }
367
368    #[fuchsia::test]
369    async fn test_criteria_tag_filter_filters_moniker() {
370        let cmd = LogCommand { tag: vec!["testcomponent".to_string()], ..empty_dump_command() };
371        let criteria = LogFilterCriteria::from(cmd);
372
373        assert!(criteria.matches(&make_log_entry(
374            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
375                timestamp: Timestamp::from_nanos(0),
376                component_url: Some("".into()),
377                moniker: "my/testcomponent".try_into().unwrap(),
378                severity: diagnostics_data::Severity::Error,
379            })
380            .set_message("included")
381            .add_tag("tag1")
382            .add_tag("tag2")
383            .build()
384            .into()
385        )));
386    }
387
388    #[fuchsia::test]
389    async fn test_criteria_exclude_tag_filters_moniker() {
390        let cmd =
391            LogCommand { exclude_tags: vec!["testcomponent".to_string()], ..empty_dump_command() };
392        let criteria = LogFilterCriteria::from(cmd);
393        assert!(!criteria.matches(&make_log_entry(
394            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
395                timestamp: Timestamp::from_nanos(0),
396                component_url: Some("".into()),
397                moniker: "my/testcomponent".try_into().unwrap(),
398                severity: diagnostics_data::Severity::Error,
399            })
400            .set_message("excluded")
401            .add_tag("tag1")
402            .add_tag("tag2")
403            .build()
404            .into()
405        )));
406    }
407
408    #[fuchsia::test]
409    async fn test_criteria_tag_filter() {
410        let cmd = LogCommand {
411            tag: vec!["tag1".to_string()],
412            exclude_tags: vec!["tag3".to_string()],
413            ..empty_dump_command()
414        };
415        let criteria = LogFilterCriteria::from(cmd);
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("tag2")
427            .build()
428            .into()
429        )));
430
431        assert!(!criteria.matches(&make_log_entry(
432            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
433                timestamp: Timestamp::from_nanos(0),
434                component_url: Some("".into()),
435                moniker: ExtendedMoniker::ComponentManager,
436                severity: diagnostics_data::Severity::Error,
437            })
438            .set_message("included")
439            .add_tag("tag2")
440            .build()
441            .into()
442        )));
443        assert!(!criteria.matches(&make_log_entry(
444            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
445                timestamp: Timestamp::from_nanos(0),
446                component_url: Some("".into()),
447                moniker: ExtendedMoniker::ComponentManager,
448                severity: diagnostics_data::Severity::Error,
449            })
450            .set_message("included")
451            .add_tag("tag1")
452            .add_tag("tag3")
453            .build()
454            .into()
455        )));
456    }
457
458    #[fuchsia::test]
459    async fn test_per_component_severity() {
460        let cmd = LogCommand {
461            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
462            set_severity: vec![OneOrMany::One(
463                parse_log_interest_selector("test_selector#DEBUG").unwrap(),
464            )],
465            ..LogCommand::default()
466        };
467        let expectations = [
468            ("test_selector", diagnostics_data::Severity::Debug, true),
469            ("other_selector", diagnostics_data::Severity::Debug, false),
470            ("other_selector", diagnostics_data::Severity::Info, true),
471        ];
472        let criteria = LogFilterCriteria::from(cmd);
473        assert_eq!(criteria.min_severity, Severity::Info);
474        for (moniker, severity, is_included) in expectations {
475            let entry = make_log_entry(
476                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
477                    timestamp: Timestamp::from_nanos(0),
478                    component_url: Some("".into()),
479                    moniker: moniker.try_into().unwrap(),
480                    severity,
481                })
482                .set_message("message")
483                .add_tag("tag1")
484                .add_tag("tag2")
485                .build()
486                .into(),
487            );
488            assert_eq!(criteria.matches(&entry), is_included);
489        }
490    }
491
492    #[fuchsia::test]
493    async fn test_per_component_severity_uses_min_match() {
494        let severities = [
495            diagnostics_data::Severity::Info,
496            diagnostics_data::Severity::Trace,
497            diagnostics_data::Severity::Debug,
498        ];
499
500        let cmd = LogCommand {
501            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
502            set_severity: vec![
503                OneOrMany::One(parse_log_interest_selector("test_selector#INFO").unwrap()),
504                OneOrMany::One(parse_log_interest_selector("test_selector#TRACE").unwrap()),
505                OneOrMany::One(parse_log_interest_selector("test_selector#DEBUG").unwrap()),
506            ],
507            ..LogCommand::default()
508        };
509        let criteria = LogFilterCriteria::from(cmd);
510
511        for severity in severities {
512            let entry = make_log_entry(
513                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
514                    timestamp: Timestamp::from_nanos(0),
515                    component_url: Some("".into()),
516                    moniker: "test_selector".try_into().unwrap(),
517                    severity,
518                })
519                .set_message("message")
520                .add_tag("tag1")
521                .add_tag("tag2")
522                .build()
523                .into(),
524            );
525            assert!(criteria.matches(&entry));
526        }
527    }
528
529    #[fuchsia::test]
530    async fn test_criteria_tag_filter_legacy() {
531        let cmd = LogCommand {
532            tag: vec!["tag1".to_string()],
533            exclude_tags: vec!["tag3".to_string()],
534            ..empty_dump_command()
535        };
536        let criteria = LogFilterCriteria::from(cmd);
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("tag2")
548            .build()
549            .into()
550        )));
551
552        assert!(!criteria.matches(&make_log_entry(
553            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
554                timestamp: Timestamp::from_nanos(0),
555                component_url: Some("".into()),
556                moniker: ExtendedMoniker::ComponentManager,
557                severity: diagnostics_data::Severity::Error,
558            })
559            .set_message("included")
560            .add_tag("tag2")
561            .build()
562            .into()
563        )));
564        assert!(!criteria.matches(&make_log_entry(
565            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
566                timestamp: Timestamp::from_nanos(0),
567                component_url: Some("".into()),
568                moniker: ExtendedMoniker::ComponentManager,
569                severity: diagnostics_data::Severity::Error,
570            })
571            .set_message("included")
572            .add_tag("tag1")
573            .add_tag("tag3")
574            .build()
575            .into()
576        )));
577    }
578
579    #[fuchsia::test]
580    async fn test_severity_filter_with_debug() {
581        let mut cmd = empty_dump_command();
582        cmd.severity = Severity::Trace;
583        let criteria = LogFilterCriteria::from(cmd);
584
585        assert!(criteria.matches(&make_log_entry(
586            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
587                timestamp: Timestamp::from_nanos(0),
588                component_url: Some("".into()),
589                moniker: "included/moniker".try_into().unwrap(),
590                severity: diagnostics_data::Severity::Error,
591            })
592            .set_message("included message")
593            .build()
594            .into()
595        )));
596        assert!(criteria.matches(&make_log_entry(
597            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
598                timestamp: Timestamp::from_nanos(0),
599                component_url: Some("".into()),
600                moniker: "included/moniker".try_into().unwrap(),
601                severity: diagnostics_data::Severity::Info,
602            })
603            .set_message("different message")
604            .build()
605            .into()
606        )));
607        assert!(criteria.matches(&make_log_entry(
608            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
609                timestamp: Timestamp::from_nanos(0),
610                component_url: Some("".into()),
611                moniker: "other/moniker".try_into().unwrap(),
612                severity: diagnostics_data::Severity::Debug,
613            })
614            .set_message("included message")
615            .build()
616            .into()
617        )));
618    }
619
620    #[fuchsia::test]
621    async fn test_pid_filter() {
622        let mut cmd = empty_dump_command();
623        cmd.pid = Some(123);
624        let criteria = LogFilterCriteria::from(cmd);
625
626        assert!(criteria.matches(&make_log_entry(
627            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
628                timestamp: Timestamp::from_nanos(0),
629                component_url: Some("".into()),
630                moniker: "included/moniker".try_into().unwrap(),
631                severity: diagnostics_data::Severity::Error,
632            })
633            .set_message("included message")
634            .set_pid(123)
635            .build()
636            .into()
637        )));
638        assert!(!criteria.matches(&make_log_entry(
639            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
640                timestamp: Timestamp::from_nanos(0),
641                component_url: Some("".into()),
642                moniker: "included/moniker".try_into().unwrap(),
643                severity: diagnostics_data::Severity::Error,
644            })
645            .set_message("included message")
646            .set_pid(456)
647            .build()
648            .into()
649        )));
650    }
651
652    struct FakeInstanceGetter;
653    #[async_trait::async_trait(?Send)]
654    impl InstanceGetter for FakeInstanceGetter {
655        async fn get_monikers_from_query(
656            &self,
657            query: &str,
658        ) -> Result<Vec<moniker::Moniker>, LogError> {
659            Ok(vec![moniker::Moniker::try_from(query).unwrap()])
660        }
661    }
662
663    #[fuchsia::test]
664    async fn test_criteria_component_filter() {
665        let cmd = LogCommand {
666            component: vec!["/core/network/netstack".to_string()],
667            ..empty_dump_command()
668        };
669
670        let mut criteria = LogFilterCriteria::from(cmd);
671        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
672
673        assert!(!criteria.matches(&make_log_entry(
674            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
675                timestamp: Timestamp::from_nanos(0),
676                component_url: Some("".into()),
677                moniker: "bootstrap/archivist".try_into().unwrap(),
678                severity: diagnostics_data::Severity::Error,
679            })
680            .set_message("excluded")
681            .build()
682            .into()
683        )));
684
685        assert!(criteria.matches(&make_log_entry(
686            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
687                timestamp: Timestamp::from_nanos(0),
688                component_url: Some("".into()),
689                moniker: "core/network/netstack".try_into().unwrap(),
690                severity: diagnostics_data::Severity::Error,
691            })
692            .set_message("included")
693            .build()
694            .into()
695        )));
696
697        assert!(!criteria.matches(&make_log_entry(
698            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
699                timestamp: Timestamp::from_nanos(0),
700                component_url: Some("".into()),
701                moniker: "core/network/dhcp".try_into().unwrap(),
702                severity: diagnostics_data::Severity::Error,
703            })
704            .set_message("included")
705            .build()
706            .into()
707        )));
708    }
709
710    #[fuchsia::test]
711    async fn test_tid_filter() {
712        let mut cmd = empty_dump_command();
713        cmd.tid = Some(123);
714        let criteria = LogFilterCriteria::from(cmd);
715
716        assert!(criteria.matches(&make_log_entry(
717            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
718                timestamp: Timestamp::from_nanos(0),
719                component_url: Some("".into()),
720                moniker: "included/moniker".try_into().unwrap(),
721                severity: diagnostics_data::Severity::Error,
722            })
723            .set_message("included message")
724            .set_tid(123)
725            .build()
726            .into()
727        )));
728        assert!(!criteria.matches(&make_log_entry(
729            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
730                timestamp: Timestamp::from_nanos(0),
731                component_url: Some("".into()),
732                moniker: "included/moniker".try_into().unwrap(),
733                severity: diagnostics_data::Severity::Error,
734            })
735            .set_message("included message")
736            .set_tid(456)
737            .build()
738            .into()
739        )));
740    }
741
742    #[fuchsia::test]
743    async fn test_setter_functions() {
744        let mut filter = LogFilterCriteria::default();
745        filter.set_min_severity(Severity::Error);
746        assert_eq!(filter.min_severity, Severity::Error);
747        filter.set_tags(["tag1"]);
748        assert_eq!(filter.tags, ["tag1"]);
749        filter.set_exclude_tags(["tag2"]);
750        assert_eq!(filter.exclude_tags, ["tag2"]);
751    }
752
753    #[fuchsia::test]
754    async fn test_criteria_moniker_message_and_severity_matches() {
755        let cmd = LogCommand {
756            filter: vec!["included".to_string()],
757            exclude: vec!["not this".to_string()],
758            severity: Severity::Error,
759            ..empty_dump_command()
760        };
761        let criteria = LogFilterCriteria::from(cmd);
762
763        assert!(criteria.matches(&make_log_entry(
764            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
765                timestamp: Timestamp::from_nanos(0),
766                component_url: Some("".into()),
767                moniker: "included/moniker".try_into().unwrap(),
768                severity: diagnostics_data::Severity::Error,
769            })
770            .set_message("included message")
771            .build()
772            .into()
773        )));
774        assert!(criteria.matches(&make_log_entry(
775            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
776                timestamp: Timestamp::from_nanos(0),
777                component_url: Some("".into()),
778                moniker: "included/moniker".try_into().unwrap(),
779                severity: diagnostics_data::Severity::Fatal,
780            })
781            .set_message("included message")
782            .build()
783            .into()
784        )));
785        assert!(criteria.matches(&make_log_entry(
786            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
787                timestamp: Timestamp::from_nanos(0),
788                component_url: Some("".into()),
789                // Include a "/" prefix on the moniker to test filter permissiveness.
790                moniker: "included/moniker".try_into().unwrap(),
791                severity: diagnostics_data::Severity::Fatal,
792            })
793            .set_message("included message")
794            .build()
795            .into()
796        )));
797        assert!(!criteria.matches(&make_log_entry(
798            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
799                timestamp: Timestamp::from_nanos(0),
800                component_url: Some("".into()),
801                moniker: "not/this/moniker".try_into().unwrap(),
802                severity: diagnostics_data::Severity::Error,
803            })
804            .set_message("different message")
805            .build()
806            .into()
807        )));
808        assert!(!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::Warn,
814            })
815            .set_message("included message")
816            .build()
817            .into()
818        )));
819        assert!(!criteria.matches(&make_log_entry(
820            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
821                timestamp: Timestamp::from_nanos(0),
822                component_url: Some("".into()),
823                moniker: "other/moniker".try_into().unwrap(),
824                severity: diagnostics_data::Severity::Error,
825            })
826            .set_message("not this message")
827            .build()
828            .into()
829        )));
830        assert!(!criteria.matches(&make_log_entry(
831            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
832                timestamp: Timestamp::from_nanos(0),
833                component_url: Some("".into()),
834                moniker: "included/moniker".try_into().unwrap(),
835                severity: diagnostics_data::Severity::Error,
836            })
837            .set_message("not this message")
838            .build()
839            .into()
840        )));
841    }
842
843    #[fuchsia::test]
844    async fn test_criteria_klog_only() {
845        let cmd = LogCommand { tag: vec!["component_manager".into()], ..empty_dump_command() };
846        let criteria = LogFilterCriteria::from(cmd);
847
848        assert!(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: "klog".try_into().unwrap(),
853                severity: diagnostics_data::Severity::Error,
854            })
855            .set_message("[component_manager] included message")
856            .build()
857            .into()
858        )));
859        assert!(!criteria.matches(&make_log_entry(
860            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
861                timestamp: Timestamp::from_nanos(0),
862                component_url: Some("".into()),
863                moniker: "klog".try_into().unwrap(),
864                severity: diagnostics_data::Severity::Error,
865            })
866            .set_message("excluded message[component_manager]")
867            .build()
868            .into()
869        )));
870        assert!(criteria.matches(&make_log_entry(
871            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
872                timestamp: Timestamp::from_nanos(0),
873                component_url: Some("".into()),
874                moniker: "klog".try_into().unwrap(),
875                severity: diagnostics_data::Severity::Error,
876            })
877            .set_message("[tag0][component_manager] included message")
878            .build()
879            .into()
880        )));
881        assert!(!criteria.matches(&make_log_entry(
882            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
883                timestamp: Timestamp::from_nanos(0),
884                component_url: Some("".into()),
885                moniker: "klog".try_into().unwrap(),
886                severity: diagnostics_data::Severity::Error,
887            })
888            .set_message("[other] excluded message")
889            .build()
890            .into()
891        )));
892        assert!(!criteria.matches(&make_log_entry(
893            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
894                timestamp: Timestamp::from_nanos(0),
895                component_url: Some("".into()),
896                moniker: "klog".try_into().unwrap(),
897                severity: diagnostics_data::Severity::Error,
898            })
899            .set_message("no tags, excluded")
900            .build()
901            .into()
902        )));
903        assert!(!criteria.matches(&make_log_entry(
904            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
905                timestamp: Timestamp::from_nanos(0),
906                component_url: Some("".into()),
907                moniker: "other/moniker".try_into().unwrap(),
908                severity: diagnostics_data::Severity::Error,
909            })
910            .set_message("[component_manager] excluded message")
911            .build()
912            .into()
913        )));
914    }
915
916    #[fuchsia::test]
917    async fn test_criteria_klog_tag_hack() {
918        let cmd = LogCommand { kernel: true, ..empty_dump_command() };
919        let mut criteria = LogFilterCriteria::from(cmd);
920
921        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
922
923        assert!(criteria.matches(&make_log_entry(
924            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
925                timestamp: Timestamp::from_nanos(0),
926                component_url: Some("".into()),
927                moniker: "klog".try_into().unwrap(),
928                severity: diagnostics_data::Severity::Error,
929            })
930            .set_message("included message")
931            .build()
932            .into()
933        )));
934        assert!(!criteria.matches(&make_log_entry(
935            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
936                timestamp: Timestamp::from_nanos(0),
937                component_url: Some("".into()),
938                moniker: "other/moniker".try_into().unwrap(),
939                severity: diagnostics_data::Severity::Error,
940            })
941            .set_message("included message")
942            .build()
943            .into()
944        )));
945    }
946
947    #[test]
948    fn filter_fiters_filename() {
949        let cmd = LogCommand { filter: vec!["sometestfile".into()], ..empty_dump_command() };
950        let criteria = LogFilterCriteria::from(cmd);
951
952        assert!(criteria.matches(&make_log_entry(
953            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
954                timestamp: Timestamp::from_nanos(0),
955                component_url: Some("".into()),
956                moniker: "core/last_segment".try_into().unwrap(),
957                severity: diagnostics_data::Severity::Error,
958            })
959            .set_file("sometestfile")
960            .set_message("hello world")
961            .build()
962            .into()
963        )));
964    }
965
966    #[fuchsia::test]
967    async fn test_empty_criteria() {
968        let cmd = empty_dump_command();
969        let criteria = LogFilterCriteria::from(cmd);
970
971        assert!(criteria.matches(&make_log_entry(
972            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
973                timestamp: Timestamp::from_nanos(0),
974                component_url: Some("".into()),
975                moniker: "included/moniker".try_into().unwrap(),
976                severity: diagnostics_data::Severity::Error,
977            })
978            .set_message("included message")
979            .build()
980            .into()
981        )));
982        assert!(criteria.matches(&make_log_entry(
983            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
984                timestamp: Timestamp::from_nanos(0),
985                component_url: Some("".into()),
986                moniker: "included/moniker".try_into().unwrap(),
987                severity: diagnostics_data::Severity::Info,
988            })
989            .set_message("different message")
990            .build()
991            .into()
992        )));
993        assert!(!criteria.matches(&make_log_entry(
994            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
995                timestamp: Timestamp::from_nanos(0),
996                component_url: Some("".into()),
997                moniker: "other/moniker".try_into().unwrap(),
998                severity: diagnostics_data::Severity::Debug,
999            })
1000            .set_message("included message")
1001            .build()
1002            .into()
1003        )));
1004
1005        let entry = make_log_entry(
1006            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1007                timestamp: Timestamp::from_nanos(0),
1008                component_url: Some("".into()),
1009                moniker: "other/moniker".try_into().unwrap(),
1010                severity: diagnostics_data::Severity::Debug,
1011            })
1012            .set_message("included message")
1013            .build()
1014            .into(),
1015        );
1016
1017        assert!(!criteria.matches(&entry));
1018    }
1019
1020    #[test]
1021    fn filter_fiters_case_sensitivity() {
1022        // Case-insensitive by default
1023        let cmd = LogCommand { filter: vec!["sometestfile".into()], ..empty_dump_command() };
1024        let criteria = LogFilterCriteria::from(cmd);
1025
1026        let entry_0 = make_log_entry(
1027            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1028                timestamp: Timestamp::from_nanos(0),
1029                component_url: Some("".into()),
1030                moniker: "core/last_segment".try_into().unwrap(),
1031                severity: diagnostics_data::Severity::Error,
1032            })
1033            .set_file("sometestfile")
1034            .set_message("hello world")
1035            .build()
1036            .into(),
1037        );
1038
1039        let entry_1 = make_log_entry(
1040            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1041                timestamp: Timestamp::from_nanos(0),
1042                component_url: Some("".into()),
1043                moniker: "core/last_segment".try_into().unwrap(),
1044                severity: diagnostics_data::Severity::Error,
1045            })
1046            .set_file("someTESTfile")
1047            .set_message("hello world")
1048            .build()
1049            .into(),
1050        );
1051        assert!(criteria.matches(&entry_0));
1052        assert!(criteria.matches(&entry_1));
1053
1054        // Case-sensitive
1055        let cmd = LogCommand {
1056            filter: vec!["sometestfile".into()],
1057            case_sensitive: true,
1058            ..empty_dump_command()
1059        };
1060        let criteria = LogFilterCriteria::from(cmd);
1061
1062        assert!(criteria.matches(&entry_0));
1063        assert!(!criteria.matches(&entry_1));
1064    }
1065
1066    #[test]
1067    fn filter_fiters_case_sensitivity_for_tags() {
1068        // Case-insensitive by default
1069        let cmd = LogCommand { tag: vec!["someTAG".into()], ..empty_dump_command() };
1070        let criteria = LogFilterCriteria::from(cmd);
1071
1072        let entry_0 = make_log_entry(
1073            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1074                timestamp: Timestamp::from_nanos(0),
1075                component_url: Some("".into()),
1076                moniker: "core/last_segment".try_into().unwrap(),
1077                severity: diagnostics_data::Severity::Error,
1078            })
1079            .add_tag("someTAG")
1080            .set_message("hello world")
1081            .build()
1082            .into(),
1083        );
1084
1085        let entry_1 = make_log_entry(
1086            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1087                timestamp: Timestamp::from_nanos(0),
1088                component_url: Some("".into()),
1089                moniker: "core/last_segment".try_into().unwrap(),
1090                severity: diagnostics_data::Severity::Error,
1091            })
1092            .add_tag("SomeTaG")
1093            .set_message("hello world")
1094            .build()
1095            .into(),
1096        );
1097        assert!(criteria.matches(&entry_0));
1098        assert!(criteria.matches(&entry_1));
1099
1100        // Case-sensitive
1101        let cmd = LogCommand {
1102            tag: vec!["someTAG".into()],
1103            case_sensitive: true,
1104            ..empty_dump_command()
1105        };
1106        let criteria = LogFilterCriteria::from(cmd);
1107
1108        assert!(criteria.matches(&entry_0));
1109        assert!(!criteria.matches(&entry_1));
1110    }
1111
1112    #[test]
1113    fn filter_fiters_case_sensitivity_for_tags_including_moniker() {
1114        // Case-insensitive by default
1115        let cmd = LogCommand { tag: vec!["someTAG".into()], ..empty_dump_command() };
1116        let criteria = LogFilterCriteria::from(cmd);
1117
1118        let entry_0 = make_log_entry(
1119            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1120                timestamp: Timestamp::from_nanos(0),
1121                component_url: Some("".into()),
1122                moniker: "core/someTAG".try_into().unwrap(),
1123                severity: diagnostics_data::Severity::Error,
1124            })
1125            .set_message("hello world")
1126            .build()
1127            .into(),
1128        );
1129
1130        let entry_1 = make_log_entry(
1131            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1132                timestamp: Timestamp::from_nanos(0),
1133                component_url: Some("".into()),
1134                moniker: "core/SomeTaG".try_into().unwrap(),
1135                severity: diagnostics_data::Severity::Error,
1136            })
1137            .set_message("hello world")
1138            .build()
1139            .into(),
1140        );
1141        assert!(criteria.matches(&entry_0));
1142        assert!(criteria.matches(&entry_1));
1143
1144        // Case-sensitive
1145        let cmd = LogCommand {
1146            tag: vec!["someTAG".into()],
1147            case_sensitive: true,
1148            ..empty_dump_command()
1149        };
1150        let criteria = LogFilterCriteria::from(cmd);
1151
1152        assert!(criteria.matches(&entry_0));
1153        assert!(!criteria.matches(&entry_1));
1154    }
1155
1156    #[test]
1157    fn tag_matches_moniker_last_segment() {
1158        // When the tags are empty, the last segment of the moniker is treated as the tag.
1159        let cmd = LogCommand { tag: vec!["last_segment".to_string()], ..empty_dump_command() };
1160        let criteria = LogFilterCriteria::from(cmd);
1161
1162        assert!(criteria.matches(&make_log_entry(
1163            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1164                timestamp: Timestamp::from_nanos(0),
1165                component_url: Some("".into()),
1166                moniker: "core/last_segment".try_into().unwrap(),
1167                severity: diagnostics_data::Severity::Error,
1168            })
1169            .set_message("hello world")
1170            .build()
1171            .into()
1172        )));
1173    }
1174}