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