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