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