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