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