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