diagnostics_data/
lib.rs

1// Copyright 2020 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
5//! # Diagnostics data
6//!
7//! This library contains the Diagnostics data schema used for inspect and logs . This is
8//! the data that the Archive returns on `fuchsia.diagnostics.ArchiveAccessor` reads.
9
10use chrono::{Local, TimeZone, Utc};
11use diagnostics_hierarchy::HierarchyMatcher;
12use fidl_fuchsia_diagnostics::{DataType, Selector};
13use fidl_fuchsia_inspect as finspect;
14use flyweights::FlyStr;
15use itertools::Itertools;
16use moniker::EXTENDED_MONIKER_COMPONENT_MANAGER_STR;
17use selectors::SelectorExt;
18use serde::de::{DeserializeOwned, Deserializer};
19use serde::{Deserialize, Serialize, Serializer};
20use std::borrow::{Borrow, Cow};
21use std::cmp::Ordering;
22use std::fmt;
23use std::hash::Hash;
24use std::ops::Deref;
25use std::str::FromStr;
26use std::sync::LazyLock;
27use std::time::Duration;
28use termion::{color, style};
29use thiserror::Error;
30
31pub use diagnostics_hierarchy::{hierarchy, DiagnosticsHierarchy, Property};
32pub use diagnostics_log_types_serde::Severity;
33pub use moniker::ExtendedMoniker;
34
35#[cfg(target_os = "fuchsia")]
36#[doc(hidden)]
37pub mod logs_legacy;
38
39#[cfg(feature = "json_schema")]
40use schemars::JsonSchema;
41
42const SCHEMA_VERSION: u64 = 1;
43const MICROS_IN_SEC: u128 = 1000000;
44const ROOT_MONIKER_REPR: &str = "<root>";
45
46static DEFAULT_TREE_NAME: LazyLock<FlyStr> =
47    LazyLock::new(|| FlyStr::new(finspect::DEFAULT_TREE_NAME));
48
49/// The possible name for a handle to inspect data. It could be a filename (being deprecated) or a
50/// name published using `fuchsia.inspect.InspectSink`.
51#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Hash, Eq)]
52#[serde(rename_all = "lowercase")]
53pub enum InspectHandleName {
54    /// The name of an `InspectHandle`. This comes from the `name` argument
55    /// in `InspectSink`.
56    Name(FlyStr),
57
58    /// The name of the file source when reading a file source of Inspect
59    /// (eg an inspect VMO file or fuchsia.inspect.Tree in out/diagnostics)
60    Filename(FlyStr),
61}
62
63impl std::fmt::Display for InspectHandleName {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "{}", self.as_ref())
66    }
67}
68
69impl InspectHandleName {
70    /// Construct an InspectHandleName::Name
71    pub fn name(n: impl Into<FlyStr>) -> Self {
72        Self::Name(n.into())
73    }
74
75    /// Construct an InspectHandleName::Filename
76    pub fn filename(n: impl Into<FlyStr>) -> Self {
77        Self::Filename(n.into())
78    }
79
80    /// If variant is Name, get the underlying value.
81    pub fn as_name(&self) -> Option<&str> {
82        if let Self::Name(n) = self {
83            Some(n.as_str())
84        } else {
85            None
86        }
87    }
88
89    /// If variant is Filename, get the underlying value
90    pub fn as_filename(&self) -> Option<&str> {
91        if let Self::Filename(f) = self {
92            Some(f.as_str())
93        } else {
94            None
95        }
96    }
97}
98
99impl AsRef<str> for InspectHandleName {
100    fn as_ref(&self) -> &str {
101        match self {
102            Self::Filename(f) => f.as_str(),
103            Self::Name(n) => n.as_str(),
104        }
105    }
106}
107
108/// The source of diagnostics data
109#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
110#[derive(Default, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
111pub enum DataSource {
112    #[default]
113    Unknown,
114    Inspect,
115    Logs,
116}
117
118pub trait MetadataError {
119    fn dropped_payload() -> Self;
120    fn message(&self) -> Option<&str>;
121}
122
123pub trait Metadata: DeserializeOwned + Serialize + Clone + Send {
124    /// The type of error returned in this metadata.
125    type Error: Clone + MetadataError;
126
127    /// Returns the timestamp at which this value was recorded.
128    fn timestamp(&self) -> Timestamp;
129
130    /// Returns the errors recorded with this value, if any.
131    fn errors(&self) -> Option<&[Self::Error]>;
132
133    /// Overrides the errors associated with this value.
134    fn set_errors(&mut self, errors: Vec<Self::Error>);
135
136    /// Returns whether any errors are recorded on this value.
137    fn has_errors(&self) -> bool {
138        self.errors().map(|e| !e.is_empty()).unwrap_or_default()
139    }
140}
141
142/// A trait implemented by marker types which denote "kinds" of diagnostics data.
143pub trait DiagnosticsData {
144    /// The type of metadata included in results of this type.
145    type Metadata: Metadata;
146
147    /// The type of key used for indexing node hierarchies in the payload.
148    type Key: AsRef<str> + Clone + DeserializeOwned + Eq + FromStr + Hash + Send + 'static;
149
150    /// Used to query for this kind of metadata in the ArchiveAccessor.
151    const DATA_TYPE: DataType;
152}
153
154/// Inspect carries snapshots of data trees hosted by components.
155#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
156pub struct Inspect;
157
158impl DiagnosticsData for Inspect {
159    type Metadata = InspectMetadata;
160    type Key = String;
161    const DATA_TYPE: DataType = DataType::Inspect;
162}
163
164impl Metadata for InspectMetadata {
165    type Error = InspectError;
166
167    fn timestamp(&self) -> Timestamp {
168        self.timestamp
169    }
170
171    fn errors(&self) -> Option<&[Self::Error]> {
172        self.errors.as_deref()
173    }
174
175    fn set_errors(&mut self, errors: Vec<Self::Error>) {
176        self.errors = Some(errors);
177    }
178}
179
180/// Logs carry streams of structured events from components.
181#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
182pub struct Logs;
183
184impl DiagnosticsData for Logs {
185    type Metadata = LogsMetadata;
186    type Key = LogsField;
187    const DATA_TYPE: DataType = DataType::Logs;
188}
189
190impl Metadata for LogsMetadata {
191    type Error = LogError;
192
193    fn timestamp(&self) -> Timestamp {
194        self.timestamp
195    }
196
197    fn errors(&self) -> Option<&[Self::Error]> {
198        self.errors.as_deref()
199    }
200
201    fn set_errors(&mut self, errors: Vec<Self::Error>) {
202        self.errors = Some(errors);
203    }
204}
205
206pub fn serialize_timestamp<S>(timestamp: &Timestamp, serializer: S) -> Result<S::Ok, S::Error>
207where
208    S: Serializer,
209{
210    serializer.serialize_i64(timestamp.into_nanos())
211}
212
213pub fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<Timestamp, D::Error>
214where
215    D: Deserializer<'de>,
216{
217    let nanos = i64::deserialize(deserializer)?;
218    Ok(Timestamp::from_nanos(nanos))
219}
220
221#[cfg(target_os = "fuchsia")]
222mod zircon {
223    pub type Timestamp = zx::BootInstant;
224
225    /// De-applies the mono-to-boot offset on this timestamp.
226    ///
227    /// This works only if called soon after `self` is produced, otherwise
228    /// the timestamp will be placed further back in time.
229    pub fn unapply_mono_to_boot_offset(timestamp: Timestamp) -> zx::MonotonicInstant {
230        let mono_now = zx::MonotonicInstant::get();
231        let boot_now = zx::BootInstant::get();
232
233        let mono_to_boot_offset_nanos = boot_now.into_nanos() - mono_now.into_nanos();
234        zx::MonotonicInstant::from_nanos(timestamp.into_nanos() - mono_to_boot_offset_nanos)
235    }
236}
237
238#[cfg(target_os = "fuchsia")]
239pub use zircon::Timestamp;
240#[cfg(target_os = "fuchsia")]
241pub use zircon::unapply_mono_to_boot_offset;
242
243#[cfg(not(target_os = "fuchsia"))]
244mod host {
245    use serde::{Deserialize, Serialize};
246    use std::fmt;
247    use std::ops::Add;
248    use std::time::Duration;
249
250    #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
251    pub struct Timestamp(i64);
252
253    impl Timestamp {
254        /// Returns the number of nanoseconds associated with this timestamp.
255        pub fn into_nanos(self) -> i64 {
256            self.0
257        }
258
259        /// Constructs a timestamp from the given nanoseconds.
260        pub fn from_nanos(nanos: i64) -> Self {
261            Self(nanos)
262        }
263    }
264
265    impl Add<Duration> for Timestamp {
266        type Output = Timestamp;
267        fn add(self, rhs: Duration) -> Self::Output {
268            Timestamp(self.0 + rhs.as_nanos() as i64)
269        }
270    }
271
272    impl fmt::Display for Timestamp {
273        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274            write!(f, "{}", self.0)
275        }
276    }
277}
278
279#[cfg(not(target_os = "fuchsia"))]
280pub use host::Timestamp;
281
282#[cfg(feature = "json_schema")]
283impl JsonSchema for Timestamp {
284    fn schema_name() -> String {
285        "integer".to_owned()
286    }
287
288    fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
289        i64::json_schema(generator)
290    }
291}
292
293/// The metadata contained in a `DiagnosticsData` object where the data source is
294/// `DataSource::Inspect`.
295#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
296pub struct InspectMetadata {
297    /// Optional vector of errors encountered by platform.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub errors: Option<Vec<InspectError>>,
300
301    /// Name of diagnostics source producing data.
302    #[serde(flatten)]
303    pub name: InspectHandleName,
304
305    /// The url with which the component was launched.
306    pub component_url: FlyStr,
307
308    /// Boot time in nanos.
309    #[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
310    pub timestamp: Timestamp,
311
312    /// When set to true, the data was escrowed. Otherwise, the data was fetched live from the
313    /// source component at runtime. When absent, it means the value is false.
314    #[serde(skip_serializing_if = "std::ops::Not::not")]
315    #[serde(default)]
316    pub escrowed: bool,
317}
318
319impl InspectMetadata {
320    /// Returns the component URL with which the component that emitted the associated Inspect data
321    /// was launched.
322    pub fn component_url(&self) -> &str {
323        self.component_url.as_str()
324    }
325}
326
327/// The metadata contained in a `DiagnosticsData` object where the data source is
328/// `DataSource::Logs`.
329#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
330#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
331pub struct LogsMetadata {
332    // TODO(https://fxbug.dev/42136318) figure out exact spelling of pid/tid context and severity
333    /// Optional vector of errors encountered by platform.
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub errors: Option<Vec<LogError>>,
336
337    /// The url with which the component was launched.
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub component_url: Option<FlyStr>,
340
341    /// Boot time in nanos.
342    #[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
343    pub timestamp: Timestamp,
344
345    /// Severity of the message.
346    // For some reason using the `with` field was causing clippy errors, so this manually uses
347    // `serialize_with` and `deserialize_with`
348    #[serde(
349        serialize_with = "diagnostics_log_types_serde::severity::serialize",
350        deserialize_with = "diagnostics_log_types_serde::severity::deserialize"
351    )]
352    pub severity: Severity,
353
354    /// Raw severity if any. This will typically be unset unless the log message carries a severity
355    /// that differs from the standard values of each severity.
356    #[serde(skip_serializing_if = "Option::is_none")]
357    raw_severity: Option<u8>,
358
359    /// Tags to add at the beginning of the message
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub tags: Option<Vec<String>>,
362
363    /// The process ID
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub pid: Option<u64>,
366
367    /// The thread ID
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub tid: Option<u64>,
370
371    /// The file name
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub file: Option<String>,
374
375    /// The line number
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub line: Option<u64>,
378
379    /// Number of dropped messages
380    /// DEPRECATED: do not set. Left for backwards compatibility with older serialized metadatas
381    /// that contain this field.
382    #[serde(skip)]
383    dropped: Option<u64>,
384
385    /// Size of the original message on the wire, in bytes.
386    /// DEPRECATED: do not set. Left for backwards compatibility with older serialized metadatas
387    /// that contain this field.
388    #[serde(skip)]
389    size_bytes: Option<usize>,
390}
391
392impl LogsMetadata {
393    /// Returns the component URL which generated this value.
394    pub fn component_url(&self) -> Option<&str> {
395        self.component_url.as_ref().map(|s| s.as_str())
396    }
397
398    /// Returns the raw severity of this log.
399    pub fn raw_severity(&self) -> u8 {
400        match self.raw_severity {
401            Some(s) => s,
402            None => self.severity as u8,
403        }
404    }
405}
406
407/// An instance of diagnostics data with typed metadata and an optional nested payload.
408#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
409pub struct Data<D: DiagnosticsData> {
410    /// The source of the data.
411    #[serde(default)]
412    // TODO(https://fxbug.dev/42135946) remove this once the Metadata enum is gone everywhere
413    pub data_source: DataSource,
414
415    /// The metadata for the diagnostics payload.
416    #[serde(bound(
417        deserialize = "D::Metadata: DeserializeOwned",
418        serialize = "D::Metadata: Serialize"
419    ))]
420    pub metadata: D::Metadata,
421
422    /// Moniker of the component that generated the payload.
423    #[serde(deserialize_with = "moniker_deserialize", serialize_with = "moniker_serialize")]
424    pub moniker: ExtendedMoniker,
425
426    /// Payload containing diagnostics data, if the payload exists, else None.
427    pub payload: Option<DiagnosticsHierarchy<D::Key>>,
428
429    /// Schema version.
430    #[serde(default)]
431    pub version: u64,
432}
433
434fn moniker_deserialize<'de, D>(deserializer: D) -> Result<ExtendedMoniker, D::Error>
435where
436    D: serde::Deserializer<'de>,
437{
438    let moniker_str = String::deserialize(deserializer)?;
439    ExtendedMoniker::parse_str(&moniker_str).map_err(serde::de::Error::custom)
440}
441
442fn moniker_serialize<S>(moniker: &ExtendedMoniker, s: S) -> Result<S::Ok, S::Error>
443where
444    S: Serializer,
445{
446    s.collect_str(moniker)
447}
448
449impl<D> Data<D>
450where
451    D: DiagnosticsData,
452{
453    /// Returns a [`Data`] with an error indicating that the payload was dropped.
454    pub fn drop_payload(&mut self) {
455        self.metadata.set_errors(vec![
456            <<D as DiagnosticsData>::Metadata as Metadata>::Error::dropped_payload(),
457        ]);
458        self.payload = None;
459    }
460
461    /// Sorts this [`Data`]'s payload if one is present.
462    pub fn sort_payload(&mut self) {
463        if let Some(payload) = &mut self.payload {
464            payload.sort();
465        }
466    }
467
468    /// Uses a set of Selectors to filter self's payload and returns the resulting
469    /// Data. If the resulting payload is empty, it returns Ok(None).
470    pub fn filter(mut self, selectors: &[Selector]) -> Result<Option<Self>, Error> {
471        let Some(hierarchy) = self.payload else {
472            return Ok(None);
473        };
474        let matching_selectors =
475            match self.moniker.match_against_selectors(selectors).collect::<Result<Vec<_>, _>>() {
476                Ok(selectors) if selectors.is_empty() => return Ok(None),
477                Ok(selectors) => selectors,
478                Err(e) => {
479                    return Err(Error::Internal(e));
480                }
481            };
482
483        // TODO(https://fxbug.dev/300319116): Cache the `HierarchyMatcher`s
484        let matcher: HierarchyMatcher = match matching_selectors.try_into() {
485            Ok(hierarchy_matcher) => hierarchy_matcher,
486            Err(e) => {
487                return Err(Error::Internal(e.into()));
488            }
489        };
490
491        self.payload = match diagnostics_hierarchy::filter_hierarchy(hierarchy, &matcher) {
492            Some(hierarchy) => Some(hierarchy),
493            None => return Ok(None),
494        };
495        Ok(Some(self))
496    }
497}
498
499/// Errors that can happen in this library.
500#[derive(Debug, Error)]
501pub enum Error {
502    #[error(transparent)]
503    Internal(#[from] anyhow::Error),
504}
505
506/// A diagnostics data object containing inspect data.
507pub type InspectData = Data<Inspect>;
508
509/// A diagnostics data object containing logs data.
510pub type LogsData = Data<Logs>;
511
512/// A diagnostics data payload containing logs data.
513pub type LogsHierarchy = DiagnosticsHierarchy<LogsField>;
514
515/// A diagnostics hierarchy property keyed by `LogsField`.
516pub type LogsProperty = Property<LogsField>;
517
518impl Data<Inspect> {
519    /// Access the name or filename within `self.metadata`.
520    pub fn name(&self) -> &str {
521        self.metadata.name.as_ref()
522    }
523}
524
525pub struct InspectDataBuilder {
526    data: Data<Inspect>,
527}
528
529impl InspectDataBuilder {
530    pub fn new(
531        moniker: ExtendedMoniker,
532        component_url: impl Into<FlyStr>,
533        timestamp: impl Into<Timestamp>,
534    ) -> Self {
535        Self {
536            data: Data {
537                data_source: DataSource::Inspect,
538                moniker,
539                payload: None,
540                version: 1,
541                metadata: InspectMetadata {
542                    errors: None,
543                    name: InspectHandleName::name(DEFAULT_TREE_NAME.clone()),
544                    component_url: component_url.into(),
545                    timestamp: timestamp.into(),
546                    escrowed: false,
547                },
548            },
549        }
550    }
551
552    pub fn escrowed(mut self, escrowed: bool) -> Self {
553        self.data.metadata.escrowed = escrowed;
554        self
555    }
556
557    pub fn with_hierarchy(
558        mut self,
559        hierarchy: DiagnosticsHierarchy<<Inspect as DiagnosticsData>::Key>,
560    ) -> Self {
561        self.data.payload = Some(hierarchy);
562        self
563    }
564
565    pub fn with_errors(mut self, errors: Vec<InspectError>) -> Self {
566        self.data.metadata.errors = Some(errors);
567        self
568    }
569
570    pub fn with_name(mut self, name: InspectHandleName) -> Self {
571        self.data.metadata.name = name;
572        self
573    }
574
575    pub fn build(self) -> Data<Inspect> {
576        self.data
577    }
578}
579
580/// Internal state of the LogsDataBuilder impl
581/// External customers should not directly access these fields.
582pub struct LogsDataBuilder {
583    /// List of errors
584    errors: Vec<LogError>,
585    /// Message in log
586    msg: Option<String>,
587    /// List of tags
588    tags: Vec<String>,
589    /// Process ID
590    pid: Option<u64>,
591    /// Thread ID
592    tid: Option<u64>,
593    /// File name
594    file: Option<String>,
595    /// Line number
596    line: Option<u64>,
597    /// BuilderArgs that was passed in at construction time
598    args: BuilderArgs,
599    /// List of KVPs from the user
600    keys: Vec<Property<LogsField>>,
601    /// Raw severity.
602    raw_severity: Option<u8>,
603}
604
605/// Arguments used to create a new [`LogsDataBuilder`].
606pub struct BuilderArgs {
607    /// The moniker for the component
608    pub moniker: ExtendedMoniker,
609    /// The timestamp of the message in nanoseconds
610    pub timestamp: Timestamp,
611    /// The component URL
612    pub component_url: Option<FlyStr>,
613    /// The message severity
614    pub severity: Severity,
615}
616
617impl LogsDataBuilder {
618    /// Constructs a new LogsDataBuilder
619    pub fn new(args: BuilderArgs) -> Self {
620        LogsDataBuilder {
621            args,
622            errors: vec![],
623            msg: None,
624            file: None,
625            line: None,
626            pid: None,
627            tags: vec![],
628            tid: None,
629            keys: vec![],
630            raw_severity: None,
631        }
632    }
633
634    /// Sets the moniker of the message.
635    #[must_use = "You must call build on your builder to consume its result"]
636    pub fn set_moniker(mut self, value: ExtendedMoniker) -> Self {
637        self.args.moniker = value;
638        self
639    }
640
641    /// Sets the URL of the message.
642    #[must_use = "You must call build on your builder to consume its result"]
643    pub fn set_url(mut self, value: Option<FlyStr>) -> Self {
644        self.args.component_url = value;
645        self
646    }
647
648    /// Sets the number of dropped messages.
649    /// If value is greater than zero, a DroppedLogs error
650    /// will also be added to the list of errors or updated if
651    /// already present.
652    #[must_use = "You must call build on your builder to consume its result"]
653    pub fn set_dropped(mut self, value: u64) -> Self {
654        if value == 0 {
655            return self;
656        }
657        let val = self.errors.iter_mut().find_map(|error| {
658            if let LogError::DroppedLogs { count } = error {
659                Some(count)
660            } else {
661                None
662            }
663        });
664        if let Some(v) = val {
665            *v = value;
666        } else {
667            self.errors.push(LogError::DroppedLogs { count: value });
668        }
669        self
670    }
671
672    /// Overrides the severity set through the args with a raw severity.
673    pub fn set_raw_severity(mut self, severity: u8) -> Self {
674        self.raw_severity = Some(severity);
675        self
676    }
677
678    /// Sets the number of rolled out messages.
679    /// If value is greater than zero, a RolledOutLogs error
680    /// will also be added to the list of errors or updated if
681    /// already present.
682    #[must_use = "You must call build on your builder to consume its result"]
683    pub fn set_rolled_out(mut self, value: u64) -> Self {
684        if value == 0 {
685            return self;
686        }
687        let val = self.errors.iter_mut().find_map(|error| {
688            if let LogError::RolledOutLogs { count } = error {
689                Some(count)
690            } else {
691                None
692            }
693        });
694        if let Some(v) = val {
695            *v = value;
696        } else {
697            self.errors.push(LogError::RolledOutLogs { count: value });
698        }
699        self
700    }
701
702    /// Sets the severity of the log. This will unset the raw severity.
703    pub fn set_severity(mut self, severity: Severity) -> Self {
704        self.args.severity = severity;
705        self.raw_severity = None;
706        self
707    }
708
709    /// Sets the process ID that logged the message
710    #[must_use = "You must call build on your builder to consume its result"]
711    pub fn set_pid(mut self, value: u64) -> Self {
712        self.pid = Some(value);
713        self
714    }
715
716    /// Sets the thread ID that logged the message
717    #[must_use = "You must call build on your builder to consume its result"]
718    pub fn set_tid(mut self, value: u64) -> Self {
719        self.tid = Some(value);
720        self
721    }
722
723    /// Constructs a LogsData from this builder
724    pub fn build(self) -> LogsData {
725        let mut args = vec![];
726        if let Some(msg) = self.msg {
727            args.push(LogsProperty::String(LogsField::MsgStructured, msg));
728        }
729        let mut payload_fields = vec![DiagnosticsHierarchy::new("message", args, vec![])];
730        if !self.keys.is_empty() {
731            let val = DiagnosticsHierarchy::new("keys", self.keys, vec![]);
732            payload_fields.push(val);
733        }
734        let mut payload = LogsHierarchy::new("root", vec![], payload_fields);
735        payload.sort();
736        let (raw_severity, severity) =
737            self.raw_severity.map(Severity::parse_exact).unwrap_or((None, self.args.severity));
738        let mut ret = LogsData::for_logs(
739            self.args.moniker,
740            Some(payload),
741            self.args.timestamp,
742            self.args.component_url,
743            severity,
744            self.errors,
745        );
746        ret.metadata.raw_severity = raw_severity;
747        ret.metadata.file = self.file;
748        ret.metadata.line = self.line;
749        ret.metadata.pid = self.pid;
750        ret.metadata.tid = self.tid;
751        ret.metadata.tags = Some(self.tags);
752        ret
753    }
754
755    /// Adds an error
756    #[must_use = "You must call build on your builder to consume its result"]
757    pub fn add_error(mut self, error: LogError) -> Self {
758        self.errors.push(error);
759        self
760    }
761
762    /// Sets the message to be printed in the log message
763    #[must_use = "You must call build on your builder to consume its result"]
764    pub fn set_message(mut self, msg: impl Into<String>) -> Self {
765        self.msg = Some(msg.into());
766        self
767    }
768
769    /// Sets the file name that printed this message.
770    #[must_use = "You must call build on your builder to consume its result"]
771    pub fn set_file(mut self, file: impl Into<String>) -> Self {
772        self.file = Some(file.into());
773        self
774    }
775
776    /// Sets the line number that printed this message.
777    #[must_use = "You must call build on your builder to consume its result"]
778    pub fn set_line(mut self, line: u64) -> Self {
779        self.line = Some(line);
780        self
781    }
782
783    /// Adds a property to the list of key value pairs that are a part of this log message.
784    #[must_use = "You must call build on your builder to consume its result"]
785    pub fn add_key(mut self, kvp: Property<LogsField>) -> Self {
786        self.keys.push(kvp);
787        self
788    }
789
790    /// Adds a tag to the list of tags that precede this log message.
791    #[must_use = "You must call build on your builder to consume its result"]
792    pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
793        self.tags.push(tag.into());
794        self
795    }
796}
797
798impl Data<Logs> {
799    /// Creates a new data instance for logs.
800    pub fn for_logs(
801        moniker: ExtendedMoniker,
802        payload: Option<LogsHierarchy>,
803        timestamp: impl Into<Timestamp>,
804        component_url: Option<FlyStr>,
805        severity: impl Into<Severity>,
806        errors: Vec<LogError>,
807    ) -> Self {
808        let errors = if errors.is_empty() { None } else { Some(errors) };
809
810        Data {
811            moniker,
812            version: SCHEMA_VERSION,
813            data_source: DataSource::Logs,
814            payload,
815            metadata: LogsMetadata {
816                timestamp: timestamp.into(),
817                component_url,
818                severity: severity.into(),
819                raw_severity: None,
820                errors,
821                file: None,
822                line: None,
823                pid: None,
824                tags: None,
825                tid: None,
826                dropped: None,
827                size_bytes: None,
828            },
829        }
830    }
831
832    /// Sets the severity from a raw severity number. Overrides the severity to match the raw
833    /// severity.
834    pub fn set_raw_severity(&mut self, raw_severity: u8) {
835        self.metadata.raw_severity = Some(raw_severity);
836        self.metadata.severity = Severity::from(raw_severity);
837    }
838
839    /// Sets the severity of the log. This will unset the raw severity.
840    pub fn set_severity(&mut self, severity: Severity) {
841        self.metadata.severity = severity;
842        self.metadata.raw_severity = None;
843    }
844
845    /// Returns the string log associated with the message, if one exists.
846    pub fn msg(&self) -> Option<&str> {
847        self.payload_message().as_ref().and_then(|p| {
848            p.properties.iter().find_map(|property| match property {
849                LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg.as_str()),
850                _ => None,
851            })
852        })
853    }
854
855    /// If the log has a message, returns a shared reference to the message contents.
856    pub fn msg_mut(&mut self) -> Option<&mut String> {
857        self.payload_message_mut().and_then(|p| {
858            p.properties.iter_mut().find_map(|property| match property {
859                LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg),
860                _ => None,
861            })
862        })
863    }
864
865    /// If the log has message, returns an exclusive reference to it.
866    pub fn payload_message(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
867        self.payload
868            .as_ref()
869            .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "message"))
870    }
871
872    /// If the log has structured keys, returns an exclusive reference to them.
873    pub fn payload_keys(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
874        self.payload
875            .as_ref()
876            .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "keys"))
877    }
878
879    pub fn metadata(&self) -> &LogsMetadata {
880        &self.metadata
881    }
882
883    /// Returns an iterator over the payload keys as strings with the format "key=value".
884    pub fn payload_keys_strings(&self) -> Box<dyn Iterator<Item = String> + Send + '_> {
885        let maybe_iter = self.payload_keys().map(|p| {
886            Box::new(p.properties.iter().filter_map(|property| match property {
887                LogsProperty::String(LogsField::Tag, _tag) => None,
888                LogsProperty::String(LogsField::ProcessId, _tag) => None,
889                LogsProperty::String(LogsField::ThreadId, _tag) => None,
890                LogsProperty::String(LogsField::Dropped, _tag) => None,
891                LogsProperty::String(LogsField::Msg, _tag) => None,
892                LogsProperty::String(LogsField::FilePath, _tag) => None,
893                LogsProperty::String(LogsField::LineNumber, _tag) => None,
894                LogsProperty::String(
895                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
896                    value,
897                ) => Some(format!("{key}={value}")),
898                LogsProperty::Bytes(key @ (LogsField::Other(_) | LogsField::MsgStructured), _) => {
899                    Some(format!("{key} = <bytes>"))
900                }
901                LogsProperty::Int(
902                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
903                    value,
904                ) => Some(format!("{key}={value}")),
905                LogsProperty::Uint(
906                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
907                    value,
908                ) => Some(format!("{key}={value}")),
909                LogsProperty::Double(
910                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
911                    value,
912                ) => Some(format!("{key}={value}")),
913                LogsProperty::Bool(
914                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
915                    value,
916                ) => Some(format!("{key}={value}")),
917                LogsProperty::DoubleArray(
918                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
919                    value,
920                ) => Some(format!("{key}={value:?}")),
921                LogsProperty::IntArray(
922                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
923                    value,
924                ) => Some(format!("{key}={value:?}")),
925                LogsProperty::UintArray(
926                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
927                    value,
928                ) => Some(format!("{key}={value:?}")),
929                LogsProperty::StringList(
930                    key @ (LogsField::Other(_) | LogsField::MsgStructured),
931                    value,
932                ) => Some(format!("{key}={value:?}")),
933                _ => None,
934            }))
935        });
936        match maybe_iter {
937            Some(i) => Box::new(i),
938            None => Box::new(std::iter::empty()),
939        }
940    }
941
942    /// If the log has a message, returns a mutable reference to it.
943    pub fn payload_message_mut(&mut self) -> Option<&mut DiagnosticsHierarchy<LogsField>> {
944        self.payload.as_mut().and_then(|p| {
945            p.children.iter_mut().find(|property| property.name.as_str() == "message")
946        })
947    }
948
949    /// Returns the file path associated with the message, if one exists.
950    pub fn file_path(&self) -> Option<&str> {
951        self.metadata.file.as_deref()
952    }
953
954    /// Returns the line number associated with the message, if one exists.
955    pub fn line_number(&self) -> Option<&u64> {
956        self.metadata.line.as_ref()
957    }
958
959    /// Returns the pid associated with the message, if one exists.
960    pub fn pid(&self) -> Option<u64> {
961        self.metadata.pid
962    }
963
964    /// Returns the tid associated with the message, if one exists.
965    pub fn tid(&self) -> Option<u64> {
966        self.metadata.tid
967    }
968
969    /// Returns the tags associated with the message, if any exist.
970    pub fn tags(&self) -> Option<&Vec<String>> {
971        self.metadata.tags.as_ref()
972    }
973
974    /// Returns the severity level of this log.
975    pub fn severity(&self) -> Severity {
976        self.metadata.severity
977    }
978
979    /// Returns number of dropped logs if reported in the message.
980    pub fn dropped_logs(&self) -> Option<u64> {
981        self.metadata.errors.as_ref().and_then(|errors| {
982            errors.iter().find_map(|e| match e {
983                LogError::DroppedLogs { count } => Some(*count),
984                _ => None,
985            })
986        })
987    }
988
989    /// Returns number of rolled out logs if reported in the message.
990    pub fn rolled_out_logs(&self) -> Option<u64> {
991        self.metadata.errors.as_ref().and_then(|errors| {
992            errors.iter().find_map(|e| match e {
993                LogError::RolledOutLogs { count } => Some(*count),
994                _ => None,
995            })
996        })
997    }
998
999    /// Returns the component name. This only makes sense for v1 components.
1000    pub fn component_name(&self) -> Cow<'_, str> {
1001        match &self.moniker {
1002            ExtendedMoniker::ComponentManager => {
1003                Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1004            }
1005            ExtendedMoniker::ComponentInstance(moniker) => {
1006                if moniker.is_root() {
1007                    Cow::Borrowed(ROOT_MONIKER_REPR)
1008                } else {
1009                    Cow::Owned(moniker.leaf().unwrap().to_string())
1010                }
1011            }
1012        }
1013    }
1014}
1015
1016/// Display options for unstructured logs.
1017#[derive(Clone, Copy, Debug)]
1018pub struct LogTextDisplayOptions {
1019    /// Whether or not to display the full moniker.
1020    pub show_full_moniker: bool,
1021
1022    /// Whether or not to display metadata like PID & TID.
1023    pub show_metadata: bool,
1024
1025    /// Whether or not to display tags provided by the log producer.
1026    pub show_tags: bool,
1027
1028    /// Whether or not to display the source location which produced the log.
1029    pub show_file: bool,
1030
1031    /// Whether to include ANSI color codes in the output.
1032    pub color: LogTextColor,
1033
1034    /// How to print timestamps for this log message.
1035    pub time_format: LogTimeDisplayFormat,
1036}
1037
1038impl Default for LogTextDisplayOptions {
1039    fn default() -> Self {
1040        Self {
1041            show_full_moniker: true,
1042            show_metadata: true,
1043            show_tags: true,
1044            show_file: true,
1045            color: Default::default(),
1046            time_format: Default::default(),
1047        }
1048    }
1049}
1050
1051/// Configuration for the color of a log line that is displayed in tools using [`LogTextPresenter`].
1052#[derive(Clone, Copy, Debug, Default)]
1053pub enum LogTextColor {
1054    /// Do not print this log with ANSI colors.
1055    #[default]
1056    None,
1057
1058    /// Display color codes according to log severity and presence of dropped or rolled out logs.
1059    BySeverity,
1060
1061    /// Highlight this message as noteworthy regardless of severity, e.g. for known spam messages.
1062    Highlight,
1063}
1064
1065impl LogTextColor {
1066    fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
1067        match self {
1068            LogTextColor::BySeverity => match severity {
1069                Severity::Fatal => {
1070                    write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
1071                }
1072                Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
1073                Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
1074                Severity::Info => (),
1075                Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
1076                Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
1077            },
1078            LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
1079            LogTextColor::None => {}
1080        }
1081        Ok(())
1082    }
1083
1084    fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1085        if let LogTextColor::BySeverity = self {
1086            // This will be reset below before the next line.
1087            write!(f, "{}", color::Fg(color::Yellow))?;
1088        }
1089        Ok(())
1090    }
1091
1092    fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1093        match self {
1094            LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
1095            LogTextColor::None => {}
1096        };
1097        Ok(())
1098    }
1099}
1100
1101/// Options for the timezone associated to the timestamp of a log line.
1102#[derive(Clone, Copy, Debug, PartialEq)]
1103pub enum Timezone {
1104    /// Display a timestamp in terms of the local timezone as reported by the operating system.
1105    Local,
1106
1107    /// Display a timestamp in terms of UTC.
1108    Utc,
1109}
1110
1111impl Timezone {
1112    fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
1113        const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
1114        match self {
1115            Timezone::Local => {
1116                Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1117            }
1118            Timezone::Utc => {
1119                Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1120            }
1121        }
1122    }
1123}
1124
1125/// Configuration for how to display the timestamp associated to a log line.
1126#[derive(Clone, Copy, Debug, Default)]
1127pub enum LogTimeDisplayFormat {
1128    /// Display the log message's timestamp as monotonic nanoseconds since boot.
1129    #[default]
1130    Original,
1131
1132    /// Display the log's timestamp as a human-readable string in ISO 8601 format.
1133    WallTime {
1134        /// The format for displaying a timestamp as a string.
1135        tz: Timezone,
1136
1137        /// The offset to apply to the original device-monotonic time before printing it as a
1138        /// human-readable timestamp.
1139        offset: i64,
1140    },
1141}
1142
1143impl LogTimeDisplayFormat {
1144    fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
1145        const NANOS_IN_SECOND: i64 = 1_000_000_000;
1146
1147        match self {
1148            // Don't try to print a human readable string if it's going to be in 1970, fall back
1149            // to monotonic.
1150            Self::Original | Self::WallTime { offset: 0, .. } => {
1151                let time: Duration =
1152                    Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
1153                write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
1154            }
1155            Self::WallTime { tz, offset } => {
1156                let adjusted = time.into_nanos() + offset;
1157                let seconds = adjusted / NANOS_IN_SECOND;
1158                let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
1159                let formatted = tz.format(seconds, rem_nanos);
1160                write!(f, "[{formatted}]")?;
1161            }
1162        }
1163        Ok(())
1164    }
1165}
1166
1167/// Used to control stringification options of Data<Logs>
1168pub struct LogTextPresenter<'a> {
1169    /// The log to parameterize
1170    log: &'a Data<Logs>,
1171
1172    /// Options for stringifying the log
1173    options: LogTextDisplayOptions,
1174}
1175
1176impl<'a> LogTextPresenter<'a> {
1177    /// Creates a new LogTextPresenter with the specified options and
1178    /// log message. This presenter is bound to the lifetime of the
1179    /// underlying log message.
1180    pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
1181        Self { log, options }
1182    }
1183}
1184
1185impl fmt::Display for Data<Logs> {
1186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1187        LogTextPresenter::new(self, Default::default()).fmt(f)
1188    }
1189}
1190
1191impl Deref for LogTextPresenter<'_> {
1192    type Target = Data<Logs>;
1193    fn deref(&self) -> &Self::Target {
1194        self.log
1195    }
1196}
1197
1198impl fmt::Display for LogTextPresenter<'_> {
1199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1200        self.options.color.begin_record(f, self.log.severity())?;
1201        self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1202
1203        if self.options.show_metadata {
1204            match self.pid() {
1205                Some(pid) => write!(f, "[{pid}]")?,
1206                None => write!(f, "[]")?,
1207            }
1208            match self.tid() {
1209                Some(tid) => write!(f, "[{tid}]")?,
1210                None => write!(f, "[]")?,
1211            }
1212        }
1213
1214        let moniker = if self.options.show_full_moniker {
1215            match &self.moniker {
1216                ExtendedMoniker::ComponentManager => {
1217                    Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1218                }
1219                ExtendedMoniker::ComponentInstance(instance) => {
1220                    if instance.is_root() {
1221                        Cow::Borrowed(ROOT_MONIKER_REPR)
1222                    } else {
1223                        Cow::Owned(instance.to_string())
1224                    }
1225                }
1226            }
1227        } else {
1228            self.component_name()
1229        };
1230        write!(f, "[{moniker}]")?;
1231
1232        if self.options.show_tags {
1233            match &self.metadata.tags {
1234                Some(tags) if !tags.is_empty() => {
1235                    let mut filtered =
1236                        tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
1237                    if filtered.peek().is_some() {
1238                        write!(f, "[{}]", filtered.join(","))?;
1239                    }
1240                }
1241                _ => {}
1242            }
1243        }
1244
1245        write!(f, " {}:", self.metadata.severity)?;
1246
1247        if self.options.show_file {
1248            match (&self.metadata.file, &self.metadata.line) {
1249                (Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
1250                (Some(file), None) => write!(f, " [{file}]")?,
1251                _ => (),
1252            }
1253        }
1254
1255        if let Some(msg) = self.msg() {
1256            write!(f, " {msg}")?;
1257        } else {
1258            write!(f, " <missing message>")?;
1259        }
1260        for kvp in self.payload_keys_strings() {
1261            write!(f, " {kvp}")?;
1262        }
1263
1264        let dropped = self.log.dropped_logs().unwrap_or_default();
1265        let rolled = self.log.rolled_out_logs().unwrap_or_default();
1266        if dropped != 0 || rolled != 0 {
1267            self.options.color.begin_lost_message_counts(f)?;
1268            if dropped != 0 {
1269                write!(f, " [dropped={dropped}]")?;
1270            }
1271            if rolled != 0 {
1272                write!(f, " [rolled={rolled}]")?;
1273            }
1274        }
1275
1276        self.options.color.end_record(f)?;
1277
1278        Ok(())
1279    }
1280}
1281
1282impl Eq for Data<Logs> {}
1283
1284impl PartialOrd for Data<Logs> {
1285    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1286        Some(self.cmp(other))
1287    }
1288}
1289
1290impl Ord for Data<Logs> {
1291    fn cmp(&self, other: &Self) -> Ordering {
1292        self.metadata.timestamp.cmp(&other.metadata.timestamp)
1293    }
1294}
1295
1296/// An enum containing well known argument names passed through logs, as well
1297/// as an `Other` variant for any other argument names.
1298///
1299/// This contains the fields of logs sent as a [`LogMessage`].
1300///
1301/// [`LogMessage`]: https://fuchsia.dev/reference/fidl/fuchsia.logger#LogMessage
1302#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
1303pub enum LogsField {
1304    ProcessId,
1305    ThreadId,
1306    Dropped,
1307    Tag,
1308    Msg,
1309    MsgStructured,
1310    FilePath,
1311    LineNumber,
1312    Other(String),
1313}
1314
1315impl fmt::Display for LogsField {
1316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1317        match self {
1318            LogsField::ProcessId => write!(f, "pid"),
1319            LogsField::ThreadId => write!(f, "tid"),
1320            LogsField::Dropped => write!(f, "num_dropped"),
1321            LogsField::Tag => write!(f, "tag"),
1322            LogsField::Msg => write!(f, "message"),
1323            LogsField::MsgStructured => write!(f, "value"),
1324            LogsField::FilePath => write!(f, "file_path"),
1325            LogsField::LineNumber => write!(f, "line_number"),
1326            LogsField::Other(name) => write!(f, "{name}"),
1327        }
1328    }
1329}
1330
1331// TODO(https://fxbug.dev/42127608) - ensure that strings reported here align with naming
1332// decisions made for the structured log format sent by other components.
1333/// The label for the process koid in the log metadata.
1334pub const PID_LABEL: &str = "pid";
1335/// The label for the thread koid in the log metadata.
1336pub const TID_LABEL: &str = "tid";
1337/// The label for the number of dropped logs in the log metadata.
1338pub const DROPPED_LABEL: &str = "num_dropped";
1339/// The label for a tag in the log metadata.
1340pub const TAG_LABEL: &str = "tag";
1341/// The label for the contents of a message in the log payload.
1342pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
1343/// The label for the message in the log payload.
1344pub const MESSAGE_LABEL: &str = "message";
1345/// The label for the file associated with a log line.
1346pub const FILE_PATH_LABEL: &str = "file";
1347/// The label for the line number in the file associated with a log line.
1348pub const LINE_NUMBER_LABEL: &str = "line";
1349
1350impl AsRef<str> for LogsField {
1351    fn as_ref(&self) -> &str {
1352        match self {
1353            Self::ProcessId => PID_LABEL,
1354            Self::ThreadId => TID_LABEL,
1355            Self::Dropped => DROPPED_LABEL,
1356            Self::Tag => TAG_LABEL,
1357            Self::Msg => MESSAGE_LABEL,
1358            Self::FilePath => FILE_PATH_LABEL,
1359            Self::LineNumber => LINE_NUMBER_LABEL,
1360            Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
1361            Self::Other(str) => str.as_str(),
1362        }
1363    }
1364}
1365
1366impl<T> From<T> for LogsField
1367where
1368    // Deref instead of AsRef b/c LogsField: AsRef<str> so this conflicts with concrete From<Self>
1369    T: Deref<Target = str>,
1370{
1371    fn from(s: T) -> Self {
1372        match s.as_ref() {
1373            PID_LABEL => Self::ProcessId,
1374            TID_LABEL => Self::ThreadId,
1375            DROPPED_LABEL => Self::Dropped,
1376            TAG_LABEL => Self::Tag,
1377            MESSAGE_LABEL => Self::Msg,
1378            FILE_PATH_LABEL => Self::FilePath,
1379            LINE_NUMBER_LABEL => Self::LineNumber,
1380            MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
1381            _ => Self::Other(s.to_string()),
1382        }
1383    }
1384}
1385
1386impl FromStr for LogsField {
1387    type Err = ();
1388    fn from_str(s: &str) -> Result<Self, Self::Err> {
1389        Ok(Self::from(s))
1390    }
1391}
1392
1393/// Possible errors that can come in a `DiagnosticsData` object where the data source is
1394/// `DataSource::Logs`.
1395#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
1396#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
1397pub enum LogError {
1398    /// Represents the number of logs that were dropped by the component writing the logs due to an
1399    /// error writing to the socket before succeeding to write a log.
1400    #[serde(rename = "dropped_logs")]
1401    DroppedLogs { count: u64 },
1402    /// Represents the number of logs that were dropped for a component by the archivist due to the
1403    /// log buffer execeeding its maximum capacity before the current message.
1404    #[serde(rename = "rolled_out_logs")]
1405    RolledOutLogs { count: u64 },
1406    #[serde(rename = "parse_record")]
1407    FailedToParseRecord(String),
1408    #[serde(rename = "other")]
1409    Other { message: String },
1410}
1411
1412const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
1413
1414impl MetadataError for LogError {
1415    fn dropped_payload() -> Self {
1416        Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
1417    }
1418
1419    fn message(&self) -> Option<&str> {
1420        match self {
1421            Self::FailedToParseRecord(msg) => Some(msg.as_str()),
1422            Self::Other { message } => Some(message.as_str()),
1423            _ => None,
1424        }
1425    }
1426}
1427
1428/// Possible error that can come in a `DiagnosticsData` object where the data source is
1429/// `DataSource::Inspect`..
1430#[derive(Debug, PartialEq, Clone, Eq)]
1431pub struct InspectError {
1432    pub message: String,
1433}
1434
1435impl MetadataError for InspectError {
1436    fn dropped_payload() -> Self {
1437        Self { message: "Schema failed to fit component budget.".into() }
1438    }
1439
1440    fn message(&self) -> Option<&str> {
1441        Some(self.message.as_str())
1442    }
1443}
1444
1445impl fmt::Display for InspectError {
1446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1447        write!(f, "{}", self.message)
1448    }
1449}
1450
1451impl Borrow<str> for InspectError {
1452    fn borrow(&self) -> &str {
1453        &self.message
1454    }
1455}
1456
1457impl Serialize for InspectError {
1458    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
1459        self.message.serialize(ser)
1460    }
1461}
1462
1463impl<'de> Deserialize<'de> for InspectError {
1464    fn deserialize<D>(de: D) -> Result<Self, D::Error>
1465    where
1466        D: Deserializer<'de>,
1467    {
1468        let message = String::deserialize(de)?;
1469        Ok(Self { message })
1470    }
1471}
1472
1473#[cfg(test)]
1474mod tests {
1475    use super::*;
1476    use diagnostics_hierarchy::hierarchy;
1477    use selectors::FastError;
1478    use serde_json::json;
1479
1480    const TEST_URL: &str = "fuchsia-pkg://test";
1481
1482    #[fuchsia::test]
1483    fn test_canonical_json_inspect_formatting() {
1484        let mut hierarchy = hierarchy! {
1485            root: {
1486                x: "foo",
1487            }
1488        };
1489
1490        hierarchy.sort();
1491        let json_schema = InspectDataBuilder::new(
1492            "a/b/c/d".try_into().unwrap(),
1493            TEST_URL,
1494            Timestamp::from_nanos(123456i64),
1495        )
1496        .with_hierarchy(hierarchy)
1497        .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1498        .build();
1499
1500        let result_json =
1501            serde_json::to_value(&json_schema).expect("serialization should succeed.");
1502
1503        let expected_json = json!({
1504          "moniker": "a/b/c/d",
1505          "version": 1,
1506          "data_source": "Inspect",
1507          "payload": {
1508            "root": {
1509              "x": "foo"
1510            }
1511          },
1512          "metadata": {
1513            "component_url": TEST_URL,
1514            "filename": "test_file_plz_ignore.inspect",
1515            "timestamp": 123456,
1516          }
1517        });
1518
1519        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1520    }
1521
1522    #[fuchsia::test]
1523    fn test_errorful_json_inspect_formatting() {
1524        let json_schema = InspectDataBuilder::new(
1525            "a/b/c/d".try_into().unwrap(),
1526            TEST_URL,
1527            Timestamp::from_nanos(123456i64),
1528        )
1529        .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1530        .with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
1531        .build();
1532
1533        let result_json =
1534            serde_json::to_value(&json_schema).expect("serialization should succeed.");
1535
1536        let expected_json = json!({
1537          "moniker": "a/b/c/d",
1538          "version": 1,
1539          "data_source": "Inspect",
1540          "payload": null,
1541          "metadata": {
1542            "component_url": TEST_URL,
1543            "errors": ["too much fun being had."],
1544            "filename": "test_file_plz_ignore.inspect",
1545            "timestamp": 123456,
1546          }
1547        });
1548
1549        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1550    }
1551
1552    fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
1553        strings
1554            .iter()
1555            .map(|s| match selectors::parse_selector::<FastError>(s) {
1556                Ok(selector) => selector,
1557                Err(e) => panic!("Couldn't parse selector {s}: {e}"),
1558            })
1559            .collect::<Vec<_>>()
1560    }
1561
1562    #[fuchsia::test]
1563    fn test_filter_returns_none_on_empty_hierarchy() {
1564        let data = InspectDataBuilder::new(
1565            "a/b/c/d".try_into().unwrap(),
1566            TEST_URL,
1567            Timestamp::from_nanos(123456i64),
1568        )
1569        .build();
1570        let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1571        assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1572    }
1573
1574    #[fuchsia::test]
1575    fn test_filter_returns_none_on_selector_mismatch() {
1576        let mut hierarchy = hierarchy! {
1577            root: {
1578                x: "foo",
1579            }
1580        };
1581        hierarchy.sort();
1582        let data = InspectDataBuilder::new(
1583            "b/c/d/e".try_into().unwrap(),
1584            TEST_URL,
1585            Timestamp::from_nanos(123456i64),
1586        )
1587        .with_hierarchy(hierarchy)
1588        .build();
1589        let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1590        assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1591    }
1592
1593    #[fuchsia::test]
1594    fn test_filter_returns_none_on_data_mismatch() {
1595        let mut hierarchy = hierarchy! {
1596            root: {
1597                x: "foo",
1598            }
1599        };
1600        hierarchy.sort();
1601        let data = InspectDataBuilder::new(
1602            "a/b/c/d".try_into().unwrap(),
1603            TEST_URL,
1604            Timestamp::from_nanos(123456i64),
1605        )
1606        .with_hierarchy(hierarchy)
1607        .build();
1608        let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1609
1610        assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
1611    }
1612
1613    #[fuchsia::test]
1614    fn test_filter_returns_matching_data() {
1615        let mut hierarchy = hierarchy! {
1616            root: {
1617                x: "foo",
1618                y: "bar",
1619            }
1620        };
1621        hierarchy.sort();
1622        let data = InspectDataBuilder::new(
1623            "a/b/c/d".try_into().unwrap(),
1624            TEST_URL,
1625            Timestamp::from_nanos(123456i64),
1626        )
1627        .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1628        .with_hierarchy(hierarchy)
1629        .build();
1630        let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
1631
1632        let expected_json = json!({
1633          "moniker": "a/b/c/d",
1634          "version": 1,
1635          "data_source": "Inspect",
1636          "payload": {
1637            "root": {
1638              "x": "foo"
1639            }
1640          },
1641          "metadata": {
1642            "component_url": TEST_URL,
1643            "filename": "test_file_plz_ignore.inspect",
1644            "timestamp": 123456,
1645          }
1646        });
1647
1648        let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
1649            .expect("serialization should succeed.");
1650
1651        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1652    }
1653
1654    #[fuchsia::test]
1655    fn default_builder_test() {
1656        let builder = LogsDataBuilder::new(BuilderArgs {
1657            component_url: Some("url".into()),
1658            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1659            severity: Severity::Info,
1660            timestamp: Timestamp::from_nanos(0),
1661        });
1662        //let tree = builder.build();
1663        let expected_json = json!({
1664          "moniker": "moniker",
1665          "version": 1,
1666          "data_source": "Logs",
1667          "payload": {
1668              "root":
1669              {
1670                  "message":{}
1671              }
1672          },
1673          "metadata": {
1674            "component_url": "url",
1675              "severity": "INFO",
1676              "tags": [],
1677
1678            "timestamp": 0,
1679          }
1680        });
1681        let result_json =
1682            serde_json::to_value(builder.build()).expect("serialization should succeed.");
1683        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1684    }
1685
1686    #[fuchsia::test]
1687    fn regular_message_test() {
1688        let builder = LogsDataBuilder::new(BuilderArgs {
1689            component_url: Some("url".into()),
1690            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1691            severity: Severity::Info,
1692            timestamp: Timestamp::from_nanos(0),
1693        })
1694        .set_message("app")
1695        .set_file("test file.cc")
1696        .set_line(420)
1697        .set_pid(1001)
1698        .set_tid(200)
1699        .set_dropped(2)
1700        .add_tag("You're")
1701        .add_tag("IT!")
1702        .add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
1703        // TODO(https://fxbug.dev/42157027): Convert to our custom DSL when possible.
1704        let expected_json = json!({
1705          "moniker": "moniker",
1706          "version": 1,
1707          "data_source": "Logs",
1708          "payload": {
1709              "root":
1710              {
1711                  "keys":{
1712                      "key":"value"
1713                  },
1714                  "message":{
1715                      "value":"app"
1716                  }
1717              }
1718          },
1719          "metadata": {
1720            "errors": [],
1721            "component_url": "url",
1722              "errors": [{"dropped_logs":{"count":2}}],
1723              "file": "test file.cc",
1724              "line": 420,
1725              "pid": 1001,
1726              "severity": "INFO",
1727              "tags": ["You're", "IT!"],
1728              "tid": 200,
1729
1730            "timestamp": 0,
1731          }
1732        });
1733        let result_json =
1734            serde_json::to_value(builder.build()).expect("serialization should succeed.");
1735        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1736    }
1737
1738    #[fuchsia::test]
1739    fn display_for_logs() {
1740        let data = LogsDataBuilder::new(BuilderArgs {
1741            timestamp: Timestamp::from_nanos(12345678000i64),
1742            component_url: Some(FlyStr::from("fake-url")),
1743            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1744            severity: Severity::Info,
1745        })
1746        .set_pid(123)
1747        .set_tid(456)
1748        .set_message("some message".to_string())
1749        .set_file("some_file.cc".to_string())
1750        .set_line(420)
1751        .add_tag("foo")
1752        .add_tag("bar")
1753        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1754        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1755        .build();
1756
1757        assert_eq!(
1758            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1759            format!("{data}")
1760        )
1761    }
1762
1763    #[fuchsia::test]
1764    fn display_for_logs_with_duplicate_moniker() {
1765        let data = LogsDataBuilder::new(BuilderArgs {
1766            timestamp: Timestamp::from_nanos(12345678000i64),
1767            component_url: Some(FlyStr::from("fake-url")),
1768            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1769            severity: Severity::Info,
1770        })
1771        .set_pid(123)
1772        .set_tid(456)
1773        .set_message("some message".to_string())
1774        .set_file("some_file.cc".to_string())
1775        .set_line(420)
1776        .add_tag("moniker")
1777        .add_tag("bar")
1778        .add_tag("moniker")
1779        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1780        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1781        .build();
1782
1783        assert_eq!(
1784            "[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
1785            format!("{data}")
1786        )
1787    }
1788
1789    #[fuchsia::test]
1790    fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
1791        let data = LogsDataBuilder::new(BuilderArgs {
1792            timestamp: Timestamp::from_nanos(12345678000i64),
1793            component_url: Some(FlyStr::from("fake-url")),
1794            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1795            severity: Severity::Info,
1796        })
1797        .set_pid(123)
1798        .set_tid(456)
1799        .set_message("some message".to_string())
1800        .set_file("some_file.cc".to_string())
1801        .set_line(420)
1802        .add_tag("moniker")
1803        .add_tag("moniker")
1804        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1805        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1806        .build();
1807
1808        assert_eq!(
1809            "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1810            format!("{data}")
1811        )
1812    }
1813
1814    #[fuchsia::test]
1815    fn display_for_logs_partial_moniker() {
1816        let data = LogsDataBuilder::new(BuilderArgs {
1817            timestamp: Timestamp::from_nanos(12345678000i64),
1818            component_url: Some(FlyStr::from("fake-url")),
1819            moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1820            severity: Severity::Info,
1821        })
1822        .set_pid(123)
1823        .set_tid(456)
1824        .set_message("some message".to_string())
1825        .set_file("some_file.cc".to_string())
1826        .set_line(420)
1827        .add_tag("foo")
1828        .add_tag("bar")
1829        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1830        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1831        .build();
1832
1833        assert_eq!(
1834            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1835            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1836                show_full_moniker: false,
1837                ..Default::default()
1838            }))
1839        )
1840    }
1841
1842    #[fuchsia::test]
1843    fn display_for_logs_exclude_metadata() {
1844        let data = LogsDataBuilder::new(BuilderArgs {
1845            timestamp: Timestamp::from_nanos(12345678000i64),
1846            component_url: Some(FlyStr::from("fake-url")),
1847            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1848            severity: Severity::Info,
1849        })
1850        .set_pid(123)
1851        .set_tid(456)
1852        .set_message("some message".to_string())
1853        .set_file("some_file.cc".to_string())
1854        .set_line(420)
1855        .add_tag("foo")
1856        .add_tag("bar")
1857        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1858        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1859        .build();
1860
1861        assert_eq!(
1862            "[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1863            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1864                show_metadata: false,
1865                ..Default::default()
1866            }))
1867        )
1868    }
1869
1870    #[fuchsia::test]
1871    fn display_for_logs_exclude_tags() {
1872        let data = LogsDataBuilder::new(BuilderArgs {
1873            timestamp: Timestamp::from_nanos(12345678000i64),
1874            component_url: Some(FlyStr::from("fake-url")),
1875            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1876            severity: Severity::Info,
1877        })
1878        .set_pid(123)
1879        .set_tid(456)
1880        .set_message("some message".to_string())
1881        .set_file("some_file.cc".to_string())
1882        .set_line(420)
1883        .add_tag("foo")
1884        .add_tag("bar")
1885        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1886        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1887        .build();
1888
1889        assert_eq!(
1890            "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1891            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1892                show_tags: false,
1893                ..Default::default()
1894            }))
1895        )
1896    }
1897
1898    #[fuchsia::test]
1899    fn display_for_logs_exclude_file() {
1900        let data = LogsDataBuilder::new(BuilderArgs {
1901            timestamp: Timestamp::from_nanos(12345678000i64),
1902            component_url: Some(FlyStr::from("fake-url")),
1903            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1904            severity: Severity::Info,
1905        })
1906        .set_pid(123)
1907        .set_tid(456)
1908        .set_message("some message".to_string())
1909        .set_file("some_file.cc".to_string())
1910        .set_line(420)
1911        .add_tag("foo")
1912        .add_tag("bar")
1913        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1914        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1915        .build();
1916
1917        assert_eq!(
1918            "[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
1919            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1920                show_file: false,
1921                ..Default::default()
1922            }))
1923        )
1924    }
1925
1926    #[fuchsia::test]
1927    fn display_for_logs_include_color_by_severity() {
1928        let data = LogsDataBuilder::new(BuilderArgs {
1929            timestamp: Timestamp::from_nanos(12345678000i64),
1930            component_url: Some(FlyStr::from("fake-url")),
1931            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1932            severity: Severity::Error,
1933        })
1934        .set_pid(123)
1935        .set_tid(456)
1936        .set_message("some message".to_string())
1937        .set_file("some_file.cc".to_string())
1938        .set_line(420)
1939        .add_tag("foo")
1940        .add_tag("bar")
1941        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1942        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1943        .build();
1944
1945        assert_eq!(
1946            format!("{}[00012.345678][123][456][moniker][foo,bar] ERROR: [some_file.cc(420)] some message test=property value=test{}", color::Fg(color::Red), style::Reset),
1947            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1948                color: LogTextColor::BySeverity,
1949                ..Default::default()
1950            }))
1951        )
1952    }
1953
1954    #[fuchsia::test]
1955    fn display_for_logs_highlight_line() {
1956        let data = LogsDataBuilder::new(BuilderArgs {
1957            timestamp: Timestamp::from_nanos(12345678000i64),
1958            component_url: Some(FlyStr::from("fake-url")),
1959            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1960            severity: Severity::Info,
1961        })
1962        .set_pid(123)
1963        .set_tid(456)
1964        .set_message("some message".to_string())
1965        .set_file("some_file.cc".to_string())
1966        .set_line(420)
1967        .add_tag("foo")
1968        .add_tag("bar")
1969        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1970        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1971        .build();
1972
1973        assert_eq!(
1974            format!("{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}", color::Fg(color::LightYellow), style::Reset),
1975            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1976                color: LogTextColor::Highlight,
1977                ..Default::default()
1978            }))
1979        )
1980    }
1981
1982    #[fuchsia::test]
1983    fn display_for_logs_with_wall_time() {
1984        let data = LogsDataBuilder::new(BuilderArgs {
1985            timestamp: Timestamp::from_nanos(12345678000i64),
1986            component_url: Some(FlyStr::from("fake-url")),
1987            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1988            severity: Severity::Info,
1989        })
1990        .set_pid(123)
1991        .set_tid(456)
1992        .set_message("some message".to_string())
1993        .set_file("some_file.cc".to_string())
1994        .set_line(420)
1995        .add_tag("foo")
1996        .add_tag("bar")
1997        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1998        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1999        .build();
2000
2001        assert_eq!(
2002            "[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2003            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2004                time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
2005                ..Default::default()
2006            }))
2007        );
2008
2009        assert_eq!(
2010            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2011            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2012                time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
2013                ..Default::default()
2014            })),
2015            "should fall back to monotonic if offset is 0"
2016        );
2017    }
2018
2019    #[fuchsia::test]
2020    fn display_for_logs_with_dropped_count() {
2021        let data = LogsDataBuilder::new(BuilderArgs {
2022            timestamp: Timestamp::from_nanos(12345678000i64),
2023            component_url: Some(FlyStr::from("fake-url")),
2024            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2025            severity: Severity::Info,
2026        })
2027        .set_dropped(5)
2028        .set_pid(123)
2029        .set_tid(456)
2030        .set_message("some message".to_string())
2031        .set_file("some_file.cc".to_string())
2032        .set_line(420)
2033        .add_tag("foo")
2034        .add_tag("bar")
2035        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2036        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2037        .build();
2038
2039        assert_eq!(
2040            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
2041            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2042        );
2043
2044        assert_eq!(
2045            format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}", color::Fg(color::Yellow), style::Reset),
2046            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2047                color: LogTextColor::BySeverity,
2048                ..Default::default()
2049            })),
2050        );
2051    }
2052
2053    #[fuchsia::test]
2054    fn display_for_logs_with_rolled_count() {
2055        let data = LogsDataBuilder::new(BuilderArgs {
2056            timestamp: Timestamp::from_nanos(12345678000i64),
2057            component_url: Some(FlyStr::from("fake-url")),
2058            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2059            severity: Severity::Info,
2060        })
2061        .set_rolled_out(10)
2062        .set_pid(123)
2063        .set_tid(456)
2064        .set_message("some message".to_string())
2065        .set_file("some_file.cc".to_string())
2066        .set_line(420)
2067        .add_tag("foo")
2068        .add_tag("bar")
2069        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2070        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2071        .build();
2072
2073        assert_eq!(
2074            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
2075            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2076        );
2077
2078        assert_eq!(
2079            format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}", color::Fg(color::Yellow), style::Reset),
2080            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2081                color: LogTextColor::BySeverity,
2082                ..Default::default()
2083            })),
2084        );
2085    }
2086
2087    #[fuchsia::test]
2088    fn display_for_logs_with_dropped_and_rolled_counts() {
2089        let data = LogsDataBuilder::new(BuilderArgs {
2090            timestamp: Timestamp::from_nanos(12345678000i64),
2091            component_url: Some(FlyStr::from("fake-url")),
2092            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2093            severity: Severity::Info,
2094        })
2095        .set_dropped(5)
2096        .set_rolled_out(10)
2097        .set_pid(123)
2098        .set_tid(456)
2099        .set_message("some message".to_string())
2100        .set_file("some_file.cc".to_string())
2101        .set_line(420)
2102        .add_tag("foo")
2103        .add_tag("bar")
2104        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2105        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2106        .build();
2107
2108        assert_eq!(
2109            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
2110            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2111        );
2112
2113        assert_eq!(
2114            format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}", color::Fg(color::Yellow), style::Reset),
2115            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2116                color: LogTextColor::BySeverity,
2117                ..Default::default()
2118            })),
2119        );
2120    }
2121
2122    #[fuchsia::test]
2123    fn display_for_logs_no_tags() {
2124        let data = LogsDataBuilder::new(BuilderArgs {
2125            timestamp: Timestamp::from_nanos(12345678000i64),
2126            component_url: Some(FlyStr::from("fake-url")),
2127            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2128            severity: Severity::Info,
2129        })
2130        .set_pid(123)
2131        .set_tid(456)
2132        .set_message("some message".to_string())
2133        .build();
2134
2135        assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{data}"))
2136    }
2137
2138    #[fuchsia::test]
2139    fn size_bytes_deserialize_backwards_compatibility() {
2140        let original_json = json!({
2141          "moniker": "a/b",
2142          "version": 1,
2143          "data_source": "Logs",
2144          "payload": {
2145            "root": {
2146              "message":{}
2147            }
2148          },
2149          "metadata": {
2150            "component_url": "url",
2151              "severity": "INFO",
2152              "tags": [],
2153
2154            "timestamp": 123,
2155          }
2156        });
2157        let expected_data = LogsDataBuilder::new(BuilderArgs {
2158            component_url: Some("url".into()),
2159            moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2160            severity: Severity::Info,
2161            timestamp: Timestamp::from_nanos(123),
2162        })
2163        .build();
2164        let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2165        assert_eq!(original_data, expected_data);
2166        // We skip deserializing the size_bytes
2167        assert_eq!(original_data.metadata.size_bytes, None);
2168    }
2169
2170    #[fuchsia::test]
2171    fn dropped_deserialize_backwards_compatibility() {
2172        let original_json = json!({
2173          "moniker": "a/b",
2174          "version": 1,
2175          "data_source": "Logs",
2176          "payload": {
2177            "root": {
2178              "message":{}
2179            }
2180          },
2181          "metadata": {
2182            "dropped": 0,
2183            "component_url": "url",
2184              "severity": "INFO",
2185              "tags": [],
2186
2187            "timestamp": 123,
2188          }
2189        });
2190        let expected_data = LogsDataBuilder::new(BuilderArgs {
2191            component_url: Some("url".into()),
2192            moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2193            severity: Severity::Info,
2194            timestamp: Timestamp::from_nanos(123),
2195        })
2196        .build();
2197        let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2198        assert_eq!(original_data, expected_data);
2199        // We skip deserializing dropped
2200        assert_eq!(original_data.metadata.dropped, None);
2201    }
2202
2203    #[fuchsia::test]
2204    fn severity_aliases() {
2205        assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
2206        assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
2207    }
2208}