1use anyhow::format_err;
6use argh::{ArgsInfo, FromArgs, TopLevelCommand};
7use chrono::{DateTime, Local, Utc};
8use chrono_english::{Dialect, parse_date_string};
9#[cfg(not(any(feature = "fdomain", feature = "ctf")))]
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;
28pub mod fxt_streamer;
29mod log_formatter;
30mod log_socket_stream;
31pub use filter::{FuzzyMatchWarning, LogFilterCriteria, disambiguate_queried_monikers};
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
38pub use log_formatter::dump_fxt_logs_from_socket;
39
40#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
42#[argh(subcommand)]
43pub enum LogSubCommand {
44 Watch(RawWatchCommand),
45 Dump(RawDumpCommand),
46 SetSeverity(SetSeverityCommand),
47}
48
49#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug, Default)]
50#[argh(subcommand, name = "set-severity")]
52pub struct SetSeverityCommand {
53 #[argh(switch)]
57 pub no_persist: bool,
58
59 #[argh(switch)]
63 pub force: bool,
64
65 #[argh(positional, from_str_fn(log_interest_selector))]
72 pub interest_selector: Vec<OneOrMany<LogInterestSelector>>,
73}
74
75pub fn parse_time(value: &str) -> Result<DetailedDateTime, String> {
76 parse_date_string(value, Local::now(), Dialect::Us)
77 .map(|time| DetailedDateTime { time, is_now: value == "now" })
78 .map_err(|e| format!("invalid date string: {e}"))
79}
80
81pub fn parse_utc_time(value: &str) -> Result<DetailedDateTime, String> {
83 parse_date_string(value, Utc::now(), Dialect::Us)
84 .map(|time| DetailedDateTime { time: time.into(), is_now: value == "now" })
85 .map_err(|e| format!("invalid date string: {e}"))
86}
87
88pub fn parse_seconds_string_as_duration(value: &str) -> Result<Duration, String> {
91 Ok(Duration::from_secs(
92 value.parse().map_err(|e| format!("value '{value}' is not a number: {e}"))?,
93 ))
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub enum TimeFormat {
99 Utc,
101 Local,
103 Boot,
105}
106
107impl std::str::FromStr for TimeFormat {
108 type Err = String;
109
110 fn from_str(s: &str) -> Result<Self, Self::Err> {
111 let lower = s.to_ascii_lowercase();
112 match lower.as_str() {
113 "local" => Ok(TimeFormat::Local),
114 "utc" => Ok(TimeFormat::Utc),
115 "boot" => Ok(TimeFormat::Boot),
116 _ => Err(format!("'{s}' is not a valid value: must be one of 'local', 'utc', 'boot'")),
117 }
118 }
119}
120
121#[derive(Clone, Debug, PartialEq)]
123pub enum LogEncoding {
124 Json,
125 Fxt,
126}
127
128impl std::str::FromStr for LogEncoding {
129 type Err = String;
130
131 fn from_str(s: &str) -> Result<Self, Self::Err> {
132 let lower = s.to_ascii_lowercase();
133 match lower.as_str() {
134 "json" => Ok(LogEncoding::Json),
135 "fxt" => Ok(LogEncoding::Fxt),
136 _ => Err(format!("'{s}' is not a valid value: must be one of 'json', 'fxt'")),
137 }
138 }
139}
140
141#[derive(PartialEq, Clone, Debug)]
145pub struct DetailedDateTime {
146 pub time: DateTime<Local>,
149 pub is_now: bool,
153}
154
155impl Deref for DetailedDateTime {
156 type Target = DateTime<Local>;
157
158 fn deref(&self) -> &Self::Target {
159 &self.time
160 }
161}
162
163#[derive(Clone, PartialEq, Debug)]
164pub enum SymbolizeMode {
165 Off,
167 Pretty,
169 Classic,
171}
172
173impl SymbolizeMode {
174 pub fn is_prettification_disabled(&self) -> bool {
175 matches!(self, SymbolizeMode::Classic)
176 }
177
178 pub fn is_symbolize_disabled(&self) -> bool {
179 matches!(self, SymbolizeMode::Off)
180 }
181}
182
183#[doc(hidden)]
185macro_rules! overlay_field {
186 (Vec, $self:ident, $child:ident, $field:ident) => {
187 $self.$field.extend($child.$field.into_iter());
188 };
189 (Option, $self:ident, $child:ident, $field:ident) => {
190 if $child.$field.is_some() {
191 $self.$field = $child.$field;
192 }
193 };
194 (bool, $self:ident, $child:ident, $field:ident) => {
195 $self.$field |= $child.$field;
196 };
197}
198
199#[doc(hidden)]
215macro_rules! __define_log_filter_args_helper {
216 (
217 [ $(#[$attr:meta])* ]
218 [ $(#[$cfg:meta])* ]
219 [ $($fields:tt)* ]
220 #[cfg $($cfg_args:tt)*]
221 $($rest:tt)*
222 ) => {
223 __define_log_filter_args_helper! {
224 [ $(#[$attr])* #[cfg $($cfg_args)*] ]
225 [ $(#[$cfg])* #[cfg $($cfg_args)*] ]
226 [ $($fields)* ]
227 $($rest)*
228 }
229 };
230
231 (
232 [ $(#[$attr:meta])* ]
233 [ $(#[$cfg:meta])* ]
234 [ $($fields:tt)* ]
235 #[$other_attr:meta]
236 $($rest:tt)*
237 ) => {
238 __define_log_filter_args_helper! {
239 [ $(#[$attr])* #[$other_attr] ]
240 [ $(#[$cfg])* ]
241 [ $($fields)* ]
242 $($rest)*
243 }
244 };
245
246 (
247 [ $(#[$attr:meta])* ]
248 [ $(#[$cfg:meta])* ]
249 [ $($fields:tt)* ]
250 $vis:vis $field:ident : $ty_outer:ident $( < $ty_inner:ty > )? $(, $($rest:tt)*)?
251 ) => {
252 __define_log_filter_args_helper! {
253 [ ]
254 [ ]
255 [
256 $($fields)*
257 (
258 [ $(#[$attr])* ]
259 [ $(#[$cfg])* ]
260 $vis $field : $ty_outer $( < $ty_inner > )?
261 )
262 ]
263 $($($rest)*)?
264 }
265 };
266
267 (
268 [ ]
269 [ ]
270 [
271 $(
272 (
273 [ $(#[$all_attr:meta])* ]
274 [ $(#[$cfg_attr:meta])* ]
275 $vis:vis $field:ident : $ty_outer:ident $( < $ty_inner:ty > )?
276 )
277 )*
278 ]
279 ) => {
280 #[derive(Clone, Debug, PartialEq)]
282 pub struct LogFilterArgs {
283 $(
284 $(#[$cfg_attr])*
285 $vis $field: $ty_outer $( < $ty_inner > )?,
286 )*
287 }
288
289 impl Default for LogFilterArgs {
290 fn default() -> Self {
291 LogFilterArgs {
292 $(
293 $(#[$cfg_attr])*
294 $field: Default::default(),
295 )*
296 }
297 }
298 }
299
300 impl LogFilterArgs {
301 pub fn merge(&mut self, other: LogFilterArgs) {
303 $(
304 $(#[$cfg_attr])*
305 overlay_field!($ty_outer, self, other, $field);
306 )*
307 }
308 }
309
310 #[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
311 #[argh(
313 subcommand,
314 name = "log",
315 description = "Display logs from a target device",
316 note = "Logs are retrieved from the target at the moment this command is called.\n\nYou may see some additional information attached to the log line:\n\n- `dropped=N`: this means that N logs attributed to the component were dropped when the component\n wrote to the log socket. This can happen when archivist cannot keep up with the rate of logs being\n emitted by the component and the component filled the log socket buffer in the kernel.\n\n- `rolled=N`: this means that N logs rolled out from the archivist buffer and ffx never saw them.\n This can happen when more logs are being ingested by the archivist across all components and the\n ffx couldn't retrieve them fast enough.\n\nSymbolization is performed in the background using the symbolizer host tool. You can pass\nadditional arguments to the symbolizer tool (for example, to add a remote symbol server) using:\n $ ffx config set proactive_log.symbolize.extra_args \"--symbol-server gs://some-url/path --symbol-server gs://some-other-url/path ...\"\n\nTo learn more about configuring the log viewer, visit https://fuchsia.dev/fuchsia-src/development/tools/ffx/commands/log",
317 example = "Dump the most recent logs and stream new ones as they happen:\n $ ffx log\n\nStream new logs starting from the current time, filtering for severity of at least \"WARN\":\n $ ffx log --severity warn --since now\n\nStream logs where the source moniker, component url and message do not include \"sys\":\n $ ffx log --exclude sys\n\nStream ERROR logs with source moniker, component url or message containing either\n\"netstack\" or \"remote-control.cm\", but not containing \"sys\":\n $ ffx log --severity error --filter netstack --filter remote-control.cm --exclude sys\n\nDump all available logs where the source moniker, component url, or message contains\n\"remote-control\":\n $ ffx log --filter remote-control dump\n\nDump all logs from the last 30 minutes logged before 5 minutes ago:\n $ ffx log --since \"30m ago\" --until \"5m ago\" dump\n\nEnable DEBUG logs from the \"core/audio\" component while logs are streaming:\n $ ffx log --set-severity core/audio#DEBUG"
318 )]
319 pub struct RawLogCommand {
320 #[argh(subcommand)]
321 pub sub_command: Option<LogSubCommand>,
322
323 #[argh(option, from_str_fn(log_interest_selector))]
332 pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
333
334 $(
335 $(#[$all_attr])*
336 $vis $field: $ty_outer $( < $ty_inner > )?,
337 )*
338 }
339
340 impl RawLogCommand {
341 pub fn into_log_command(self) -> LogCommand {
342 LogCommand {
343 sub_command: self.sub_command,
344 set_severity: self.set_severity,
345 filters: LogFilterArgs {
346 $(
347 $(#[$cfg_attr])*
348 $field: self.$field,
349 )*
350 },
351 }
352 }
353 }
354
355 #[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
356 #[argh(subcommand, name = "dump")]
358 pub struct RawDumpCommand {
359 #[argh(option)]
361 pub tail: Option<usize>,
362
363 $(
364 $(#[$all_attr])*
365 $vis $field: $ty_outer $( < $ty_inner > )?,
366 )*
367 }
368
369 impl Default for RawDumpCommand {
370 fn default() -> Self {
371 let filters = LogFilterArgs::default();
372 Self {
373 tail: None,
374 $(
375 $(#[$cfg_attr])*
376 $field: filters.$field,
377 )*
378 }
379 }
380 }
381
382 impl RawDumpCommand {
383 pub fn into_filter_args(self) -> LogFilterArgs {
384 LogFilterArgs {
385 $(
386 $(#[$cfg_attr])*
387 $field: self.$field,
388 )*
389 }
390 }
391 }
392
393 #[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
394 #[argh(subcommand, name = "watch")]
396 pub struct RawWatchCommand {
397 $(
398 $(#[$all_attr])*
399 $vis $field: $ty_outer $( < $ty_inner > )?,
400 )*
401 }
402
403 impl Default for RawWatchCommand {
404 fn default() -> Self {
405 let filters = LogFilterArgs::default();
406 Self {
407 $(
408 $(#[$cfg_attr])*
409 $field: filters.$field,
410 )*
411 }
412 }
413 }
414
415 impl RawWatchCommand {
416 pub fn into_filter_args(self) -> LogFilterArgs {
417 LogFilterArgs {
418 $(
419 $(#[$cfg_attr])*
420 $field: self.$field,
421 )*
422 }
423 }
424 }
425 };
426}
427
428macro_rules! define_log_filter_args {
461 ($($tokens:tt)*) => {
462 __define_log_filter_args_helper! {
463 [ ]
464 [ ]
465 [ ]
466 $($tokens)*
467 }
468 };
469}
470
471define_log_filter_args! {
472 #[argh(option)]
475 pub filter: Vec<String>,
476
477 #[argh(option)]
479 pub moniker: Vec<String>,
480
481 #[argh(option)]
484 pub component: Vec<String>,
485
486 #[argh(option)]
489 pub exclude: Vec<String>,
490
491 #[argh(option)]
493 pub exclude_regex: Vec<String>,
494
495 #[argh(option)]
497 pub exclude_regex_file: Option<String>,
498
499 #[argh(option)]
501 pub tag: Vec<String>,
502
503 #[argh(option)]
505 pub exclude_tags: Vec<String>,
506
507 #[argh(option)]
510 pub severity: Option<Severity>,
511
512 #[argh(switch)]
514 pub kernel: bool,
515
516 #[argh(option, from_str_fn(parse_time))]
518 pub since: Option<DetailedDateTime>,
519
520 #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
523 pub since_boot: Option<Duration>,
524
525 #[argh(option, from_str_fn(parse_time))]
527 pub until: Option<DetailedDateTime>,
528
529 #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
532 pub until_boot: Option<Duration>,
533
534 #[argh(switch)]
536 pub hide_tags: bool,
537
538 #[argh(switch)]
540 pub hide_file: bool,
541
542 #[argh(switch)]
546 pub no_color: bool,
547
548 #[argh(switch)]
551 pub case_sensitive: bool,
552
553 #[argh(switch)]
555 pub show_metadata: bool,
556
557 #[argh(switch)]
560 pub show_full_moniker: bool,
561
562 #[argh(switch)]
564 pub prefer_url_component_name: bool,
565
566 #[argh(switch)]
568 pub hide_moniker: bool,
569
570 #[argh(option)]
574 pub clock: Option<TimeFormat>,
575
576 #[cfg(not(target_os = "fuchsia"))]
581 #[argh(option)]
582 pub symbolize: Option<SymbolizeMode>,
583
584 #[argh(option)]
586 pub pid: Option<u64>,
587
588 #[argh(option)]
590 pub tid: Option<u64>,
591
592 #[argh(switch)]
597 pub force_set_severity: bool,
598
599 #[cfg(target_os = "fuchsia")]
602 #[argh(option)]
603 pub encoding: Option<LogEncoding>,
604
605 #[cfg(target_os = "fuchsia")]
607 #[argh(switch)]
608 pub json: bool,
609
610 #[cfg(not(target_os = "fuchsia"))]
612 #[argh(switch)]
613 pub disable_reconnect: bool,
614}
615
616#[derive(Default, Clone, Debug, PartialEq)]
617pub struct LogCommand {
619 pub sub_command: Option<LogSubCommand>,
620 pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
621 pub filters: LogFilterArgs,
622}
623
624impl LogCommand {
625 pub fn merge_subcommand(&mut self) {
627 match &self.sub_command {
628 Some(LogSubCommand::Dump(raw_dump)) => {
629 self.filters.merge(raw_dump.clone().into_filter_args());
630 }
631 Some(LogSubCommand::Watch(raw_watch)) => {
632 self.filters.merge(raw_watch.clone().into_filter_args());
633 }
634 _ => {}
635 }
636 }
637}
638
639impl FromArgs for LogCommand {
640 fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, argh::EarlyExit> {
641 let cli = match RawLogCommand::from_args(command_name, args) {
642 Ok(cli) => cli,
643 Err(mut early_exit) => {
644 if early_exit.status.is_err() {
645 if args.iter().any(|arg| arg.starts_with("--dump")) {
646 early_exit.output.push_str(
647 "\nNote: 'dump' is a sub-command of 'ffx log', not a flag. Use: ffx log dump [options]\n",
648 );
649 } else if args.contains(&"dump") {
650 if args.iter().any(|arg| arg.starts_with("--limit")) {
651 early_exit.output.push_str(
652 "\nNote: 'ffx log dump' does not take --limit. To limit output lines, use '--tail <count>' or pipe to head/tail.\n",
653 );
654 }
655 if args.iter().any(|arg| arg.starts_with("--grep")) {
656 early_exit.output.push_str(
657 "\nNote: 'ffx log dump' does not take --grep. To filter log snapshot output, use '--filter <pattern>' or pipe to grep: ffx log dump | grep <pattern>\n",
658 );
659 }
660 }
661 }
662 return Err(early_exit);
663 }
664 };
665 let mut cmd = cli.into_log_command();
666 cmd.merge_subcommand();
667 Ok(cmd)
668 }
669}
670
671impl ArgsInfo for LogCommand {
672 fn get_args_info() -> argh::CommandInfoWithArgs {
673 RawLogCommand::get_args_info()
674 }
675}
676
677impl argh::SubCommand for LogCommand {
678 const COMMAND: &'static argh::CommandInfo = RawLogCommand::COMMAND;
679}
680
681#[derive(PartialEq, Debug)]
683pub enum LogProcessingResult {
684 Exit,
686 Continue,
688}
689
690impl FromStr for SymbolizeMode {
691 type Err = anyhow::Error;
692
693 fn from_str(s: &str) -> Result<Self, Self::Err> {
694 let s = s.to_lowercase();
695 match s.as_str() {
696 "off" => Ok(SymbolizeMode::Off),
697 "pretty" => Ok(SymbolizeMode::Pretty),
698 "classic" => Ok(SymbolizeMode::Classic),
699 other => Err(format_err!("invalid symbolize flag: {}", other)),
700 }
701 }
702}
703
704#[derive(Error, Debug)]
705pub enum LogError {
706 #[error(transparent)]
707 UnknownError(#[from] anyhow::Error),
708 #[error("No boot timestamp")]
709 NoBootTimestamp,
710 #[error(transparent)]
711 IOError(#[from] std::io::Error),
712 #[error(transparent)]
713 RegexError(#[from] regex_lite::Error),
714 #[error("Cannot use dump with --since now")]
715 DumpWithSinceNow,
716 #[error("No symbolizer configuration provided")]
717 NoSymbolizerConfig,
718 #[error(transparent)]
719 FfxError(#[from] FfxError),
720 #[error(transparent)]
721 Utf8Error(#[from] FromUtf8Error),
722 #[error(transparent)]
723 FidlError(#[from] fidl::Error),
724 #[error(transparent)]
725 FormatterError(#[from] FormatterError),
726 #[error("Deprecated flag: `{flag}`, use: `{new_flag}`")]
727 DeprecatedFlag { flag: &'static str, new_flag: &'static str },
728 #[error(
729 "Fuzzy matching for '{query}' failed due to too many matches, please re-try with one of these:\n{matches}"
730 )]
731 FuzzyMatchTooManyMatches { query: String, matches: String },
732 #[error(
733 "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."
734 )]
735 SearchParameterNotFound(String),
736}
737
738impl LogError {
739 fn too_many_fuzzy_matches(matches: impl Iterator<Item = String>, query: &str) -> Self {
740 let mut result = String::new();
741 for component in matches {
742 result.push_str(&component);
743 result.push('\n');
744 }
745
746 Self::FuzzyMatchTooManyMatches { matches: result, query: query.to_string() }
747 }
748
749 pub fn is_broken_pipe(&self) -> bool {
750 match self {
751 LogError::IOError(error) => error.kind() == std::io::ErrorKind::BrokenPipe,
752 LogError::FormatterError(formatter_error) => formatter_error.is_broken_pipe(),
753 LogError::UnknownError(err) => {
754 if let Some(writer_err) = err.downcast_ref::<writer::Error>() {
755 writer_err.is_broken_pipe()
756 } else if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
757 io_err.kind() == std::io::ErrorKind::BrokenPipe
758 } else {
759 false
760 }
761 }
762
763 LogError::NoBootTimestamp
764 | LogError::DumpWithSinceNow
765 | LogError::NoSymbolizerConfig
766 | LogError::RegexError(_)
767 | LogError::FfxError(_)
768 | LogError::Utf8Error(_)
769 | LogError::FidlError(_)
770 | LogError::DeprecatedFlag { .. }
771 | LogError::FuzzyMatchTooManyMatches { .. }
772 | LogError::SearchParameterNotFound(_) => false,
773 }
774 }
775}
776
777#[async_trait::async_trait(?Send)]
779pub trait InstanceGetter {
780 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError>;
781}
782
783#[cfg(not(feature = "ctf"))]
784#[async_trait::async_trait(?Send)]
785impl InstanceGetter for RealmQueryProxy {
786 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
787 Ok(get_instances_from_query(query, self)
788 .await?
789 .into_iter()
790 .map(|value| value.moniker)
791 .collect())
792 }
793}
794
795#[cfg(feature = "ctf")]
796#[async_trait::async_trait(?Send)]
797impl InstanceGetter for RealmQueryProxy {
798 async fn get_monikers_from_query(&self, _query: &str) -> Result<Vec<Moniker>, LogError> {
799 unreachable!("get_monikers_from_query is not supported in CTF tests.");
800 }
801}
802
803impl LogCommand {
804 #[must_use]
806 pub fn severity(&self) -> Severity {
807 self.filters.severity.unwrap_or(Severity::Info)
808 }
809
810 #[must_use]
812 pub fn clock(&self) -> TimeFormat {
813 self.filters.clock.clone().unwrap_or(TimeFormat::Boot)
814 }
815
816 #[cfg(not(target_os = "fuchsia"))]
818 #[must_use]
819 pub fn symbolize(&self) -> SymbolizeMode {
820 self.filters.symbolize.clone().unwrap_or(SymbolizeMode::Pretty)
821 }
822
823 #[cfg(target_os = "fuchsia")]
825 #[must_use]
826 pub fn encoding(&self) -> LogEncoding {
827 self.filters.encoding.clone().unwrap_or(LogEncoding::Json)
828 }
829
830 #[must_use]
832 pub fn filter(&self) -> &[String] {
833 &self.filters.filter
834 }
835
836 #[must_use]
838 pub fn moniker(&self) -> &[String] {
839 &self.filters.moniker
840 }
841
842 #[must_use]
844 pub fn component(&self) -> &[String] {
845 &self.filters.component
846 }
847
848 #[must_use]
850 pub fn exclude(&self) -> &[String] {
851 &self.filters.exclude
852 }
853
854 #[must_use]
856 pub fn exclude_regex(&self) -> &[String] {
857 &self.filters.exclude_regex
858 }
859
860 #[must_use]
862 pub fn tag(&self) -> &[String] {
863 &self.filters.tag
864 }
865
866 #[must_use]
868 pub fn exclude_tags(&self) -> &[String] {
869 &self.filters.exclude_tags
870 }
871
872 #[must_use]
874 pub fn hide_tags(&self) -> bool {
875 self.filters.hide_tags
876 }
877
878 #[must_use]
880 pub fn no_color(&self) -> bool {
881 self.filters.no_color
882 }
883
884 pub fn set_no_color(&mut self, no_color: bool) {
886 self.filters.no_color = no_color;
887 }
888
889 #[must_use]
891 pub fn show_metadata(&self) -> bool {
892 self.filters.show_metadata
893 }
894
895 #[must_use]
897 pub fn hide_file(&self) -> bool {
898 self.filters.hide_file
899 }
900
901 #[must_use]
903 pub fn hide_moniker(&self) -> bool {
904 self.filters.hide_moniker
905 }
906
907 #[must_use]
909 pub fn show_full_moniker(&self) -> bool {
910 self.filters.show_full_moniker
911 }
912
913 #[must_use]
915 pub fn prefer_url_component_name(&self) -> bool {
916 self.filters.prefer_url_component_name
917 }
918
919 #[must_use]
921 pub fn since(&self) -> Option<&DetailedDateTime> {
922 self.filters.since.as_ref()
923 }
924
925 #[must_use]
927 pub fn until(&self) -> Option<&DetailedDateTime> {
928 self.filters.until.as_ref()
929 }
930
931 #[must_use]
933 pub fn since_boot(&self) -> Option<Duration> {
934 self.filters.since_boot
935 }
936
937 #[must_use]
939 pub fn until_boot(&self) -> Option<Duration> {
940 self.filters.until_boot
941 }
942
943 #[cfg(target_os = "fuchsia")]
945 #[must_use]
946 pub fn json(&self) -> bool {
947 self.filters.json
948 }
949
950 #[must_use]
952 pub fn exclude_regex_file(&self) -> Option<&str> {
953 self.filters.exclude_regex_file.as_deref()
954 }
955
956 #[must_use]
958 pub fn kernel(&self) -> bool {
959 self.filters.kernel
960 }
961
962 #[must_use]
964 pub fn force_set_severity(&self) -> bool {
965 self.filters.force_set_severity
966 }
967
968 #[must_use]
970 pub fn case_sensitive(&self) -> bool {
971 self.filters.case_sensitive
972 }
973
974 #[must_use]
976 pub fn pid(&self) -> Option<u64> {
977 self.filters.pid
978 }
979
980 #[must_use]
982 pub fn tid(&self) -> Option<u64> {
983 self.filters.tid
984 }
985
986 #[cfg(not(target_os = "fuchsia"))]
988 #[must_use]
989 pub fn disable_reconnect(&self) -> bool {
990 self.filters.disable_reconnect
991 }
992
993 async fn map_interest_selectors<'a>(
994 realm_query: &impl InstanceGetter,
995 interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
996 ) -> Result<impl Iterator<Item = Cow<'a, LogInterestSelector>>, LogError> {
997 let selectors = Self::get_selectors_and_monikers(interest_selectors);
998 let mut translated_selectors = vec![];
999 for (moniker, selector) in selectors {
1000 let instances = realm_query.get_monikers_from_query(moniker.as_str()).await?;
1002 let instances = disambiguate_queried_monikers(moniker.as_str(), instances);
1003 if instances.len() == 1 {
1005 let mut translated_selector = selector.clone();
1006 translated_selector.selector = instances[0].clone().into_component_selector();
1007 translated_selectors.push((Cow::Owned(translated_selector), instances));
1008 } else {
1009 translated_selectors.push((Cow::Borrowed(selector), instances));
1010 }
1011 }
1012 if translated_selectors.iter().any(|(_, matches)| matches.len() > 1) {
1013 let mut err_output = vec![];
1014 writeln!(
1015 &mut err_output,
1016 "WARN: One or more of your selectors appears to be ambiguous"
1017 )?;
1018 writeln!(&mut err_output, "and may not match any components on your system.\n")?;
1019 writeln!(
1020 &mut err_output,
1021 "If this is unintentional you can explicitly match using the"
1022 )?;
1023 writeln!(&mut err_output, "following command:\n")?;
1024 writeln!(&mut err_output, "ffx log \\")?;
1025 let mut output = vec![];
1026 for (oselector, instances) in translated_selectors {
1027 for selector in instances {
1028 writeln!(
1029 output,
1030 "\t--set-severity {}#{} \\",
1031 sanitize_moniker_for_selectors(selector.to_string().as_str())
1032 .replace("\\", "\\\\"),
1033 format!("{:?}", oselector.interest.min_severity.unwrap()).to_uppercase()
1034 )?;
1035 }
1036 }
1037 let _ = output.pop();
1039 let _ = output.pop();
1040 let _ = output.pop();
1041
1042 writeln!(&mut err_output, "{}", String::from_utf8(output).unwrap())?;
1043 writeln!(&mut err_output, "\nIf this is intentional, you can disable this with")?;
1044 writeln!(&mut err_output, "ffx log --force-set-severity.")?;
1045
1046 ffx_bail!("{}", String::from_utf8(err_output)?);
1047 }
1048 Ok(translated_selectors.into_iter().map(|(selector, _)| selector))
1049 }
1050
1051 pub fn validate_cmd_flags_with_warnings(&mut self) -> Result<Vec<&'static str>, LogError> {
1052 let mut warnings = vec![];
1053
1054 if !self.filters.moniker.is_empty() {
1055 warnings.push("WARNING: --moniker is deprecated, use --component instead");
1056 if self.filters.component.is_empty() {
1057 self.filters.component = std::mem::take(&mut self.filters.moniker);
1058 } else {
1059 warnings.push("WARNING: ignoring --moniker arguments in favor of --component");
1060 }
1061 }
1062
1063 Ok(warnings)
1064 }
1065
1066 pub async fn maybe_set_interest(
1070 &self,
1071 log_settings_client: &LogSettingsProxy,
1072 realm_query: &impl InstanceGetter,
1073 ) -> Result<(), LogError> {
1074 let (set_severity, force_set_severity, persist) =
1075 if let Some(LogSubCommand::SetSeverity(options)) = &self.sub_command {
1076 let default_cmd = LogCommand {
1078 sub_command: Some(LogSubCommand::SetSeverity(options.clone())),
1079 ..Default::default()
1080 };
1081 if &default_cmd != self {
1082 ffx_bail!("Cannot combine set-severity with other options.");
1083 }
1084 (&options.interest_selector, options.force, !options.no_persist)
1085 } else {
1086 (&self.set_severity, self.filters.force_set_severity, false)
1087 };
1088
1089 if persist || !set_severity.is_empty() {
1090 let selectors = if force_set_severity {
1091 set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
1092 } else {
1093 let new_selectors =
1094 Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
1095 .await?
1096 .map(|s| s.into_owned())
1097 .collect::<Vec<_>>();
1098 if new_selectors.is_empty() {
1099 set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
1100 } else {
1101 new_selectors
1102 }
1103 };
1104 log_settings_client
1105 .set_component_interest(
1106 &flex_fuchsia_diagnostics::LogSettingsSetComponentInterestRequest {
1107 selectors: Some(selectors),
1108 persist: Some(persist),
1109 ..Default::default()
1110 },
1111 )
1112 .await?;
1113 }
1114
1115 Ok(())
1116 }
1117
1118 fn get_selectors_and_monikers<'a>(
1119 interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
1120 ) -> Vec<(String, &'a LogInterestSelector)> {
1121 let mut selectors = vec![];
1122 for selector in interest_selectors {
1123 let segments = selector.selector.moniker_segments.as_ref().unwrap();
1124 let mut full_moniker = String::new();
1125 for segment in segments {
1126 match segment {
1127 flex_fuchsia_diagnostics::StringSelector::ExactMatch(segment) => {
1128 if full_moniker.is_empty() {
1129 full_moniker.push_str(segment);
1130 } else {
1131 full_moniker.push('/');
1132 full_moniker.push_str(segment);
1133 }
1134 }
1135 _ => {
1136 return vec![];
1139 }
1140 }
1141 }
1142 selectors.push((full_moniker, selector));
1143 }
1144 selectors
1145 }
1146}
1147
1148impl TopLevelCommand for LogCommand {}
1149
1150fn log_interest_selector(s: &str) -> Result<OneOrMany<LogInterestSelector>, String> {
1151 if s.contains(",") {
1152 let many: Result<Vec<LogInterestSelector>, String> = s
1153 .split(",")
1154 .map(|value| selectors::parse_log_interest_selector(value).map_err(|e| e.to_string()))
1155 .collect();
1156 Ok(OneOrMany::Many(many?))
1157 } else {
1158 Ok(OneOrMany::One(selectors::parse_log_interest_selector(s).map_err(|s| s.to_string())?))
1159 }
1160}
1161
1162#[cfg(test)]
1163mod test {
1164 use super::*;
1165 use assert_matches::assert_matches;
1166 use async_trait::async_trait;
1167 use fidl::endpoints::create_proxy;
1168 use flex_fuchsia_diagnostics::{LogSettingsMarker, LogSettingsRequest};
1169 use futures_util::StreamExt;
1170 use futures_util::future::Either;
1171 use futures_util::stream::FuturesUnordered;
1172 use selectors::parse_log_interest_selector;
1173
1174 #[derive(Default)]
1175 struct FakeInstanceGetter {
1176 output: Vec<Moniker>,
1177 expected_selector: Option<String>,
1178 }
1179
1180 #[async_trait(?Send)]
1181 impl InstanceGetter for FakeInstanceGetter {
1182 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
1183 if let Some(expected) = &self.expected_selector {
1184 assert_eq!(expected, query);
1185 }
1186 Ok(self.output.clone())
1187 }
1188 }
1189
1190 #[fuchsia::test]
1191 async fn test_symbolize_mode_from_str() {
1192 assert_matches!(SymbolizeMode::from_str("off"), Ok(value) if value == SymbolizeMode::Off);
1193 assert_matches!(
1194 SymbolizeMode::from_str("pretty"),
1195 Ok(value) if value == SymbolizeMode::Pretty
1196 );
1197 assert_matches!(
1198 SymbolizeMode::from_str("classic"),
1199 Ok(value) if value == SymbolizeMode::Classic
1200 );
1201 }
1202
1203 #[fuchsia::test]
1204 async fn maybe_set_interest_errors_additional_arguments_passed_to_set_interest() {
1205 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1206 let getter = FakeInstanceGetter {
1207 expected_selector: Some("ambiguous_selector".into()),
1208 output: vec![
1209 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1210 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1211 ],
1212 };
1213 let cmd = LogCommand {
1216 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1217 interest_selector: vec![OneOrMany::One(
1218 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1219 )],
1220 force: false,
1221 no_persist: false,
1222 })),
1223 filters: LogFilterArgs { hide_file: true, ..LogFilterArgs::default() },
1224 ..LogCommand::default()
1225 };
1226 let mut set_interest_result = None;
1227
1228 let mut scheduler = FuturesUnordered::new();
1229 scheduler.push(Either::Left(async {
1230 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1231 drop(settings_proxy);
1232 }));
1233 scheduler.push(Either::Right(async {
1234 let request = settings_server.into_stream().next().await;
1235 assert_matches!(request, None);
1237 }));
1238 while scheduler.next().await.is_some() {}
1239 drop(scheduler);
1240
1241 let error = format!("{}", set_interest_result.unwrap().unwrap_err());
1242
1243 const EXPECTED_INTEREST_ERROR: &str = "Cannot combine set-severity with other options.";
1244 assert_eq!(error, EXPECTED_INTEREST_ERROR);
1245 }
1246
1247 #[fuchsia::test]
1248 async fn maybe_set_interest_errors_if_ambiguous_selector() {
1249 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1250 let getter = FakeInstanceGetter {
1251 expected_selector: Some("ambiguous_selector".into()),
1252 output: vec![
1253 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1254 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1255 ],
1256 };
1257 let cmd = LogCommand {
1260 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1261 set_severity: vec![OneOrMany::One(
1262 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1263 )],
1264 ..LogCommand::default()
1265 };
1266 let mut set_interest_result = None;
1267
1268 let mut scheduler = FuturesUnordered::new();
1269 scheduler.push(Either::Left(async {
1270 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1271 drop(settings_proxy);
1272 }));
1273 scheduler.push(Either::Right(async {
1274 let request = settings_server.into_stream().next().await;
1275 assert_matches!(request, None);
1277 }));
1278 while scheduler.next().await.is_some() {}
1279 drop(scheduler);
1280
1281 let error = format!("{}", set_interest_result.unwrap().unwrap_err());
1282
1283 const EXPECTED_INTEREST_ERROR: &str = r#"WARN: One or more of your selectors appears to be ambiguous
1284and may not match any components on your system.
1285
1286If this is unintentional you can explicitly match using the
1287following command:
1288
1289ffx log \
1290 --set-severity core/some/ambiguous_selector\\:thing/test#INFO \
1291 --set-severity core/other/ambiguous_selector\\:thing/test#INFO
1292
1293If this is intentional, you can disable this with
1294ffx log --force-set-severity.
1295"#;
1296 assert_eq!(error, EXPECTED_INTEREST_ERROR);
1297 }
1298
1299 #[fuchsia::test]
1300 async fn logger_translates_selector_if_one_match() {
1301 let cmd = LogCommand {
1302 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1303 set_severity: vec![OneOrMany::One(
1304 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1305 )],
1306 ..LogCommand::default()
1307 };
1308 let mut set_interest_result = None;
1309 let getter = FakeInstanceGetter {
1310 expected_selector: Some("ambiguous_selector".into()),
1311 output: vec![Moniker::try_from("core/some/ambiguous_selector").unwrap()],
1312 };
1313 let mut scheduler = FuturesUnordered::new();
1314 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1315 scheduler.push(Either::Left(async {
1316 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1317 drop(settings_proxy);
1318 }));
1319 scheduler.push(Either::Right(async {
1320 let request = settings_server.into_stream().next().await;
1321 let (payload, responder) = assert_matches!(
1322 request,
1323 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1324 (payload, responder)
1325 );
1326 responder.send().unwrap();
1327 assert_eq!(
1328 payload.selectors,
1329 Some(vec![
1330 parse_log_interest_selector("core/some/ambiguous_selector#INFO").unwrap()
1331 ])
1332 );
1333 }));
1334 while scheduler.next().await.is_some() {}
1335 drop(scheduler);
1336 assert_matches!(set_interest_result, Some(Ok(())));
1337 }
1338
1339 #[fuchsia::test]
1340 async fn logger_translates_selector_disambiguating_subcomponents() {
1341 let cmd = LogCommand {
1342 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1343 set_severity: vec![OneOrMany::One(
1344 parse_log_interest_selector("archivist#INFO").unwrap(),
1345 )],
1346 ..LogCommand::default()
1347 };
1348 let mut set_interest_result = None;
1349 let getter = FakeInstanceGetter {
1350 expected_selector: Some("archivist".into()),
1351 output: vec![
1352 Moniker::try_from("bootstrap/archivist").unwrap(),
1353 Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1354 ],
1355 };
1356 let mut scheduler = FuturesUnordered::new();
1357 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1358 scheduler.push(Either::Left(async {
1359 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1360 drop(settings_proxy);
1361 }));
1362 scheduler.push(Either::Right(async {
1363 let request = settings_server.into_stream().next().await;
1364 let (payload, responder) = assert_matches!(
1365 request,
1366 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1367 (payload, responder)
1368 );
1369 responder.send().unwrap();
1370 assert_eq!(
1371 payload.selectors,
1372 Some(vec![parse_log_interest_selector("bootstrap/archivist#INFO").unwrap()])
1373 );
1374 }));
1375 while scheduler.next().await.is_some() {}
1376 drop(scheduler);
1377 assert_matches!(set_interest_result, Some(Ok(())));
1378 }
1379
1380 #[fuchsia::test]
1381 async fn logger_uses_specified_selectors_if_no_results_returned() {
1382 let cmd = LogCommand {
1383 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1384 set_severity: vec![OneOrMany::One(
1385 parse_log_interest_selector("core/something/a:b/elements:main/otherstuff:*#DEBUG")
1386 .unwrap(),
1387 )],
1388 ..LogCommand::default()
1389 };
1390 let mut set_interest_result = None;
1391 let getter = FakeInstanceGetter {
1392 expected_selector: Some("core/something/a:b/elements:main/otherstuff:*#DEBUG".into()),
1393 output: vec![],
1394 };
1395 let scheduler = FuturesUnordered::new();
1396 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1397 scheduler.push(Either::Left(async {
1398 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1399 drop(settings_proxy);
1400 }));
1401 scheduler.push(Either::Right(async {
1402 let request = settings_server.into_stream().next().await;
1403 let (payload, responder) = assert_matches!(
1404 request,
1405 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1406 (payload, responder)
1407 );
1408 responder.send().unwrap();
1409 assert_eq!(
1410 payload.selectors,
1411 Some(vec![
1412 parse_log_interest_selector(
1413 "core/something/a:b/elements:main/otherstuff:*#DEBUG"
1414 )
1415 .unwrap()
1416 ])
1417 );
1418 }));
1419 scheduler.map(|_| Ok(())).forward(futures::sink::drain()).await.unwrap();
1420 assert_matches!(set_interest_result, Some(Ok(())));
1421 }
1422
1423 #[fuchsia::test]
1424 async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used() {
1425 let cmd = LogCommand {
1426 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1427 no_persist: true,
1428 interest_selector: vec![OneOrMany::One(
1429 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1430 )],
1431 force: true,
1432 })),
1433 ..LogCommand::default()
1434 };
1435 let getter = FakeInstanceGetter {
1436 expected_selector: Some("ambiguous_selector".into()),
1437 output: vec![
1438 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1439 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1440 ],
1441 };
1442 let mut set_interest_result = None;
1443 let mut scheduler = FuturesUnordered::new();
1444 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1445 scheduler.push(Either::Left(async {
1446 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1447 drop(settings_proxy);
1448 }));
1449 scheduler.push(Either::Right(async {
1450 let request = settings_server.into_stream().next().await;
1451 let (payload, responder) = assert_matches!(
1452 request,
1453 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1454 (payload, responder)
1455 );
1456 responder.send().unwrap();
1457 assert_eq!(
1458 payload.selectors,
1459 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1460 );
1461 }));
1462 while scheduler.next().await.is_some() {}
1463 drop(scheduler);
1464 assert_matches!(set_interest_result, Some(Ok(())));
1465 }
1466
1467 #[fuchsia::test]
1468 async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used_persistent() {
1469 let cmd = LogCommand {
1470 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1471 no_persist: false,
1472 interest_selector: vec![log_socket_stream::OneOrMany::One(
1473 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1474 )],
1475 force: true,
1476 })),
1477 ..LogCommand::default()
1478 };
1479 let getter = FakeInstanceGetter {
1480 expected_selector: Some("ambiguous_selector".into()),
1481 output: vec![
1482 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1483 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1484 ],
1485 };
1486 let mut set_interest_result = None;
1487 let mut scheduler = FuturesUnordered::new();
1488 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1489 scheduler.push(Either::Left(async {
1490 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1491 drop(settings_proxy);
1492 }));
1493 scheduler.push(Either::Right(async {
1494 let request = settings_server.into_stream().next().await;
1495 let (payload, responder) = assert_matches!(
1496 request,
1497 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1498 (payload, responder)
1499 );
1500 responder.send().unwrap();
1501 assert_eq!(
1502 payload.selectors,
1503 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1504 );
1505 assert_eq!(payload.persist, Some(true));
1506 }));
1507 while scheduler.next().await.is_some() {}
1508 drop(scheduler);
1509 assert_matches!(set_interest_result, Some(Ok(())));
1510 }
1511
1512 #[fuchsia::test]
1513 async fn logger_prints_ignores_ambiguity_if_machine_output_is_used() {
1514 let cmd = LogCommand {
1515 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1516 set_severity: vec![OneOrMany::One(
1517 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1518 )],
1519 filters: LogFilterArgs { force_set_severity: true, ..LogFilterArgs::default() },
1520 };
1521 let getter = FakeInstanceGetter {
1522 expected_selector: Some("ambiguous_selector".into()),
1523 output: vec![
1524 Moniker::try_from("core/some/collection:thing/test").unwrap(),
1525 Moniker::try_from("core/other/collection:thing/test").unwrap(),
1526 ],
1527 };
1528 let mut set_interest_result = None;
1529 let mut scheduler = FuturesUnordered::new();
1530 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1531 scheduler.push(Either::Left(async {
1532 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1533 drop(settings_proxy);
1534 }));
1535 scheduler.push(Either::Right(async {
1536 let request = settings_server.into_stream().next().await;
1537 let (payload, responder) = assert_matches!(
1538 request,
1539 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1540 (payload, responder)
1541 );
1542 responder.send().unwrap();
1543 assert_eq!(
1544 payload.selectors,
1545 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1546 );
1547 }));
1548 while scheduler.next().await.is_some() {}
1549 drop(scheduler);
1550 assert_matches!(set_interest_result, Some(Ok(())));
1551 }
1552 #[test]
1553 fn test_parse_selector() {
1554 assert_eq!(
1555 log_interest_selector("core/audio#DEBUG").unwrap(),
1556 OneOrMany::One(parse_log_interest_selector("core/audio#DEBUG").unwrap())
1557 );
1558 }
1559
1560 #[test]
1561 fn test_parse_selector_with_commas() {
1562 assert_eq!(
1563 log_interest_selector("core/audio#DEBUG,bootstrap/archivist#TRACE").unwrap(),
1564 OneOrMany::Many(vec![
1565 parse_log_interest_selector("core/audio#DEBUG").unwrap(),
1566 parse_log_interest_selector("bootstrap/archivist#TRACE").unwrap()
1567 ])
1568 );
1569 }
1570
1571 #[test]
1572 fn test_parse_time() {
1573 assert!(parse_time("now").unwrap().is_now);
1574 let date_string = "04/20/2020";
1575 let res = parse_time(date_string).unwrap();
1576 assert!(!res.is_now);
1577 assert_eq!(
1578 res.date_naive(),
1579 parse_date_string(date_string, Local::now(), Dialect::Us).unwrap().date_naive()
1580 );
1581 }
1582
1583 #[test]
1584 fn test_log_error_is_broken_pipe() {
1585 assert!(
1586 LogError::IOError(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1587 .is_broken_pipe()
1588 );
1589 assert!(
1590 LogError::UnknownError(anyhow::Error::new(std::io::Error::new(
1591 std::io::ErrorKind::BrokenPipe,
1592 "broken pipe"
1593 )))
1594 .is_broken_pipe()
1595 );
1596 assert!(!LogError::IOError(std::io::Error::other("other")).is_broken_pipe());
1597 assert!(!LogError::NoBootTimestamp.is_broken_pipe());
1598 }
1599
1600 #[test]
1601 fn test_raw_dump_command_into_filter_args() {
1602 let raw_dump = RawDumpCommand {
1603 tail: Some(42),
1604 filter: vec!["foo".to_string()],
1605 severity: Some(Severity::Warn),
1606 hide_tags: true,
1607 ..RawDumpCommand::default()
1608 };
1609 let filters = raw_dump.into_filter_args();
1610 assert_eq!(filters.filter, vec!["foo".to_string()]);
1611 assert_eq!(filters.severity, Some(Severity::Warn));
1612 assert!(filters.hide_tags);
1613 }
1614
1615 #[test]
1616 fn test_raw_watch_command_into_filter_args() {
1617 let raw_watch = RawWatchCommand {
1618 tag: vec!["my_tag".to_string()],
1619 no_color: true,
1620 ..RawWatchCommand::default()
1621 };
1622 let filters = raw_watch.into_filter_args();
1623 assert_eq!(filters.tag, vec!["my_tag".to_string()]);
1624 assert!(filters.no_color);
1625 }
1626
1627 #[test]
1628 fn test_merge_subcommand_dump_overlay() {
1629 let cmd = LogCommand::from_args(
1630 &["log"],
1631 &[
1632 "--severity",
1633 "info",
1634 "--filter",
1635 "top_filter",
1636 "dump",
1637 "--tail",
1638 "10",
1639 "--severity",
1640 "debug",
1641 "--filter",
1642 "sub_filter",
1643 ],
1644 )
1645 .unwrap();
1646
1647 assert_eq!(cmd.severity(), Severity::Debug);
1648 assert_eq!(cmd.filter(), &["top_filter".to_string(), "sub_filter".to_string()]);
1649 if let Some(LogSubCommand::Dump(dump)) = &cmd.sub_command {
1650 assert_eq!(dump.tail, Some(10));
1651 } else {
1652 panic!("expected Dump subcommand");
1653 }
1654 }
1655
1656 #[test]
1657 fn test_merge_subcommand_watch_overlay() {
1658 let cmd = LogCommand::from_args(
1659 &["log"],
1660 &["--tag", "t1", "watch", "--tag", "t2", "--hide-tags"],
1661 )
1662 .unwrap();
1663
1664 assert_eq!(cmd.tag(), &["t1".to_string(), "t2".to_string()]);
1665 assert!(cmd.hide_tags());
1666 }
1667
1668 #[test]
1669 fn test_merge_subcommand_validate_warnings_on_merged_moniker() {
1670 let mut cmd =
1671 LogCommand::from_args(&["log"], &["dump", "--moniker", "my_moniker"]).unwrap();
1672
1673 assert_eq!(cmd.moniker(), &["my_moniker".to_string()]);
1674 let warnings = cmd.validate_cmd_flags_with_warnings().unwrap();
1675 assert!(!warnings.is_empty());
1676 assert_eq!(cmd.component(), &["my_moniker".to_string()]);
1677 assert!(cmd.moniker().is_empty());
1678 }
1679
1680 #[cfg(not(target_os = "fuchsia"))]
1681 #[fuchsia::test]
1682 async fn test_symbolize_accessor() {
1683 let cmd_default = LogCommand::from_args(&["ffx", "log"], &["dump"]).unwrap();
1684 assert_eq!(cmd_default.symbolize(), SymbolizeMode::Pretty);
1685
1686 let cmd_custom =
1687 LogCommand::from_args(&["ffx", "log"], &["dump", "--symbolize", "off"]).unwrap();
1688 assert_eq!(cmd_custom.symbolize(), SymbolizeMode::Off);
1689 }
1690
1691 #[cfg(target_os = "fuchsia")]
1692 #[fuchsia::test]
1693 async fn test_encoding_accessor() {
1694 let cmd_default = LogCommand::from_args(&["ffx", "log"], &["dump"]).unwrap();
1695 assert_eq!(cmd_default.encoding(), LogEncoding::Json);
1696
1697 let cmd_custom =
1698 LogCommand::from_args(&["ffx", "log"], &["dump", "--encoding", "fxt"]).unwrap();
1699 assert_eq!(cmd_custom.encoding(), LogEncoding::Fxt);
1700 }
1701
1702 #[fuchsia::test]
1703 async fn test_subcommand_moniker_deprecation() {
1704 let mut cmd =
1705 LogCommand::from_args(&["ffx", "log"], &["dump", "--moniker", "foo"]).unwrap();
1706 assert_eq!(cmd.filters.moniker, vec!["foo"]);
1707
1708 let warnings = cmd.validate_cmd_flags_with_warnings().unwrap();
1709 assert_eq!(warnings, vec!["WARNING: --moniker is deprecated, use --component instead"]);
1710 assert_eq!(cmd.filters.component, vec!["foo"]);
1711 assert!(cmd.filters.moniker.is_empty());
1712
1713 let mut cmd_both = LogCommand::from_args(
1714 &["ffx", "log"],
1715 &["dump", "--moniker", "foo", "--component", "bar"],
1716 )
1717 .unwrap();
1718 let warnings_both = cmd_both.validate_cmd_flags_with_warnings().unwrap();
1719 assert_eq!(
1720 warnings_both,
1721 vec![
1722 "WARNING: --moniker is deprecated, use --component instead",
1723 "WARNING: ignoring --moniker arguments in favor of --component"
1724 ]
1725 );
1726 assert_eq!(cmd_both.filters.component, vec!["bar"]);
1727 }
1728
1729 #[test]
1730 fn test_subcommand_enum_overrides() {
1731 let cmd =
1732 LogCommand::from_args(&["log"], &["--severity", "warn", "dump", "--severity", "info"])
1733 .unwrap();
1734 assert_eq!(cmd.severity(), Severity::Info);
1735 assert_eq!(cmd.filters.severity, Some(Severity::Info));
1736 }
1737
1738 #[test]
1739 fn test_merge_subcommand_no_subcommand_or_set_severity() {
1740 let initial_filters = LogFilterArgs {
1741 severity: Some(Severity::Warn),
1742 filter: vec!["test_filter".into()],
1743 no_color: true,
1744 ..Default::default()
1745 };
1746 let mut cmd_none = LogCommand {
1747 sub_command: None,
1748 set_severity: vec![],
1749 filters: initial_filters.clone(),
1750 };
1751 cmd_none.merge_subcommand();
1752 assert_eq!(cmd_none.filters, initial_filters);
1753 assert_eq!(cmd_none.sub_command, None);
1754
1755 let set_severity_cmd =
1756 SetSeverityCommand { no_persist: true, force: true, interest_selector: vec![] };
1757 let mut cmd_set_sev = LogCommand {
1758 sub_command: Some(LogSubCommand::SetSeverity(set_severity_cmd.clone())),
1759 set_severity: vec![],
1760 filters: initial_filters.clone(),
1761 };
1762 cmd_set_sev.merge_subcommand();
1763 assert_eq!(cmd_set_sev.filters, initial_filters);
1764 assert_eq!(cmd_set_sev.sub_command, Some(LogSubCommand::SetSeverity(set_severity_cmd)));
1765 }
1766
1767 #[test]
1768 fn test_macro_raw_command_conversions() {
1769 #[cfg(not(target_os = "fuchsia"))]
1770 let raw_dump = RawDumpCommand::from_args(
1771 &["dump"],
1772 &[
1773 "--tail",
1774 "50",
1775 "--severity",
1776 "error",
1777 "--filter",
1778 "dump_filter",
1779 "--symbolize",
1780 "off",
1781 "--disable-reconnect",
1782 ],
1783 )
1784 .unwrap();
1785
1786 #[cfg(target_os = "fuchsia")]
1787 let raw_dump = RawDumpCommand::from_args(
1788 &["dump"],
1789 &[
1790 "--tail",
1791 "50",
1792 "--severity",
1793 "error",
1794 "--filter",
1795 "dump_filter",
1796 "--encoding",
1797 "fxt",
1798 "--json",
1799 ],
1800 )
1801 .unwrap();
1802
1803 assert_eq!(raw_dump.tail, Some(50));
1804 let dump_filters = raw_dump.into_filter_args();
1805 assert_eq!(dump_filters.severity, Some(Severity::Error));
1806 assert_eq!(dump_filters.filter, vec!["dump_filter"]);
1807 #[cfg(not(target_os = "fuchsia"))]
1808 {
1809 assert_eq!(dump_filters.symbolize, Some(SymbolizeMode::Off));
1810 assert!(dump_filters.disable_reconnect);
1811 }
1812 #[cfg(target_os = "fuchsia")]
1813 {
1814 assert_eq!(dump_filters.encoding, Some(LogEncoding::Fxt));
1815 assert!(dump_filters.json);
1816 }
1817
1818 #[cfg(not(target_os = "fuchsia"))]
1819 let raw_watch = RawWatchCommand::from_args(
1820 &["watch"],
1821 &["--severity", "warn", "--tag", "watch_tag", "--symbolize", "classic"],
1822 )
1823 .unwrap();
1824
1825 #[cfg(target_os = "fuchsia")]
1826 let raw_watch = RawWatchCommand::from_args(
1827 &["watch"],
1828 &["--severity", "warn", "--tag", "watch_tag", "--encoding", "json"],
1829 )
1830 .unwrap();
1831
1832 let watch_filters = raw_watch.into_filter_args();
1833 assert_eq!(watch_filters.severity, Some(Severity::Warn));
1834 assert_eq!(watch_filters.tag, vec!["watch_tag"]);
1835 #[cfg(not(target_os = "fuchsia"))]
1836 assert_eq!(watch_filters.symbolize, Some(SymbolizeMode::Classic));
1837 #[cfg(target_os = "fuchsia")]
1838 assert_eq!(watch_filters.encoding, Some(LogEncoding::Json));
1839 }
1840
1841 #[test]
1842 fn test_dump_help_text() {
1843 let help_err = RawDumpCommand::from_args(&["dump"], &["--help"]).unwrap_err();
1844 let help_output = help_err.output;
1845 assert!(help_output.contains("--tail"), "dump help should include --tail");
1846 assert!(help_output.contains("--severity"), "dump help should include --severity");
1847 assert!(help_output.contains("--filter"), "dump help should include --filter");
1848 assert!(help_output.contains("--since"), "dump help should include --since");
1849 assert!(help_output.contains("--until"), "dump help should include --until");
1850 }
1851
1852 #[test]
1853 fn test_watch_help_text() {
1854 let help_err = RawWatchCommand::from_args(&["watch"], &["--help"]).unwrap_err();
1855 let help_output = help_err.output;
1856 assert!(help_output.contains("--severity"), "watch help should include --severity");
1857 assert!(help_output.contains("--filter"), "watch help should include --filter");
1858 assert!(help_output.contains("--since"), "watch help should include --since");
1859 assert!(help_output.contains("--until"), "watch help should include --until");
1860 }
1861
1862 #[test]
1863 fn test_option_field_subcommand_overlay() {
1864 let cmd_sub_some = LogCommand::from_args(
1866 &["log"],
1867 &[
1868 "dump",
1869 "--pid",
1870 "123",
1871 "--tid",
1872 "456",
1873 "--since",
1874 "10m ago",
1875 "--until",
1876 "5m ago",
1877 "--exclude-regex-file",
1878 "/path/sub.txt",
1879 ],
1880 )
1881 .unwrap();
1882
1883 assert_eq!(cmd_sub_some.pid(), Some(123));
1884 assert_eq!(cmd_sub_some.tid(), Some(456));
1885 assert!(cmd_sub_some.since().is_some());
1886 assert!(cmd_sub_some.until().is_some());
1887 assert_eq!(cmd_sub_some.exclude_regex_file(), Some("/path/sub.txt"));
1888
1889 let cmd_sub_override = LogCommand::from_args(
1891 &["log"],
1892 &[
1893 "--pid",
1894 "11",
1895 "--tid",
1896 "22",
1897 "--since",
1898 "20m ago",
1899 "--until",
1900 "15m ago",
1901 "--exclude-regex-file",
1902 "/path/top.txt",
1903 "dump",
1904 "--pid",
1905 "123",
1906 "--tid",
1907 "456",
1908 "--since",
1909 "10m ago",
1910 "--until",
1911 "5m ago",
1912 "--exclude-regex-file",
1913 "/path/sub.txt",
1914 ],
1915 )
1916 .unwrap();
1917
1918 assert_eq!(cmd_sub_override.pid(), Some(123));
1919 assert_eq!(cmd_sub_override.tid(), Some(456));
1920 assert_eq!(cmd_sub_override.exclude_regex_file(), Some("/path/sub.txt"));
1921 assert!(cmd_sub_override.since().is_some());
1922 assert!(cmd_sub_override.until().is_some());
1923
1924 let cmd_top_some = LogCommand::from_args(
1926 &["log"],
1927 &[
1928 "--pid",
1929 "11",
1930 "--tid",
1931 "22",
1932 "--since",
1933 "20m ago",
1934 "--until",
1935 "15m ago",
1936 "--exclude-regex-file",
1937 "/path/top.txt",
1938 "dump",
1939 ],
1940 )
1941 .unwrap();
1942
1943 assert_eq!(cmd_top_some.pid(), Some(11));
1944 assert_eq!(cmd_top_some.tid(), Some(22));
1945 assert_eq!(cmd_top_some.exclude_regex_file(), Some("/path/top.txt"));
1946 assert!(cmd_top_some.since().is_some());
1947 assert!(cmd_top_some.until().is_some());
1948 }
1949
1950 #[test]
1951 fn test_enum_subcommand_overlay_and_preservation() {
1952 #[cfg(not(target_os = "fuchsia"))]
1954 let cmd = LogCommand::from_args(
1955 &["log"],
1956 &["--severity", "warn", "--clock", "local", "--symbolize", "off", "dump"],
1957 )
1958 .unwrap();
1959
1960 #[cfg(target_os = "fuchsia")]
1961 let cmd = LogCommand::from_args(
1962 &["log"],
1963 &["--severity", "warn", "--clock", "local", "--encoding", "fxt", "dump"],
1964 )
1965 .unwrap();
1966
1967 assert_eq!(cmd.severity(), Severity::Warn);
1968 assert_eq!(cmd.clock(), TimeFormat::Local);
1969 #[cfg(not(target_os = "fuchsia"))]
1970 assert_eq!(cmd.symbolize(), SymbolizeMode::Off);
1971 #[cfg(target_os = "fuchsia")]
1972 assert_eq!(cmd.encoding(), LogEncoding::Fxt);
1973
1974 #[cfg(not(target_os = "fuchsia"))]
1976 let cmd_override = LogCommand::from_args(
1977 &["log"],
1978 &[
1979 "--severity",
1980 "warn",
1981 "--clock",
1982 "local",
1983 "--symbolize",
1984 "off",
1985 "dump",
1986 "--severity",
1987 "error",
1988 "--clock",
1989 "utc",
1990 "--symbolize",
1991 "classic",
1992 ],
1993 )
1994 .unwrap();
1995
1996 #[cfg(target_os = "fuchsia")]
1997 let cmd_override = LogCommand::from_args(
1998 &["log"],
1999 &[
2000 "--severity",
2001 "warn",
2002 "--clock",
2003 "local",
2004 "dump",
2005 "--severity",
2006 "error",
2007 "--clock",
2008 "utc",
2009 "--encoding",
2010 "fxt",
2011 ],
2012 )
2013 .unwrap();
2014
2015 assert_eq!(cmd_override.severity(), Severity::Error);
2016 assert_eq!(cmd_override.clock(), TimeFormat::Utc);
2017 #[cfg(not(target_os = "fuchsia"))]
2018 assert_eq!(cmd_override.symbolize(), SymbolizeMode::Classic);
2019 #[cfg(target_os = "fuchsia")]
2020 assert_eq!(cmd_override.encoding(), LogEncoding::Fxt);
2021 }
2022
2023 #[test]
2024 fn test_boolean_flag_cumulative_or() {
2025 let cmd = LogCommand::from_args(
2026 &["log"],
2027 &["--no-color", "--case-sensitive", "dump", "--hide-file", "--kernel"],
2028 )
2029 .unwrap();
2030
2031 assert!(cmd.no_color());
2032 assert!(cmd.case_sensitive());
2033 assert!(cmd.hide_file());
2034 assert!(cmd.kernel());
2035 assert!(!cmd.hide_tags());
2036 assert!(!cmd.show_metadata());
2037
2038 let cmd_both = LogCommand::from_args(&["log"], &["--kernel", "dump", "--kernel"]).unwrap();
2040
2041 assert!(cmd_both.kernel());
2042 }
2043
2044 #[test]
2045 fn test_log_command_accessors() {
2046 let default_cmd = LogCommand::default();
2048 assert_eq!(default_cmd.pid(), None);
2049 assert_eq!(default_cmd.tid(), None);
2050 assert!(!default_cmd.hide_file());
2051 #[cfg(target_os = "fuchsia")]
2052 assert!(!default_cmd.json());
2053 assert!(default_cmd.exclude().is_empty());
2054 assert!(default_cmd.exclude_regex().is_empty());
2055 assert_eq!(default_cmd.since(), None);
2056 assert_eq!(default_cmd.until(), None);
2057 assert!(!default_cmd.kernel());
2058 assert!(!default_cmd.case_sensitive());
2059 #[cfg(not(target_os = "fuchsia"))]
2060 assert!(!default_cmd.disable_reconnect());
2061
2062 #[cfg(not(target_os = "fuchsia"))]
2064 let args = &[
2065 "--pid",
2066 "123",
2067 "--tid",
2068 "456",
2069 "--hide-file",
2070 "--exclude",
2071 "bad_tag",
2072 "--exclude",
2073 "other_tag",
2074 "--exclude-regex",
2075 "^error.*",
2076 "--since",
2077 "10m ago",
2078 "--until",
2079 "5m ago",
2080 "--kernel",
2081 "--case-sensitive",
2082 "--disable-reconnect",
2083 ];
2084
2085 #[cfg(target_os = "fuchsia")]
2086 let args = &[
2087 "--pid",
2088 "123",
2089 "--tid",
2090 "456",
2091 "--hide-file",
2092 "--exclude",
2093 "bad_tag",
2094 "--exclude",
2095 "other_tag",
2096 "--exclude-regex",
2097 "^error.*",
2098 "--since",
2099 "10m ago",
2100 "--until",
2101 "5m ago",
2102 "--kernel",
2103 "--case-sensitive",
2104 "--json",
2105 ];
2106
2107 let cmd = LogCommand::from_args(&["log"], args).unwrap();
2108
2109 assert_eq!(cmd.pid(), Some(123));
2110 assert_eq!(cmd.tid(), Some(456));
2111 assert!(cmd.hide_file());
2112 #[cfg(target_os = "fuchsia")]
2113 assert!(cmd.json());
2114 assert_eq!(cmd.exclude(), &["bad_tag".to_string(), "other_tag".to_string()]);
2115 assert_eq!(cmd.exclude_regex(), &["^error.*".to_string()]);
2116 assert!(cmd.since().is_some());
2117 assert!(cmd.until().is_some());
2118 assert!(cmd.kernel());
2119 assert!(cmd.case_sensitive());
2120 #[cfg(not(target_os = "fuchsia"))]
2121 assert!(cmd.disable_reconnect());
2122 }
2123
2124 #[test]
2125 fn test_dump_diagnostics_unsupported_flags() {
2126 let err = LogCommand::from_args(&["ffx", "log"], &["--dump"]).unwrap_err();
2127 assert!(err.status.is_err());
2128 assert!(err.output.contains("Unrecognized argument: --dump"));
2129 assert!(err.output.contains(
2130 "Note: 'dump' is a sub-command of 'ffx log', not a flag. Use: ffx log dump [options]"
2131 ));
2132
2133 let err = LogCommand::from_args(&["ffx", "log"], &["dump", "--limit", "100"]).unwrap_err();
2134 assert!(err.status.is_err());
2135 assert!(err.output.contains("Unrecognized argument: --limit"));
2136 assert!(err.output.contains(
2137 "Note: 'ffx log dump' does not take --limit. To limit output lines, use '--tail <count>' or pipe to head/tail."
2138 ));
2139
2140 let err =
2141 LogCommand::from_args(&["ffx", "log"], &["dump", "--limit", "100", "--limit", "200"])
2142 .unwrap_err();
2143 assert!(err.status.is_err());
2144 assert_eq!(err.output.matches("Note: 'ffx log dump' does not take --limit.").count(), 1);
2145
2146 let err = LogCommand::from_args(&["ffx", "log"], &["dump", "--grep", "hello"]).unwrap_err();
2147 assert!(err.status.is_err());
2148 assert!(err.output.contains("Unrecognized argument: --grep"));
2149 assert!(err.output.contains(
2150 "Note: 'ffx log dump' does not take --grep. To filter log snapshot output, use '--filter <pattern>' or pipe to grep: ffx log dump | grep <pattern>"
2151 ));
2152 }
2153}