log_command/
lib.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 anyhow::format_err;
6use argh::{ArgsInfo, FromArgs, TopLevelCommand};
7use chrono::{DateTime, Local};
8use chrono_english::{parse_date_string, Dialect};
9use component_debug::query::get_instances_from_query;
10use diagnostics_data::Severity;
11use errors::{ffx_bail, FfxError};
12use fidl_fuchsia_diagnostics::{LogInterestSelector, LogSettingsProxy};
13use fidl_fuchsia_sys2::RealmQueryProxy;
14pub use log_socket_stream::OneOrMany;
15use moniker::Moniker;
16use selectors::{sanitize_moniker_for_selectors, SelectorExt};
17use std::borrow::Cow;
18use std::io::Write;
19use std::ops::Deref;
20use std::str::FromStr;
21use std::string::FromUtf8Error;
22use std::time::Duration;
23use thiserror::Error;
24mod filter;
25mod log_formatter;
26mod log_socket_stream;
27pub use log_formatter::{
28    dump_logs_from_socket, BootTimeAccessor, DefaultLogFormatter, FormatterError, LogData,
29    LogEntry, Symbolize, Timestamp, WriterContainer, TIMESTAMP_FORMAT,
30};
31pub use log_socket_stream::{JsonDeserializeError, LogsDataStream};
32
33// Subcommand for ffx log (either watch or dump).
34#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
35#[argh(subcommand)]
36pub enum LogSubCommand {
37    Watch(WatchCommand),
38    Dump(DumpCommand),
39    SetSeverity(SetSeverityCommand),
40}
41
42#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug, Default)]
43/// Sets the severity, but doesn't view any logs.
44#[argh(subcommand, name = "set-severity")]
45pub struct SetSeverityCommand {
46    /// if true, doesn't persist the interest setting
47    /// and blocks forever, keeping the connection open.
48    /// Interest settings will be reset when the command exits.
49    #[argh(switch)]
50    pub no_persist: bool,
51
52    /// if enabled, selectors will be passed directly to Archivist without any filtering.
53    /// If disabled and no matching components are found, the user will be prompted to
54    /// either enable this or be given a list of selectors to choose from.
55    #[argh(switch)]
56    pub force: bool,
57
58    /// configure the log settings on the target device for components matching
59    /// the given selector. This modifies the minimum log severity level emitted
60    /// by components during the logging session.
61    /// Specify using the format <component-selector>#<log-level>, with level
62    /// as one of FATAL|ERROR|WARN|INFO|DEBUG|TRACE.
63    /// May be repeated.
64    #[argh(positional, from_str_fn(log_interest_selector))]
65    pub interest_selector: Vec<OneOrMany<LogInterestSelector>>,
66}
67
68#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
69/// Watches for and prints logs from a target. Default if no sub-command is specified.
70#[argh(subcommand, name = "watch")]
71pub struct WatchCommand {}
72
73#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
74/// Dumps all log from a given target's session.
75#[argh(subcommand, name = "dump")]
76pub struct DumpCommand {}
77
78pub fn parse_time(value: &str) -> Result<DetailedDateTime, String> {
79    parse_date_string(value, Local::now(), Dialect::Us)
80        .map(|time| DetailedDateTime { time, is_now: value == "now" })
81        .map_err(|e| format!("invalid date string: {}", e))
82}
83
84/// Parses a duration from a string. The input is in seconds
85/// and the output is a Rust duration.
86pub fn parse_seconds_string_as_duration(value: &str) -> Result<Duration, String> {
87    Ok(Duration::from_secs(
88        value.parse().map_err(|e| format!("value '{}' is not a number: {}", value, e))?,
89    ))
90}
91
92// Time format for displaying logs
93#[derive(Clone, Debug, PartialEq)]
94pub enum TimeFormat {
95    // UTC time
96    Utc,
97    // Local time
98    Local,
99    // Boot time
100    Boot,
101}
102
103impl std::str::FromStr for TimeFormat {
104    type Err = String;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        let lower = s.to_ascii_lowercase();
108        match lower.as_str() {
109            "local" => Ok(TimeFormat::Local),
110            "utc" => Ok(TimeFormat::Utc),
111            "boot" => Ok(TimeFormat::Boot),
112            _ => {
113                Err(format!("'{}' is not a valid value: must be one of 'local', 'utc', 'boot'", s))
114            }
115        }
116    }
117}
118
119/// Date/time structure containing a "now"
120/// field, set if it should be interpreted as the
121/// current time (used to call Subscribe instead of SnapshotThenSubscribe).
122#[derive(PartialEq, Clone, Debug)]
123pub struct DetailedDateTime {
124    /// The absolute timestamp as specified by the user
125    /// or the current timestamp if 'now' is specified.
126    pub time: DateTime<Local>,
127    /// Whether or not the DateTime was "now".
128    /// If the DateTime is "now", logs will be collected in subscribe
129    /// mode, instead of SnapshotThenSubscribe.
130    pub is_now: bool,
131}
132
133impl Deref for DetailedDateTime {
134    type Target = DateTime<Local>;
135
136    fn deref(&self) -> &Self::Target {
137        &self.time
138    }
139}
140
141#[derive(Clone, PartialEq, Debug)]
142pub enum SymbolizeMode {
143    /// Disable all symbolization
144    Off,
145    /// Use prettified symbolization
146    Pretty,
147    /// Use classic (non-prettified) symbolization
148    Classic,
149}
150
151impl SymbolizeMode {
152    pub fn is_prettification_disabled(&self) -> bool {
153        matches!(self, SymbolizeMode::Classic)
154    }
155
156    pub fn is_symbolize_disabled(&self) -> bool {
157        matches!(self, SymbolizeMode::Off)
158    }
159}
160
161#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
162#[argh(
163    subcommand,
164    name = "log",
165    description = "Display logs from a target device",
166    note = "Logs are retrieve from the target at the moment this command is called.
167
168You may see some additional information attached to the log line:
169
170- `dropped=N`: this means that N logs attributed to the component were dropped when the component
171  wrote to the log socket. This can happen when archivist cannot keep up with the rate of logs being
172  emitted by the component and the component filled the log socket buffer in the kernel.
173
174- `rolled=N`: this means that N logs rolled out from the archivist buffer and ffx never saw them.
175  This can happen when more logs are being ingested by the archivist across all components and the
176  ffx couldn't retrieve them fast enough.
177
178Symbolization is performed in the background using the symbolizer host tool. You can pass
179additional arguments to the symbolizer tool (for example, to add a remote symbol server) using:
180  $ ffx config set proactive_log.symbolize.extra_args \"--symbol-server gs://some-url/path --symbol-server gs://some-other-url/path ...\"
181
182To learn more about configuring the log viewer, visit https://fuchsia.dev/fuchsia-src/development/tools/ffx/commands/log",
183    example = "\
184Dump the most recent logs and stream new ones as they happen:
185  $ ffx log
186
187Stream new logs starting from the current time, filtering for severity of at least \"WARN\":
188  $ ffx log --severity warn --since now
189
190Stream logs where the source moniker, component url and message do not include \"sys\":
191  $ ffx log --exclude sys
192
193Stream ERROR logs with source moniker, component url or message containing either
194\"netstack\" or \"remote-control.cm\", but not containing \"sys\":
195  $ ffx log --severity error --filter netstack --filter remote-control.cm --exclude sys
196
197Dump all available logs where the source moniker, component url, or message contains
198\"remote-control\":
199  $ ffx log --filter remote-control dump
200
201Dump all logs from the last 30 minutes logged before 5 minutes ago:
202  $ ffx log --since \"30m ago\" --until \"5m ago\" dump
203
204Enable DEBUG logs from the \"core/audio\" component while logs are streaming:
205  $ ffx log --set-severity core/audio#DEBUG"
206)]
207pub struct LogCommand {
208    #[argh(subcommand)]
209    pub sub_command: Option<LogSubCommand>,
210
211    /// filter for a string in either the message, component or url.
212    /// May be repeated.
213    #[argh(option)]
214    pub filter: Vec<String>,
215
216    /// DEPRECATED: use --component
217    #[argh(option)]
218    pub moniker: Vec<String>,
219
220    /// fuzzy search for a component by moniker or url.
221    /// May be repeated.
222    #[argh(option)]
223    pub component: Vec<String>,
224
225    /// exclude a string in either the message, component or url.
226    /// May be repeated.
227    #[argh(option)]
228    pub exclude: Vec<String>,
229
230    /// filter for only logs with a given tag. May be repeated.
231    #[argh(option)]
232    pub tag: Vec<String>,
233
234    /// exclude logs with a given tag. May be repeated.
235    #[argh(option)]
236    pub exclude_tags: Vec<String>,
237
238    /// set the minimum severity. Accepted values (from lower to higher) are: trace, debug, info,
239    /// warn (or warning), error, fatal. This field is case insensitive.
240    #[argh(option, default = "Severity::Info")]
241    pub severity: Severity,
242
243    /// outputs only kernel logs, unless combined with --component.
244    #[argh(switch)]
245    pub kernel: bool,
246
247    /// show only logs after a certain time (exclusive)
248    #[argh(option, from_str_fn(parse_time))]
249    pub since: Option<DetailedDateTime>,
250
251    /// show only logs after a certain time (as a boot
252    /// timestamp: seconds from the target's boot time).
253    #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
254    pub since_boot: Option<Duration>,
255
256    /// show only logs until a certain time (exclusive)
257    #[argh(option, from_str_fn(parse_time))]
258    pub until: Option<DetailedDateTime>,
259
260    /// show only logs until a certain time (as a a boot
261    /// timestamp: seconds since the target's boot time).
262    #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
263    pub until_boot: Option<Duration>,
264
265    /// hide the tag field from output (does not exclude any log messages)
266    #[argh(switch)]
267    pub hide_tags: bool,
268
269    /// hide the file and line number field from output (does not exclude any log messages)
270    #[argh(switch)]
271    pub hide_file: bool,
272
273    /// disable coloring logs according to severity.
274    /// Note that you can permanently disable this with
275    /// `ffx config set log_cmd.color false`
276    #[argh(switch)]
277    pub no_color: bool,
278
279    /// if enabled, text filtering options are case-sensitive
280    /// this applies to --filter and --exclude.
281    #[argh(switch)]
282    pub case_sensitive: bool,
283
284    /// shows process-id and thread-id in log output
285    #[argh(switch)]
286    pub show_metadata: bool,
287
288    /// shows the full moniker in log output. By default this is false and only the last segment
289    /// of the moniker is printed.
290    #[argh(switch)]
291    pub show_full_moniker: bool,
292
293    /// how to display log timestamps.
294    /// Options are "utc", "local", or "boot" (i.e. nanos since target boot).
295    /// Default is boot.
296    #[argh(option, default = "TimeFormat::Boot")]
297    pub clock: TimeFormat,
298
299    /// configure symbolization options. Valid options are:
300    /// - pretty (default): pretty concise symbolization
301    /// - off: disables all symbolization
302    /// - classic: traditional, non-prettified symbolization
303    #[cfg(not(target_os = "fuchsia"))]
304    #[argh(option, default = "SymbolizeMode::Pretty")]
305    pub symbolize: SymbolizeMode,
306
307    /// configure the log settings on the target device for components matching
308    /// the given selector. This modifies the minimum log severity level emitted
309    /// by components during the logging session.
310    /// Specify using the format <component-selector>#<log-level>, with level
311    /// as one of FATAL|ERROR|WARN|INFO|DEBUG|TRACE.
312    /// May be repeated and it's also possible to pass multiple comma-separated
313    /// strings per invocation.
314    /// Cannot be used in conjunction with --set-severity-persist.
315    #[argh(option, from_str_fn(log_interest_selector))]
316    pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
317
318    /// filters by pid
319    #[argh(option)]
320    pub pid: Option<u64>,
321
322    /// filters by tid
323    #[argh(option)]
324    pub tid: Option<u64>,
325
326    /// if enabled, selectors will be passed directly to Archivist without any filtering.
327    /// If disabled and no matching components are found, the user will be prompted to
328    /// either enable this or be given a list of selectors to choose from.
329    /// This applies to both --set-severity and --set-severity-persist.
330    #[argh(switch)]
331    pub force_set_severity: bool,
332
333    /// enables structured JSON logs.
334    #[cfg(target_os = "fuchsia")]
335    #[argh(switch)]
336    pub json: bool,
337}
338
339impl Default for LogCommand {
340    fn default() -> Self {
341        LogCommand {
342            filter: vec![],
343            moniker: vec![],
344            component: vec![],
345            exclude: vec![],
346            tag: vec![],
347            exclude_tags: vec![],
348            hide_tags: false,
349            hide_file: false,
350            clock: TimeFormat::Boot,
351            no_color: false,
352            kernel: false,
353            severity: Severity::Info,
354            show_metadata: false,
355            force_set_severity: false,
356            since: None,
357            since_boot: None,
358            until: None,
359            case_sensitive: false,
360            until_boot: None,
361            sub_command: None,
362            set_severity: vec![],
363            show_full_moniker: false,
364            pid: None,
365            tid: None,
366            #[cfg(target_os = "fuchsia")]
367            json: false,
368            #[cfg(not(target_os = "fuchsia"))]
369            symbolize: SymbolizeMode::Pretty,
370        }
371    }
372}
373
374/// Result returned from processing logs
375#[derive(PartialEq, Debug)]
376pub enum LogProcessingResult {
377    /// The caller should exit
378    Exit,
379    /// The caller should continue processing logs
380    Continue,
381}
382
383impl FromStr for SymbolizeMode {
384    type Err = anyhow::Error;
385
386    fn from_str(s: &str) -> Result<Self, Self::Err> {
387        let s = s.to_lowercase();
388        match s.as_str() {
389            "off" => Ok(SymbolizeMode::Off),
390            "pretty" => Ok(SymbolizeMode::Pretty),
391            "classic" => Ok(SymbolizeMode::Classic),
392            other => Err(format_err!("invalid symbolize flag: {}", other)),
393        }
394    }
395}
396
397#[derive(Error, Debug)]
398pub enum LogError {
399    #[error(transparent)]
400    UnknownError(#[from] anyhow::Error),
401    #[error("No boot timestamp")]
402    NoBootTimestamp,
403    #[error(transparent)]
404    IOError(#[from] std::io::Error),
405    #[error("Cannot use dump with --since now")]
406    DumpWithSinceNow,
407    #[error("No symbolizer configuration provided")]
408    NoSymbolizerConfig,
409    #[error(transparent)]
410    FfxError(#[from] FfxError),
411    #[error(transparent)]
412    Utf8Error(#[from] FromUtf8Error),
413    #[error(transparent)]
414    FidlError(#[from] fidl::Error),
415    #[error(transparent)]
416    FormatterError(#[from] FormatterError),
417    #[error("Deprecated flag: `{flag}`, use: `{new_flag}`")]
418    DeprecatedFlag { flag: &'static str, new_flag: &'static str },
419    #[error(
420        "Fuzzy matching failed due to too many matches, please re-try with one of these:\n{0}"
421    )]
422    FuzzyMatchTooManyMatches(String),
423    #[error("No running components were found matching {0}")]
424    SearchParameterNotFound(String),
425}
426
427impl LogError {
428    fn too_many_fuzzy_matches(matches: impl Iterator<Item = String>) -> Self {
429        let mut result = String::new();
430        for component in matches {
431            result.push_str(&component);
432            result.push('\n');
433        }
434
435        Self::FuzzyMatchTooManyMatches(result)
436    }
437}
438
439/// Trait used to get available instances given a moniker query.
440#[async_trait::async_trait(?Send)]
441pub trait InstanceGetter {
442    async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError>;
443}
444
445#[async_trait::async_trait(?Send)]
446impl InstanceGetter for RealmQueryProxy {
447    async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
448        Ok(get_instances_from_query(query, self)
449            .await?
450            .into_iter()
451            .map(|value| value.moniker)
452            .collect())
453    }
454}
455
456impl LogCommand {
457    async fn map_interest_selectors<'a>(
458        realm_query: &impl InstanceGetter,
459        interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
460    ) -> Result<impl Iterator<Item = Cow<'a, LogInterestSelector>>, LogError> {
461        let selectors = Self::get_selectors_and_monikers(interest_selectors);
462        let mut translated_selectors = vec![];
463        for (moniker, selector) in selectors {
464            // Attempt to translate to a single instance
465            let instances = realm_query.get_monikers_from_query(moniker.as_str()).await?;
466            // If exactly one match, perform rewrite
467            if instances.len() == 1 {
468                let mut translated_selector = selector.clone();
469                translated_selector.selector = instances[0].clone().into_component_selector();
470                translated_selectors.push((Cow::Owned(translated_selector), instances));
471            } else {
472                translated_selectors.push((Cow::Borrowed(selector), instances));
473            }
474        }
475        if translated_selectors.iter().any(|(_, matches)| matches.len() > 1) {
476            let mut err_output = vec![];
477            writeln!(
478                &mut err_output,
479                "WARN: One or more of your selectors appears to be ambiguous"
480            )?;
481            writeln!(&mut err_output, "and may not match any components on your system.\n")?;
482            writeln!(
483                &mut err_output,
484                "If this is unintentional you can explicitly match using the"
485            )?;
486            writeln!(&mut err_output, "following command:\n")?;
487            writeln!(&mut err_output, "ffx log \\")?;
488            let mut output = vec![];
489            for (oselector, instances) in translated_selectors {
490                for selector in instances {
491                    writeln!(
492                        output,
493                        "\t--set-severity {}#{} \\",
494                        sanitize_moniker_for_selectors(selector.to_string().as_str())
495                            .replace("\\", "\\\\"),
496                        format!("{:?}", oselector.interest.min_severity.unwrap()).to_uppercase()
497                    )?;
498                }
499            }
500            // Intentionally ignored, removes the newline, space, and \
501            let _ = output.pop();
502            let _ = output.pop();
503            let _ = output.pop();
504
505            writeln!(&mut err_output, "{}", String::from_utf8(output).unwrap())?;
506            writeln!(&mut err_output, "\nIf this is intentional, you can disable this with")?;
507            writeln!(&mut err_output, "ffx log --force-set-severity.")?;
508
509            ffx_bail!("{}", String::from_utf8(err_output)?);
510        }
511        Ok(translated_selectors.into_iter().map(|(selector, _)| selector))
512    }
513
514    pub fn validate_cmd_flags_with_warnings(&mut self) -> Result<Vec<&'static str>, LogError> {
515        let mut warnings = vec![];
516
517        if !self.moniker.is_empty() {
518            warnings.push("WARNING: --moniker is deprecated, use --component instead");
519            if self.component.is_empty() {
520                self.component = std::mem::take(&mut self.moniker);
521            } else {
522                warnings.push("WARNING: ignoring --moniker arguments in favor of --component");
523            }
524        }
525
526        Ok(warnings)
527    }
528
529    /// Sets interest based on configured selectors.
530    /// If a single ambiguous match is found, the monikers in the selectors
531    /// are automatically re-written.
532    pub async fn maybe_set_interest(
533        &self,
534        log_settings_client: &LogSettingsProxy,
535        realm_query: &impl InstanceGetter,
536    ) -> Result<(), LogError> {
537        let (set_severity, force_set_severity, persist) =
538            if let Some(LogSubCommand::SetSeverity(options)) = &self.sub_command {
539                // No other argument can exist in conjunction with SetSeverity
540                let default_cmd = LogCommand {
541                    sub_command: Some(LogSubCommand::SetSeverity(options.clone())),
542                    ..Default::default()
543                };
544                if &default_cmd != self {
545                    ffx_bail!("Cannot combine set-severity with other options.");
546                }
547                (&options.interest_selector, options.force, !options.no_persist)
548            } else {
549                (&self.set_severity, self.force_set_severity, false)
550            };
551        if persist {
552            let selectors = if force_set_severity {
553                set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
554            } else {
555                let new_selectors =
556                    Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
557                        .await?
558                        .map(|s| s.into_owned())
559                        .collect::<Vec<_>>();
560                if new_selectors.is_empty() {
561                    set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
562                } else {
563                    new_selectors
564                }
565            };
566            log_settings_client
567                .set_component_interest(
568                    &fidl_fuchsia_diagnostics::LogSettingsSetComponentInterestRequest {
569                        selectors: Some(selectors),
570                        persist: Some(true),
571                        ..Default::default()
572                    },
573                )
574                .await?;
575        } else if !set_severity.is_empty() {
576            let selectors = if force_set_severity {
577                set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
578            } else {
579                let new_selectors =
580                    Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
581                        .await?
582                        .map(|s| s.into_owned())
583                        .collect::<Vec<_>>();
584                if new_selectors.is_empty() {
585                    set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
586                } else {
587                    new_selectors
588                }
589            };
590
591            log_settings_client.set_interest(&selectors).await?;
592        }
593
594        Ok(())
595    }
596
597    fn get_selectors_and_monikers<'a>(
598        interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
599    ) -> Vec<(String, &'a LogInterestSelector)> {
600        let mut selectors = vec![];
601        for selector in interest_selectors {
602            let segments = selector.selector.moniker_segments.as_ref().unwrap();
603            let mut full_moniker = String::new();
604            for segment in segments {
605                match segment {
606                    fidl_fuchsia_diagnostics::StringSelector::ExactMatch(segment) => {
607                        if full_moniker.is_empty() {
608                            full_moniker.push_str(segment);
609                        } else {
610                            full_moniker.push('/');
611                            full_moniker.push_str(segment);
612                        }
613                    }
614                    _ => {
615                        // If the user passed a non-exact match we assume they
616                        // know what they're doing and skip this logic.
617                        return vec![];
618                    }
619                }
620            }
621            selectors.push((full_moniker, selector));
622        }
623        selectors
624    }
625}
626
627impl TopLevelCommand for LogCommand {}
628
629fn log_interest_selector(s: &str) -> Result<OneOrMany<LogInterestSelector>, String> {
630    if s.contains(",") {
631        let many: Result<Vec<LogInterestSelector>, String> = s
632            .split(",")
633            .map(|value| selectors::parse_log_interest_selector(value).map_err(|e| e.to_string()))
634            .collect();
635        Ok(OneOrMany::Many(many?))
636    } else {
637        Ok(OneOrMany::One(selectors::parse_log_interest_selector(s).map_err(|s| s.to_string())?))
638    }
639}
640
641#[cfg(test)]
642mod test {
643    use super::*;
644    use assert_matches::assert_matches;
645    use async_trait::async_trait;
646    use fidl::endpoints::create_proxy;
647    use fidl_fuchsia_diagnostics::{LogSettingsMarker, LogSettingsRequest};
648    use futures_util::future::Either;
649    use futures_util::stream::FuturesUnordered;
650    use futures_util::StreamExt;
651    use selectors::parse_log_interest_selector;
652
653    #[derive(Default)]
654    struct FakeInstanceGetter {
655        output: Vec<Moniker>,
656        expected_selector: Option<String>,
657    }
658
659    #[async_trait(?Send)]
660    impl InstanceGetter for FakeInstanceGetter {
661        async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
662            if let Some(expected) = &self.expected_selector {
663                assert_eq!(expected, query);
664            }
665            Ok(self.output.clone())
666        }
667    }
668
669    #[fuchsia::test]
670    async fn test_symbolize_mode_from_str() {
671        assert_matches!(SymbolizeMode::from_str("off"), Ok(value) if value == SymbolizeMode::Off);
672        assert_matches!(
673            SymbolizeMode::from_str("pretty"),
674            Ok(value) if value == SymbolizeMode::Pretty
675        );
676        assert_matches!(
677            SymbolizeMode::from_str("classic"),
678            Ok(value) if value == SymbolizeMode::Classic
679        );
680    }
681
682    #[fuchsia::test]
683    async fn maybe_set_interest_errors_additional_arguments_passed_to_set_interest() {
684        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
685        let getter = FakeInstanceGetter {
686            expected_selector: Some("ambiguous_selector".into()),
687            output: vec![
688                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
689                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
690            ],
691        };
692        // Main should return an error
693
694        let cmd = LogCommand {
695            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
696                interest_selector: vec![OneOrMany::One(
697                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
698                )],
699                force: false,
700                no_persist: false,
701            })),
702            hide_file: true,
703            ..LogCommand::default()
704        };
705        let mut set_interest_result = None;
706
707        let mut scheduler = FuturesUnordered::new();
708        scheduler.push(Either::Left(async {
709            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
710            drop(settings_proxy);
711        }));
712        scheduler.push(Either::Right(async {
713            let request = settings_server.into_stream().next().await;
714            // The channel should be closed without sending any requests.
715            assert_matches!(request, None);
716        }));
717        while scheduler.next().await.is_some() {}
718        drop(scheduler);
719
720        let error = format!("{}", set_interest_result.unwrap().unwrap_err());
721
722        const EXPECTED_INTEREST_ERROR: &str = "Cannot combine set-severity with other options.";
723        assert_eq!(error, EXPECTED_INTEREST_ERROR);
724    }
725
726    #[fuchsia::test]
727    async fn maybe_set_interest_errors_if_ambiguous_selector() {
728        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
729        let getter = FakeInstanceGetter {
730            expected_selector: Some("ambiguous_selector".into()),
731            output: vec![
732                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
733                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
734            ],
735        };
736        // Main should return an error
737
738        let cmd = LogCommand {
739            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
740            set_severity: vec![OneOrMany::One(
741                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
742            )],
743            ..LogCommand::default()
744        };
745        let mut set_interest_result = None;
746
747        let mut scheduler = FuturesUnordered::new();
748        scheduler.push(Either::Left(async {
749            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
750            drop(settings_proxy);
751        }));
752        scheduler.push(Either::Right(async {
753            let request = settings_server.into_stream().next().await;
754            // The channel should be closed without sending any requests.
755            assert_matches!(request, None);
756        }));
757        while scheduler.next().await.is_some() {}
758        drop(scheduler);
759
760        let error = format!("{}", set_interest_result.unwrap().unwrap_err());
761
762        const EXPECTED_INTEREST_ERROR: &str = r#"WARN: One or more of your selectors appears to be ambiguous
763and may not match any components on your system.
764
765If this is unintentional you can explicitly match using the
766following command:
767
768ffx log \
769	--set-severity core/some/ambiguous_selector\\:thing/test#INFO \
770	--set-severity core/other/ambiguous_selector\\:thing/test#INFO
771
772If this is intentional, you can disable this with
773ffx log --force-set-severity.
774"#;
775        assert_eq!(error, EXPECTED_INTEREST_ERROR);
776    }
777
778    #[fuchsia::test]
779    async fn logger_translates_selector_if_one_match() {
780        let cmd = LogCommand {
781            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
782            set_severity: vec![OneOrMany::One(
783                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
784            )],
785            ..LogCommand::default()
786        };
787        let mut set_interest_result = None;
788        let getter = FakeInstanceGetter {
789            expected_selector: Some("ambiguous_selector".into()),
790            output: vec![Moniker::try_from("core/some/ambiguous_selector").unwrap()],
791        };
792        let mut scheduler = FuturesUnordered::new();
793        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
794        scheduler.push(Either::Left(async {
795            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
796            drop(settings_proxy);
797        }));
798        scheduler.push(Either::Right(async {
799            let request = settings_server.into_stream().next().await;
800            let (selectors, responder) = assert_matches!(
801                request,
802                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
803                (selectors, responder)
804            );
805            responder.send().unwrap();
806            assert_eq!(
807                selectors,
808                vec![parse_log_interest_selector("core/some/ambiguous_selector#INFO").unwrap()]
809            );
810        }));
811        while scheduler.next().await.is_some() {}
812        drop(scheduler);
813        assert_matches!(set_interest_result, Some(Ok(())));
814    }
815
816    #[fuchsia::test]
817    async fn logger_uses_specified_selectors_if_no_results_returned() {
818        let cmd = LogCommand {
819            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
820            set_severity: vec![OneOrMany::One(
821                parse_log_interest_selector("core/something/a:b/elements:main/otherstuff:*#DEBUG")
822                    .unwrap(),
823            )],
824            ..LogCommand::default()
825        };
826        let mut set_interest_result = None;
827        let getter = FakeInstanceGetter {
828            expected_selector: Some("core/something/a:b/elements:main/otherstuff:*#DEBUG".into()),
829            output: vec![],
830        };
831        let scheduler = FuturesUnordered::new();
832        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
833        scheduler.push(Either::Left(async {
834            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
835            drop(settings_proxy);
836        }));
837        scheduler.push(Either::Right(async {
838            let request = settings_server.into_stream().next().await;
839            let (selectors, responder) = assert_matches!(
840                request,
841                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
842                (selectors, responder)
843            );
844            responder.send().unwrap();
845            assert_eq!(
846                selectors,
847                vec![parse_log_interest_selector(
848                    "core/something/a:b/elements:main/otherstuff:*#DEBUG"
849                )
850                .unwrap()]
851            );
852        }));
853        scheduler.map(|_| Ok(())).forward(futures::sink::drain()).await.unwrap();
854        assert_matches!(set_interest_result, Some(Ok(())));
855    }
856
857    #[fuchsia::test]
858    async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used() {
859        let cmd = LogCommand {
860            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
861                no_persist: true,
862                interest_selector: vec![OneOrMany::One(
863                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
864                )],
865                force: true,
866            })),
867            ..LogCommand::default()
868        };
869        let getter = FakeInstanceGetter {
870            expected_selector: Some("ambiguous_selector".into()),
871            output: vec![
872                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
873                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
874            ],
875        };
876        let mut set_interest_result = None;
877        let mut scheduler = FuturesUnordered::new();
878        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
879        scheduler.push(Either::Left(async {
880            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
881            drop(settings_proxy);
882        }));
883        scheduler.push(Either::Right(async {
884            let request = settings_server.into_stream().next().await;
885            let (selectors, responder) = assert_matches!(
886                request,
887                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
888                (selectors, responder)
889            );
890            responder.send().unwrap();
891            assert_eq!(
892                selectors,
893                vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()]
894            );
895        }));
896        while scheduler.next().await.is_some() {}
897        drop(scheduler);
898        assert_matches!(set_interest_result, Some(Ok(())));
899    }
900
901    #[fuchsia::test]
902    async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used_persistent() {
903        let cmd = LogCommand {
904            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
905                no_persist: false,
906                interest_selector: vec![log_socket_stream::OneOrMany::One(
907                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
908                )],
909                force: true,
910            })),
911            ..LogCommand::default()
912        };
913        let getter = FakeInstanceGetter {
914            expected_selector: Some("ambiguous_selector".into()),
915            output: vec![
916                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
917                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
918            ],
919        };
920        let mut set_interest_result = None;
921        let mut scheduler = FuturesUnordered::new();
922        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
923        scheduler.push(Either::Left(async {
924            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
925            drop(settings_proxy);
926        }));
927        scheduler.push(Either::Right(async {
928            let request = settings_server.into_stream().next().await;
929            let (payload, responder) = assert_matches!(
930                request,
931                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
932                (payload, responder)
933            );
934            assert_eq!(payload.persist, Some(true));
935            responder.send().unwrap();
936            assert_eq!(
937                payload.selectors.unwrap(),
938                vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()]
939            );
940        }));
941        while scheduler.next().await.is_some() {}
942        drop(scheduler);
943        assert_matches!(set_interest_result, Some(Ok(())));
944    }
945
946    #[fuchsia::test]
947    async fn logger_prints_ignores_ambiguity_if_machine_output_is_used() {
948        let cmd = LogCommand {
949            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
950            set_severity: vec![OneOrMany::One(
951                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
952            )],
953            force_set_severity: true,
954            ..LogCommand::default()
955        };
956        let getter = FakeInstanceGetter {
957            expected_selector: Some("ambiguous_selector".into()),
958            output: vec![
959                Moniker::try_from("core/some/collection:thing/test").unwrap(),
960                Moniker::try_from("core/other/collection:thing/test").unwrap(),
961            ],
962        };
963        let mut set_interest_result = None;
964        let mut scheduler = FuturesUnordered::new();
965        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
966        scheduler.push(Either::Left(async {
967            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
968            drop(settings_proxy);
969        }));
970        scheduler.push(Either::Right(async {
971            let request = settings_server.into_stream().next().await;
972            let (selectors, responder) = assert_matches!(
973                request,
974                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
975                (selectors, responder)
976            );
977            responder.send().unwrap();
978            assert_eq!(
979                selectors,
980                vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()]
981            );
982        }));
983        while scheduler.next().await.is_some() {}
984        drop(scheduler);
985        assert_matches!(set_interest_result, Some(Ok(())));
986    }
987    #[test]
988    fn test_parse_selector() {
989        assert_eq!(
990            log_interest_selector("core/audio#DEBUG").unwrap(),
991            OneOrMany::One(parse_log_interest_selector("core/audio#DEBUG").unwrap())
992        );
993    }
994
995    #[test]
996    fn test_parse_selector_with_commas() {
997        assert_eq!(
998            log_interest_selector("core/audio#DEBUG,bootstrap/archivist#TRACE").unwrap(),
999            OneOrMany::Many(vec![
1000                parse_log_interest_selector("core/audio#DEBUG").unwrap(),
1001                parse_log_interest_selector("bootstrap/archivist#TRACE").unwrap()
1002            ])
1003        );
1004    }
1005
1006    #[test]
1007    fn test_parse_time() {
1008        assert!(parse_time("now").unwrap().is_now);
1009        let date_string = "04/20/2020";
1010        let res = parse_time(date_string).unwrap();
1011        assert!(!res.is_now);
1012        assert_eq!(
1013            res.date_naive(),
1014            parse_date_string(date_string, Local::now(), Dialect::Us).unwrap().date_naive()
1015        );
1016    }
1017}