Skip to main content

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