Skip to main content

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