log_command/
log_formatter.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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/// Type of data in a log entry
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
32pub enum LogData {
33    /// A log entry from the target
34    TargetLog(LogsData),
35}
36
37impl LogData {
38    /// Gets the LogData as a target log.
39    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/// A log entry from either the host, target, or
59/// a symbolized log.
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61pub struct LogEntry {
62    /// The log
63    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
77// Required if we want to use ffx's built-in I/O, but
78// this isn't really applicable to us because we have
79// custom formatting rules.
80impl 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/// A trait for symbolizing log entries
87#[async_trait(?Send)]
88pub trait Symbolize {
89    /// Symbolizes a LogEntry and optionally produces a result.
90    /// The symbolizer may choose to discard the result.
91    /// This method may be called multiple times concurrently.
92    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
126/// Reads logs from a socket and formats them using the given formatter and symbolizer.
127pub 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    /// Sets the boot timestamp in nanoseconds since the Unix epoch.
165    fn set_boot_timestamp(&mut self, _boot_ts_nanos: Timestamp);
166
167    /// Returns the boot timestamp in nanoseconds since the Unix epoch.
168    fn get_boot_timestamp(&self) -> Timestamp;
169}
170
171/// Timestamp filter which is either either boot-based or UTC-based.
172#[derive(Clone, Debug)]
173pub struct DeviceOrLocalTimestamp {
174    /// Timestamp in boot time
175    pub timestamp: Timestamp,
176    /// True if this filter should be applied to boot time,
177    /// false if UTC time.
178    pub is_boot: bool,
179}
180
181impl DeviceOrLocalTimestamp {
182    /// Creates a DeviceOrLocalTimestamp from a real-time date/time or
183    /// a boot date/time. Returns None if both rtc and boot are None.
184    /// Returns None if the timestamp is "now".
185    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/// Log formatter options
207#[derive(Clone, Debug)]
208pub struct LogFormatterOptions {
209    /// Text display options
210    pub display: Option<LogTextDisplayOptions>,
211    /// Only display logs since the specified time.
212    pub since: Option<DeviceOrLocalTimestamp>,
213    /// Only display logs until the specified time.
214    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/// Log formatter error
224#[derive(Error, Debug)]
225pub enum FormatterError {
226    /// An unknown error occurred
227    #[error(transparent)]
228    Other(#[from] anyhow::Error),
229    /// An IO error occurred
230    #[error(transparent)]
231    IO(#[from] std::io::Error),
232}
233
234/// Default formatter implementation
235pub 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
245/// Converts from UTC time to boot time.
246fn 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
287/// Object which contains a Writer that can be borrowed
288pub 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    /// Creates a new DefaultLogFormatter with the given writer and options.
309    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                    // For host logs, don't apply the boot time offset
348                    // as this is with reference to the UTC timeline
349                    if let LogTimeDisplayFormat::WallTime { ref mut offset, .. } =
350                        options_for_this_line_only.display.as_mut().unwrap().time_format
351                    {
352                        // 1 nanosecond so that LogTimeDisplayFormat in diagnostics_data
353                        // knows that we have a valid UTC offset. It normally falls back if
354                        // the UTC offset is 0. It prints at millisecond precision so being
355                        // off by +1 nanosecond isn't an issue.
356                        *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    /// Creates a new DefaultLogFormatter from command-line arguments.
370    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                                // This will receive a correct value when logging actually starts,
392                                // see `set_boot_timestamp()` method on the log formatter.
393                                offset: 0,
394                            },
395                            TimeFormat::Utc => LogTimeDisplayFormat::WallTime {
396                                tz: Timezone::Utc,
397                                // This will receive a correct value when logging actually starts,
398                                // see `set_boot_timestamp()` method on the log formatter.
399                                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                &timestamp.timestamp,
429            )
430        } else {
431            callback(&log_entry.utc_timestamp(self.boot_ts_nanos), &timestamp.timestamp)
432        }
433    }
434
435    // This function's arguments are copied to make lifetimes in push_log easier since borrowing
436    // &self would complicate spam highlighting.
437    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                // TODO(https://fxbug.dev/42072442): Add support for log spam redaction and other
451                // features listed in the design doc.
452                writeln!(self.writer, "{}", LogTextPresenter::new(&data, text_options))?;
453            }
454        }
455        Ok(())
456    }
457}
458
459#[allow(dead_code)] // TODO(https://fxbug.dev/421409178)
460/// Symbolizer that does nothing.
461pub 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/// Trait for formatting logs one at a time.
471#[async_trait(?Send)]
472pub trait LogFormatter {
473    /// Formats a log entry and writes it to the output.
474    async fn push_log(&mut self, log_entry: LogEntry) -> Result<LogProcessingResult, LogError>;
475
476    /// Returns true if the formatter is configured to output in UTC time format.
477    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    /// Symbolizer that prints "Fuchsia".
531    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        // Boot timestamp is supported when using JSON output (for filtering)
597        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        // Ensure the end has a valid timestamp
651        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        // test since and until args for the LogFormatter
725        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        // test since and until args for the LogFormatter
826        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        // Drop every other log.
969        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}