1use crate::filter::LogFilterCriteria;
6use crate::log_socket_stream::{JsonDeserializeError, LogsDataStream};
7use crate::{
8 DetailedDateTime, InstanceGetter, LogCommand, LogError, LogProcessingResult, TimeFormat,
9};
10use anyhow::Result;
11use async_trait::async_trait;
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::io::Write;
22use std::time::Duration;
23use thiserror::Error;
24use writer::ToolIO;
25
26pub use diagnostics_data::Timestamp;
27
28pub const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
29
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
32pub enum LogData {
33 TargetLog(LogsData),
35}
36
37impl LogData {
38 pub fn as_target_log(&self) -> Option<&LogsData> {
40 match self {
41 LogData::TargetLog(log) => Some(log),
42 }
43 }
44
45 pub fn as_target_log_mut(&mut self) -> Option<&mut LogsData> {
46 match self {
47 LogData::TargetLog(log) => Some(log),
48 }
49 }
50}
51
52impl From<LogsData> for LogData {
53 fn from(data: LogsData) -> Self {
54 Self::TargetLog(data)
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61pub struct LogEntry {
62 pub data: LogData,
64}
65
66impl LogEntry {
67 fn utc_timestamp(&self, boot_ts: Option<Timestamp>) -> Timestamp {
68 Timestamp::from_nanos(match &self.data {
69 LogData::TargetLog(data) => {
70 data.metadata.timestamp.into_nanos()
71 + boot_ts.map(|value| value.into_nanos()).unwrap_or(0)
72 }
73 })
74 }
75}
76
77impl Display for LogEntry {
81 fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 unreachable!("UNSUPPORTED -- This type cannot be formatted with std format.");
83 }
84}
85
86#[async_trait(?Send)]
88pub trait Symbolize {
89 async fn symbolize(&self, entry: LogEntry) -> Option<LogEntry>;
93}
94
95async fn handle_value<S>(one: Data<Logs>, symbolizer: &S) -> Option<LogEntry>
96where
97 S: Symbolize + ?Sized,
98{
99 let entry = LogEntry { data: one.into() };
100 symbolizer.symbolize(entry).await
101}
102
103fn generate_timestamp_message(boot_timestamp: Timestamp) -> LogEntry {
104 LogEntry {
105 data: LogData::TargetLog(
106 LogsDataBuilder::new(diagnostics_data::BuilderArgs {
107 moniker: "ffx".try_into().unwrap(),
108 timestamp: Timestamp::from_nanos(0),
109 component_url: Some("ffx".into()),
110 severity: Severity::Info,
111 })
112 .set_message("Logging started")
113 .add_key(LogsProperty::String(
114 LogsField::Other("utc_time_now".into()),
115 chrono::Utc::now().to_rfc3339(),
116 ))
117 .add_key(LogsProperty::Int(
118 LogsField::Other("current_boot_timestamp".to_string()),
119 boot_timestamp.into_nanos(),
120 ))
121 .build(),
122 ),
123 }
124}
125
126pub async fn dump_logs_from_socket<F, S>(
128 socket: flex_client::AsyncSocket,
129 formatter: &mut F,
130 symbolizer: &S,
131 include_timestamp: bool,
132) -> Result<LogProcessingResult, JsonDeserializeError>
133where
134 F: LogFormatter + BootTimeAccessor,
135 S: Symbolize + ?Sized,
136{
137 let mut decoder = Box::pin(LogsDataStream::new(socket).fuse());
138 let mut symbolize_pending = FuturesUnordered::new();
139 if include_timestamp && !formatter.is_utc_time_format() {
140 formatter.push_log(generate_timestamp_message(formatter.get_boot_timestamp())).await?;
141 }
142 while let Some(value) = select! {
143 res = decoder.next() => Some(Either::Left(res)),
144 res = symbolize_pending.next() => Some(Either::Right(res)),
145 complete => None,
146 } {
147 match value {
148 Either::Left(Some(result)) => match result {
149 Ok(log) => symbolize_pending.push(handle_value(log, symbolizer)),
150 Err(e) => return Err(e),
151 },
152 Either::Right(Some(Some(symbolized))) => match formatter.push_log(symbolized).await? {
153 LogProcessingResult::Exit => {
154 return Ok(LogProcessingResult::Exit);
155 }
156 LogProcessingResult::Continue => {}
157 },
158 _ => {}
159 }
160 }
161 Ok(LogProcessingResult::Continue)
162}
163
164pub trait BootTimeAccessor {
165 fn set_boot_timestamp(&mut self, _boot_ts_nanos: Timestamp);
167
168 fn get_boot_timestamp(&self) -> Timestamp;
170}
171
172#[derive(Clone, Debug)]
174pub struct DeviceOrLocalTimestamp {
175 pub timestamp: Timestamp,
177 pub is_boot: bool,
180}
181
182impl DeviceOrLocalTimestamp {
183 pub fn new(
187 rtc: Option<&DetailedDateTime>,
188 boot: Option<&Duration>,
189 ) -> Option<DeviceOrLocalTimestamp> {
190 rtc.as_ref()
191 .filter(|value| !value.is_now)
192 .map(|value| DeviceOrLocalTimestamp {
193 timestamp: Timestamp::from_nanos(
194 value.naive_utc().and_utc().timestamp_nanos_opt().unwrap(),
195 ),
196 is_boot: false,
197 })
198 .or_else(|| {
199 boot.map(|value| DeviceOrLocalTimestamp {
200 timestamp: Timestamp::from_nanos(value.as_nanos() as i64),
201 is_boot: true,
202 })
203 })
204 }
205}
206
207#[derive(Clone, Debug)]
209pub struct LogFormatterOptions {
210 pub display: Option<LogTextDisplayOptions>,
212 pub since: Option<DeviceOrLocalTimestamp>,
214 pub until: Option<DeviceOrLocalTimestamp>,
216}
217
218impl Default for LogFormatterOptions {
219 fn default() -> Self {
220 LogFormatterOptions { display: Some(Default::default()), since: None, until: None }
221 }
222}
223
224#[derive(Error, Debug)]
226pub enum FormatterError {
227 #[error(transparent)]
229 Other(#[from] anyhow::Error),
230 #[error(transparent)]
232 IO(#[from] std::io::Error),
233}
234
235pub struct DefaultLogFormatter<W>
237where
238 W: Write + ToolIO<OutputItem = LogEntry>,
239{
240 writer: W,
241 filters: LogFilterCriteria,
242 options: LogFormatterOptions,
243 boot_ts_nanos: Option<Timestamp>,
244}
245
246fn utc_to_boot(boot_ts: Timestamp, utc: Timestamp) -> Timestamp {
248 Timestamp::from_nanos(utc.into_nanos() - boot_ts.into_nanos())
249}
250
251#[async_trait(?Send)]
252impl<W> LogFormatter for DefaultLogFormatter<W>
253where
254 W: Write + ToolIO<OutputItem = LogEntry>,
255{
256 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError> {
257 self.push_log_internal(log_entry, true).await
258 }
259
260 fn is_utc_time_format(&self) -> bool {
261 self.options.display.iter().any(|options| match options.time_format {
262 LogTimeDisplayFormat::Original => false,
263 LogTimeDisplayFormat::WallTime { tz, offset: _ } => tz == Timezone::Utc,
264 })
265 }
266}
267
268impl<W> BootTimeAccessor for DefaultLogFormatter<W>
269where
270 W: Write + ToolIO<OutputItem = LogEntry>,
271{
272 fn set_boot_timestamp(&mut self, boot_ts_nanos: Timestamp) {
273 if let Some(LogTextDisplayOptions {
274 time_format: LogTimeDisplayFormat::WallTime { offset, .. },
275 ..
276 }) = &mut self.options.display
277 {
278 *offset = boot_ts_nanos.into_nanos();
279 }
280 self.boot_ts_nanos = Some(boot_ts_nanos);
281 }
282 fn get_boot_timestamp(&self) -> Timestamp {
283 debug_assert!(self.boot_ts_nanos.is_some());
284 self.boot_ts_nanos.unwrap_or_else(|| Timestamp::from_nanos(0))
285 }
286}
287
288pub trait WriterContainer<W>
290where
291 W: Write + ToolIO<OutputItem = LogEntry>,
292{
293 fn writer(&mut self) -> &mut W;
294}
295
296impl<W> WriterContainer<W> for DefaultLogFormatter<W>
297where
298 W: Write + ToolIO<OutputItem = LogEntry>,
299{
300 fn writer(&mut self) -> &mut W {
301 &mut self.writer
302 }
303}
304
305impl<W> DefaultLogFormatter<W>
306where
307 W: Write + ToolIO<OutputItem = LogEntry>,
308{
309 pub fn new(filters: LogFilterCriteria, writer: W, options: LogFormatterOptions) -> Self {
311 Self { filters, writer, options, boot_ts_nanos: None }
312 }
313
314 pub async fn expand_monikers(&mut self, getter: &impl InstanceGetter) -> Result<(), LogError> {
315 self.filters.expand_monikers(getter).await
316 }
317
318 pub async fn push_unfiltered_log(
319 &mut self,
320 log_entry: LogEntry,
321 ) -> Result<LogProcessingResult, LogError> {
322 self.push_log_internal(log_entry, false).await
323 }
324
325 async fn push_log_internal(
326 &mut self,
327 log_entry: LogEntry,
328 enable_filters: bool,
329 ) -> Result<LogProcessingResult, LogError> {
330 if enable_filters {
331 if self.filter_by_timestamp(&log_entry, self.options.since.as_ref(), |a, b| a <= b) {
332 return Ok(LogProcessingResult::Continue);
333 }
334
335 if self.filter_by_timestamp(&log_entry, self.options.until.as_ref(), |a, b| a >= b) {
336 return Ok(LogProcessingResult::Exit);
337 }
338
339 if !self.filters.matches(&log_entry) {
340 return Ok(LogProcessingResult::Continue);
341 }
342 }
343 match self.options.display {
344 Some(text_options) => {
345 let mut options_for_this_line_only = self.options.clone();
346 options_for_this_line_only.display = Some(text_options);
347 if !enable_filters {
348 if let LogTimeDisplayFormat::WallTime { ref mut offset, .. } =
351 options_for_this_line_only.display.as_mut().unwrap().time_format
352 {
353 *offset = 1;
358 };
359 }
360 self.format_text_log(options_for_this_line_only, log_entry)?;
361 }
362 None => {
363 self.writer.item(&log_entry).map_err(|err| LogError::UnknownError(err.into()))?;
364 }
365 };
366
367 Ok(LogProcessingResult::Continue)
368 }
369
370 pub fn new_from_args(cmd: &LogCommand, writer: W) -> Self {
372 let is_json = writer.is_machine();
373
374 DefaultLogFormatter::new(
375 LogFilterCriteria::from(cmd.clone()),
376 writer,
377 LogFormatterOptions {
378 display: if is_json {
379 None
380 } else {
381 Some(LogTextDisplayOptions {
382 show_tags: !cmd.hide_tags,
383 color: if cmd.no_color {
384 LogTextColor::None
385 } else {
386 LogTextColor::BySeverity
387 },
388 show_metadata: cmd.show_metadata,
389 time_format: match cmd.clock {
390 TimeFormat::Boot => LogTimeDisplayFormat::Original,
391 TimeFormat::Local => LogTimeDisplayFormat::WallTime {
392 tz: Timezone::Local,
393 offset: 0,
396 },
397 TimeFormat::Utc => LogTimeDisplayFormat::WallTime {
398 tz: Timezone::Utc,
399 offset: 0,
402 },
403 },
404 show_file: !cmd.hide_file,
405 show_moniker: !cmd.hide_moniker,
406 show_full_moniker: cmd.show_full_moniker,
407 prefer_url_component_name: cmd.prefer_url_component_name,
408 })
409 },
410 since: DeviceOrLocalTimestamp::new(cmd.since.as_ref(), cmd.since_boot.as_ref()),
411 until: DeviceOrLocalTimestamp::new(cmd.until.as_ref(), cmd.until_boot.as_ref()),
412 },
413 )
414 }
415
416 fn filter_by_timestamp(
417 &self,
418 log_entry: &LogEntry,
419 timestamp: Option<&DeviceOrLocalTimestamp>,
420 callback: impl Fn(&Timestamp, &Timestamp) -> bool,
421 ) -> bool {
422 let Some(timestamp) = timestamp else {
423 return false;
424 };
425 if timestamp.is_boot {
426 callback(
427 &utc_to_boot(
428 self.get_boot_timestamp(),
429 log_entry.utc_timestamp(self.boot_ts_nanos),
430 ),
431 ×tamp.timestamp,
432 )
433 } else {
434 callback(&log_entry.utc_timestamp(self.boot_ts_nanos), ×tamp.timestamp)
435 }
436 }
437
438 fn format_text_log(
441 &mut self,
442 options: LogFormatterOptions,
443 log_entry: LogEntry,
444 ) -> Result<(), FormatterError> {
445 let text_options = match options.display {
446 Some(o) => o,
447 None => {
448 unreachable!("If we are here, we can only be formatting text");
449 }
450 };
451 match log_entry {
452 LogEntry { data: LogData::TargetLog(data), .. } => {
453 writeln!(self.writer, "{}", LogTextPresenter::new(&data, text_options))?;
456 }
457 }
458 Ok(())
459 }
460}
461
462#[allow(dead_code)] pub struct NoOpSymbolizer;
465
466#[async_trait(?Send)]
467impl Symbolize for NoOpSymbolizer {
468 async fn symbolize(&self, entry: LogEntry) -> Option<LogEntry> {
469 Some(entry)
470 }
471}
472
473#[async_trait(?Send)]
475pub trait LogFormatter {
476 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError>;
478
479 fn is_utc_time_format(&self) -> bool;
481}
482
483#[cfg(test)]
484mod test {
485 use crate::parse_time;
486 use assert_matches::assert_matches;
487 use diagnostics_data::{LogsDataBuilder, Severity};
488 use std::cell::Cell;
489 use writer::{Format, JsonWriter, TestBuffers};
490
491 use super::*;
492
493 const DEFAULT_TS_NANOS: u64 = 1615535969000000000;
494
495 struct FakeFormatter {
496 logs: Vec<LogEntry>,
497 boot_timestamp: Timestamp,
498 is_utc_time_format: bool,
499 }
500
501 impl FakeFormatter {
502 fn new() -> Self {
503 Self {
504 logs: Vec::new(),
505 boot_timestamp: Timestamp::from_nanos(0),
506 is_utc_time_format: false,
507 }
508 }
509 }
510
511 impl BootTimeAccessor for FakeFormatter {
512 fn set_boot_timestamp(&mut self, boot_ts_nanos: Timestamp) {
513 self.boot_timestamp = boot_ts_nanos;
514 }
515
516 fn get_boot_timestamp(&self) -> Timestamp {
517 self.boot_timestamp
518 }
519 }
520
521 #[async_trait(?Send)]
522 impl LogFormatter for FakeFormatter {
523 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError> {
524 self.logs.push(log_entry);
525 Ok(LogProcessingResult::Continue)
526 }
527
528 fn is_utc_time_format(&self) -> bool {
529 self.is_utc_time_format
530 }
531 }
532
533 pub struct FakeFuchsiaSymbolizer;
535
536 fn set_log_msg(entry: &mut LogEntry, msg: impl Into<String>) {
537 *entry.data.as_target_log_mut().unwrap().msg_mut().unwrap() = msg.into();
538 }
539
540 #[async_trait(?Send)]
541 impl Symbolize for FakeFuchsiaSymbolizer {
542 async fn symbolize(&self, mut entry: LogEntry) -> Option<LogEntry> {
543 set_log_msg(&mut entry, "Fuchsia");
544 Some(entry)
545 }
546 }
547
548 struct FakeSymbolizerCallback {
549 should_discard: Cell<bool>,
550 }
551
552 impl FakeSymbolizerCallback {
553 fn new() -> Self {
554 Self { should_discard: Cell::new(true) }
555 }
556 }
557
558 async fn dump_logs_from_socket<F, S>(
559 socket: fuchsia_async::Socket,
560 formatter: &mut F,
561 symbolizer: &S,
562 ) -> Result<LogProcessingResult, JsonDeserializeError>
563 where
564 F: LogFormatter + BootTimeAccessor,
565 S: Symbolize + ?Sized,
566 {
567 super::dump_logs_from_socket(socket, formatter, symbolizer, false).await
568 }
569
570 #[async_trait(?Send)]
571 impl Symbolize for FakeSymbolizerCallback {
572 async fn symbolize(&self, mut input: LogEntry) -> Option<LogEntry> {
573 self.should_discard.set(!self.should_discard.get());
574 if self.should_discard.get() {
575 None
576 } else {
577 set_log_msg(&mut input, "symbolized log");
578 Some(input)
579 }
580 }
581 }
582
583 #[fuchsia::test]
584 async fn test_boot_timestamp_setter() {
585 let buffers = TestBuffers::default();
586 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
587 let options = LogFormatterOptions {
588 display: Some(LogTextDisplayOptions {
589 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
590 ..Default::default()
591 }),
592 ..Default::default()
593 };
594 let mut formatter =
595 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
596 formatter.set_boot_timestamp(Timestamp::from_nanos(1234));
597 assert_eq!(formatter.get_boot_timestamp(), Timestamp::from_nanos(1234));
598
599 let buffers = TestBuffers::default();
601 let output = JsonWriter::<LogEntry>::new_test(None, &buffers);
602 let options = LogFormatterOptions { display: None, ..Default::default() };
603 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), output, options);
604 formatter.set_boot_timestamp(Timestamp::from_nanos(1234));
605 assert_eq!(formatter.get_boot_timestamp(), Timestamp::from_nanos(1234));
606 }
607
608 #[fuchsia::test]
609 async fn test_format_single_message() {
610 let symbolizer = NoOpSymbolizer {};
611 let mut formatter = FakeFormatter::new();
612 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
613 moniker: "ffx".try_into().unwrap(),
614 timestamp: Timestamp::from_nanos(0),
615 component_url: Some("ffx".into()),
616 severity: Severity::Info,
617 })
618 .set_message("Hello world!")
619 .build();
620 let (sender, receiver) = zx::Socket::create_stream();
621 sender
622 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
623 .expect("failed to write target log");
624 drop(sender);
625 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
626 .await
627 .unwrap();
628 assert_eq!(formatter.logs, vec![LogEntry { data: LogData::TargetLog(target_log) }]);
629 }
630
631 #[fuchsia::test]
632 async fn test_format_utc_timestamp() {
633 let symbolizer = NoOpSymbolizer {};
634 let mut formatter = FakeFormatter::new();
635 formatter.set_boot_timestamp(Timestamp::from_nanos(DEFAULT_TS_NANOS as i64));
636 let (_, receiver) = zx::Socket::create_stream();
637 super::dump_logs_from_socket(
638 flex_client::socket_to_async(receiver),
639 &mut formatter,
640 &symbolizer,
641 true,
642 )
643 .await
644 .unwrap();
645 let target_log = formatter.logs[0].data.as_target_log().unwrap();
646 let properties = target_log.payload_keys().unwrap();
647 assert_eq!(target_log.msg().unwrap(), "Logging started");
648
649 chrono::DateTime::parse_from_rfc3339(
651 properties.get_property("utc_time_now").unwrap().string().unwrap(),
652 )
653 .unwrap();
654 assert_eq!(
655 properties.get_property("current_boot_timestamp").unwrap().int().unwrap(),
656 DEFAULT_TS_NANOS as i64
657 );
658 }
659
660 #[fuchsia::test]
661 async fn test_format_utc_timestamp_does_not_print_if_utc_time() {
662 let symbolizer = NoOpSymbolizer {};
663 let mut formatter = FakeFormatter::new();
664 formatter.is_utc_time_format = true;
665 formatter.set_boot_timestamp(Timestamp::from_nanos(DEFAULT_TS_NANOS as i64));
666 let (_, receiver) = zx::Socket::create_stream();
667 super::dump_logs_from_socket(
668 flex_client::socket_to_async(receiver),
669 &mut formatter,
670 &symbolizer,
671 true,
672 )
673 .await
674 .unwrap();
675 assert_eq!(formatter.logs.len(), 0);
676 }
677
678 #[fuchsia::test]
679 async fn test_format_multiple_messages() {
680 let symbolizer = NoOpSymbolizer {};
681 let mut formatter = FakeFormatter::new();
682 let (sender, receiver) = zx::Socket::create_stream();
683 let target_log_0 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
684 moniker: "ffx".try_into().unwrap(),
685 timestamp: Timestamp::from_nanos(0),
686 component_url: Some("ffx".into()),
687 severity: Severity::Info,
688 })
689 .set_message("Hello world!")
690 .set_pid(1)
691 .set_tid(2)
692 .build();
693 let target_log_1 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
694 moniker: "ffx".try_into().unwrap(),
695 timestamp: Timestamp::from_nanos(1),
696 component_url: Some("ffx".into()),
697 severity: Severity::Info,
698 })
699 .set_message("Hello world 2!")
700 .build();
701 sender
702 .write(serde_json::to_string(&vec![&target_log_0, &target_log_1]).unwrap().as_bytes())
703 .expect("failed to write target log");
704 drop(sender);
705 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
706 .await
707 .unwrap();
708 assert_eq!(
709 formatter.logs,
710 vec![
711 LogEntry { data: LogData::TargetLog(target_log_0) },
712 LogEntry { data: LogData::TargetLog(target_log_1) }
713 ]
714 );
715 }
716
717 #[fuchsia::test]
718 async fn test_format_timestamp_filter() {
719 let symbolizer = NoOpSymbolizer {};
721 let buffers = TestBuffers::default();
722 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
723 let mut formatter = DefaultLogFormatter::new(
724 LogFilterCriteria::default(),
725 stdout,
726 LogFormatterOptions {
727 since: Some(DeviceOrLocalTimestamp {
728 timestamp: Timestamp::from_nanos(1),
729 is_boot: true,
730 }),
731 until: Some(DeviceOrLocalTimestamp {
732 timestamp: Timestamp::from_nanos(3),
733 is_boot: true,
734 }),
735 ..Default::default()
736 },
737 );
738 formatter.set_boot_timestamp(Timestamp::from_nanos(0));
739
740 let (sender, receiver) = zx::Socket::create_stream();
741 let target_log_0 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
742 moniker: "ffx".try_into().unwrap(),
743 timestamp: Timestamp::from_nanos(0),
744 component_url: Some("ffx".into()),
745 severity: Severity::Info,
746 })
747 .set_message("Hello world!")
748 .build();
749 let target_log_1 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
750 moniker: "ffx".try_into().unwrap(),
751 timestamp: Timestamp::from_nanos(1),
752 component_url: Some("ffx".into()),
753 severity: Severity::Info,
754 })
755 .set_message("Hello world 2!")
756 .build();
757 let target_log_2 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
758 moniker: "ffx".try_into().unwrap(),
759 timestamp: Timestamp::from_nanos(2),
760 component_url: Some("ffx".into()),
761 severity: Severity::Info,
762 })
763 .set_pid(1)
764 .set_tid(2)
765 .set_message("Hello world 3!")
766 .build();
767 let target_log_3 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
768 moniker: "ffx".try_into().unwrap(),
769 timestamp: Timestamp::from_nanos(3),
770 component_url: Some("ffx".into()),
771 severity: Severity::Info,
772 })
773 .set_message("Hello world 4!")
774 .set_pid(1)
775 .set_tid(2)
776 .build();
777 sender
778 .write(
779 serde_json::to_string(&vec![
780 &target_log_0,
781 &target_log_1,
782 &target_log_2,
783 &target_log_3,
784 ])
785 .unwrap()
786 .as_bytes(),
787 )
788 .expect("failed to write target log");
789 drop(sender);
790 assert_matches!(
791 dump_logs_from_socket(
792 flex_client::socket_to_async(receiver),
793 &mut formatter,
794 &symbolizer,
795 )
796 .await,
797 Ok(LogProcessingResult::Exit)
798 );
799 assert_eq!(
800 buffers.stdout.into_string(),
801 "[00000.000000][1][2][ffx] INFO: Hello world 3!\n"
802 );
803 }
804
805 fn make_log_with_timestamp(timestamp: i64) -> LogsData {
806 LogsDataBuilder::new(diagnostics_data::BuilderArgs {
807 moniker: "ffx".try_into().unwrap(),
808 timestamp: Timestamp::from_nanos(timestamp),
809 component_url: Some("ffx".into()),
810 severity: Severity::Info,
811 })
812 .set_message(format!("Hello world {timestamp}!"))
813 .set_pid(1)
814 .set_tid(2)
815 .build()
816 }
817
818 #[fuchsia::test]
819 async fn test_format_timestamp_filter_utc() {
820 let symbolizer = NoOpSymbolizer {};
822 let buffers = TestBuffers::default();
823 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
824 let mut formatter = DefaultLogFormatter::new(
825 LogFilterCriteria::default(),
826 stdout,
827 LogFormatterOptions {
828 since: Some(DeviceOrLocalTimestamp {
829 timestamp: Timestamp::from_nanos(1),
830 is_boot: false,
831 }),
832 until: Some(DeviceOrLocalTimestamp {
833 timestamp: Timestamp::from_nanos(3),
834 is_boot: false,
835 }),
836 display: Some(LogTextDisplayOptions {
837 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
838 ..Default::default()
839 }),
840 },
841 );
842 formatter.set_boot_timestamp(Timestamp::from_nanos(1));
843
844 let (sender, receiver) = zx::Socket::create_stream();
845 let logs = (0..4).map(make_log_with_timestamp).collect::<Vec<_>>();
846 sender
847 .write(serde_json::to_string(&logs).unwrap().as_bytes())
848 .expect("failed to write target log");
849 drop(sender);
850 assert_matches!(
851 dump_logs_from_socket(
852 flex_client::socket_to_async(receiver),
853 &mut formatter,
854 &symbolizer,
855 )
856 .await,
857 Ok(LogProcessingResult::Exit)
858 );
859 assert_eq!(
860 buffers.stdout.into_string(),
861 "[1970-01-01 00:00:00.000][1][2][ffx] INFO: Hello world 1!\n"
862 );
863 }
864
865 fn logs_data_builder() -> LogsDataBuilder {
866 diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
867 timestamp: Timestamp::from_nanos(default_ts().as_nanos() as i64),
868 component_url: Some("component_url".into()),
869 moniker: "some/moniker".try_into().unwrap(),
870 severity: diagnostics_data::Severity::Warn,
871 })
872 .set_pid(1)
873 .set_tid(2)
874 }
875
876 fn default_ts() -> Duration {
877 Duration::from_nanos(DEFAULT_TS_NANOS)
878 }
879
880 fn log_entry() -> LogEntry {
881 LogEntry {
882 data: LogData::TargetLog(
883 logs_data_builder().add_tag("tag1").add_tag("tag2").set_message("message").build(),
884 ),
885 }
886 }
887
888 #[fuchsia::test]
889 async fn test_default_formatter() {
890 let buffers = TestBuffers::default();
891 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
892 let options = LogFormatterOptions::default();
893 let mut formatter =
894 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
895 formatter.push_log(log_entry()).await.unwrap();
896 drop(formatter);
897 assert_eq!(
898 buffers.into_stdout_str(),
899 "[1615535969.000000][1][2][some/moniker][tag1,tag2] WARN: message\n"
900 );
901 }
902
903 #[fuchsia::test]
904 async fn test_default_formatter_with_hidden_metadata() {
905 let buffers = TestBuffers::default();
906 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
907 let options = LogFormatterOptions {
908 display: Some(LogTextDisplayOptions { show_metadata: false, ..Default::default() }),
909 ..LogFormatterOptions::default()
910 };
911 let mut formatter =
912 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
913 formatter.push_log(log_entry()).await.unwrap();
914 drop(formatter);
915 assert_eq!(
916 buffers.into_stdout_str(),
917 "[1615535969.000000][some/moniker][tag1,tag2] WARN: message\n"
918 );
919 }
920
921 #[fuchsia::test]
922 async fn test_default_formatter_with_json() {
923 let buffers = TestBuffers::default();
924 let stdout = JsonWriter::<LogEntry>::new_test(Some(Format::Json), &buffers);
925 let options = LogFormatterOptions { display: None, ..Default::default() };
926 {
927 let mut formatter =
928 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
929 formatter.push_log(log_entry()).await.unwrap();
930 }
931 assert_eq!(
932 serde_json::from_str::<LogEntry>(&buffers.into_stdout_str()).unwrap(),
933 log_entry()
934 );
935 }
936
937 fn emit_log(sender: &mut zx::Socket, msg: &str, timestamp: i64) -> Data<Logs> {
938 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
939 moniker: "ffx".try_into().unwrap(),
940 timestamp: Timestamp::from_nanos(timestamp),
941 component_url: Some("ffx".into()),
942 severity: Severity::Info,
943 })
944 .set_message(msg)
945 .build();
946
947 sender
948 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
949 .expect("failed to write target log");
950 target_log
951 }
952
953 #[fuchsia::test]
954 async fn test_default_formatter_discards_when_told_by_symbolizer() {
955 let mut formatter = FakeFormatter::new();
956 let (mut sender, receiver) = zx::Socket::create_stream();
957 let mut target_log_0 = emit_log(&mut sender, "Hello world!", 0);
958 emit_log(&mut sender, "Dropped world!", 1);
959 let mut target_log_2 = emit_log(&mut sender, "Hello world!", 2);
960 emit_log(&mut sender, "Dropped world!", 3);
961 let mut target_log_4 = emit_log(&mut sender, "Hello world!", 4);
962 drop(sender);
963 let symbolizer = FakeSymbolizerCallback::new();
965 *target_log_0.msg_mut().unwrap() = "symbolized log".into();
966 *target_log_2.msg_mut().unwrap() = "symbolized log".into();
967 *target_log_4.msg_mut().unwrap() = "symbolized log".into();
968 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
969 .await
970 .unwrap();
971 assert_eq!(
972 formatter.logs,
973 vec![
974 LogEntry { data: LogData::TargetLog(target_log_0) },
975 LogEntry { data: LogData::TargetLog(target_log_2) },
976 LogEntry { data: LogData::TargetLog(target_log_4) }
977 ],
978 );
979 }
980
981 #[fuchsia::test]
982 async fn test_symbolized_output() {
983 let symbolizer = FakeFuchsiaSymbolizer;
984 let buffers = TestBuffers::default();
985 let output = JsonWriter::<LogEntry>::new_test(None, &buffers);
986 let mut formatter = DefaultLogFormatter::new(
987 LogFilterCriteria::default(),
988 output,
989 LogFormatterOptions { ..Default::default() },
990 );
991 formatter.set_boot_timestamp(Timestamp::from_nanos(0));
992 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
993 moniker: "ffx".try_into().unwrap(),
994 timestamp: Timestamp::from_nanos(0),
995 component_url: Some("ffx".into()),
996 severity: Severity::Info,
997 })
998 .set_pid(1)
999 .set_tid(2)
1000 .set_message("Hello world!")
1001 .build();
1002 let (sender, receiver) = zx::Socket::create_stream();
1003 sender
1004 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
1005 .expect("failed to write target log");
1006 drop(sender);
1007 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
1008 .await
1009 .unwrap();
1010 assert_eq!(buffers.stdout.into_string(), "[00000.000000][1][2][ffx] INFO: Fuchsia\n");
1011 }
1012
1013 #[test]
1014 fn test_device_or_local_timestamp_returns_none_if_now_is_passed() {
1015 assert_matches!(DeviceOrLocalTimestamp::new(Some(&parse_time("now").unwrap()), None), None);
1016 }
1017}