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 async_trait::async_trait;
13use diagnostics_data::{
14 Data, LogTextColor, LogTextDisplayOptions, LogTextPresenter, LogTimeDisplayFormat, Logs,
15 LogsData, LogsDataBuilder, LogsField, LogsProperty, Severity, Timezone,
16};
17use futures_util::future::Either;
18use futures_util::stream::FuturesUnordered;
19use futures_util::{StreamExt, select};
20use serde::{Deserialize, Serialize};
21use std::fmt::Display;
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
87#[async_trait(?Send)]
89pub trait Symbolize {
90 async fn symbolize(&self, entry: LogEntry) -> Option<LogEntry>;
94}
95
96async fn handle_value<S>(one: Data<Logs>, symbolizer: &S) -> Option<LogEntry>
97where
98 S: Symbolize + ?Sized,
99{
100 let entry = LogEntry { data: one.into() };
101 symbolizer.symbolize(entry).await
102}
103
104fn generate_timestamp_message(boot_timestamp: Timestamp) -> LogEntry {
105 LogEntry {
106 data: LogData::TargetLog(
107 LogsDataBuilder::new(diagnostics_data::BuilderArgs {
108 moniker: "ffx".try_into().unwrap(),
109 timestamp: Timestamp::from_nanos(0),
110 component_url: Some("ffx".into()),
111 severity: Severity::Info,
112 })
113 .set_message("Logging started")
114 .add_key(LogsProperty::String(
115 LogsField::Other("utc_time_now".into()),
116 chrono::Utc::now().to_rfc3339(),
117 ))
118 .add_key(LogsProperty::Int(
119 LogsField::Other("current_boot_timestamp".to_string()),
120 boot_timestamp.into_nanos(),
121 ))
122 .build(),
123 ),
124 }
125}
126
127pub async fn dump_logs_from_socket<F, S>(
129 socket: flex_client::AsyncSocket,
130 formatter: &mut F,
131 symbolizer: &S,
132 include_timestamp: bool,
133) -> Result<LogProcessingResult, JsonDeserializeError>
134where
135 F: LogFormatter + BootTimeAccessor,
136 S: Symbolize + ?Sized,
137{
138 let mut decoder = Box::pin(LogsDataStream::new(socket).fuse());
139 let mut symbolize_pending = FuturesUnordered::new();
140 if include_timestamp && !formatter.is_utc_time_format() {
141 formatter.push_log(generate_timestamp_message(formatter.get_boot_timestamp())).await?;
142 }
143 while let Some(value) = select! {
144 res = decoder.next() => Some(Either::Left(res)),
145 res = symbolize_pending.next() => Some(Either::Right(res)),
146 complete => None,
147 } {
148 match value {
149 Either::Left(Some(result)) => match result {
150 Ok(log) => symbolize_pending.push(handle_value(log, symbolizer)),
151 Err(e) => return Err(e),
152 },
153 Either::Right(Some(Some(symbolized))) => match formatter.push_log(symbolized).await? {
154 LogProcessingResult::Exit => {
155 formatter.flush().await?;
156 return Ok(LogProcessingResult::Exit);
157 }
158 LogProcessingResult::Continue => {}
159 },
160 _ => {}
161 }
162 }
163 formatter.flush().await?;
164 Ok(LogProcessingResult::Continue)
165}
166
167pub async fn dump_fxt_logs_from_socket<F, S>(
169 socket: flex_client::AsyncSocket,
170 formatter: &mut F,
171 symbolizer: &S,
172 include_timestamp: bool,
173) -> Result<LogProcessingResult, LogError>
174where
175 F: LogFormatter + BootTimeAccessor,
176 S: Symbolize + ?Sized,
177{
178 let streamer = crate::fxt_streamer::FxtStreamer::new(socket);
179 let mut decoder = std::pin::pin!(streamer.stream());
180 let mut symbolize_pending = FuturesUnordered::new();
181 if include_timestamp && !formatter.is_utc_time_format() {
182 formatter.push_log(generate_timestamp_message(formatter.get_boot_timestamp())).await?;
183 }
184 while let Some(value) = select! {
185 res = decoder.next() => Some(Either::Left(res)),
186 res = symbolize_pending.next() => Some(Either::Right(res)),
187 complete => None,
188 } {
189 match value {
190 Either::Left(Some(result)) => match result {
191 Ok(log) => symbolize_pending.push(handle_value(log, symbolizer)),
192 Err(e) => return Err(e),
193 },
194 Either::Right(Some(Some(symbolized))) => match formatter.push_log(symbolized).await? {
195 LogProcessingResult::Exit => {
196 formatter.flush().await?;
197 return Ok(LogProcessingResult::Exit);
198 }
199 LogProcessingResult::Continue => {}
200 },
201 _ => {}
202 }
203 }
204 formatter.flush().await?;
205 Ok(LogProcessingResult::Continue)
206}
207
208pub trait BootTimeAccessor {
209 fn set_boot_timestamp(&mut self, _boot_ts_nanos: Timestamp);
211
212 fn get_boot_timestamp(&self) -> Timestamp;
214}
215
216#[derive(Clone, Debug)]
218pub struct DeviceOrLocalTimestamp {
219 pub timestamp: Timestamp,
221 pub is_boot: bool,
224}
225
226impl DeviceOrLocalTimestamp {
227 pub fn new(
231 rtc: Option<&DetailedDateTime>,
232 boot: Option<&Duration>,
233 ) -> Option<DeviceOrLocalTimestamp> {
234 rtc.as_ref()
235 .filter(|value| !value.is_now)
236 .map(|value| DeviceOrLocalTimestamp {
237 timestamp: Timestamp::from_nanos(
238 value.naive_utc().and_utc().timestamp_nanos_opt().unwrap(),
239 ),
240 is_boot: false,
241 })
242 .or_else(|| {
243 boot.map(|value| DeviceOrLocalTimestamp {
244 timestamp: Timestamp::from_nanos(value.as_nanos() as i64),
245 is_boot: true,
246 })
247 })
248 }
249}
250
251#[derive(Clone, Debug)]
253pub struct LogFormatterOptions {
254 pub display: Option<LogTextDisplayOptions>,
256 pub since: Option<DeviceOrLocalTimestamp>,
258 pub until: Option<DeviceOrLocalTimestamp>,
260 pub tail: Option<usize>,
262}
263
264impl Default for LogFormatterOptions {
265 fn default() -> Self {
266 LogFormatterOptions {
267 display: Some(Default::default()),
268 since: None,
269 until: None,
270 tail: None,
271 }
272 }
273}
274
275#[derive(Error, Debug)]
277pub enum FormatterError {
278 #[error(transparent)]
280 Other(#[from] anyhow::Error),
281 #[error(transparent)]
283 IO(#[from] std::io::Error),
284}
285
286impl FormatterError {
287 pub fn is_broken_pipe(&self) -> bool {
288 match self {
289 FormatterError::IO(error) => error.kind() == std::io::ErrorKind::BrokenPipe,
290 FormatterError::Other(_) => false,
291 }
292 }
293}
294
295pub struct DefaultLogFormatter<W>
297where
298 W: Write + ToolIO<OutputItem = LogEntry>,
299{
300 writer: W,
301 filters: LogFilterCriteria,
302 options: LogFormatterOptions,
303 boot_ts_nanos: Option<Timestamp>,
304 tail_queue: std::collections::VecDeque<LogEntry>,
305 tail_limit: Option<usize>,
306}
307
308fn utc_to_boot(boot_ts: Timestamp, utc: Timestamp) -> Timestamp {
310 Timestamp::from_nanos(utc.into_nanos() - boot_ts.into_nanos())
311}
312
313#[async_trait(?Send)]
314impl<W> LogFormatter for DefaultLogFormatter<W>
315where
316 W: Write + ToolIO<OutputItem = LogEntry>,
317{
318 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError> {
319 self.push_log_internal(log_entry, true).await.or_else(|err| {
320 if err.is_broken_pipe() { Ok(LogProcessingResult::Exit) } else { Err(err) }
321 })
322 }
323
324 fn is_utc_time_format(&self) -> bool {
325 self.options.display.iter().any(|options| match options.time_format {
326 LogTimeDisplayFormat::Original => false,
327 LogTimeDisplayFormat::WallTime { tz, offset: _ } => tz == Timezone::Utc,
328 })
329 }
330
331 async fn flush(&mut self) -> Result<(), LogError> {
332 self.flush_tail().await
333 }
334}
335
336impl<W> BootTimeAccessor for DefaultLogFormatter<W>
337where
338 W: Write + ToolIO<OutputItem = LogEntry>,
339{
340 fn set_boot_timestamp(&mut self, boot_ts_nanos: Timestamp) {
341 if let Some(LogTextDisplayOptions {
342 time_format: LogTimeDisplayFormat::WallTime { offset, .. },
343 ..
344 }) = &mut self.options.display
345 {
346 *offset = boot_ts_nanos.into_nanos();
347 }
348 self.boot_ts_nanos = Some(boot_ts_nanos);
349 }
350 fn get_boot_timestamp(&self) -> Timestamp {
351 debug_assert!(self.boot_ts_nanos.is_some());
352 self.boot_ts_nanos.unwrap_or_else(|| Timestamp::from_nanos(0))
353 }
354}
355
356pub trait WriterContainer<W>
358where
359 W: Write + ToolIO<OutputItem = LogEntry>,
360{
361 fn writer(&mut self) -> &mut W;
362}
363
364impl<W> WriterContainer<W> for DefaultLogFormatter<W>
365where
366 W: Write + ToolIO<OutputItem = LogEntry>,
367{
368 fn writer(&mut self) -> &mut W {
369 &mut self.writer
370 }
371}
372
373impl<W> DefaultLogFormatter<W>
374where
375 W: Write + ToolIO<OutputItem = LogEntry>,
376{
377 pub fn new(filters: LogFilterCriteria, writer: W, options: LogFormatterOptions) -> Self {
379 let tail_limit = options.tail;
380 Self {
381 filters,
382 writer,
383 options,
384 boot_ts_nanos: None,
385 tail_queue: std::collections::VecDeque::new(),
386 tail_limit,
387 }
388 }
389
390 pub async fn expand_monikers(&mut self, getter: &impl InstanceGetter) -> Result<(), LogError> {
391 let warnings = self.filters.expand_monikers(getter).await?;
392 for warning in warnings {
393 writeln!(
394 self.writer.stderr(),
395 "WARN: Provided moniker '{}' was not an exact match. Using fuzzy match '{}' instead. Please check your component topology variations.",
396 warning.query,
397 warning.resolved
398 )?;
399 }
400 Ok(())
401 }
402
403 pub async fn push_unfiltered_log(
404 &mut self,
405 log_entry: LogEntry,
406 ) -> Result<LogProcessingResult, LogError> {
407 self.push_log_internal(log_entry, false).await
408 }
409
410 async fn flush_tail(&mut self) -> Result<(), LogError> {
411 while let Some(log_entry) = self.tail_queue.pop_front() {
412 self.write_log_entry(log_entry, true)?;
413 }
414 Ok(())
415 }
416
417 fn write_log_entry(
418 &mut self,
419 log_entry: LogEntry,
420 enable_filters: bool,
421 ) -> Result<(), LogError> {
422 match self.options.display {
423 Some(text_options) => {
424 let mut options_for_this_line_only = self.options.clone();
425 options_for_this_line_only.display = Some(text_options);
426 if !enable_filters
429 && let LogTimeDisplayFormat::WallTime { ref mut offset, .. } =
430 options_for_this_line_only.display.as_mut().unwrap().time_format
431 {
432 *offset = 1;
437 }
438 self.format_text_log(options_for_this_line_only, log_entry)
439 .map_err(LogError::FormatterError)?;
440 }
441 None => {
442 self.writer.item(&log_entry).map_err(|err| LogError::UnknownError(err.into()))?;
443 }
444 };
445 Ok(())
446 }
447
448 async fn push_log_internal(
449 &mut self,
450 log_entry: LogEntry,
451 enable_filters: bool,
452 ) -> Result<LogProcessingResult, LogError> {
453 if enable_filters {
454 if self.filter_by_timestamp(&log_entry, self.options.since.as_ref(), |a, b| a <= b) {
455 return Ok(LogProcessingResult::Continue);
456 }
457
458 if self.filter_by_timestamp(&log_entry, self.options.until.as_ref(), |a, b| a >= b) {
459 return Ok(LogProcessingResult::Exit);
460 }
461
462 if !self.filters.matches(&log_entry) {
463 return Ok(LogProcessingResult::Continue);
464 }
465 }
466
467 if let Some(limit) = self.tail_limit {
468 self.tail_queue.push_back(log_entry);
469 if self.tail_queue.len() > limit {
470 self.tail_queue.pop_front();
471 }
472 } else {
473 self.write_log_entry(log_entry, enable_filters)?;
474 }
475 Ok(LogProcessingResult::Continue)
476 }
477
478 pub fn new_from_args(cmd: &LogCommand, writer: W) -> Result<Self, LogError> {
480 let is_json = writer.is_machine();
481
482 Ok(DefaultLogFormatter::new(
483 LogFilterCriteria::try_from(cmd.clone())?,
484 writer,
485 LogFormatterOptions {
486 display: if is_json {
487 None
488 } else {
489 Some(LogTextDisplayOptions {
490 show_tags: !cmd.hide_tags(),
491 color: if cmd.no_color() {
492 LogTextColor::None
493 } else {
494 LogTextColor::BySeverity
495 },
496 show_metadata: cmd.show_metadata(),
497 time_format: match cmd.clock() {
498 TimeFormat::Boot => LogTimeDisplayFormat::Original,
499 TimeFormat::Local => LogTimeDisplayFormat::WallTime {
500 tz: Timezone::Local,
501 offset: 0,
504 },
505 TimeFormat::Utc => LogTimeDisplayFormat::WallTime {
506 tz: Timezone::Utc,
507 offset: 0,
510 },
511 },
512 show_file: !cmd.hide_file(),
513 show_moniker: !cmd.hide_moniker(),
514 show_full_moniker: cmd.show_full_moniker(),
515 prefer_url_component_name: cmd.prefer_url_component_name(),
516 })
517 },
518 since: DeviceOrLocalTimestamp::new(cmd.since(), cmd.since_boot().as_ref()),
519 until: DeviceOrLocalTimestamp::new(cmd.until(), cmd.until_boot().as_ref()),
520 tail: match &cmd.sub_command {
521 Some(LogSubCommand::Dump(dump)) => dump.tail,
522 _ => None,
523 },
524 },
525 ))
526 }
527
528 fn filter_by_timestamp(
529 &self,
530 log_entry: &LogEntry,
531 timestamp: Option<&DeviceOrLocalTimestamp>,
532 callback: impl Fn(&Timestamp, &Timestamp) -> bool,
533 ) -> bool {
534 let Some(timestamp) = timestamp else {
535 return false;
536 };
537 if timestamp.is_boot {
538 callback(
539 &utc_to_boot(
540 self.get_boot_timestamp(),
541 log_entry.utc_timestamp(self.boot_ts_nanos),
542 ),
543 ×tamp.timestamp,
544 )
545 } else {
546 callback(&log_entry.utc_timestamp(self.boot_ts_nanos), ×tamp.timestamp)
547 }
548 }
549
550 fn format_text_log(
553 &mut self,
554 options: LogFormatterOptions,
555 log_entry: LogEntry,
556 ) -> Result<(), FormatterError> {
557 let text_options = match options.display {
558 Some(o) => o,
559 None => {
560 unreachable!("If we are here, we can only be formatting text");
561 }
562 };
563 match log_entry {
564 LogEntry { data: LogData::TargetLog(data), .. } => {
565 writeln!(self.writer, "{}", LogTextPresenter::new(&data, text_options))?;
568 }
569 }
570 Ok(())
571 }
572}
573
574#[allow(dead_code)] pub struct NoOpSymbolizer;
577
578#[async_trait(?Send)]
579impl Symbolize for NoOpSymbolizer {
580 async fn symbolize(&self, entry: LogEntry) -> Option<LogEntry> {
581 Some(entry)
582 }
583}
584
585#[async_trait(?Send)]
587pub trait LogFormatter {
588 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError>;
590
591 fn is_utc_time_format(&self) -> bool;
593
594 async fn flush(&mut self) -> Result<(), LogError> {
596 Ok(())
597 }
598}
599
600#[cfg(test)]
601mod test {
602 use crate::parse_time;
603 use assert_matches::assert_matches;
604 use diagnostics_data::{LogsDataBuilder, Severity};
605 use std::cell::Cell;
606 use writer::{Format, JsonWriter, TestBuffers};
607
608 use super::*;
609
610 const DEFAULT_TS_NANOS: u64 = 1615535969000000000;
611
612 struct FakeFormatter {
613 logs: Vec<LogEntry>,
614 boot_timestamp: Timestamp,
615 is_utc_time_format: bool,
616 }
617
618 impl FakeFormatter {
619 fn new() -> Self {
620 Self {
621 logs: Vec::new(),
622 boot_timestamp: Timestamp::from_nanos(0),
623 is_utc_time_format: false,
624 }
625 }
626 }
627
628 impl BootTimeAccessor for FakeFormatter {
629 fn set_boot_timestamp(&mut self, boot_ts_nanos: Timestamp) {
630 self.boot_timestamp = boot_ts_nanos;
631 }
632
633 fn get_boot_timestamp(&self) -> Timestamp {
634 self.boot_timestamp
635 }
636 }
637
638 #[async_trait(?Send)]
639 impl LogFormatter for FakeFormatter {
640 async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError> {
641 self.logs.push(log_entry);
642 Ok(LogProcessingResult::Continue)
643 }
644
645 fn is_utc_time_format(&self) -> bool {
646 self.is_utc_time_format
647 }
648 }
649
650 pub struct FakeFuchsiaSymbolizer;
652
653 fn set_log_msg(entry: &mut LogEntry, msg: impl Into<String>) {
654 *entry.data.as_target_log_mut().unwrap().msg_mut().unwrap() = msg.into();
655 }
656
657 #[async_trait(?Send)]
658 impl Symbolize for FakeFuchsiaSymbolizer {
659 async fn symbolize(&self, mut entry: LogEntry) -> Option<LogEntry> {
660 set_log_msg(&mut entry, "Fuchsia");
661 Some(entry)
662 }
663 }
664
665 struct FakeSymbolizerCallback {
666 should_discard: Cell<bool>,
667 }
668
669 impl FakeSymbolizerCallback {
670 fn new() -> Self {
671 Self { should_discard: Cell::new(true) }
672 }
673 }
674
675 async fn dump_logs_from_socket<F, S>(
676 socket: fuchsia_async::Socket,
677 formatter: &mut F,
678 symbolizer: &S,
679 ) -> Result<LogProcessingResult, JsonDeserializeError>
680 where
681 F: LogFormatter + BootTimeAccessor,
682 S: Symbolize + ?Sized,
683 {
684 super::dump_logs_from_socket(socket, formatter, symbolizer, false).await
685 }
686
687 #[async_trait(?Send)]
688 impl Symbolize for FakeSymbolizerCallback {
689 async fn symbolize(&self, mut input: LogEntry) -> Option<LogEntry> {
690 self.should_discard.set(!self.should_discard.get());
691 if self.should_discard.get() {
692 None
693 } else {
694 set_log_msg(&mut input, "symbolized log");
695 Some(input)
696 }
697 }
698 }
699
700 #[fuchsia::test]
701 async fn test_boot_timestamp_setter() {
702 let buffers = TestBuffers::default();
703 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
704 let options = LogFormatterOptions {
705 display: Some(LogTextDisplayOptions {
706 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
707 ..Default::default()
708 }),
709 ..Default::default()
710 };
711 let mut formatter =
712 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
713 formatter.set_boot_timestamp(Timestamp::from_nanos(1234));
714 assert_eq!(formatter.get_boot_timestamp(), Timestamp::from_nanos(1234));
715
716 let buffers = TestBuffers::default();
718 let output = JsonWriter::<LogEntry>::new_test(None, &buffers);
719 let options = LogFormatterOptions { display: None, ..Default::default() };
720 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), output, options);
721 formatter.set_boot_timestamp(Timestamp::from_nanos(1234));
722 assert_eq!(formatter.get_boot_timestamp(), Timestamp::from_nanos(1234));
723 }
724
725 #[fuchsia::test]
726 async fn test_format_single_message() {
727 let symbolizer = NoOpSymbolizer {};
728 let mut formatter = FakeFormatter::new();
729 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
730 moniker: "ffx".try_into().unwrap(),
731 timestamp: Timestamp::from_nanos(0),
732 component_url: Some("ffx".into()),
733 severity: Severity::Info,
734 })
735 .set_message("Hello world!")
736 .build();
737 let (sender, receiver) = zx::Socket::create_stream();
738 sender
739 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
740 .expect("failed to write target log");
741 drop(sender);
742 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
743 .await
744 .unwrap();
745 assert_eq!(formatter.logs, vec![LogEntry { data: LogData::TargetLog(target_log) }]);
746 }
747
748 #[fuchsia::test]
749 async fn test_format_utc_timestamp() {
750 let symbolizer = NoOpSymbolizer {};
751 let mut formatter = FakeFormatter::new();
752 formatter.set_boot_timestamp(Timestamp::from_nanos(DEFAULT_TS_NANOS as i64));
753 let (_, receiver) = zx::Socket::create_stream();
754 super::dump_logs_from_socket(
755 flex_client::socket_to_async(receiver),
756 &mut formatter,
757 &symbolizer,
758 true,
759 )
760 .await
761 .unwrap();
762 let target_log = formatter.logs[0].data.as_target_log().unwrap();
763 let properties = target_log.payload_keys().unwrap();
764 assert_eq!(target_log.msg().unwrap(), "Logging started");
765
766 chrono::DateTime::parse_from_rfc3339(
768 properties.get_property("utc_time_now").unwrap().string().unwrap(),
769 )
770 .unwrap();
771 assert_eq!(
772 properties.get_property("current_boot_timestamp").unwrap().int().unwrap(),
773 DEFAULT_TS_NANOS as i64
774 );
775 }
776
777 #[fuchsia::test]
778 async fn test_format_utc_timestamp_does_not_print_if_utc_time() {
779 let symbolizer = NoOpSymbolizer {};
780 let mut formatter = FakeFormatter::new();
781 formatter.is_utc_time_format = true;
782 formatter.set_boot_timestamp(Timestamp::from_nanos(DEFAULT_TS_NANOS as i64));
783 let (_, receiver) = zx::Socket::create_stream();
784 super::dump_logs_from_socket(
785 flex_client::socket_to_async(receiver),
786 &mut formatter,
787 &symbolizer,
788 true,
789 )
790 .await
791 .unwrap();
792 assert_eq!(formatter.logs.len(), 0);
793 }
794
795 #[fuchsia::test]
796 async fn test_format_multiple_messages() {
797 let symbolizer = NoOpSymbolizer {};
798 let mut formatter = FakeFormatter::new();
799 let (sender, receiver) = zx::Socket::create_stream();
800 let target_log_0 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
801 moniker: "ffx".try_into().unwrap(),
802 timestamp: Timestamp::from_nanos(0),
803 component_url: Some("ffx".into()),
804 severity: Severity::Info,
805 })
806 .set_message("Hello world!")
807 .set_pid(1)
808 .set_tid(2)
809 .build();
810 let target_log_1 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
811 moniker: "ffx".try_into().unwrap(),
812 timestamp: Timestamp::from_nanos(1),
813 component_url: Some("ffx".into()),
814 severity: Severity::Info,
815 })
816 .set_message("Hello world 2!")
817 .build();
818 sender
819 .write(serde_json::to_string(&vec![&target_log_0, &target_log_1]).unwrap().as_bytes())
820 .expect("failed to write target log");
821 drop(sender);
822 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
823 .await
824 .unwrap();
825 assert_eq!(
826 formatter.logs,
827 vec![
828 LogEntry { data: LogData::TargetLog(target_log_0) },
829 LogEntry { data: LogData::TargetLog(target_log_1) }
830 ]
831 );
832 }
833
834 #[fuchsia::test]
835 async fn test_format_timestamp_filter() {
836 let symbolizer = NoOpSymbolizer {};
838 let buffers = TestBuffers::default();
839 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
840 let mut formatter = DefaultLogFormatter::new(
841 LogFilterCriteria::default(),
842 stdout,
843 LogFormatterOptions {
844 since: Some(DeviceOrLocalTimestamp {
845 timestamp: Timestamp::from_nanos(1),
846 is_boot: true,
847 }),
848 until: Some(DeviceOrLocalTimestamp {
849 timestamp: Timestamp::from_nanos(3),
850 is_boot: true,
851 }),
852 ..Default::default()
853 },
854 );
855 formatter.set_boot_timestamp(Timestamp::from_nanos(0));
856
857 let (sender, receiver) = zx::Socket::create_stream();
858 let target_log_0 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
859 moniker: "ffx".try_into().unwrap(),
860 timestamp: Timestamp::from_nanos(0),
861 component_url: Some("ffx".into()),
862 severity: Severity::Info,
863 })
864 .set_message("Hello world!")
865 .build();
866 let target_log_1 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
867 moniker: "ffx".try_into().unwrap(),
868 timestamp: Timestamp::from_nanos(1),
869 component_url: Some("ffx".into()),
870 severity: Severity::Info,
871 })
872 .set_message("Hello world 2!")
873 .build();
874 let target_log_2 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
875 moniker: "ffx".try_into().unwrap(),
876 timestamp: Timestamp::from_nanos(2),
877 component_url: Some("ffx".into()),
878 severity: Severity::Info,
879 })
880 .set_pid(1)
881 .set_tid(2)
882 .set_message("Hello world 3!")
883 .build();
884 let target_log_3 = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
885 moniker: "ffx".try_into().unwrap(),
886 timestamp: Timestamp::from_nanos(3),
887 component_url: Some("ffx".into()),
888 severity: Severity::Info,
889 })
890 .set_message("Hello world 4!")
891 .set_pid(1)
892 .set_tid(2)
893 .build();
894 sender
895 .write(
896 serde_json::to_string(&vec![
897 &target_log_0,
898 &target_log_1,
899 &target_log_2,
900 &target_log_3,
901 ])
902 .unwrap()
903 .as_bytes(),
904 )
905 .expect("failed to write target log");
906 drop(sender);
907 assert_matches!(
908 dump_logs_from_socket(
909 flex_client::socket_to_async(receiver),
910 &mut formatter,
911 &symbolizer,
912 )
913 .await,
914 Ok(LogProcessingResult::Exit)
915 );
916 assert_eq!(
917 buffers.stdout.into_string(),
918 "[00000.000000][1][2][ffx] INFO: Hello world 3!\n"
919 );
920 }
921
922 fn make_log_with_timestamp(timestamp: i64) -> LogsData {
923 LogsDataBuilder::new(diagnostics_data::BuilderArgs {
924 moniker: "ffx".try_into().unwrap(),
925 timestamp: Timestamp::from_nanos(timestamp),
926 component_url: Some("ffx".into()),
927 severity: Severity::Info,
928 })
929 .set_message(format!("Hello world {timestamp}!"))
930 .set_pid(1)
931 .set_tid(2)
932 .build()
933 }
934
935 #[fuchsia::test]
936 async fn test_format_timestamp_filter_utc() {
937 let symbolizer = NoOpSymbolizer {};
939 let buffers = TestBuffers::default();
940 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
941 let mut formatter = DefaultLogFormatter::new(
942 LogFilterCriteria::default(),
943 stdout,
944 LogFormatterOptions {
945 since: Some(DeviceOrLocalTimestamp {
946 timestamp: Timestamp::from_nanos(1),
947 is_boot: false,
948 }),
949 until: Some(DeviceOrLocalTimestamp {
950 timestamp: Timestamp::from_nanos(3),
951 is_boot: false,
952 }),
953 display: Some(LogTextDisplayOptions {
954 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
955 ..Default::default()
956 }),
957 ..Default::default()
958 },
959 );
960 formatter.set_boot_timestamp(Timestamp::from_nanos(1));
961
962 let (sender, receiver) = zx::Socket::create_stream();
963 let logs = (0..4).map(make_log_with_timestamp).collect::<Vec<_>>();
964 sender
965 .write(serde_json::to_string(&logs).unwrap().as_bytes())
966 .expect("failed to write target log");
967 drop(sender);
968 assert_matches!(
969 dump_logs_from_socket(
970 flex_client::socket_to_async(receiver),
971 &mut formatter,
972 &symbolizer,
973 )
974 .await,
975 Ok(LogProcessingResult::Exit)
976 );
977 assert_eq!(
978 buffers.stdout.into_string(),
979 "[1970-01-01 00:00:00.000][1][2][ffx] INFO: Hello world 1!\n"
980 );
981 }
982
983 fn logs_data_builder() -> LogsDataBuilder {
984 diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
985 timestamp: Timestamp::from_nanos(default_ts().as_nanos() as i64),
986 component_url: Some("component_url".into()),
987 moniker: "some/moniker".try_into().unwrap(),
988 severity: diagnostics_data::Severity::Warn,
989 })
990 .set_pid(1)
991 .set_tid(2)
992 }
993
994 fn default_ts() -> Duration {
995 Duration::from_nanos(DEFAULT_TS_NANOS)
996 }
997
998 fn log_entry() -> LogEntry {
999 LogEntry {
1000 data: LogData::TargetLog(
1001 logs_data_builder().add_tag("tag1").add_tag("tag2").set_message("message").build(),
1002 ),
1003 }
1004 }
1005
1006 #[fuchsia::test]
1007 async fn test_default_formatter() {
1008 let buffers = TestBuffers::default();
1009 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1010 let options = LogFormatterOptions::default();
1011 let mut formatter =
1012 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1013 formatter.push_log(log_entry()).await.unwrap();
1014 drop(formatter);
1015 assert_eq!(
1016 buffers.into_stdout_str(),
1017 "[1615535969.000000][1][2][some/moniker][tag1,tag2] WARN: message\n"
1018 );
1019 }
1020
1021 #[fuchsia::test]
1022 async fn test_default_formatter_with_hidden_metadata() {
1023 let buffers = TestBuffers::default();
1024 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1025 let options = LogFormatterOptions {
1026 display: Some(LogTextDisplayOptions { show_metadata: false, ..Default::default() }),
1027 ..LogFormatterOptions::default()
1028 };
1029 let mut formatter =
1030 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1031 formatter.push_log(log_entry()).await.unwrap();
1032 drop(formatter);
1033 assert_eq!(
1034 buffers.into_stdout_str(),
1035 "[1615535969.000000][some/moniker][tag1,tag2] WARN: message\n"
1036 );
1037 }
1038
1039 #[fuchsia::test]
1040 async fn test_default_formatter_with_json() {
1041 let buffers = TestBuffers::default();
1042 let stdout = JsonWriter::<LogEntry>::new_test(Some(Format::Json), &buffers);
1043 let options = LogFormatterOptions { display: None, ..Default::default() };
1044 {
1045 let mut formatter =
1046 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1047 formatter.push_log(log_entry()).await.unwrap();
1048 }
1049 assert_eq!(
1050 serde_json::from_str::<LogEntry>(&buffers.into_stdout_str()).unwrap(),
1051 log_entry()
1052 );
1053 }
1054
1055 fn emit_log(sender: &mut zx::Socket, msg: &str, timestamp: i64) -> Data<Logs> {
1056 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1057 moniker: "ffx".try_into().unwrap(),
1058 timestamp: Timestamp::from_nanos(timestamp),
1059 component_url: Some("ffx".into()),
1060 severity: Severity::Info,
1061 })
1062 .set_message(msg)
1063 .build();
1064
1065 sender
1066 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
1067 .expect("failed to write target log");
1068 target_log
1069 }
1070
1071 #[fuchsia::test]
1072 async fn test_default_formatter_discards_when_told_by_symbolizer() {
1073 let mut formatter = FakeFormatter::new();
1074 let (mut sender, receiver) = zx::Socket::create_stream();
1075 let mut target_log_0 = emit_log(&mut sender, "Hello world!", 0);
1076 emit_log(&mut sender, "Dropped world!", 1);
1077 let mut target_log_2 = emit_log(&mut sender, "Hello world!", 2);
1078 emit_log(&mut sender, "Dropped world!", 3);
1079 let mut target_log_4 = emit_log(&mut sender, "Hello world!", 4);
1080 drop(sender);
1081 let symbolizer = FakeSymbolizerCallback::new();
1083 *target_log_0.msg_mut().unwrap() = "symbolized log".into();
1084 *target_log_2.msg_mut().unwrap() = "symbolized log".into();
1085 *target_log_4.msg_mut().unwrap() = "symbolized log".into();
1086 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
1087 .await
1088 .unwrap();
1089 assert_eq!(
1090 formatter.logs,
1091 vec![
1092 LogEntry { data: LogData::TargetLog(target_log_0) },
1093 LogEntry { data: LogData::TargetLog(target_log_2) },
1094 LogEntry { data: LogData::TargetLog(target_log_4) }
1095 ],
1096 );
1097 }
1098
1099 #[fuchsia::test]
1100 async fn test_symbolized_output() {
1101 let symbolizer = FakeFuchsiaSymbolizer;
1102 let buffers = TestBuffers::default();
1103 let output = JsonWriter::<LogEntry>::new_test(None, &buffers);
1104 let mut formatter = DefaultLogFormatter::new(
1105 LogFilterCriteria::default(),
1106 output,
1107 LogFormatterOptions { ..Default::default() },
1108 );
1109 formatter.set_boot_timestamp(Timestamp::from_nanos(0));
1110 let target_log = LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1111 moniker: "ffx".try_into().unwrap(),
1112 timestamp: Timestamp::from_nanos(0),
1113 component_url: Some("ffx".into()),
1114 severity: Severity::Info,
1115 })
1116 .set_pid(1)
1117 .set_tid(2)
1118 .set_message("Hello world!")
1119 .build();
1120 let (sender, receiver) = zx::Socket::create_stream();
1121 sender
1122 .write(serde_json::to_string(&target_log).unwrap().as_bytes())
1123 .expect("failed to write target log");
1124 drop(sender);
1125 dump_logs_from_socket(flex_client::socket_to_async(receiver), &mut formatter, &symbolizer)
1126 .await
1127 .unwrap();
1128 assert_eq!(buffers.stdout.into_string(), "[00000.000000][1][2][ffx] INFO: Fuchsia\n");
1129 }
1130
1131 #[test]
1132 fn test_device_or_local_timestamp_returns_none_if_now_is_passed() {
1133 assert_matches!(DeviceOrLocalTimestamp::new(Some(&parse_time("now").unwrap()), None), None);
1134 }
1135
1136 struct BrokenPipeWriter;
1137 impl std::io::Write for BrokenPipeWriter {
1138 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
1139 Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1140 }
1141
1142 fn flush(&mut self) -> std::io::Result<()> {
1143 Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1144 }
1145 }
1146
1147 impl ToolIO for BrokenPipeWriter {
1148 type OutputItem = LogEntry;
1149 fn is_machine(&self) -> bool {
1150 false
1151 }
1152
1153 fn stderr(&mut self) -> &mut dyn std::io::Write {
1154 self
1155 }
1156
1157 fn item(&mut self, _value: &Self::OutputItem) -> writer::Result<()> {
1158 Err(writer::Error::Io(std::io::Error::new(
1159 std::io::ErrorKind::BrokenPipe,
1160 "broken pipe",
1161 )))
1162 }
1163 }
1164
1165 #[fuchsia::test]
1166 async fn test_default_formatter_exits_on_broken_pipe() {
1167 let stdout = BrokenPipeWriter;
1168 let options = LogFormatterOptions::default();
1169 let mut formatter =
1170 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options.clone());
1171 let result = formatter.push_log(log_entry()).await;
1172 assert_matches!(result, Ok(LogProcessingResult::Exit));
1173 }
1174
1175 #[test]
1176 fn test_formatter_error_is_broken_pipe() {
1177 assert!(
1178 FormatterError::IO(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1179 .is_broken_pipe()
1180 );
1181 assert!(!FormatterError::IO(std::io::Error::other("other")).is_broken_pipe());
1182 assert!(!FormatterError::Other(anyhow::anyhow!("other")).is_broken_pipe());
1183 }
1184
1185 #[cfg(not(feature = "fdomain"))]
1186 #[fuchsia::test]
1187 async fn test_json_and_fxt_output_identical() {
1188 use diagnostics_data::ExtendedMoniker;
1189 use diagnostics_log_encoding::encode::{Encoder, EncoderOpts, ResizableBuffer};
1190 use diagnostics_log_encoding::{Argument, Header, LOG_CONTROL_BIT, MONIKER, Record, URL};
1191 use diagnostics_message::MonikerWithUrl;
1192 use flyweights::FlyStr;
1193 use zerocopy::{FromBytes, IntoBytes};
1194
1195 let symbolizer = NoOpSymbolizer {};
1196 let options = LogFormatterOptions::default();
1197
1198 let fn_encode = |record: Record<'_>, tag: u32| -> Vec<u8> {
1199 let mut encoder = Encoder::new(
1200 std::io::Cursor::new(ResizableBuffer::from(Vec::new())),
1201 EncoderOpts::default(),
1202 );
1203 encoder.write_record(record).unwrap();
1204 let mut bytes = encoder.take().into_inner().into_inner();
1205 let mut header = Header::read_from_bytes(&bytes[0..8]).unwrap();
1206 header.set_tag(tag);
1207 bytes[0..8].copy_from_slice(header.as_bytes());
1208 bytes
1209 };
1210
1211 let manifest_bytes = fn_encode(
1212 Record {
1213 timestamp: zx::BootInstant::from_nanos(0),
1214 severity: 0x30, arguments: vec![
1216 Argument::other(MONIKER, "core/foo"),
1217 Argument::other(URL, "fuchsia-pkg://foo"),
1218 ],
1219 },
1220 1 | LOG_CONTROL_BIT,
1221 );
1222
1223 let log_bytes1 = fn_encode(
1224 Record {
1225 timestamp: zx::BootInstant::from_nanos(123456),
1226 severity: 0x30, arguments: vec![
1228 Argument::pid(zx::Koid::from_raw(1000)),
1229 Argument::tid(zx::Koid::from_raw(2000)),
1230 Argument::tag("my_tag"),
1231 Argument::message("Hello identical world!"),
1232 ],
1233 },
1234 1,
1235 );
1236
1237 let log_bytes2 = fn_encode(
1238 Record {
1239 timestamp: zx::BootInstant::from_nanos(123457),
1240 severity: 0x40, arguments: vec![
1242 Argument::file("src/main.rs"),
1243 Argument::line(42),
1244 Argument::dropped(5),
1245 Argument::message("Warning with source location and dropped count"),
1246 ],
1247 },
1248 1,
1249 );
1250
1251 let log_bytes3_signed = fn_encode(
1252 Record {
1253 timestamp: zx::BootInstant::from_nanos(123458),
1254 severity: 0x50, arguments: vec![
1256 Argument::message("Error with custom signed int"),
1257 Argument::new("signed_val", -12345i64),
1258 ],
1259 },
1260 1,
1261 );
1262
1263 let log_bytes3_unsigned = fn_encode(
1264 Record {
1265 timestamp: zx::BootInstant::from_nanos(123458),
1266 severity: 0x50, arguments: vec![
1268 Argument::message("Error with custom unsigned int"),
1269 Argument::new("unsigned_val", 67890u64),
1270 ],
1271 },
1272 1,
1273 );
1274
1275 let log_bytes3_bool = fn_encode(
1276 Record {
1277 timestamp: zx::BootInstant::from_nanos(123458),
1278 severity: 0x50, arguments: vec![
1280 Argument::message("Error with custom boolean"),
1281 Argument::new("bool_val", true),
1282 ],
1283 },
1284 1,
1285 );
1286
1287 let log_bytes3_str = fn_encode(
1288 Record {
1289 timestamp: zx::BootInstant::from_nanos(123458),
1290 severity: 0x50, arguments: vec![
1292 Argument::message("Error with custom string"),
1293 Argument::new("string_val", "custom string"),
1294 ],
1295 },
1296 1,
1297 );
1298
1299 let log_bytes4_pi = fn_encode(
1300 Record {
1301 timestamp: zx::BootInstant::from_nanos(123459),
1302 severity: 0x60, arguments: vec![
1304 Argument::message("Fatal with float pi"),
1305 Argument::new("float_pi", std::f64::consts::PI),
1306 ],
1307 },
1308 1,
1309 );
1310
1311 let log_bytes4_zero = fn_encode(
1312 Record {
1313 timestamp: zx::BootInstant::from_nanos(123459),
1314 severity: 0x60, arguments: vec![
1316 Argument::message("Fatal with float zero"),
1317 Argument::new("float_zero", 0.0f64),
1318 ],
1319 },
1320 1,
1321 );
1322
1323 let log_bytes4_large = fn_encode(
1324 Record {
1325 timestamp: zx::BootInstant::from_nanos(123459),
1326 severity: 0x60, arguments: vec![
1328 Argument::message("Fatal with float large"),
1329 Argument::new("float_large", 123456.789f64),
1330 ],
1331 },
1332 1,
1333 );
1334
1335 let all_records = [
1336 &log_bytes1,
1337 &log_bytes2,
1338 &log_bytes3_signed,
1339 &log_bytes3_unsigned,
1340 &log_bytes3_bool,
1341 &log_bytes3_str,
1342 &log_bytes4_pi,
1343 &log_bytes4_zero,
1344 &log_bytes4_large,
1345 ];
1346
1347 let buffers_json = TestBuffers::default();
1349 let stdout_json = JsonWriter::<LogEntry>::new_test(None, &buffers_json);
1350 let mut formatter_json =
1351 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout_json, options.clone());
1352 formatter_json.set_boot_timestamp(Timestamp::from_nanos(0));
1353
1354 let source = MonikerWithUrl {
1355 moniker: ExtendedMoniker::parse_str("core/foo").unwrap(),
1356 url: FlyStr::new("fuchsia-pkg://foo"),
1357 };
1358
1359 let (sender_json, receiver_json) = zx::Socket::create_stream();
1360 let mut json_stream_bytes = Vec::new();
1361 for record_bytes in &all_records {
1362 let target_log =
1363 diagnostics_message::from_structured(source.clone(), record_bytes).unwrap();
1364 serde_json::to_writer(&mut json_stream_bytes, &target_log).unwrap();
1365 json_stream_bytes.push(b'\n');
1366 }
1367 sender_json.write(&json_stream_bytes).expect("failed to write target logs");
1368 drop(sender_json);
1369
1370 super::dump_logs_from_socket(
1371 flex_client::socket_to_async(receiver_json),
1372 &mut formatter_json,
1373 &symbolizer,
1374 false,
1375 )
1376 .await
1377 .unwrap();
1378
1379 let buffers_fxt = TestBuffers::default();
1381 let stdout_fxt = JsonWriter::<LogEntry>::new_test(None, &buffers_fxt);
1382 let mut formatter_fxt =
1383 DefaultLogFormatter::new(LogFilterCriteria::default(), stdout_fxt, options.clone());
1384 formatter_fxt.set_boot_timestamp(Timestamp::from_nanos(0));
1385
1386 let (sender_fxt, receiver_fxt) = zx::Socket::create_stream();
1387 sender_fxt.write(&manifest_bytes).unwrap();
1388 for record_bytes in &all_records {
1389 sender_fxt.write(record_bytes).unwrap();
1390 }
1391 drop(sender_fxt);
1392
1393 super::dump_fxt_logs_from_socket(
1394 flex_client::socket_to_async(receiver_fxt),
1395 &mut formatter_fxt,
1396 &symbolizer,
1397 false,
1398 )
1399 .await
1400 .unwrap();
1401
1402 assert_eq!(buffers_json.stdout.into_string(), buffers_fxt.stdout.into_string());
1404 }
1405
1406 #[fuchsia::test]
1407 async fn test_default_formatter_tail_limit() {
1408 let buffers = TestBuffers::default();
1409 let stdout = JsonWriter::<LogEntry>::new_test(None, &buffers);
1410 let options = LogFormatterOptions { tail: Some(2), ..Default::default() };
1411 let mut formatter = DefaultLogFormatter::new(LogFilterCriteria::default(), stdout, options);
1412
1413 let entry1 = LogEntry {
1414 data: LogData::TargetLog(
1415 logs_data_builder().add_tag("tag1").set_message("msg1").build(),
1416 ),
1417 };
1418 let entry2 = LogEntry {
1419 data: LogData::TargetLog(
1420 logs_data_builder().add_tag("tag1").set_message("msg2").build(),
1421 ),
1422 };
1423 let entry3 = LogEntry {
1424 data: LogData::TargetLog(
1425 logs_data_builder().add_tag("tag1").set_message("msg3").build(),
1426 ),
1427 };
1428
1429 formatter.push_log(entry1).await.unwrap();
1430 formatter.push_log(entry2).await.unwrap();
1431 formatter.push_log(entry3).await.unwrap();
1432
1433 let stdout_snapshot = buffers.stdout.clone().into_string();
1435 assert_eq!(stdout_snapshot, "");
1436
1437 formatter.flush().await.unwrap();
1438
1439 let output = buffers.into_stdout_str();
1441 assert!(!output.contains("msg1"));
1442 assert!(output.contains("msg2"));
1443 assert!(output.contains("msg3"));
1444 }
1445}