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