1use crate::filter::LogFilterCriteria;
6use crate::log_socket_stream::{JsonDeserializeError, LogsDataStream};
7use crate::{
8 DetailedDateTime, InstanceGetter, LogCommand, LogError, LogProcessingResult, LogSubCommand,
9 TimeFormat,
10};
11use anyhow::Result;
12use diagnostics_data::{
13 Data, LogTextColor, LogTextDisplayOptions, LogTextPresenter, LogTimeDisplayFormat, Logs,
14 LogsData, LogsDataBuilder, LogsField, LogsProperty, Severity, Timezone,
15};
16use futures_util::future::Either;
17use futures_util::stream::FuturesUnordered;
18use futures_util::{StreamExt, select};
19use serde::{Deserialize, Serialize};
20use std::fmt::Display;
21use std::future::Future;
22use std::io::Write;
23use std::time::Duration;
24use thiserror::Error;
25use writer::ToolIO;
26
27pub use diagnostics_data::Timestamp;
28
29pub const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
30
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33pub enum LogData {
34 TargetLog(LogsData),
36}
37
38impl LogData {
39 pub fn as_target_log(&self) -> Option<&LogsData> {
41 match self {
42 LogData::TargetLog(log) => Some(log),
43 }
44 }
45
46 pub fn as_target_log_mut(&mut self) -> Option<&mut LogsData> {
47 match self {
48 LogData::TargetLog(log) => Some(log),
49 }
50 }
51}
52
53impl From<LogsData> for LogData {
54 fn from(data: LogsData) -> Self {
55 Self::TargetLog(data)
56 }
57}
58
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
62pub struct LogEntry {
63 pub data: LogData,
65}
66
67impl LogEntry {
68 fn utc_timestamp(&self, boot_ts: Option<Timestamp>) -> Timestamp {
69 Timestamp::from_nanos(match &self.data {
70 LogData::TargetLog(data) => {
71 data.metadata.timestamp.into_nanos()
72 + boot_ts.map(|value| value.into_nanos()).unwrap_or(0)
73 }
74 })
75 }
76}
77
78impl Display for LogEntry {
82 fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 unreachable!("UNSUPPORTED -- This type cannot be formatted with std format.");
84 }
85}
86
87pub trait Symbolize {
89 fn symbolize(&self, entry: LogEntry) -> impl Future<Output = Option<LogEntry>>;
93}
94
95impl<S: Symbolize> Symbolize for Option<S> {
96 async fn symbolize(&self, entry: LogEntry) -> Option<LogEntry> {
97 match self {
98 Some(s) => s.symbolize(entry).await,
99 None => Some(entry),
100 }
101 }
102}
103
104impl<S: Symbolize + ?Sized> Symbolize for &S {
105 fn symbolize(&self, entry: LogEntry) -> impl Future<Output = Option<LogEntry>> {
106 (**self).symbolize(entry)
107 }
108}
109
110async fn handle_value<S>(one: Data<Logs>, symbolizer: &S) -> Option<LogEntry>
111where
112 S: Symbolize + ?Sized,
113{
114 let entry = LogEntry { data: one.into() };
115 symbolizer.symbolize(entry).await
116}
117
118fn generate_timestamp_message(boot_timestamp: Timestamp) -> LogEntry {
119 LogEntry {
120 data: LogData::TargetLog(
121 LogsDataBuilder::new(diagnostics_data::BuilderArgs {
122 moniker: "ffx".try_into().unwrap(),
123 timestamp: Timestamp::from_nanos(0),
124 component_url: Some("ffx".into()),
125 severity: Severity::Info,
126 })
127 .set_message("Logging started")
128 .add_key(LogsProperty::String(
129 LogsField::Other("utc_time_now".into()),
130 chrono::Utc::now().to_rfc3339(),
131 ))
132 .add_key(LogsProperty::Int(
133 LogsField::Other("current_boot_timestamp".to_string()),
134 boot_timestamp.into_nanos(),
135 ))
136 .build(),
137 ),
138 }
139}
140
141pub async fn dump_logs_from_socket<F, S>(
143 socket: flex_client::AsyncSocket,
144 formatter: &mut F,
145 symbolizer: &S,
146 include_timestamp: bool,
147) -> Result<LogProcessingResult, JsonDeserializeError>
148where
149 F: LogFormatter + BootTimeAccessor,
150 S: Symbolize + ?Sized,
151{
152 let mut decoder = Box::pin(LogsDataStream::new(socket).fuse());
153 let mut symbolize_pending = FuturesUnordered::new();
154 if include_timestamp && !formatter.is_utc_time_format() {
155 formatter.push_log(generate_timestamp_message(formatter.get_boot_timestamp())).await?;
156 }
157 while let Some(value) = select! {
158 res = decoder.next() => Some(Either::Left(res)),
159 res = symbolize_pending.next() => Some(Either::Right(res)),
160 complete => None,
161 } {
162 match value {
163 Either::Left(Some(result)) => match result {
164 Ok(log) => symbolize_pending.push(handle_value(log, symbolizer)),
165 Err(e) => return Err(e),
166 },
167 Either::Right(Some(Some(symbolized))) => match formatter.push_log(symbolized).await? {
168 LogProcessingResult::Exit => {
169 formatter.flush().await?;
170 return Ok(LogProcessingResult::Exit);
171 }
172 LogProcessingResult::Continue => {}
173 },
174 _ => {}
175 }
176 }
177 formatter.flush().await?;
178 Ok(LogProcessingResult::Continue)
179}
180
181pub async fn dump_fxt_logs_from_socket<F, S>(
183 socket: flex_client::AsyncSocket,
184 formatter: &mut F,
185 symbolizer: &S,
186 include_timestamp: bool,
187) -> Result<LogProcessingResult, LogError>
188where
189 F: LogFormatter + BootTimeAccessor,
190 S: Symbolize + ?Sized,
191{
192 let streamer = crate::fxt_streamer::FxtStreamer::new(socket);
193 let mut decoder = std::pin::pin!(streamer.stream());
194 let mut symbolize_pending = FuturesUnordered::new();
195 if include_timestamp && !formatter.is_utc_time_format() {
196 formatter.push_log(generate_timestamp_message(formatter.get_boot_timestamp())).await?;
197 }
198 while let Some(value) = select! {
199 res = decoder.next() => Some(Either::Left(res)),
200 res = symbolize_pending.next() => Some(Either::Right(res)),
201 complete => None,
202 } {
203 match value {
204 Either::Left(Some(result)) => match result {
205 Ok(log) => symbolize_pending.push(handle_value(log, symbolizer)),
206 Err(e) => return Err(e),
207 },
208 Either::Right(Some(Some(symbolized))) => match formatter.push_log(symbolized).await? {
209 LogProcessingResult::Exit => {
210 formatter.flush().await?;
211 return Ok(LogProcessingResult::Exit);
212 }
213 LogProcessingResult::Continue => {}
214 },
215 _ => {}
216 }
217 }
218 formatter.flush().await?;
219 Ok(LogProcessingResult::Continue)
220}
221
222pub trait BootTimeAccessor {
223 fn set_boot_timestamp(&mut self, _boot_ts_nanos: Timestamp);
225
226 fn get_boot_timestamp(&self) -> Timestamp;
228}
229
230#[derive(Clone, Debug)]
232pub struct DeviceOrLocalTimestamp {
233 pub timestamp: Timestamp,
235 pub is_boot: bool,
238}
239
240impl DeviceOrLocalTimestamp {
241 pub fn new(
245 rtc: Option<&DetailedDateTime>,
246 boot: Option<&Duration>,
247 ) -> Option<DeviceOrLocalTimestamp> {
248 rtc.as_ref()
249 .filter(|value| !value.is_now)
250 .map(|value| DeviceOrLocalTimestamp {
251 timestamp: Timestamp::from_nanos(
252 value.naive_utc().and_utc().timestamp_nanos_opt().unwrap(),
253 ),
254 is_boot: false,
255 })
256 .or_else(|| {
257 boot.map(|value| DeviceOrLocalTimestamp {
258 timestamp: Timestamp::from_nanos(value.as_nanos() as i64),
259 is_boot: true,
260 })
261 })
262 }
263}
264
265#[derive(Clone, Debug)]
267pub struct LogFormatterOptions {
268 pub display: Option<LogTextDisplayOptions>,
270 pub since: Option<DeviceOrLocalTimestamp>,
272 pub until: Option<DeviceOrLocalTimestamp>,
274 pub tail: Option<usize>,
276 pub sort: bool,
278}
279
280impl Default for LogFormatterOptions {
281 fn default() -> Self {
282 LogFormatterOptions {
283 display: Some(Default::default()),
284 since: None,
285 until: None,
286 tail: None,
287 sort: false,
288 }
289 }
290}
291
292#[derive(Error, Debug)]
294pub enum FormatterError {
295 #[error(transparent)]
297 Other(#[from] anyhow::Error),
298 #[error(transparent)]
300 IO(#[from] std::io::Error),
301}
302
303impl FormatterError {
304 pub fn is_broken_pipe(&self) -> bool {
305 match self {
306 FormatterError::IO(error) => error.kind() == std::io::ErrorKind::BrokenPipe,
307 FormatterError::Other(_) => false,
308 }
309 }
310}
311
312const REORDER_WINDOW_SIZE: usize = 64;
314
315const MAX_REORDER_SKEW_NANOS: i64 = 1_000_000_000;
319
320pub struct DefaultLogFormatter<W>
322where
323 W: Write + ToolIO<OutputItem = LogEntry>,
324{
325 writer: W,
326 filters: LogFilterCriteria,
327 options: LogFormatterOptions,
328 boot_ts_nanos: Option<Timestamp>,
329 last_timestamp: Option<Timestamp>,
332 tail_queue: std::collections::VecDeque<LogEntry>,
333 tail_limit: Option<usize>,
334}
335
336fn utc_to_boot(boot_ts: Timestamp, utc: Timestamp) -> Timestamp {
338 Timestamp::from_nanos(utc.into_nanos() - boot_ts.into_nanos())
339}
340
341impl<W> LogFormatter for DefaultLogFormatter<W>
342where
343 W: Write + ToolIO<OutputItem = LogEntry>,
344{
345 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError> {
346 self.push_log_internal(log_entry, true).await.or_else(|err| {
347 if err.is_broken_pipe() { Ok(LogProcessingResult::Exit) } else { Err(err) }
348 })
349 }
350
351 fn is_utc_time_format(&self) -> bool {
352 self.options.display.iter().any(|options| match options.time_format {
353 LogTimeDisplayFormat::Original => false,
354 LogTimeDisplayFormat::WallTime { tz, offset: _ } => tz == Timezone::Utc,
355 })
356 }
357
358 async fn flush(&mut self) -> Result<(), LogError> {
359 self.flush_tail().await
360 }
361}
362
363impl<W> BootTimeAccessor for DefaultLogFormatter<W>
364where
365 W: Write + ToolIO<OutputItem = LogEntry>,
366{
367 fn set_boot_timestamp(&mut self, boot_ts_nanos: Timestamp) {
368 if let Some(LogTextDisplayOptions {
369 time_format: LogTimeDisplayFormat::WallTime { offset, .. },
370 ..
371 }) = &mut self.options.display
372 {
373 *offset = boot_ts_nanos.into_nanos();
374 }
375 self.boot_ts_nanos = Some(boot_ts_nanos);
376 self.last_timestamp = None;
379 }
380 fn get_boot_timestamp(&self) -> Timestamp {
381 debug_assert!(self.boot_ts_nanos.is_some());
382 self.boot_ts_nanos.unwrap_or_else(|| Timestamp::from_nanos(0))
383 }
384}
385
386pub trait WriterContainer<W>
388where
389 W: Write + ToolIO<OutputItem = LogEntry>,
390{
391 fn writer(&mut self) -> &mut W;
392}
393
394impl<W> WriterContainer<W> for DefaultLogFormatter<W>
395where
396 W: Write + ToolIO<OutputItem = LogEntry>,
397{
398 fn writer(&mut self) -> &mut W {
399 &mut self.writer
400 }
401}
402
403impl<W> DefaultLogFormatter<W>
404where
405 W: Write + ToolIO<OutputItem = LogEntry>,
406{
407 pub fn new(filters: LogFilterCriteria, writer: W, options: LogFormatterOptions) -> Self {
409 let tail_limit = options.tail;
410 Self {
411 filters,
412 writer,
413 options,
414 boot_ts_nanos: None,
415 last_timestamp: None,
416 tail_queue: std::collections::VecDeque::new(),
417 tail_limit,
418 }
419 }
420
421 pub async fn expand_monikers(&mut self, getter: &impl InstanceGetter) -> Result<(), LogError> {
422 let warnings = self.filters.expand_monikers(getter).await?;
423 for warning in warnings {
424 writeln!(
425 self.writer.stderr(),
426 "WARN: Provided moniker '{}' was not an exact match. Using fuzzy match '{}' instead. Please check your component topology variations.",
427 warning.query,
428 warning.resolved
429 )?;
430 }
431 Ok(())
432 }
433
434 pub async fn push_unfiltered_log(
435 &mut self,
436 log_entry: LogEntry,
437 ) -> Result<LogProcessingResult, LogError> {
438 self.push_log_internal(log_entry, false).await
439 }
440
441 fn insert_into_reorder_window(&mut self, log_entry: LogEntry) {
449 let LogData::TargetLog(new_data) = &log_entry.data;
450 let new_ts = new_data.metadata.timestamp.into_nanos();
451 let mut insert_pos = self.tail_queue.len();
452 while insert_pos > 0 {
453 let LogData::TargetLog(prev_data) = &self.tail_queue[insert_pos - 1].data;
454 let prev_ts = prev_data.metadata.timestamp.into_nanos();
455 let diff = prev_ts.saturating_sub(new_ts);
456 if diff > 0 && diff <= MAX_REORDER_SKEW_NANOS {
457 insert_pos -= 1;
458 } else {
459 break;
460 }
461 }
462 self.tail_queue.insert(insert_pos, log_entry);
463 }
464
465 async fn flush_tail(&mut self) -> Result<(), LogError> {
466 if let Some(limit) = self.tail_limit
470 && self.tail_queue.len() > limit
471 {
472 let drain_count = self.tail_queue.len() - limit;
473 self.tail_queue.drain(..drain_count);
474 }
475 while let Some(log_entry) = self.tail_queue.pop_front() {
476 if let Err(err) = self.write_log_entry(log_entry, true) {
477 if err.is_broken_pipe() {
478 return Ok(());
479 }
480 return Err(err);
481 }
482 }
483 Ok(())
484 }
485
486 fn write_log_entry(
487 &mut self,
488 log_entry: LogEntry,
489 enable_filters: bool,
490 ) -> Result<(), LogError> {
491 let is_out_of_order = if enable_filters {
492 let LogData::TargetLog(data) = &log_entry.data;
493 let ts = data.metadata.timestamp;
494 let out_of_order = self.last_timestamp.is_some_and(|last_ts| ts < last_ts);
498 self.last_timestamp = Some(ts);
499 out_of_order
500 } else {
501 false
502 };
503 match self.options.display {
504 Some(mut text_options) => {
505 text_options.highlight_timestamp = is_out_of_order;
506 let mut options_for_this_line_only = self.options.clone();
507 options_for_this_line_only.display = Some(text_options);
508 if !enable_filters
511 && let LogTimeDisplayFormat::WallTime { ref mut offset, .. } =
512 options_for_this_line_only.display.as_mut().unwrap().time_format
513 {
514 *offset = 1;
519 }
520 self.format_text_log(options_for_this_line_only, log_entry)
521 .map_err(LogError::FormatterError)?;
522 }
523 None => {
524 self.writer.item(&log_entry).map_err(|err| LogError::UnknownError(err.into()))?;
525 }
526 };
527 Ok(())
528 }
529
530 async fn push_log_internal(
531 &mut self,
532 log_entry: LogEntry,
533 enable_filters: bool,
534 ) -> Result<LogProcessingResult, LogError> {
535 if enable_filters {
536 if self.filter_by_timestamp(&log_entry, self.options.since.as_ref(), |a, b| a <= b) {
537 return Ok(LogProcessingResult::Continue);
538 }
539
540 if self.filter_by_timestamp(&log_entry, self.options.until.as_ref(), |a, b| a >= b) {
541 if self.options.sort {
542 return Ok(LogProcessingResult::Continue);
547 }
548 return Ok(LogProcessingResult::Exit);
549 }
550
551 if !self.filters.matches(&log_entry) {
552 return Ok(LogProcessingResult::Continue);
553 }
554 }
555
556 if self.options.sort {
557 self.insert_into_reorder_window(log_entry);
558 if let Some(limit) = self.tail_limit {
559 let window = limit.max(REORDER_WINDOW_SIZE);
562 while self.tail_queue.len() > window {
563 self.tail_queue.pop_front();
564 }
565 } else {
566 while self.tail_queue.len() > REORDER_WINDOW_SIZE {
569 let oldest = self.tail_queue.pop_front().unwrap();
570 self.write_log_entry(oldest, enable_filters)?;
571 }
572 }
573 } else if let Some(limit) = self.tail_limit {
574 self.tail_queue.push_back(log_entry);
575 if self.tail_queue.len() > limit {
576 self.tail_queue.pop_front();
577 }
578 } else {
579 self.write_log_entry(log_entry, enable_filters)?;
580 }
581 Ok(LogProcessingResult::Continue)
582 }
583
584 pub fn new_from_args(cmd: &LogCommand, writer: W) -> Result<Self, LogError> {
586 let is_json = writer.is_machine();
587
588 Ok(DefaultLogFormatter::new(
589 LogFilterCriteria::try_from(cmd.clone())?,
590 writer,
591 LogFormatterOptions {
592 display: if is_json {
593 None
594 } else {
595 Some(LogTextDisplayOptions {
596 show_tags: !cmd.hide_tags(),
597 color: if cmd.no_color() {
598 LogTextColor::None
599 } else {
600 LogTextColor::BySeverity
601 },
602 show_metadata: cmd.show_metadata(),
603 time_format: match cmd.clock() {
604 TimeFormat::Boot => LogTimeDisplayFormat::Original,
605 TimeFormat::Local => LogTimeDisplayFormat::WallTime {
606 tz: Timezone::Local,
607 offset: 0,
610 },
611 TimeFormat::Utc => LogTimeDisplayFormat::WallTime {
612 tz: Timezone::Utc,
613 offset: 0,
616 },
617 },
618 show_file: !cmd.hide_file(),
619 show_moniker: !cmd.hide_moniker(),
620 show_full_moniker: cmd.show_full_moniker(),
621 prefer_url_component_name: cmd.prefer_url_component_name(),
622 highlight_timestamp: false,
623 })
624 },
625 since: DeviceOrLocalTimestamp::new(cmd.since(), cmd.since_boot().as_ref()),
626 until: DeviceOrLocalTimestamp::new(cmd.until(), cmd.until_boot().as_ref()),
627 tail: match &cmd.sub_command {
628 Some(LogSubCommand::Dump(dump)) => dump.tail,
629 _ => None,
630 },
631 sort: matches!(cmd.sub_command, Some(LogSubCommand::Dump(..))),
632 },
633 ))
634 }
635
636 fn filter_by_timestamp(
637 &self,
638 log_entry: &LogEntry,
639 timestamp: Option<&DeviceOrLocalTimestamp>,
640 callback: impl Fn(&Timestamp, &Timestamp) -> bool,
641 ) -> bool {
642 let Some(timestamp) = timestamp else {
643 return false;
644 };
645 if timestamp.is_boot {
646 callback(
647 &utc_to_boot(
648 self.get_boot_timestamp(),
649 log_entry.utc_timestamp(self.boot_ts_nanos),
650 ),
651 ×tamp.timestamp,
652 )
653 } else {
654 callback(&log_entry.utc_timestamp(self.boot_ts_nanos), ×tamp.timestamp)
655 }
656 }
657
658 fn format_text_log(
661 &mut self,
662 options: LogFormatterOptions,
663 log_entry: LogEntry,
664 ) -> Result<(), FormatterError> {
665 let text_options = match options.display {
666 Some(o) => o,
667 None => {
668 unreachable!("If we are here, we can only be formatting text");
669 }
670 };
671 match log_entry {
672 LogEntry { data: LogData::TargetLog(data), .. } => {
673 writeln!(self.writer, "{}", LogTextPresenter::new(&data, text_options))?;
676 }
677 }
678 Ok(())
679 }
680}
681
682#[allow(dead_code)] pub struct NoOpSymbolizer;
685
686impl Symbolize for NoOpSymbolizer {
687 async fn symbolize(&self, entry: LogEntry) -> Option<LogEntry> {
688 Some(entry)
689 }
690}
691
692pub trait LogFormatter {
694 fn push_log(
696 &mut self,
697 log_entry: LogEntry,
698 ) -> impl Future<Output = Result<LogProcessingResult, LogError>>;
699
700 fn is_utc_time_format(&self) -> bool;
702
703 fn flush(&mut self) -> impl Future<Output = Result<(), LogError>> {
705 async { Ok(()) }
706 }
707}
708
709#[cfg(test)]
710mod test {
711 use crate::parse_time;
712 use assert_matches::assert_matches;
713 use diagnostics_data::{LogsDataBuilder, Severity};
714 use std::cell::Cell;
715 use writer::{Format, JsonWriter, TestBuffers};
716
717 use super::*;
718
719 const DEFAULT_TS_NANOS: u64 = 1615535969000000000;
720
721 struct FakeFormatter {
722 logs: Vec<LogEntry>,
723 boot_timestamp: Timestamp,
724 is_utc_time_format: bool,
725 }
726
727 impl FakeFormatter {
728 fn new() -> Self {
729 Self {
730 logs: Vec::new(),
731 boot_timestamp: Timestamp::from_nanos(0),
732 is_utc_time_format: false,
733 }
734 }
735 }
736
737 impl BootTimeAccessor for FakeFormatter {
738 fn set_boot_timestamp(&mut self, boot_ts_nanos: Timestamp) {
739 self.boot_timestamp = boot_ts_nanos;
740 }
741
742 fn get_boot_timestamp(&self) -> Timestamp {
743 self.boot_timestamp
744 }
745 }
746
747 impl LogFormatter for FakeFormatter {
748 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError> {
749 self.logs.push(log_entry);
750 Ok(LogProcessingResult::Continue)
751 }
752
753 fn is_utc_time_format(&self) -> bool {
754 self.is_utc_time_format
755 }
756 }
757
758 pub struct FakeFuchsiaSymbolizer;
760
761 fn set_log_msg(entry: &mut LogEntry, msg: impl Into<String>) {
762 *entry.data.as_target_log_mut().unwrap().msg_mut().unwrap() = msg.into();
763 }
764
765 impl Symbolize for FakeFuchsiaSymbolizer {
766 async fn symbolize(&self, mut entry: LogEntry) -> Option<LogEntry> {
767 set_log_msg(&mut entry, "Fuchsia");
768 Some(entry)
769 }
770 }
771
772 struct FakeSymbolizerCallback {
773 should_discard: Cell<bool>,
774 }
775
776 impl FakeSymbolizerCallback {
777 fn new() -> Self {
778 Self { should_discard: Cell::new(true) }
779 }
780 }
781
782 async fn dump_logs_from_socket<F, S>(
783 socket: fuchsia_async::Socket,
784 formatter: &mut F,
785 symbolizer: &S,
786 ) -> Result<LogProcessingResult, JsonDeserializeError>
787 where
788 F: LogFormatter + BootTimeAccessor,
789 S: Symbolize + ?Sized,
790 {
791 super::dump_logs_from_socket(socket, formatter, symbolizer, false).await
792 }
793
794 impl Symbolize for FakeSymbolizerCallback {
795 async fn symbolize(&self, mut input: LogEntry) -> Option<LogEntry> {
796 self.should_discard.set(!self.should_discard.get());
797 if self.should_discard.get() {
798 None
799 } else {
800 set_log_msg(&mut input, "symbolized log");
801 Some(input)
802 }
803 }
804 }
805
806 #[fuchsia::test]
807 async fn test_boot_timestamp_setter() {
808 let buffers = TestBuffers::default();
809 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
810 let options = LogFormatterOptions {
811 display: Some(LogTextDisplayOptions {
812 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
813 ..Default::default()
814 }),
815 ..Default::default()
816 };
817 let mut formatter =
818 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
819 formatter.set_boot_timestamp(Timestamp::from_nanos(1234));
820 assert_eq!(formatter.get_boot_timestamp(), Timestamp::from_nanos(1234));
821
822 let buffers = TestBuffers::default();
824 let output = JsonWriter::<LogEntry>::new_test(None, &buffers);
825 let options = LogFormatterOptions { display: None, ..Default::default() };
826 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), output, options);
827 formatter.set_boot_timestamp(Timestamp::from_nanos(1234));
828 assert_eq!(formatter.get_boot_timestamp(), Timestamp::from_nanos(1234));
829 }
830
831 #[fuchsia::test]
832 async fn test_format_single_message() {
833 let symbolizer = NoOpSymbolizer {};
834 let mut formatter = FakeFormatter::new();
835 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
836 moniker: "ffx".try_into().unwrap(),
837 timestamp: Timestamp::from_nanos(0),
838 component_url: Some("ffx".into()),
839 severity: Severity::Info,
840 })
841 .set_message("Hello world!")
842 .build();
843 let (sender, receiver) = zx::Socket::create_stream();
844 sender
845 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
846 .expect("failed to write target log");
847 drop(sender);
848 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
849 .await
850 .unwrap();
851 assert_eq!(formatter.logs, vec![LogEntry { data: LogData::TargetLog(target_log) }]);
852 }
853
854 #[fuchsia::test]
855 async fn test_format_utc_timestamp() {
856 let symbolizer = NoOpSymbolizer {};
857 let mut formatter = FakeFormatter::new();
858 formatter.set_boot_timestamp(Timestamp::from_nanos(DEFAULT_TS_NANOS as i64));
859 let (_, receiver) = zx::Socket::create_stream();
860 super::dump_logs_from_socket(
861 flex_client::socket_to_async(receiver),
862 &mut formatter,
863 &symbolizer,
864 true,
865 )
866 .await
867 .unwrap();
868 let target_log = formatter.logs[0].data.as_target_log().unwrap();
869 let properties = target_log.payload_keys().unwrap();
870 assert_eq!(target_log.msg().unwrap(), "Logging started");
871
872 chrono::DateTime::parse_from_rfc3339(
874 properties.get_property("utc_time_now").unwrap().string().unwrap(),
875 )
876 .unwrap();
877 assert_eq!(
878 properties.get_property("current_boot_timestamp").unwrap().int().unwrap(),
879 DEFAULT_TS_NANOS as i64
880 );
881 }
882
883 #[fuchsia::test]
884 async fn test_format_utc_timestamp_does_not_print_if_utc_time() {
885 let symbolizer = NoOpSymbolizer {};
886 let mut formatter = FakeFormatter::new();
887 formatter.is_utc_time_format = true;
888 formatter.set_boot_timestamp(Timestamp::from_nanos(DEFAULT_TS_NANOS as i64));
889 let (_, receiver) = zx::Socket::create_stream();
890 super::dump_logs_from_socket(
891 flex_client::socket_to_async(receiver),
892 &mut formatter,
893 &symbolizer,
894 true,
895 )
896 .await
897 .unwrap();
898 assert_eq!(formatter.logs.len(), 0);
899 }
900
901 #[fuchsia::test]
902 async fn test_format_multiple_messages() {
903 let symbolizer = NoOpSymbolizer {};
904 let mut formatter = FakeFormatter::new();
905 let (sender, receiver) = zx::Socket::create_stream();
906 let target_log_0 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
907 moniker: "ffx".try_into().unwrap(),
908 timestamp: Timestamp::from_nanos(0),
909 component_url: Some("ffx".into()),
910 severity: Severity::Info,
911 })
912 .set_message("Hello world!")
913 .set_pid(1)
914 .set_tid(2)
915 .build();
916 let target_log_1 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
917 moniker: "ffx".try_into().unwrap(),
918 timestamp: Timestamp::from_nanos(1),
919 component_url: Some("ffx".into()),
920 severity: Severity::Info,
921 })
922 .set_message("Hello world 2!")
923 .build();
924 sender
925 .write(serde_json::to_string(&vec![&target_log_0, &target_log_1]).unwrap().as_bytes())
926 .expect("failed to write target log");
927 drop(sender);
928 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
929 .await
930 .unwrap();
931 assert_eq!(
932 formatter.logs,
933 vec![
934 LogEntry { data: LogData::TargetLog(target_log_0) },
935 LogEntry { data: LogData::TargetLog(target_log_1) }
936 ]
937 );
938 }
939
940 #[fuchsia::test]
941 async fn test_format_timestamp_filter() {
942 let symbolizer = NoOpSymbolizer {};
944 let buffers = TestBuffers::default();
945 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
946 let mut formatter = DefaultLogFormatter::new(
947 LogFilterCriteria::default(),
948 stdout,
949 LogFormatterOptions {
950 since: Some(DeviceOrLocalTimestamp {
951 timestamp: Timestamp::from_nanos(1),
952 is_boot: true,
953 }),
954 until: Some(DeviceOrLocalTimestamp {
955 timestamp: Timestamp::from_nanos(3),
956 is_boot: true,
957 }),
958 ..Default::default()
959 },
960 );
961 formatter.set_boot_timestamp(Timestamp::from_nanos(0));
962
963 let (sender, receiver) = zx::Socket::create_stream();
964 let target_log_0 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
965 moniker: "ffx".try_into().unwrap(),
966 timestamp: Timestamp::from_nanos(0),
967 component_url: Some("ffx".into()),
968 severity: Severity::Info,
969 })
970 .set_message("Hello world!")
971 .build();
972 let target_log_1 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
973 moniker: "ffx".try_into().unwrap(),
974 timestamp: Timestamp::from_nanos(1),
975 component_url: Some("ffx".into()),
976 severity: Severity::Info,
977 })
978 .set_message("Hello world 2!")
979 .build();
980 let target_log_2 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
981 moniker: "ffx".try_into().unwrap(),
982 timestamp: Timestamp::from_nanos(2),
983 component_url: Some("ffx".into()),
984 severity: Severity::Info,
985 })
986 .set_pid(1)
987 .set_tid(2)
988 .set_message("Hello world 3!")
989 .build();
990 let target_log_3 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
991 moniker: "ffx".try_into().unwrap(),
992 timestamp: Timestamp::from_nanos(3),
993 component_url: Some("ffx".into()),
994 severity: Severity::Info,
995 })
996 .set_message("Hello world 4!")
997 .set_pid(1)
998 .set_tid(2)
999 .build();
1000 sender
1001 .write(
1002 serde_json::to_string(&vec![
1003 &target_log_0,
1004 &target_log_1,
1005 &target_log_2,
1006 &target_log_3,
1007 ])
1008 .unwrap()
1009 .as_bytes(),
1010 )
1011 .expect("failed to write target log");
1012 drop(sender);
1013 assert_matches!(
1014 dump_logs_from_socket(
1015 flex_client::socket_to_async(receiver),
1016 &mut formatter,
1017 &symbolizer,
1018 )
1019 .await,
1020 Ok(LogProcessingResult::Exit)
1021 );
1022 assert_eq!(
1023 buffers.stdout.into_string(),
1024 "[00000.000000][1][2][ffx] INFO: Hello world 3!\n"
1025 );
1026 }
1027
1028 fn make_log_with_timestamp(timestamp: i64) -> LogsData {
1029 LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1030 moniker: "ffx".try_into().unwrap(),
1031 timestamp: Timestamp::from_nanos(timestamp),
1032 component_url: Some("ffx".into()),
1033 severity: Severity::Info,
1034 })
1035 .set_message(format!("Hello world {timestamp}!"))
1036 .set_pid(1)
1037 .set_tid(2)
1038 .build()
1039 }
1040
1041 #[fuchsia::test]
1042 async fn test_format_timestamp_filter_utc() {
1043 let symbolizer = NoOpSymbolizer {};
1045 let buffers = TestBuffers::default();
1046 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1047 let mut formatter = DefaultLogFormatter::new(
1048 LogFilterCriteria::default(),
1049 stdout,
1050 LogFormatterOptions {
1051 since: Some(DeviceOrLocalTimestamp {
1052 timestamp: Timestamp::from_nanos(1),
1053 is_boot: false,
1054 }),
1055 until: Some(DeviceOrLocalTimestamp {
1056 timestamp: Timestamp::from_nanos(3),
1057 is_boot: false,
1058 }),
1059 display: Some(LogTextDisplayOptions {
1060 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
1061 ..Default::default()
1062 }),
1063 ..Default::default()
1064 },
1065 );
1066 formatter.set_boot_timestamp(Timestamp::from_nanos(1));
1067
1068 let (sender, receiver) = zx::Socket::create_stream();
1069 let logs = (0..4).map(make_log_with_timestamp).collect::<Vec<_>>();
1070 sender
1071 .write(serde_json::to_string(&logs).unwrap().as_bytes())
1072 .expect("failed to write target log");
1073 drop(sender);
1074 assert_matches!(
1075 dump_logs_from_socket(
1076 flex_client::socket_to_async(receiver),
1077 &mut formatter,
1078 &symbolizer,
1079 )
1080 .await,
1081 Ok(LogProcessingResult::Exit)
1082 );
1083 assert_eq!(
1084 buffers.stdout.into_string(),
1085 "[1970-01-01 00:00:00.000][1][2][ffx] INFO: Hello world 1!\n"
1086 );
1087 }
1088
1089 fn logs_data_builder() -> LogsDataBuilder {
1090 diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1091 timestamp: Timestamp::from_nanos(default_ts().as_nanos() as i64),
1092 component_url: Some("component_url".into()),
1093 moniker: "some/moniker".try_into().unwrap(),
1094 severity: diagnostics_data::Severity::Warn,
1095 })
1096 .set_pid(1)
1097 .set_tid(2)
1098 }
1099
1100 fn default_ts() -> Duration {
1101 Duration::from_nanos(DEFAULT_TS_NANOS)
1102 }
1103
1104 fn log_entry() -> LogEntry {
1105 LogEntry {
1106 data: LogData::TargetLog(
1107 logs_data_builder().add_tag("tag1").add_tag("tag2").set_message("message").build(),
1108 ),
1109 }
1110 }
1111
1112 #[fuchsia::test]
1113 async fn test_default_formatter() {
1114 let buffers = TestBuffers::default();
1115 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1116 let options = LogFormatterOptions::default();
1117 let mut formatter =
1118 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1119 formatter.push_log(log_entry()).await.unwrap();
1120 drop(formatter);
1121 assert_eq!(
1122 buffers.into_stdout_str(),
1123 "[1615535969.000000][1][2][some/moniker][tag1,tag2] WARN: message\n"
1124 );
1125 }
1126
1127 #[fuchsia::test]
1128 async fn test_default_formatter_with_hidden_metadata() {
1129 let buffers = TestBuffers::default();
1130 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1131 let options = LogFormatterOptions {
1132 display: Some(LogTextDisplayOptions { show_metadata: false, ..Default::default() }),
1133 ..LogFormatterOptions::default()
1134 };
1135 let mut formatter =
1136 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1137 formatter.push_log(log_entry()).await.unwrap();
1138 drop(formatter);
1139 assert_eq!(
1140 buffers.into_stdout_str(),
1141 "[1615535969.000000][some/moniker][tag1,tag2] WARN: message\n"
1142 );
1143 }
1144
1145 #[fuchsia::test]
1146 async fn test_default_formatter_with_json() {
1147 let buffers = TestBuffers::default();
1148 let stdout = JsonWriter::<LogEntry>::new_test(Some(Format::Json), &buffers);
1149 let options = LogFormatterOptions { display: None, ..Default::default() };
1150 {
1151 let mut formatter =
1152 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1153 formatter.push_log(log_entry()).await.unwrap();
1154 }
1155 assert_eq!(
1156 serde_json::from_str::<LogEntry>(&buffers.into_stdout_str()).unwrap(),
1157 log_entry()
1158 );
1159 }
1160
1161 fn emit_log(sender: &mut zx::Socket, msg: &str, timestamp: i64) -> Data<Logs> {
1162 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1163 moniker: "ffx".try_into().unwrap(),
1164 timestamp: Timestamp::from_nanos(timestamp),
1165 component_url: Some("ffx".into()),
1166 severity: Severity::Info,
1167 })
1168 .set_message(msg)
1169 .build();
1170
1171 sender
1172 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
1173 .expect("failed to write target log");
1174 target_log
1175 }
1176
1177 #[fuchsia::test]
1178 async fn test_default_formatter_discards_when_told_by_symbolizer() {
1179 let mut formatter = FakeFormatter::new();
1180 let (mut sender, receiver) = zx::Socket::create_stream();
1181 let mut target_log_0 = emit_log(&mut sender, "Hello world!", 0);
1182 emit_log(&mut sender, "Dropped world!", 1);
1183 let mut target_log_2 = emit_log(&mut sender, "Hello world!", 2);
1184 emit_log(&mut sender, "Dropped world!", 3);
1185 let mut target_log_4 = emit_log(&mut sender, "Hello world!", 4);
1186 drop(sender);
1187 let symbolizer = FakeSymbolizerCallback::new();
1189 *target_log_0.msg_mut().unwrap() = "symbolized log".into();
1190 *target_log_2.msg_mut().unwrap() = "symbolized log".into();
1191 *target_log_4.msg_mut().unwrap() = "symbolized log".into();
1192 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
1193 .await
1194 .unwrap();
1195 assert_eq!(
1196 formatter.logs,
1197 vec![
1198 LogEntry { data: LogData::TargetLog(target_log_0) },
1199 LogEntry { data: LogData::TargetLog(target_log_2) },
1200 LogEntry { data: LogData::TargetLog(target_log_4) }
1201 ],
1202 );
1203 }
1204
1205 #[fuchsia::test]
1206 async fn test_symbolized_output() {
1207 let symbolizer = FakeFuchsiaSymbolizer;
1208 let buffers = TestBuffers::default();
1209 let output = JsonWriter::<LogEntry>::new_test(None, &buffers);
1210 let mut formatter = DefaultLogFormatter::new(
1211 LogFilterCriteria::default(),
1212 output,
1213 LogFormatterOptions { ..Default::default() },
1214 );
1215 formatter.set_boot_timestamp(Timestamp::from_nanos(0));
1216 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1217 moniker: "ffx".try_into().unwrap(),
1218 timestamp: Timestamp::from_nanos(0),
1219 component_url: Some("ffx".into()),
1220 severity: Severity::Info,
1221 })
1222 .set_pid(1)
1223 .set_tid(2)
1224 .set_message("Hello world!")
1225 .build();
1226 let (sender, receiver) = zx::Socket::create_stream();
1227 sender
1228 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
1229 .expect("failed to write target log");
1230 drop(sender);
1231 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
1232 .await
1233 .unwrap();
1234 assert_eq!(buffers.stdout.into_string(), "[00000.000000][1][2][ffx] INFO: Fuchsia\n");
1235 }
1236
1237 #[test]
1238 fn test_device_or_local_timestamp_returns_none_if_now_is_passed() {
1239 assert_matches!(DeviceOrLocalTimestamp::new(Some(&parse_time("now").unwrap()), None), None);
1240 }
1241
1242 struct BrokenPipeWriter;
1243 impl std::io::Write for BrokenPipeWriter {
1244 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
1245 Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1246 }
1247
1248 fn flush(&mut self) -> std::io::Result<()> {
1249 Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1250 }
1251 }
1252
1253 impl ToolIO for BrokenPipeWriter {
1254 type OutputItem = LogEntry;
1255 fn is_machine(&self) -> bool {
1256 false
1257 }
1258
1259 fn stderr(&mut self) -> &mut (dyn std::io::Write + Send + Sync) {
1260 self
1261 }
1262
1263 fn item(&mut self, _value: &Self::OutputItem) -> writer::Result<()> {
1264 Err(writer::Error::Io(std::io::Error::new(
1265 std::io::ErrorKind::BrokenPipe,
1266 "broken pipe",
1267 )))
1268 }
1269 }
1270
1271 #[fuchsia::test]
1272 async fn test_default_formatter_exits_on_broken_pipe() {
1273 let stdout = BrokenPipeWriter;
1274 let options = LogFormatterOptions::default();
1275 let mut formatter =
1276 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1277 let result = formatter.push_log(log_entry()).await;
1278 assert_matches!(result, Ok(LogProcessingResult::Exit));
1279 }
1280
1281 #[test]
1282 fn test_formatter_error_is_broken_pipe() {
1283 assert!(
1284 FormatterError::IO(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1285 .is_broken_pipe()
1286 );
1287 assert!(!FormatterError::IO(std::io::Error::other("other")).is_broken_pipe());
1288 assert!(!FormatterError::Other(anyhow::anyhow!("other")).is_broken_pipe());
1289 }
1290
1291 #[cfg(not(feature = "fdomain"))]
1292 #[fuchsia::test]
1293 async fn test_json_and_fxt_output_identical() {
1294 use diagnostics_data::ExtendedMoniker;
1295 use diagnostics_log_encoding::encode::{Encoder, EncoderOpts, ResizableBuffer};
1296 use diagnostics_log_encoding::{Argument, Header, LOG_CONTROL_BIT, MONIKER, Record, URL};
1297 use diagnostics_message::MonikerWithUrl;
1298 use flyweights::FlyStr;
1299 use zerocopy::{FromBytes, IntoBytes};
1300
1301 let symbolizer = NoOpSymbolizer {};
1302 let options = LogFormatterOptions::default();
1303
1304 let fn_encode = |record: Record<'_>, tag: u32| -> Vec<u8> {
1305 let mut encoder = Encoder::new(
1306 std::io::Cursor::new(ResizableBuffer::from(Vec::new())),
1307 EncoderOpts::default(),
1308 );
1309 encoder.write_record(record).unwrap();
1310 let mut bytes = encoder.take().into_inner().into_inner();
1311 let mut header = Header::read_from_bytes(&bytes[0..8]).unwrap();
1312 header.set_tag(tag);
1313 bytes[0..8].copy_from_slice(header.as_bytes());
1314 bytes
1315 };
1316
1317 let manifest_bytes = fn_encode(
1318 Record {
1319 timestamp: zx::BootInstant::from_nanos(0),
1320 severity: 0x30, arguments: vec![
1322 Argument::other(MONIKER, "core/foo"),
1323 Argument::other(URL, "fuchsia-pkg://foo"),
1324 ],
1325 },
1326 1 | LOG_CONTROL_BIT,
1327 );
1328
1329 let log_bytes1 = fn_encode(
1330 Record {
1331 timestamp: zx::BootInstant::from_nanos(123456),
1332 severity: 0x30, arguments: vec![
1334 Argument::pid(zx::Koid::from_raw(1000)),
1335 Argument::tid(zx::Koid::from_raw(2000)),
1336 Argument::tag("my_tag"),
1337 Argument::message("Hello identical world!"),
1338 ],
1339 },
1340 1,
1341 );
1342
1343 let log_bytes2 = fn_encode(
1344 Record {
1345 timestamp: zx::BootInstant::from_nanos(123457),
1346 severity: 0x40, arguments: vec![
1348 Argument::file("src/main.rs"),
1349 Argument::line(42),
1350 Argument::dropped(5),
1351 Argument::message("Warning with source location and dropped count"),
1352 ],
1353 },
1354 1,
1355 );
1356
1357 let log_bytes3_signed = fn_encode(
1358 Record {
1359 timestamp: zx::BootInstant::from_nanos(123458),
1360 severity: 0x50, arguments: vec![
1362 Argument::message("Error with custom signed int"),
1363 Argument::new("signed_val", -12345i64),
1364 ],
1365 },
1366 1,
1367 );
1368
1369 let log_bytes3_unsigned = fn_encode(
1370 Record {
1371 timestamp: zx::BootInstant::from_nanos(123458),
1372 severity: 0x50, arguments: vec![
1374 Argument::message("Error with custom unsigned int"),
1375 Argument::new("unsigned_val", 67890u64),
1376 ],
1377 },
1378 1,
1379 );
1380
1381 let log_bytes3_bool = fn_encode(
1382 Record {
1383 timestamp: zx::BootInstant::from_nanos(123458),
1384 severity: 0x50, arguments: vec![
1386 Argument::message("Error with custom boolean"),
1387 Argument::new("bool_val", true),
1388 ],
1389 },
1390 1,
1391 );
1392
1393 let log_bytes3_str = fn_encode(
1394 Record {
1395 timestamp: zx::BootInstant::from_nanos(123458),
1396 severity: 0x50, arguments: vec![
1398 Argument::message("Error with custom string"),
1399 Argument::new("string_val", "custom string"),
1400 ],
1401 },
1402 1,
1403 );
1404
1405 let log_bytes4_pi = fn_encode(
1406 Record {
1407 timestamp: zx::BootInstant::from_nanos(123459),
1408 severity: 0x60, arguments: vec![
1410 Argument::message("Fatal with float pi"),
1411 Argument::new("float_pi", std::f64::consts::PI),
1412 ],
1413 },
1414 1,
1415 );
1416
1417 let log_bytes4_zero = fn_encode(
1418 Record {
1419 timestamp: zx::BootInstant::from_nanos(123459),
1420 severity: 0x60, arguments: vec![
1422 Argument::message("Fatal with float zero"),
1423 Argument::new("float_zero", 0.0f64),
1424 ],
1425 },
1426 1,
1427 );
1428
1429 let log_bytes4_large = fn_encode(
1430 Record {
1431 timestamp: zx::BootInstant::from_nanos(123459),
1432 severity: 0x60, arguments: vec![
1434 Argument::message("Fatal with float large"),
1435 Argument::new("float_large", 123456.789f64),
1436 ],
1437 },
1438 1,
1439 );
1440
1441 let all_records = [
1442 &log_bytes1,
1443 &log_bytes2,
1444 &log_bytes3_signed,
1445 &log_bytes3_unsigned,
1446 &log_bytes3_bool,
1447 &log_bytes3_str,
1448 &log_bytes4_pi,
1449 &log_bytes4_zero,
1450 &log_bytes4_large,
1451 ];
1452
1453 let buffers_json = TestBuffers::default();
1455 let stdout_json = JsonWriter::<LogEntry>::new_test(None, &buffers_json);
1456 let mut formatter_json =
1457 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout_json, options.clone());
1458 formatter_json.set_boot_timestamp(Timestamp::from_nanos(0));
1459
1460 let source = MonikerWithUrl {
1461 moniker: ExtendedMoniker::parse_str("core/foo").unwrap(),
1462 url: FlyStr::new("fuchsia-pkg://foo"),
1463 };
1464
1465 let (sender_json, receiver_json) = zx::Socket::create_stream();
1466 let mut json_stream_bytes = Vec::new();
1467 for record_bytes in &all_records {
1468 let target_log =
1469 diagnostics_message::from_structured(source.clone(), record_bytes).unwrap();
1470 serde_json::to_writer(&mut json_stream_bytes, &target_log).unwrap();
1471 json_stream_bytes.push(b'\n');
1472 }
1473 sender_json.write(&json_stream_bytes).expect("failed to write target logs");
1474 drop(sender_json);
1475
1476 super::dump_logs_from_socket(
1477 flex_client::socket_to_async(receiver_json),
1478 &mut formatter_json,
1479 &symbolizer,
1480 false,
1481 )
1482 .await
1483 .unwrap();
1484
1485 let buffers_fxt = TestBuffers::default();
1487 let stdout_fxt = JsonWriter::<LogEntry>::new_test(None, &buffers_fxt);
1488 let mut formatter_fxt =
1489 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout_fxt, options.clone());
1490 formatter_fxt.set_boot_timestamp(Timestamp::from_nanos(0));
1491
1492 let (sender_fxt, receiver_fxt) = zx::Socket::create_stream();
1493 sender_fxt.write(&manifest_bytes).unwrap();
1494 for record_bytes in &all_records {
1495 sender_fxt.write(record_bytes).unwrap();
1496 }
1497 drop(sender_fxt);
1498
1499 super::dump_fxt_logs_from_socket(
1500 flex_client::socket_to_async(receiver_fxt),
1501 &mut formatter_fxt,
1502 &symbolizer,
1503 false,
1504 )
1505 .await
1506 .unwrap();
1507
1508 assert_eq!(buffers_json.stdout.into_string(), buffers_fxt.stdout.into_string());
1510 }
1511
1512 #[fuchsia::test]
1513 async fn test_default_formatter_tail_limit() {
1514 let buffers = TestBuffers::default();
1515 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1516 let options = LogFormatterOptions { tail: Some(2), ..Default::default() };
1517 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options);
1518
1519 let entry1 = LogEntry {
1520 data: LogData::TargetLog(
1521 logs_data_builder().add_tag("tag1").set_message("msg1").build(),
1522 ),
1523 };
1524 let entry2 = LogEntry {
1525 data: LogData::TargetLog(
1526 logs_data_builder().add_tag("tag1").set_message("msg2").build(),
1527 ),
1528 };
1529 let entry3 = LogEntry {
1530 data: LogData::TargetLog(
1531 logs_data_builder().add_tag("tag1").set_message("msg3").build(),
1532 ),
1533 };
1534
1535 formatter.push_log(entry1).await.unwrap();
1536 formatter.push_log(entry2).await.unwrap();
1537 formatter.push_log(entry3).await.unwrap();
1538
1539 let stdout_snapshot = buffers.stdout.clone().into_string();
1541 assert_eq!(stdout_snapshot, "");
1542
1543 formatter.flush().await.unwrap();
1544
1545 let output = buffers.into_stdout_str();
1547 assert!(!output.contains("msg1"));
1548 assert!(output.contains("msg2"));
1549 assert!(output.contains("msg3"));
1550 }
1551
1552 #[fuchsia::test]
1553 async fn test_default_formatter_sorts_when_enabled() {
1554 let buffers = TestBuffers::default();
1555 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1556 let options = LogFormatterOptions {
1557 display: Some(LogTextDisplayOptions {
1558 color: LogTextColor::BySeverity,
1559 show_metadata: false,
1560 ..Default::default()
1561 }),
1562 sort: true,
1563 ..Default::default()
1564 };
1565 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options);
1566
1567 formatter
1568 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_300_000_000)) })
1569 .await
1570 .unwrap();
1571 formatter
1572 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_100_000_000)) })
1573 .await
1574 .unwrap();
1575 formatter
1576 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_200_000_000)) })
1577 .await
1578 .unwrap();
1579
1580 assert_eq!(buffers.stdout.clone().into_string(), "");
1581 formatter.flush().await.unwrap();
1582
1583 assert_eq!(
1584 buffers.into_stdout_str(),
1585 concat!(
1586 "[00001.100000][ffx] INFO: Hello world 1100000000!\u{1b}[m\n",
1587 "[00001.200000][ffx] INFO: Hello world 1200000000!\u{1b}[m\n",
1588 "[00001.300000][ffx] INFO: Hello world 1300000000!\u{1b}[m\n",
1589 )
1590 );
1591 }
1592
1593 #[fuchsia::test]
1594 async fn test_default_formatter_sort_window_flushes_progressively() {
1595 let buffers = TestBuffers::default();
1596 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1597 let options = LogFormatterOptions {
1598 display: Some(LogTextDisplayOptions { show_metadata: false, ..Default::default() }),
1599 sort: true,
1600 ..Default::default()
1601 };
1602 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options);
1603
1604 formatter
1605 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_200_000_000)) })
1606 .await
1607 .unwrap();
1608 formatter
1609 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_100_000_000)) })
1610 .await
1611 .unwrap();
1612 for _ in 0..64 {
1613 formatter
1614 .push_log(LogEntry {
1615 data: LogData::TargetLog(make_log_with_timestamp(1_300_000_000)),
1616 })
1617 .await
1618 .unwrap();
1619 }
1620
1621 assert_eq!(
1624 buffers.stdout.clone().into_string(),
1625 concat!(
1626 "[00001.100000][ffx] INFO: Hello world 1100000000!\n",
1627 "[00001.200000][ffx] INFO: Hello world 1200000000!\n",
1628 )
1629 );
1630 }
1631
1632 #[fuchsia::test]
1633 async fn test_default_formatter_does_not_reorder_bogus_timestamps() {
1634 let buffers = TestBuffers::default();
1635 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1636 let options = LogFormatterOptions {
1637 display: Some(LogTextDisplayOptions {
1638 color: LogTextColor::BySeverity,
1639 show_metadata: false,
1640 ..Default::default()
1641 }),
1642 sort: true,
1643 ..Default::default()
1644 };
1645 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options);
1646
1647 for ts in [
1648 10_000_000_000,
1649 999_000_000_000, 999_100_000_000, 10_200_000_000,
1652 10_100_000_000, 1_000_000_000, 10_300_000_000,
1655 ] {
1656 formatter
1657 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(ts)) })
1658 .await
1659 .unwrap();
1660 }
1661 formatter.flush().await.unwrap();
1662
1663 assert_eq!(
1664 buffers.into_stdout_str(),
1665 concat!(
1666 "[00010.000000][ffx] INFO: Hello world 10000000000!\u{1b}[m\n",
1667 "[00999.000000][ffx] INFO: Hello world 999000000000!\u{1b}[m\n",
1668 "[00999.100000][ffx] INFO: Hello world 999100000000!\u{1b}[m\n",
1669 "\u{1b}[38;5;6m[00010.100000]\u{1b}[m[ffx] INFO: Hello world 10100000000!\u{1b}[m\n",
1670 "[00010.200000][ffx] INFO: Hello world 10200000000!\u{1b}[m\n",
1671 "\u{1b}[38;5;6m[00001.000000]\u{1b}[m[ffx] INFO: Hello world 1000000000!\u{1b}[m\n",
1672 "[00010.300000][ffx] INFO: Hello world 10300000000!\u{1b}[m\n",
1673 )
1674 );
1675 }
1676
1677 #[fuchsia::test]
1678 async fn test_default_formatter_sorts_before_tail_limit() {
1679 let buffers = TestBuffers::default();
1680 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1681 let options = LogFormatterOptions {
1682 display: Some(LogTextDisplayOptions { show_metadata: false, ..Default::default() }),
1683 sort: true,
1684 tail: Some(2),
1685 ..Default::default()
1686 };
1687 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options);
1688
1689 formatter
1690 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_300_000_000)) })
1691 .await
1692 .unwrap();
1693 formatter
1694 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_100_000_000)) })
1695 .await
1696 .unwrap();
1697 formatter
1698 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_200_000_000)) })
1699 .await
1700 .unwrap();
1701
1702 formatter.flush().await.unwrap();
1703
1704 assert_eq!(
1705 buffers.into_stdout_str(),
1706 concat!(
1707 "[00001.200000][ffx] INFO: Hello world 1200000000!\n",
1708 "[00001.300000][ffx] INFO: Hello world 1300000000!\n",
1709 )
1710 );
1711 }
1712
1713 #[fuchsia::test]
1714 async fn test_default_formatter_highlights_out_of_order_timestamps() {
1715 let buffers = TestBuffers::default();
1716 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1717 let options = LogFormatterOptions {
1718 display: Some(LogTextDisplayOptions {
1719 color: LogTextColor::BySeverity,
1720 show_metadata: false,
1721 ..Default::default()
1722 }),
1723 sort: false,
1724 ..Default::default()
1725 };
1726 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options);
1727
1728 for ts in [
1729 1_300_000_000,
1730 1_100_000_000,
1731 1_200_000_000,
1732 1_400_000_000,
1733 999_000_000_000, 999_100_000_000, 5_500_000_000, 5_400_000_000, 5_600_000_000, ] {
1739 formatter
1740 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(ts)) })
1741 .await
1742 .unwrap();
1743 }
1744
1745 formatter.set_boot_timestamp(Timestamp::from_nanos(0));
1747 formatter
1748 .push_log(LogEntry { data: LogData::TargetLog(make_log_with_timestamp(1_000_000_000)) })
1749 .await
1750 .unwrap();
1751
1752 assert_eq!(
1753 buffers.into_stdout_str(),
1754 concat!(
1755 "[00001.300000][ffx] INFO: Hello world 1300000000!\u{1b}[m\n",
1756 "\u{1b}[38;5;6m[00001.100000]\u{1b}[m[ffx] INFO: Hello world 1100000000!\u{1b}[m\n",
1757 "[00001.200000][ffx] INFO: Hello world 1200000000!\u{1b}[m\n",
1758 "[00001.400000][ffx] INFO: Hello world 1400000000!\u{1b}[m\n",
1759 "[00999.000000][ffx] INFO: Hello world 999000000000!\u{1b}[m\n",
1760 "[00999.100000][ffx] INFO: Hello world 999100000000!\u{1b}[m\n",
1761 "\u{1b}[38;5;6m[00005.500000]\u{1b}[m[ffx] INFO: Hello world 5500000000!\u{1b}[m\n",
1762 "\u{1b}[38;5;6m[00005.400000]\u{1b}[m[ffx] INFO: Hello world 5400000000!\u{1b}[m\n",
1763 "[00005.600000][ffx] INFO: Hello world 5600000000!\u{1b}[m\n",
1764 "[00001.000000][ffx] INFO: Hello world 1000000000!\u{1b}[m\n",
1765 )
1766 );
1767 }
1768}