1use anyhow::format_err;
6use argh::{ArgsInfo, FromArgs, TopLevelCommand};
7use chrono::{DateTime, Local, Utc};
8use chrono_english::{Dialect, parse_date_string};
9#[cfg(not(feature = "fdomain"))]
10use component_debug::query::get_instances_from_query;
11#[cfg(feature = "fdomain")]
12use component_debug_fdomain::query::get_instances_from_query;
13use diagnostics_data::Severity;
14use errors::{FfxError, ffx_bail};
15use flex_fuchsia_diagnostics::{LogInterestSelector, LogSettingsProxy};
16use flex_fuchsia_sys2::RealmQueryProxy;
17pub use log_socket_stream::OneOrMany;
18use moniker::Moniker;
19use selectors::{SelectorExt, sanitize_moniker_for_selectors};
20use std::borrow::Cow;
21use std::io::Write;
22use std::ops::Deref;
23use std::str::FromStr;
24use std::string::FromUtf8Error;
25use std::time::Duration;
26use thiserror::Error;
27mod filter;
28#[cfg(not(feature = "fdomain"))]
29mod fxt_streamer;
30mod log_formatter;
31mod log_socket_stream;
32pub use log_formatter::{
33 BootTimeAccessor, DefaultLogFormatter, FormatterError, LogData, LogEntry, Symbolize,
34 TIMESTAMP_FORMAT, Timestamp, WriterContainer, dump_logs_from_socket,
35};
36pub use log_socket_stream::{JsonDeserializeError, LogsDataStream};
37
38#[cfg(not(feature = "fdomain"))]
39pub use log_formatter::dump_fxt_logs_from_socket;
40
41#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
43#[argh(subcommand)]
44pub enum LogSubCommand {
45 Watch(WatchCommand),
46 Dump(DumpCommand),
47 SetSeverity(SetSeverityCommand),
48}
49
50#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug, Default)]
51#[argh(subcommand, name = "set-severity")]
53pub struct SetSeverityCommand {
54 #[argh(switch)]
58 pub no_persist: bool,
59
60 #[argh(switch)]
64 pub force: bool,
65
66 #[argh(positional, from_str_fn(log_interest_selector))]
73 pub interest_selector: Vec<OneOrMany<LogInterestSelector>>,
74}
75
76#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
77#[argh(subcommand, name = "watch")]
79pub struct WatchCommand {}
80
81#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug, Default)]
82#[argh(subcommand, name = "dump")]
84pub struct DumpCommand {
85 #[argh(option)]
87 pub tail: Option<usize>,
88}
89
90pub fn parse_time(value: &str) -> Result<DetailedDateTime, String> {
91 parse_date_string(value, Local::now(), Dialect::Us)
92 .map(|time| DetailedDateTime { time, is_now: value == "now" })
93 .map_err(|e| format!("invalid date string: {e}"))
94}
95
96pub fn parse_utc_time(value: &str) -> Result<DetailedDateTime, String> {
98 parse_date_string(value, Utc::now(), Dialect::Us)
99 .map(|time| DetailedDateTime { time: time.into(), is_now: value == "now" })
100 .map_err(|e| format!("invalid date string: {e}"))
101}
102
103pub fn parse_seconds_string_as_duration(value: &str) -> Result<Duration, String> {
106 Ok(Duration::from_secs(
107 value.parse().map_err(|e| format!("value '{value}' is not a number: {e}"))?,
108 ))
109}
110
111#[derive(Clone, Debug, PartialEq)]
113pub enum TimeFormat {
114 Utc,
116 Local,
118 Boot,
120}
121
122impl std::str::FromStr for TimeFormat {
123 type Err = String;
124
125 fn from_str(s: &str) -> Result<Self, Self::Err> {
126 let lower = s.to_ascii_lowercase();
127 match lower.as_str() {
128 "local" => Ok(TimeFormat::Local),
129 "utc" => Ok(TimeFormat::Utc),
130 "boot" => Ok(TimeFormat::Boot),
131 _ => Err(format!("'{s}' is not a valid value: must be one of 'local', 'utc', 'boot'")),
132 }
133 }
134}
135
136#[derive(Clone, Debug, PartialEq)]
138pub enum LogEncoding {
139 Json,
140 Fxt,
141}
142
143impl std::str::FromStr for LogEncoding {
144 type Err = String;
145
146 fn from_str(s: &str) -> Result<Self, Self::Err> {
147 let lower = s.to_ascii_lowercase();
148 match lower.as_str() {
149 "json" => Ok(LogEncoding::Json),
150 "fxt" => Ok(LogEncoding::Fxt),
151 _ => Err(format!("'{s}' is not a valid value: must be one of 'json', 'fxt'")),
152 }
153 }
154}
155
156#[derive(PartialEq, Clone, Debug)]
160pub struct DetailedDateTime {
161 pub time: DateTime<Local>,
164 pub is_now: bool,
168}
169
170impl Deref for DetailedDateTime {
171 type Target = DateTime<Local>;
172
173 fn deref(&self) -> &Self::Target {
174 &self.time
175 }
176}
177
178#[derive(Clone, PartialEq, Debug)]
179pub enum SymbolizeMode {
180 Off,
182 Pretty,
184 Classic,
186}
187
188impl SymbolizeMode {
189 pub fn is_prettification_disabled(&self) -> bool {
190 matches!(self, SymbolizeMode::Classic)
191 }
192
193 pub fn is_symbolize_disabled(&self) -> bool {
194 matches!(self, SymbolizeMode::Off)
195 }
196}
197
198#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
199#[argh(
200 subcommand,
201 name = "log",
202 description = "Display logs from a target device",
203 note = "Logs are retrieve from the target at the moment this command is called.
204
205You may see some additional information attached to the log line:
206
207- `dropped=N`: this means that N logs attributed to the component were dropped when the component
208 wrote to the log socket. This can happen when archivist cannot keep up with the rate of logs being
209 emitted by the component and the component filled the log socket buffer in the kernel.
210
211- `rolled=N`: this means that N logs rolled out from the archivist buffer and ffx never saw them.
212 This can happen when more logs are being ingested by the archivist across all components and the
213 ffx couldn't retrieve them fast enough.
214
215Symbolization is performed in the background using the symbolizer host tool. You can pass
216additional arguments to the symbolizer tool (for example, to add a remote symbol server) using:
217 $ ffx config set proactive_log.symbolize.extra_args \"--symbol-server gs://some-url/path --symbol-server gs://some-other-url/path ...\"
218
219To learn more about configuring the log viewer, visit https://fuchsia.dev/fuchsia-src/development/tools/ffx/commands/log",
220 example = "\
221Dump the most recent logs and stream new ones as they happen:
222 $ ffx log
223
224Stream new logs starting from the current time, filtering for severity of at least \"WARN\":
225 $ ffx log --severity warn --since now
226
227Stream logs where the source moniker, component url and message do not include \"sys\":
228 $ ffx log --exclude sys
229
230Stream ERROR logs with source moniker, component url or message containing either
231\"netstack\" or \"remote-control.cm\", but not containing \"sys\":
232 $ ffx log --severity error --filter netstack --filter remote-control.cm --exclude sys
233
234Dump all available logs where the source moniker, component url, or message contains
235\"remote-control\":
236 $ ffx log --filter remote-control dump
237
238Dump all logs from the last 30 minutes logged before 5 minutes ago:
239 $ ffx log --since \"30m ago\" --until \"5m ago\" dump
240
241Enable DEBUG logs from the \"core/audio\" component while logs are streaming:
242 $ ffx log --set-severity core/audio#DEBUG"
243)]
244pub struct LogCommand {
245 #[argh(subcommand)]
246 pub sub_command: Option<LogSubCommand>,
247
248 #[argh(switch, hidden_help)]
252 pub dump: bool,
253
254 #[argh(option)]
257 pub filter: Vec<String>,
258
259 #[argh(option)]
261 pub moniker: Vec<String>,
262
263 #[argh(option)]
266 pub component: Vec<String>,
267
268 #[argh(option)]
271 pub exclude: Vec<String>,
272
273 #[argh(option)]
275 pub exclude_regex: Vec<String>,
276
277 #[argh(option)]
279 pub exclude_regex_file: Option<String>,
280
281 #[argh(option)]
283 pub tag: Vec<String>,
284
285 #[argh(option)]
287 pub exclude_tags: Vec<String>,
288
289 #[argh(option, default = "Severity::Info")]
292 pub severity: Severity,
293
294 #[argh(switch)]
296 pub kernel: bool,
297
298 #[argh(option, from_str_fn(parse_time))]
300 pub since: Option<DetailedDateTime>,
301
302 #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
305 pub since_boot: Option<Duration>,
306
307 #[argh(option, from_str_fn(parse_time))]
309 pub until: Option<DetailedDateTime>,
310
311 #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
314 pub until_boot: Option<Duration>,
315
316 #[argh(switch)]
318 pub hide_tags: bool,
319
320 #[argh(switch)]
322 pub hide_file: bool,
323
324 #[argh(switch)]
328 pub no_color: bool,
329
330 #[argh(switch)]
333 pub case_sensitive: bool,
334
335 #[argh(switch)]
337 pub show_metadata: bool,
338
339 #[argh(switch)]
342 pub show_full_moniker: bool,
343
344 #[argh(switch)]
346 pub prefer_url_component_name: bool,
347
348 #[argh(switch)]
350 pub hide_moniker: bool,
351
352 #[argh(option, default = "TimeFormat::Boot")]
356 pub clock: TimeFormat,
357
358 #[cfg(not(target_os = "fuchsia"))]
363 #[argh(option, default = "SymbolizeMode::Pretty")]
364 pub symbolize: SymbolizeMode,
365
366 #[argh(option, from_str_fn(log_interest_selector))]
375 pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
376
377 #[argh(option)]
379 pub pid: Option<u64>,
380
381 #[argh(option)]
383 pub tid: Option<u64>,
384
385 #[argh(switch)]
390 pub force_set_severity: bool,
391
392 #[cfg(target_os = "fuchsia")]
395 #[argh(option, default = "LogEncoding::Json")]
396 pub encoding: LogEncoding,
397
398 #[cfg(target_os = "fuchsia")]
400 #[argh(switch)]
401 pub json: bool,
402
403 #[cfg(not(target_os = "fuchsia"))]
405 #[argh(switch)]
406 pub disable_reconnect: bool,
407}
408
409impl Default for LogCommand {
410 fn default() -> Self {
411 LogCommand {
412 filter: vec![],
413 moniker: vec![],
414 component: vec![],
415 exclude: vec![],
416 exclude_regex: vec![],
417 exclude_regex_file: None,
418 tag: vec![],
419 exclude_tags: vec![],
420 hide_tags: false,
421 hide_file: false,
422 clock: TimeFormat::Boot,
423 no_color: false,
424 kernel: false,
425 severity: Severity::Info,
426 show_metadata: false,
427 force_set_severity: false,
428 since: None,
429 since_boot: None,
430 until: None,
431 case_sensitive: false,
432 until_boot: None,
433 sub_command: None,
434 dump: false,
435 set_severity: vec![],
436 show_full_moniker: false,
437 prefer_url_component_name: false,
438 hide_moniker: false,
439 pid: None,
440 tid: None,
441 #[cfg(target_os = "fuchsia")]
442 encoding: LogEncoding::Json,
443 #[cfg(target_os = "fuchsia")]
444 json: false,
445 #[cfg(not(target_os = "fuchsia"))]
446 disable_reconnect: false,
447 #[cfg(not(target_os = "fuchsia"))]
448 symbolize: SymbolizeMode::Pretty,
449 }
450 }
451}
452
453#[derive(PartialEq, Debug)]
455pub enum LogProcessingResult {
456 Exit,
458 Continue,
460}
461
462impl FromStr for SymbolizeMode {
463 type Err = anyhow::Error;
464
465 fn from_str(s: &str) -> Result<Self, Self::Err> {
466 let s = s.to_lowercase();
467 match s.as_str() {
468 "off" => Ok(SymbolizeMode::Off),
469 "pretty" => Ok(SymbolizeMode::Pretty),
470 "classic" => Ok(SymbolizeMode::Classic),
471 other => Err(format_err!("invalid symbolize flag: {}", other)),
472 }
473 }
474}
475
476#[derive(Error, Debug)]
477pub enum LogError {
478 #[error(transparent)]
479 UnknownError(#[from] anyhow::Error),
480 #[error("No boot timestamp")]
481 NoBootTimestamp,
482 #[error(transparent)]
483 IOError(#[from] std::io::Error),
484 #[error(transparent)]
485 RegexError(#[from] regex_lite::Error),
486 #[error("Cannot use dump with --since now")]
487 DumpWithSinceNow,
488 #[error("No symbolizer configuration provided")]
489 NoSymbolizerConfig,
490 #[error(transparent)]
491 FfxError(#[from] FfxError),
492 #[error(transparent)]
493 Utf8Error(#[from] FromUtf8Error),
494 #[error(transparent)]
495 FidlError(#[from] fidl::Error),
496 #[error(transparent)]
497 FormatterError(#[from] FormatterError),
498 #[error("Deprecated flag: `{flag}`, use: `{new_flag}`")]
499 DeprecatedFlag { flag: &'static str, new_flag: &'static str },
500 #[error("Fuzzy matching failed due to too many matches, please re-try with one of these:\n{0}")]
501 FuzzyMatchTooManyMatches(String),
502 #[error(
503 "No running components were found matching {0}. Please ensure the component is running and the moniker is correct. Run 'ffx component list' to see running components."
504 )]
505 SearchParameterNotFound(String),
506}
507
508impl LogError {
509 fn too_many_fuzzy_matches(matches: impl Iterator<Item = String>) -> Self {
510 let mut result = String::new();
511 for component in matches {
512 result.push_str(&component);
513 result.push('\n');
514 }
515
516 Self::FuzzyMatchTooManyMatches(result)
517 }
518
519 pub fn is_broken_pipe(&self) -> bool {
520 match self {
521 LogError::IOError(error) => error.kind() == std::io::ErrorKind::BrokenPipe,
522 LogError::FormatterError(formatter_error) => formatter_error.is_broken_pipe(),
523 LogError::UnknownError(err) => {
524 if let Some(writer_err) = err.downcast_ref::<writer::Error>() {
525 writer_err.is_broken_pipe()
526 } else if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
527 io_err.kind() == std::io::ErrorKind::BrokenPipe
528 } else {
529 false
530 }
531 }
532
533 LogError::NoBootTimestamp
534 | LogError::DumpWithSinceNow
535 | LogError::NoSymbolizerConfig
536 | LogError::RegexError(_)
537 | LogError::FfxError(_)
538 | LogError::Utf8Error(_)
539 | LogError::FidlError(_)
540 | LogError::DeprecatedFlag { .. }
541 | LogError::FuzzyMatchTooManyMatches(_)
542 | LogError::SearchParameterNotFound(_) => false,
543 }
544 }
545}
546
547#[async_trait::async_trait(?Send)]
549pub trait InstanceGetter {
550 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError>;
551}
552
553#[async_trait::async_trait(?Send)]
554impl InstanceGetter for RealmQueryProxy {
555 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
556 Ok(get_instances_from_query(query, self)
557 .await?
558 .into_iter()
559 .map(|value| value.moniker)
560 .collect())
561 }
562}
563
564impl LogCommand {
565 async fn map_interest_selectors<'a>(
566 realm_query: &impl InstanceGetter,
567 interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
568 ) -> Result<impl Iterator<Item = Cow<'a, LogInterestSelector>>, LogError> {
569 let selectors = Self::get_selectors_and_monikers(interest_selectors);
570 let mut translated_selectors = vec![];
571 for (moniker, selector) in selectors {
572 let instances = realm_query.get_monikers_from_query(moniker.as_str()).await?;
574 if instances.len() == 1 {
576 let mut translated_selector = selector.clone();
577 translated_selector.selector = instances[0].clone().into_component_selector();
578 translated_selectors.push((Cow::Owned(translated_selector), instances));
579 } else {
580 translated_selectors.push((Cow::Borrowed(selector), instances));
581 }
582 }
583 if translated_selectors.iter().any(|(_, matches)| matches.len() > 1) {
584 let mut err_output = vec![];
585 writeln!(
586 &mut err_output,
587 "WARN: One or more of your selectors appears to be ambiguous"
588 )?;
589 writeln!(&mut err_output, "and may not match any components on your system.\n")?;
590 writeln!(
591 &mut err_output,
592 "If this is unintentional you can explicitly match using the"
593 )?;
594 writeln!(&mut err_output, "following command:\n")?;
595 writeln!(&mut err_output, "ffx log \\")?;
596 let mut output = vec![];
597 for (oselector, instances) in translated_selectors {
598 for selector in instances {
599 writeln!(
600 output,
601 "\t--set-severity {}#{} \\",
602 sanitize_moniker_for_selectors(selector.to_string().as_str())
603 .replace("\\", "\\\\"),
604 format!("{:?}", oselector.interest.min_severity.unwrap()).to_uppercase()
605 )?;
606 }
607 }
608 let _ = output.pop();
610 let _ = output.pop();
611 let _ = output.pop();
612
613 writeln!(&mut err_output, "{}", String::from_utf8(output).unwrap())?;
614 writeln!(&mut err_output, "\nIf this is intentional, you can disable this with")?;
615 writeln!(&mut err_output, "ffx log --force-set-severity.")?;
616
617 ffx_bail!("{}", String::from_utf8(err_output)?);
618 }
619 Ok(translated_selectors.into_iter().map(|(selector, _)| selector))
620 }
621
622 pub fn validate_cmd_flags_with_warnings(&mut self) -> Result<Vec<&'static str>, LogError> {
623 let mut warnings = vec![];
624
625 if !self.moniker.is_empty() {
626 warnings.push("WARNING: --moniker is deprecated, use --component instead");
627 if self.component.is_empty() {
628 self.component = std::mem::take(&mut self.moniker);
629 } else {
630 warnings.push("WARNING: ignoring --moniker arguments in favor of --component");
631 }
632 }
633
634 Ok(warnings)
635 }
636
637 pub async fn maybe_set_interest(
641 &self,
642 log_settings_client: &LogSettingsProxy,
643 realm_query: &impl InstanceGetter,
644 ) -> Result<(), LogError> {
645 let (set_severity, force_set_severity, persist) =
646 if let Some(LogSubCommand::SetSeverity(options)) = &self.sub_command {
647 let default_cmd = LogCommand {
649 sub_command: Some(LogSubCommand::SetSeverity(options.clone())),
650 ..Default::default()
651 };
652 if &default_cmd != self {
653 ffx_bail!("Cannot combine set-severity with other options.");
654 }
655 (&options.interest_selector, options.force, !options.no_persist)
656 } else {
657 (&self.set_severity, self.force_set_severity, false)
658 };
659
660 if persist || !set_severity.is_empty() {
661 let selectors = if force_set_severity {
662 set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
663 } else {
664 let new_selectors =
665 Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
666 .await?
667 .map(|s| s.into_owned())
668 .collect::<Vec<_>>();
669 if new_selectors.is_empty() {
670 set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
671 } else {
672 new_selectors
673 }
674 };
675 log_settings_client
676 .set_component_interest(
677 &flex_fuchsia_diagnostics::LogSettingsSetComponentInterestRequest {
678 selectors: Some(selectors),
679 persist: Some(persist),
680 ..Default::default()
681 },
682 )
683 .await?;
684 }
685
686 Ok(())
687 }
688
689 fn get_selectors_and_monikers<'a>(
690 interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
691 ) -> Vec<(String, &'a LogInterestSelector)> {
692 let mut selectors = vec![];
693 for selector in interest_selectors {
694 let segments = selector.selector.moniker_segments.as_ref().unwrap();
695 let mut full_moniker = String::new();
696 for segment in segments {
697 match segment {
698 flex_fuchsia_diagnostics::StringSelector::ExactMatch(segment) => {
699 if full_moniker.is_empty() {
700 full_moniker.push_str(segment);
701 } else {
702 full_moniker.push('/');
703 full_moniker.push_str(segment);
704 }
705 }
706 _ => {
707 return vec![];
710 }
711 }
712 }
713 selectors.push((full_moniker, selector));
714 }
715 selectors
716 }
717}
718
719impl TopLevelCommand for LogCommand {}
720
721fn log_interest_selector(s: &str) -> Result<OneOrMany<LogInterestSelector>, String> {
722 if s.contains(",") {
723 let many: Result<Vec<LogInterestSelector>, String> = s
724 .split(",")
725 .map(|value| selectors::parse_log_interest_selector(value).map_err(|e| e.to_string()))
726 .collect();
727 Ok(OneOrMany::Many(many?))
728 } else {
729 Ok(OneOrMany::One(selectors::parse_log_interest_selector(s).map_err(|s| s.to_string())?))
730 }
731}
732
733#[cfg(test)]
734mod test {
735 use super::*;
736 use assert_matches::assert_matches;
737 use async_trait::async_trait;
738 use fidl::endpoints::create_proxy;
739 use flex_fuchsia_diagnostics::{LogSettingsMarker, LogSettingsRequest};
740 use futures_util::StreamExt;
741 use futures_util::future::Either;
742 use futures_util::stream::FuturesUnordered;
743 use selectors::parse_log_interest_selector;
744
745 #[derive(Default)]
746 struct FakeInstanceGetter {
747 output: Vec<Moniker>,
748 expected_selector: Option<String>,
749 }
750
751 #[async_trait(?Send)]
752 impl InstanceGetter for FakeInstanceGetter {
753 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
754 if let Some(expected) = &self.expected_selector {
755 assert_eq!(expected, query);
756 }
757 Ok(self.output.clone())
758 }
759 }
760
761 #[fuchsia::test]
762 async fn test_symbolize_mode_from_str() {
763 assert_matches!(SymbolizeMode::from_str("off"), Ok(value) if value == SymbolizeMode::Off);
764 assert_matches!(
765 SymbolizeMode::from_str("pretty"),
766 Ok(value) if value == SymbolizeMode::Pretty
767 );
768 assert_matches!(
769 SymbolizeMode::from_str("classic"),
770 Ok(value) if value == SymbolizeMode::Classic
771 );
772 }
773
774 #[fuchsia::test]
775 async fn maybe_set_interest_errors_additional_arguments_passed_to_set_interest() {
776 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
777 let getter = FakeInstanceGetter {
778 expected_selector: Some("ambiguous_selector".into()),
779 output: vec![
780 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
781 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
782 ],
783 };
784 let cmd = LogCommand {
787 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
788 interest_selector: vec![OneOrMany::One(
789 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
790 )],
791 force: false,
792 no_persist: false,
793 })),
794 hide_file: true,
795 ..LogCommand::default()
796 };
797 let mut set_interest_result = None;
798
799 let mut scheduler = FuturesUnordered::new();
800 scheduler.push(Either::Left(async {
801 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
802 drop(settings_proxy);
803 }));
804 scheduler.push(Either::Right(async {
805 let request = settings_server.into_stream().next().await;
806 assert_matches!(request, None);
808 }));
809 while scheduler.next().await.is_some() {}
810 drop(scheduler);
811
812 let error = format!("{}", set_interest_result.unwrap().unwrap_err());
813
814 const EXPECTED_INTEREST_ERROR: &str = "Cannot combine set-severity with other options.";
815 assert_eq!(error, EXPECTED_INTEREST_ERROR);
816 }
817
818 #[fuchsia::test]
819 async fn maybe_set_interest_errors_if_ambiguous_selector() {
820 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
821 let getter = FakeInstanceGetter {
822 expected_selector: Some("ambiguous_selector".into()),
823 output: vec![
824 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
825 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
826 ],
827 };
828 let cmd = LogCommand {
831 sub_command: Some(LogSubCommand::Dump(DumpCommand::default())),
832 set_severity: vec![OneOrMany::One(
833 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
834 )],
835 ..LogCommand::default()
836 };
837 let mut set_interest_result = None;
838
839 let mut scheduler = FuturesUnordered::new();
840 scheduler.push(Either::Left(async {
841 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
842 drop(settings_proxy);
843 }));
844 scheduler.push(Either::Right(async {
845 let request = settings_server.into_stream().next().await;
846 assert_matches!(request, None);
848 }));
849 while scheduler.next().await.is_some() {}
850 drop(scheduler);
851
852 let error = format!("{}", set_interest_result.unwrap().unwrap_err());
853
854 const EXPECTED_INTEREST_ERROR: &str = r#"WARN: One or more of your selectors appears to be ambiguous
855and may not match any components on your system.
856
857If this is unintentional you can explicitly match using the
858following command:
859
860ffx log \
861 --set-severity core/some/ambiguous_selector\\:thing/test#INFO \
862 --set-severity core/other/ambiguous_selector\\:thing/test#INFO
863
864If this is intentional, you can disable this with
865ffx log --force-set-severity.
866"#;
867 assert_eq!(error, EXPECTED_INTEREST_ERROR);
868 }
869
870 #[fuchsia::test]
871 async fn logger_translates_selector_if_one_match() {
872 let cmd = LogCommand {
873 sub_command: Some(LogSubCommand::Dump(DumpCommand::default())),
874 set_severity: vec![OneOrMany::One(
875 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
876 )],
877 ..LogCommand::default()
878 };
879 let mut set_interest_result = None;
880 let getter = FakeInstanceGetter {
881 expected_selector: Some("ambiguous_selector".into()),
882 output: vec![Moniker::try_from("core/some/ambiguous_selector").unwrap()],
883 };
884 let mut scheduler = FuturesUnordered::new();
885 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
886 scheduler.push(Either::Left(async {
887 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
888 drop(settings_proxy);
889 }));
890 scheduler.push(Either::Right(async {
891 let request = settings_server.into_stream().next().await;
892 let (payload, responder) = assert_matches!(
893 request,
894 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
895 (payload, responder)
896 );
897 responder.send().unwrap();
898 assert_eq!(
899 payload.selectors,
900 Some(vec![
901 parse_log_interest_selector("core/some/ambiguous_selector#INFO").unwrap()
902 ])
903 );
904 }));
905 while scheduler.next().await.is_some() {}
906 drop(scheduler);
907 assert_matches!(set_interest_result, Some(Ok(())));
908 }
909
910 #[fuchsia::test]
911 async fn logger_uses_specified_selectors_if_no_results_returned() {
912 let cmd = LogCommand {
913 sub_command: Some(LogSubCommand::Dump(DumpCommand::default())),
914 set_severity: vec![OneOrMany::One(
915 parse_log_interest_selector("core/something/a:b/elements:main/otherstuff:*#DEBUG")
916 .unwrap(),
917 )],
918 ..LogCommand::default()
919 };
920 let mut set_interest_result = None;
921 let getter = FakeInstanceGetter {
922 expected_selector: Some("core/something/a:b/elements:main/otherstuff:*#DEBUG".into()),
923 output: vec![],
924 };
925 let scheduler = FuturesUnordered::new();
926 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
927 scheduler.push(Either::Left(async {
928 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
929 drop(settings_proxy);
930 }));
931 scheduler.push(Either::Right(async {
932 let request = settings_server.into_stream().next().await;
933 let (payload, responder) = assert_matches!(
934 request,
935 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
936 (payload, responder)
937 );
938 responder.send().unwrap();
939 assert_eq!(
940 payload.selectors,
941 Some(vec![
942 parse_log_interest_selector(
943 "core/something/a:b/elements:main/otherstuff:*#DEBUG"
944 )
945 .unwrap()
946 ])
947 );
948 }));
949 scheduler.map(|_| Ok(())).forward(futures::sink::drain()).await.unwrap();
950 assert_matches!(set_interest_result, Some(Ok(())));
951 }
952
953 #[fuchsia::test]
954 async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used() {
955 let cmd = LogCommand {
956 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
957 no_persist: true,
958 interest_selector: vec![OneOrMany::One(
959 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
960 )],
961 force: true,
962 })),
963 ..LogCommand::default()
964 };
965 let getter = FakeInstanceGetter {
966 expected_selector: Some("ambiguous_selector".into()),
967 output: vec![
968 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
969 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
970 ],
971 };
972 let mut set_interest_result = None;
973 let mut scheduler = FuturesUnordered::new();
974 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
975 scheduler.push(Either::Left(async {
976 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
977 drop(settings_proxy);
978 }));
979 scheduler.push(Either::Right(async {
980 let request = settings_server.into_stream().next().await;
981 let (payload, responder) = assert_matches!(
982 request,
983 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
984 (payload, responder)
985 );
986 responder.send().unwrap();
987 assert_eq!(
988 payload.selectors,
989 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
990 );
991 }));
992 while scheduler.next().await.is_some() {}
993 drop(scheduler);
994 assert_matches!(set_interest_result, Some(Ok(())));
995 }
996
997 #[fuchsia::test]
998 async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used_persistent() {
999 let cmd = LogCommand {
1000 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1001 no_persist: false,
1002 interest_selector: vec![log_socket_stream::OneOrMany::One(
1003 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1004 )],
1005 force: true,
1006 })),
1007 ..LogCommand::default()
1008 };
1009 let getter = FakeInstanceGetter {
1010 expected_selector: Some("ambiguous_selector".into()),
1011 output: vec![
1012 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1013 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1014 ],
1015 };
1016 let mut set_interest_result = None;
1017 let mut scheduler = FuturesUnordered::new();
1018 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1019 scheduler.push(Either::Left(async {
1020 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1021 drop(settings_proxy);
1022 }));
1023 scheduler.push(Either::Right(async {
1024 let request = settings_server.into_stream().next().await;
1025 let (payload, responder) = assert_matches!(
1026 request,
1027 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1028 (payload, responder)
1029 );
1030 responder.send().unwrap();
1031 assert_eq!(
1032 payload.selectors,
1033 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1034 );
1035 assert_eq!(payload.persist, Some(true));
1036 }));
1037 while scheduler.next().await.is_some() {}
1038 drop(scheduler);
1039 assert_matches!(set_interest_result, Some(Ok(())));
1040 }
1041
1042 #[fuchsia::test]
1043 async fn logger_prints_ignores_ambiguity_if_machine_output_is_used() {
1044 let cmd = LogCommand {
1045 sub_command: Some(LogSubCommand::Dump(DumpCommand::default())),
1046 set_severity: vec![OneOrMany::One(
1047 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1048 )],
1049 force_set_severity: true,
1050 ..LogCommand::default()
1051 };
1052 let getter = FakeInstanceGetter {
1053 expected_selector: Some("ambiguous_selector".into()),
1054 output: vec![
1055 Moniker::try_from("core/some/collection:thing/test").unwrap(),
1056 Moniker::try_from("core/other/collection:thing/test").unwrap(),
1057 ],
1058 };
1059 let mut set_interest_result = None;
1060 let mut scheduler = FuturesUnordered::new();
1061 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1062 scheduler.push(Either::Left(async {
1063 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1064 drop(settings_proxy);
1065 }));
1066 scheduler.push(Either::Right(async {
1067 let request = settings_server.into_stream().next().await;
1068 let (payload, responder) = assert_matches!(
1069 request,
1070 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1071 (payload, responder)
1072 );
1073 responder.send().unwrap();
1074 assert_eq!(
1075 payload.selectors,
1076 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1077 );
1078 }));
1079 while scheduler.next().await.is_some() {}
1080 drop(scheduler);
1081 assert_matches!(set_interest_result, Some(Ok(())));
1082 }
1083 #[test]
1084 fn test_parse_selector() {
1085 assert_eq!(
1086 log_interest_selector("core/audio#DEBUG").unwrap(),
1087 OneOrMany::One(parse_log_interest_selector("core/audio#DEBUG").unwrap())
1088 );
1089 }
1090
1091 #[test]
1092 fn test_parse_selector_with_commas() {
1093 assert_eq!(
1094 log_interest_selector("core/audio#DEBUG,bootstrap/archivist#TRACE").unwrap(),
1095 OneOrMany::Many(vec![
1096 parse_log_interest_selector("core/audio#DEBUG").unwrap(),
1097 parse_log_interest_selector("bootstrap/archivist#TRACE").unwrap()
1098 ])
1099 );
1100 }
1101
1102 #[test]
1103 fn test_parse_time() {
1104 assert!(parse_time("now").unwrap().is_now);
1105 let date_string = "04/20/2020";
1106 let res = parse_time(date_string).unwrap();
1107 assert!(!res.is_now);
1108 assert_eq!(
1109 res.date_naive(),
1110 parse_date_string(date_string, Local::now(), Dialect::Us).unwrap().date_naive()
1111 );
1112 }
1113
1114 #[test]
1115 fn test_log_error_is_broken_pipe() {
1116 assert!(
1117 LogError::IOError(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1118 .is_broken_pipe()
1119 );
1120 assert!(
1121 LogError::UnknownError(anyhow::Error::new(std::io::Error::new(
1122 std::io::ErrorKind::BrokenPipe,
1123 "broken pipe"
1124 )))
1125 .is_broken_pipe()
1126 );
1127 assert!(!LogError::IOError(std::io::Error::other("other")).is_broken_pipe());
1128 assert!(!LogError::NoBootTimestamp.is_broken_pipe());
1129 }
1130}