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