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    /// Whether to highlight the timestamp (e.g. when a log message is out of chronological order).
1144    pub highlight_timestamp: bool,
1145}
1146
1147impl Default for LogTextDisplayOptions {
1148    fn default() -> Self {
1149        Self {
1150            show_moniker: true,
1151            show_full_moniker: true,
1152            prefer_url_component_name: false,
1153            show_metadata: true,
1154            show_tags: true,
1155            show_file: true,
1156            color: Default::default(),
1157            time_format: Default::default(),
1158            highlight_timestamp: false,
1159        }
1160    }
1161}
1162
1163/// Configuration for the color of a log line that is displayed in tools using [`LogTextPresenter`].
1164#[derive(Clone, Copy, Debug, Default)]
1165pub enum LogTextColor {
1166    /// Do not print this log with ANSI colors.
1167    #[default]
1168    None,
1169
1170    /// Display color codes according to log severity and presence of dropped or rolled out logs.
1171    BySeverity,
1172
1173    /// Highlight this message as noteworthy regardless of severity, e.g. for known spam messages.
1174    Highlight,
1175}
1176
1177impl LogTextColor {
1178    fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
1179        match self {
1180            LogTextColor::BySeverity => match severity {
1181                Severity::Fatal => {
1182                    write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
1183                }
1184                Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
1185                Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
1186                Severity::Info => (),
1187                Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
1188                Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
1189            },
1190            LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
1191            LogTextColor::None => {}
1192        }
1193        Ok(())
1194    }
1195
1196    fn begin_timestamp_highlight(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1197        match self {
1198            LogTextColor::BySeverity | LogTextColor::Highlight => {
1199                write!(f, "{}", color::Fg(color::Cyan))?
1200            }
1201            LogTextColor::None => {}
1202        }
1203        Ok(())
1204    }
1205
1206    fn end_timestamp_highlight(
1207        &self,
1208        f: &mut fmt::Formatter<'_>,
1209        severity: Severity,
1210    ) -> fmt::Result {
1211        match self {
1212            LogTextColor::BySeverity | LogTextColor::Highlight => {
1213                write!(f, "{}", style::Reset)?;
1214                self.begin_record(f, severity)?;
1215            }
1216            LogTextColor::None => {}
1217        }
1218        Ok(())
1219    }
1220
1221    fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1222        if let LogTextColor::BySeverity = self {
1223            // This will be reset below before the next line.
1224            write!(f, "{}", color::Fg(color::Yellow))?;
1225        }
1226        Ok(())
1227    }
1228
1229    fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1230        match self {
1231            LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
1232            LogTextColor::None => {}
1233        };
1234        Ok(())
1235    }
1236}
1237
1238/// Options for the timezone associated to the timestamp of a log line.
1239#[derive(Clone, Copy, Debug, PartialEq)]
1240pub enum Timezone {
1241    /// Display a timestamp in terms of the local timezone as reported by the operating system.
1242    Local,
1243
1244    /// Display a timestamp in terms of UTC.
1245    Utc,
1246}
1247
1248impl Timezone {
1249    fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
1250        const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
1251        match self {
1252            Timezone::Local => {
1253                Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1254            }
1255            Timezone::Utc => {
1256                Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1257            }
1258        }
1259    }
1260}
1261
1262/// Configuration for how to display the timestamp associated to a log line.
1263#[derive(Clone, Copy, Debug, Default)]
1264pub enum LogTimeDisplayFormat {
1265    /// Display the log message's timestamp as monotonic nanoseconds since boot.
1266    #[default]
1267    Original,
1268
1269    /// Display the log's timestamp as a human-readable string in ISO 8601 format.
1270    WallTime {
1271        /// The format for displaying a timestamp as a string.
1272        tz: Timezone,
1273
1274        /// The offset to apply to the original device-monotonic time before printing it as a
1275        /// human-readable timestamp.
1276        offset: i64,
1277    },
1278}
1279
1280impl LogTimeDisplayFormat {
1281    fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
1282        const NANOS_IN_SECOND: i64 = 1_000_000_000;
1283
1284        match self {
1285            // Don't try to print a human readable string if it's going to be in 1970, fall back
1286            // to monotonic.
1287            Self::Original | Self::WallTime { offset: 0, .. } => {
1288                let time: Duration =
1289                    Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
1290                write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
1291            }
1292            Self::WallTime { tz, offset } => {
1293                let adjusted = time.into_nanos() + offset;
1294                let seconds = adjusted / NANOS_IN_SECOND;
1295                let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
1296                let formatted = tz.format(seconds, rem_nanos);
1297                write!(f, "[{formatted}]")?;
1298            }
1299        }
1300        Ok(())
1301    }
1302}
1303
1304/// Used to control stringification options of Data<Logs>
1305pub struct LogTextPresenter<'a> {
1306    /// The log to parameterize
1307    log: &'a Data<Logs>,
1308
1309    /// Options for stringifying the log
1310    options: LogTextDisplayOptions,
1311}
1312
1313impl<'a> LogTextPresenter<'a> {
1314    /// Creates a new LogTextPresenter with the specified options and
1315    /// log message. This presenter is bound to the lifetime of the
1316    /// underlying log message.
1317    pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
1318        Self { log, options }
1319    }
1320}
1321
1322impl fmt::Display for Data<Logs> {
1323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1324        LogTextPresenter::new(self, Default::default()).fmt(f)
1325    }
1326}
1327
1328impl Deref for LogTextPresenter<'_> {
1329    type Target = Data<Logs>;
1330    fn deref(&self) -> &Self::Target {
1331        self.log
1332    }
1333}
1334
1335impl fmt::Display for LogTextPresenter<'_> {
1336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1337        if self.options.highlight_timestamp {
1338            self.options.color.begin_timestamp_highlight(f)?;
1339            self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1340            self.options.color.end_timestamp_highlight(f, self.log.severity())?;
1341        } else {
1342            self.options.color.begin_record(f, self.log.severity())?;
1343            self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1344        }
1345
1346        if self.options.show_metadata {
1347            match self.pid() {
1348                Some(pid) => write!(f, "[{pid}]")?,
1349                None => write!(f, "[]")?,
1350            }
1351            match self.tid() {
1352                Some(tid) => write!(f, "[{tid}]")?,
1353                None => write!(f, "[]")?,
1354            }
1355        }
1356
1357        let moniker = if self.options.show_full_moniker {
1358            match &self.moniker {
1359                ExtendedMoniker::ComponentManager => {
1360                    Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1361                }
1362                ExtendedMoniker::ComponentInstance(instance) => {
1363                    if instance.is_root() {
1364                        Cow::Borrowed(ROOT_MONIKER_REPR)
1365                    } else {
1366                        Cow::Owned(instance.to_string())
1367                    }
1368                }
1369            }
1370        } else {
1371            if self.options.prefer_url_component_name {
1372                self.component_name_by_url()
1373            } else {
1374                self.component_name()
1375            }
1376        };
1377        if self.options.show_moniker {
1378            write!(f, "[{moniker}]")?;
1379        }
1380
1381        if self.options.show_tags {
1382            match &self.metadata.tags {
1383                Some(tags) if !tags.is_empty() => {
1384                    let mut filtered =
1385                        tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
1386                    if filtered.peek().is_some() {
1387                        write!(f, "[{}]", filtered.join(","))?;
1388                    }
1389                }
1390                _ => {}
1391            }
1392        }
1393
1394        write!(f, " {}:", self.metadata.severity)?;
1395
1396        if self.options.show_file {
1397            match (&self.metadata.file, &self.metadata.line) {
1398                (Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
1399                (Some(file), None) => write!(f, " [{file}]")?,
1400                _ => (),
1401            }
1402        }
1403
1404        if let Some(mut msg) = self.msg() {
1405            if let Some(nul) = msg.find("\0") {
1406                msg = &msg[0..nul];
1407            }
1408            write!(f, " {msg}")?;
1409        } else {
1410            write!(f, " <missing message>")?;
1411        }
1412        for kvp in self.payload_keys_strings() {
1413            write!(f, " {kvp}")?;
1414        }
1415
1416        let dropped = self.log.dropped_logs().unwrap_or_default();
1417        let rolled = self.log.rolled_out_logs().unwrap_or_default();
1418        if dropped != 0 || rolled != 0 {
1419            self.options.color.begin_lost_message_counts(f)?;
1420            if dropped != 0 {
1421                write!(f, " [dropped={dropped}]")?;
1422            }
1423            if rolled != 0 {
1424                write!(f, " [rolled={rolled}]")?;
1425            }
1426        }
1427
1428        self.options.color.end_record(f)?;
1429
1430        Ok(())
1431    }
1432}
1433
1434impl Eq for Data<Logs> {}
1435
1436impl PartialOrd for Data<Logs> {
1437    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1438        Some(self.cmp(other))
1439    }
1440}
1441
1442impl Ord for Data<Logs> {
1443    fn cmp(&self, other: &Self) -> Ordering {
1444        self.metadata.timestamp.cmp(&other.metadata.timestamp)
1445    }
1446}
1447
1448/// An enum containing well known argument names passed through logs, as well
1449/// as an `Other` variant for any other argument names.
1450///
1451/// This contains the fields of logs sent as a [`LogMessage`].
1452///
1453/// [`LogMessage`]: https://fuchsia.dev/reference/fidl/fuchsia.logger#LogMessage
1454#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
1455pub enum LogsField {
1456    ProcessId,
1457    ThreadId,
1458    Dropped,
1459    Tag,
1460    Msg,
1461    MsgStructured,
1462    FilePath,
1463    LineNumber,
1464    Other(String),
1465}
1466
1467impl fmt::Display for LogsField {
1468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1469        match self {
1470            LogsField::ProcessId => write!(f, "pid"),
1471            LogsField::ThreadId => write!(f, "tid"),
1472            LogsField::Dropped => write!(f, "num_dropped"),
1473            LogsField::Tag => write!(f, "tag"),
1474            LogsField::Msg => write!(f, "message"),
1475            LogsField::MsgStructured => write!(f, "value"),
1476            LogsField::FilePath => write!(f, "file_path"),
1477            LogsField::LineNumber => write!(f, "line_number"),
1478            LogsField::Other(name) => write!(f, "{name}"),
1479        }
1480    }
1481}
1482
1483// TODO(https://fxbug.dev/42127608) - ensure that strings reported here align with naming
1484// decisions made for the structured log format sent by other components.
1485/// The label for the process koid in the log metadata.
1486pub const PID_LABEL: &str = "pid";
1487/// The label for the thread koid in the log metadata.
1488pub const TID_LABEL: &str = "tid";
1489/// The label for the number of dropped logs in the log metadata.
1490pub const DROPPED_LABEL: &str = "num_dropped";
1491/// The label for a tag in the log metadata.
1492pub const TAG_LABEL: &str = "tag";
1493/// The label for the contents of a message in the log payload.
1494pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
1495/// The label for the message in the log payload.
1496pub const MESSAGE_LABEL: &str = "message";
1497/// The label for the file associated with a log line.
1498pub const FILE_PATH_LABEL: &str = "file";
1499/// The label for the line number in the file associated with a log line.
1500pub const LINE_NUMBER_LABEL: &str = "line";
1501
1502impl AsRef<str> for LogsField {
1503    fn as_ref(&self) -> &str {
1504        match self {
1505            Self::ProcessId => PID_LABEL,
1506            Self::ThreadId => TID_LABEL,
1507            Self::Dropped => DROPPED_LABEL,
1508            Self::Tag => TAG_LABEL,
1509            Self::Msg => MESSAGE_LABEL,
1510            Self::FilePath => FILE_PATH_LABEL,
1511            Self::LineNumber => LINE_NUMBER_LABEL,
1512            Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
1513            Self::Other(str) => str.as_str(),
1514        }
1515    }
1516}
1517
1518impl<T> From<T> for LogsField
1519where
1520    // Deref instead of AsRef b/c LogsField: AsRef<str> so this conflicts with concrete From<Self>
1521    T: Deref<Target = str>,
1522{
1523    fn from(s: T) -> Self {
1524        match s.as_ref() {
1525            PID_LABEL => Self::ProcessId,
1526            TID_LABEL => Self::ThreadId,
1527            DROPPED_LABEL => Self::Dropped,
1528            TAG_LABEL => Self::Tag,
1529            MESSAGE_LABEL => Self::Msg,
1530            FILE_PATH_LABEL => Self::FilePath,
1531            LINE_NUMBER_LABEL => Self::LineNumber,
1532            MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
1533            _ => Self::Other(s.to_string()),
1534        }
1535    }
1536}
1537
1538impl FromStr for LogsField {
1539    type Err = ();
1540    fn from_str(s: &str) -> Result<Self, Self::Err> {
1541        Ok(Self::from(s))
1542    }
1543}
1544
1545/// Possible errors that can come in a `DiagnosticsData` object where the data source is
1546/// `DataSource::Logs`.
1547#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
1548#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
1549pub enum LogError {
1550    /// Represents the number of logs that were dropped by the component writing the logs due to an
1551    /// error writing to the socket before succeeding to write a log.
1552    #[serde(rename = "dropped_logs")]
1553    DroppedLogs { count: u64 },
1554    /// Represents the number of logs that were dropped for a component by the archivist due to the
1555    /// log buffer execeeding its maximum capacity before the current message.
1556    #[serde(rename = "rolled_out_logs")]
1557    RolledOutLogs { count: u64 },
1558    #[serde(rename = "parse_record")]
1559    FailedToParseRecord(String),
1560    #[serde(rename = "other")]
1561    Other { message: String },
1562}
1563
1564const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
1565
1566impl MetadataError for LogError {
1567    fn dropped_payload() -> Self {
1568        Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
1569    }
1570
1571    fn message(&self) -> Option<&str> {
1572        match self {
1573            Self::FailedToParseRecord(msg) => Some(msg.as_str()),
1574            Self::Other { message } => Some(message.as_str()),
1575            _ => None,
1576        }
1577    }
1578}
1579
1580/// Possible error that can come in a `DiagnosticsData` object where the data source is
1581/// `DataSource::Inspect`..
1582#[derive(Debug, PartialEq, Clone, Eq)]
1583pub struct InspectError {
1584    pub message: String,
1585}
1586
1587impl MetadataError for InspectError {
1588    fn dropped_payload() -> Self {
1589        Self { message: "Schema failed to fit component budget.".into() }
1590    }
1591
1592    fn message(&self) -> Option<&str> {
1593        Some(self.message.as_str())
1594    }
1595}
1596
1597impl fmt::Display for InspectError {
1598    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1599        write!(f, "{}", self.message)
1600    }
1601}
1602
1603impl Borrow<str> for InspectError {
1604    fn borrow(&self) -> &str {
1605        &self.message
1606    }
1607}
1608
1609impl Serialize for InspectError {
1610    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
1611        self.message.serialize(ser)
1612    }
1613}
1614
1615impl<'de> Deserialize<'de> for InspectError {
1616    fn deserialize<D>(de: D) -> Result<Self, D::Error>
1617    where
1618        D: Deserializer<'de>,
1619    {
1620        let message = String::deserialize(de)?;
1621        Ok(Self { message })
1622    }
1623}
1624
1625#[cfg(test)]
1626mod tests {
1627    use super::*;
1628    use diagnostics_hierarchy::hierarchy;
1629    use selectors::FastError;
1630    use serde_json::json;
1631    use test_case::test_case;
1632
1633    const TEST_URL: &str = "fuchsia-pkg://test";
1634
1635    #[fuchsia::test]
1636    fn test_canonical_json_inspect_formatting() {
1637        let mut hierarchy = hierarchy! {
1638            root: {
1639                x: "foo",
1640            }
1641        };
1642
1643        hierarchy.sort();
1644        let json_schema = InspectDataBuilder::new(
1645            "a/b/c/d".try_into().unwrap(),
1646            TEST_URL,
1647            Timestamp::from_nanos(123456i64),
1648        )
1649        .with_hierarchy(hierarchy)
1650        .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1651        .build();
1652
1653        let result_json =
1654            serde_json::to_value(&json_schema).expect("serialization should succeed.");
1655
1656        let expected_json = json!({
1657          "moniker": "a/b/c/d",
1658          "version": 1,
1659          "data_source": "Inspect",
1660          "payload": {
1661            "root": {
1662              "x": "foo"
1663            }
1664          },
1665          "metadata": {
1666            "component_url": TEST_URL,
1667            "filename": "test_file_plz_ignore.inspect",
1668            "timestamp": 123456,
1669          }
1670        });
1671
1672        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1673    }
1674
1675    #[fuchsia::test]
1676    fn test_errorful_json_inspect_formatting() {
1677        let json_schema = InspectDataBuilder::new(
1678            "a/b/c/d".try_into().unwrap(),
1679            TEST_URL,
1680            Timestamp::from_nanos(123456i64),
1681        )
1682        .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1683        .with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
1684        .build();
1685
1686        let result_json =
1687            serde_json::to_value(&json_schema).expect("serialization should succeed.");
1688
1689        let expected_json = json!({
1690          "moniker": "a/b/c/d",
1691          "version": 1,
1692          "data_source": "Inspect",
1693          "payload": null,
1694          "metadata": {
1695            "component_url": TEST_URL,
1696            "errors": ["too much fun being had."],
1697            "filename": "test_file_plz_ignore.inspect",
1698            "timestamp": 123456,
1699          }
1700        });
1701
1702        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1703    }
1704
1705    fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
1706        strings
1707            .iter()
1708            .map(|s| match selectors::parse_selector::<FastError>(s) {
1709                Ok(selector) => selector,
1710                Err(e) => panic!("Couldn't parse selector {s}: {e}"),
1711            })
1712            .collect::<Vec<_>>()
1713    }
1714
1715    #[fuchsia::test]
1716    fn test_filter_returns_none_on_empty_hierarchy() {
1717        let data = InspectDataBuilder::new(
1718            "a/b/c/d".try_into().unwrap(),
1719            TEST_URL,
1720            Timestamp::from_nanos(123456i64),
1721        )
1722        .build();
1723        let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1724        assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1725    }
1726
1727    #[fuchsia::test]
1728    fn test_filter_returns_none_on_selector_mismatch() {
1729        let mut hierarchy = hierarchy! {
1730            root: {
1731                x: "foo",
1732            }
1733        };
1734        hierarchy.sort();
1735        let data = InspectDataBuilder::new(
1736            "b/c/d/e".try_into().unwrap(),
1737            TEST_URL,
1738            Timestamp::from_nanos(123456i64),
1739        )
1740        .with_hierarchy(hierarchy)
1741        .build();
1742        let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1743        assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1744    }
1745
1746    #[fuchsia::test]
1747    fn test_filter_returns_none_on_data_mismatch() {
1748        let mut hierarchy = hierarchy! {
1749            root: {
1750                x: "foo",
1751            }
1752        };
1753        hierarchy.sort();
1754        let data = InspectDataBuilder::new(
1755            "a/b/c/d".try_into().unwrap(),
1756            TEST_URL,
1757            Timestamp::from_nanos(123456i64),
1758        )
1759        .with_hierarchy(hierarchy)
1760        .build();
1761        let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1762
1763        assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
1764    }
1765
1766    #[fuchsia::test]
1767    fn test_filter_returns_matching_data() {
1768        let mut hierarchy = hierarchy! {
1769            root: {
1770                x: "foo",
1771                y: "bar",
1772            }
1773        };
1774        hierarchy.sort();
1775        let data = InspectDataBuilder::new(
1776            "a/b/c/d".try_into().unwrap(),
1777            TEST_URL,
1778            Timestamp::from_nanos(123456i64),
1779        )
1780        .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1781        .with_hierarchy(hierarchy)
1782        .build();
1783        let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
1784
1785        let expected_json = json!({
1786          "moniker": "a/b/c/d",
1787          "version": 1,
1788          "data_source": "Inspect",
1789          "payload": {
1790            "root": {
1791              "x": "foo"
1792            }
1793          },
1794          "metadata": {
1795            "component_url": TEST_URL,
1796            "filename": "test_file_plz_ignore.inspect",
1797            "timestamp": 123456,
1798          }
1799        });
1800
1801        let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
1802            .expect("serialization should succeed.");
1803
1804        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1805    }
1806
1807    #[fuchsia::test]
1808    fn default_builder_test() {
1809        let builder = LogsDataBuilder::new(BuilderArgs {
1810            component_url: Some("url".into()),
1811            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1812            severity: Severity::Info,
1813            timestamp: Timestamp::from_nanos(0),
1814        });
1815        //let tree = builder.build();
1816        let expected_json = json!({
1817          "moniker": "moniker",
1818          "version": 1,
1819          "data_source": "Logs",
1820          "payload": {
1821              "root":
1822              {
1823                  "message":{}
1824              }
1825          },
1826          "metadata": {
1827            "component_url": "url",
1828              "severity": "INFO",
1829              "tags": [],
1830
1831            "timestamp": 0,
1832          }
1833        });
1834        let result_json =
1835            serde_json::to_value(builder.build()).expect("serialization should succeed.");
1836        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1837    }
1838
1839    #[fuchsia::test]
1840    fn regular_message_test() {
1841        let builder = LogsDataBuilder::new(BuilderArgs {
1842            component_url: Some("url".into()),
1843            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1844            severity: Severity::Info,
1845            timestamp: Timestamp::from_nanos(0),
1846        })
1847        .set_message("app")
1848        .set_file("test file.cc")
1849        .set_line(420)
1850        .set_pid(1001)
1851        .set_tid(200)
1852        .set_dropped(2)
1853        .add_tag("You're")
1854        .add_tag("IT!")
1855        .add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
1856        // TODO(https://fxbug.dev/42157027): Convert to our custom DSL when possible.
1857        let expected_json = json!({
1858          "moniker": "moniker",
1859          "version": 1,
1860          "data_source": "Logs",
1861          "payload": {
1862              "root":
1863              {
1864                  "keys":{
1865                      "key":"value"
1866                  },
1867                  "message":{
1868                      "value":"app"
1869                  }
1870              }
1871          },
1872          "metadata": {
1873            "errors": [],
1874            "component_url": "url",
1875              "errors": [{"dropped_logs":{"count":2}}],
1876              "file": "test file.cc",
1877              "line": 420,
1878              "pid": 1001,
1879              "severity": "INFO",
1880              "tags": ["You're", "IT!"],
1881              "tid": 200,
1882
1883            "timestamp": 0,
1884          }
1885        });
1886        let result_json =
1887            serde_json::to_value(builder.build()).expect("serialization should succeed.");
1888        pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1889    }
1890
1891    #[fuchsia::test]
1892    fn display_for_logs() {
1893        let data = LogsDataBuilder::new(BuilderArgs {
1894            timestamp: Timestamp::from_nanos(12345678000i64),
1895            component_url: Some(FlyStr::from("fake-url")),
1896            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1897            severity: Severity::Info,
1898        })
1899        .set_pid(123)
1900        .set_tid(456)
1901        .set_message("some message".to_string())
1902        .set_file("some_file.cc".to_string())
1903        .set_line(420)
1904        .add_tag("foo")
1905        .add_tag("bar")
1906        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1907        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1908        .build();
1909
1910        assert_eq!(
1911            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1912            format!("{data}")
1913        )
1914    }
1915
1916    #[fuchsia::test]
1917    fn display_for_logs_with_duplicate_moniker() {
1918        let data = LogsDataBuilder::new(BuilderArgs {
1919            timestamp: Timestamp::from_nanos(12345678000i64),
1920            component_url: Some(FlyStr::from("fake-url")),
1921            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1922            severity: Severity::Info,
1923        })
1924        .set_pid(123)
1925        .set_tid(456)
1926        .set_message("some message".to_string())
1927        .set_file("some_file.cc".to_string())
1928        .set_line(420)
1929        .add_tag("moniker")
1930        .add_tag("bar")
1931        .add_tag("moniker")
1932        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1933        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1934        .build();
1935
1936        assert_eq!(
1937            "[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
1938            format!("{data}")
1939        )
1940    }
1941
1942    #[fuchsia::test]
1943    fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
1944        let data = LogsDataBuilder::new(BuilderArgs {
1945            timestamp: Timestamp::from_nanos(12345678000i64),
1946            component_url: Some(FlyStr::from("fake-url")),
1947            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1948            severity: Severity::Info,
1949        })
1950        .set_pid(123)
1951        .set_tid(456)
1952        .set_message("some message".to_string())
1953        .set_file("some_file.cc".to_string())
1954        .set_line(420)
1955        .add_tag("moniker")
1956        .add_tag("moniker")
1957        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1958        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1959        .build();
1960
1961        assert_eq!(
1962            "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1963            format!("{data}")
1964        )
1965    }
1966
1967    #[fuchsia::test]
1968    fn test_component_name_by_url() {
1969        let data = LogsDataBuilder::new(BuilderArgs {
1970            timestamp: Timestamp::from_nanos(0),
1971            component_url: Some(FlyStr::from(
1972                "fuchsia-pkg://fuchsia.com/my-pkg#meta/my-component.cm",
1973            )),
1974            moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1975            severity: Severity::Info,
1976        })
1977        .build();
1978        assert_eq!(data.component_name_by_url(), "my-component");
1979
1980        let data = LogsDataBuilder::new(BuilderArgs {
1981            timestamp: Timestamp::from_nanos(0),
1982            component_url: Some(FlyStr::from("fuchsia-pkg://fuchsia.com/my-pkg#meta/my-component")),
1983            moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1984            severity: Severity::Info,
1985        })
1986        .build();
1987        assert_eq!(data.component_name_by_url(), "my-component");
1988
1989        let data = LogsDataBuilder::new(BuilderArgs {
1990            timestamp: Timestamp::from_nanos(0),
1991            component_url: Some(FlyStr::from("")),
1992            moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1993            severity: Severity::Info,
1994        })
1995        .build();
1996        assert_eq!(data.component_name_by_url(), "moniker");
1997
1998        let data = LogsDataBuilder::new(BuilderArgs {
1999            timestamp: Timestamp::from_nanos(0),
2000            component_url: None,
2001            moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
2002            severity: Severity::Info,
2003        })
2004        .build();
2005        assert_eq!(data.component_name_by_url(), "moniker");
2006    }
2007
2008    #[fuchsia::test]
2009    fn display_for_logs_partial_moniker() {
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("test/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][123][456][fake-url][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2029            format!(
2030                "{}",
2031                LogTextPresenter::new(
2032                    &data,
2033                    LogTextDisplayOptions {
2034                        show_full_moniker: false,
2035                        prefer_url_component_name: true,
2036                        ..Default::default()
2037                    }
2038                )
2039            )
2040        )
2041    }
2042
2043    #[fuchsia::test]
2044    fn display_for_logs_exclude_metadata() {
2045        let data = LogsDataBuilder::new(BuilderArgs {
2046            timestamp: Timestamp::from_nanos(12345678000i64),
2047            component_url: Some(FlyStr::from("fake-url")),
2048            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2049            severity: Severity::Info,
2050        })
2051        .set_pid(123)
2052        .set_tid(456)
2053        .set_message("some message".to_string())
2054        .set_file("some_file.cc".to_string())
2055        .set_line(420)
2056        .add_tag("foo")
2057        .add_tag("bar")
2058        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2059        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2060        .build();
2061
2062        assert_eq!(
2063            "[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2064            format!(
2065                "{}",
2066                LogTextPresenter::new(
2067                    &data,
2068                    LogTextDisplayOptions { show_metadata: false, ..Default::default() }
2069                )
2070            )
2071        )
2072    }
2073
2074    #[fuchsia::test]
2075    fn display_for_logs_exclude_tags() {
2076        let data = LogsDataBuilder::new(BuilderArgs {
2077            timestamp: Timestamp::from_nanos(12345678000i64),
2078            component_url: Some(FlyStr::from("fake-url")),
2079            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2080            severity: Severity::Info,
2081        })
2082        .set_pid(123)
2083        .set_tid(456)
2084        .set_message("some message".to_string())
2085        .set_file("some_file.cc".to_string())
2086        .set_line(420)
2087        .add_tag("foo")
2088        .add_tag("bar")
2089        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2090        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2091        .build();
2092
2093        assert_eq!(
2094            "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
2095            format!(
2096                "{}",
2097                LogTextPresenter::new(
2098                    &data,
2099                    LogTextDisplayOptions { show_tags: false, ..Default::default() }
2100                )
2101            )
2102        )
2103    }
2104
2105    #[fuchsia::test]
2106    fn display_for_logs_exclude_file() {
2107        let data = LogsDataBuilder::new(BuilderArgs {
2108            timestamp: Timestamp::from_nanos(12345678000i64),
2109            component_url: Some(FlyStr::from("fake-url")),
2110            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2111            severity: Severity::Info,
2112        })
2113        .set_pid(123)
2114        .set_tid(456)
2115        .set_message("some message".to_string())
2116        .set_file("some_file.cc".to_string())
2117        .set_line(420)
2118        .add_tag("foo")
2119        .add_tag("bar")
2120        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2121        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2122        .build();
2123
2124        assert_eq!(
2125            "[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
2126            format!(
2127                "{}",
2128                LogTextPresenter::new(
2129                    &data,
2130                    LogTextDisplayOptions { show_file: false, ..Default::default() }
2131                )
2132            )
2133        )
2134    }
2135
2136    #[fuchsia::test]
2137    fn display_for_logs_include_color_by_severity() {
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::Error,
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] ERROR: [some_file.cc(420)] some message test=property value=test{}",
2158                color::Fg(color::Red),
2159                style::Reset
2160            ),
2161            format!(
2162                "{}",
2163                LogTextPresenter::new(
2164                    &data,
2165                    LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2166                )
2167            )
2168        )
2169    }
2170
2171    #[fuchsia::test]
2172    fn display_for_logs_highlight_line() {
2173        let data = LogsDataBuilder::new(BuilderArgs {
2174            timestamp: Timestamp::from_nanos(12345678000i64),
2175            component_url: Some(FlyStr::from("fake-url")),
2176            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2177            severity: Severity::Info,
2178        })
2179        .set_pid(123)
2180        .set_tid(456)
2181        .set_message("some message".to_string())
2182        .set_file("some_file.cc".to_string())
2183        .set_line(420)
2184        .add_tag("foo")
2185        .add_tag("bar")
2186        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2187        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2188        .build();
2189
2190        assert_eq!(
2191            format!(
2192                "{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}",
2193                color::Fg(color::LightYellow),
2194                style::Reset
2195            ),
2196            LogTextPresenter::new(
2197                &data,
2198                LogTextDisplayOptions { color: LogTextColor::Highlight, ..Default::default() }
2199            )
2200            .to_string()
2201        )
2202    }
2203
2204    #[fuchsia::test]
2205    fn display_for_logs_highlight_timestamp() {
2206        let info_data = LogsDataBuilder::new(BuilderArgs {
2207            timestamp: Timestamp::from_nanos(12345678000i64),
2208            component_url: Some(FlyStr::from("fake-url")),
2209            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2210            severity: Severity::Info,
2211        })
2212        .set_pid(123)
2213        .set_tid(456)
2214        .set_message("some message".to_string())
2215        .build();
2216
2217        assert_eq!(
2218            format!(
2219                "{}[00012.345678]{}[123][456][moniker] INFO: some message{}",
2220                color::Fg(color::Cyan),
2221                style::Reset,
2222                style::Reset
2223            ),
2224            LogTextPresenter::new(
2225                &info_data,
2226                LogTextDisplayOptions {
2227                    color: LogTextColor::BySeverity,
2228                    highlight_timestamp: true,
2229                    ..Default::default()
2230                }
2231            )
2232            .to_string()
2233        );
2234
2235        let error_data = LogsDataBuilder::new(BuilderArgs {
2236            timestamp: Timestamp::from_nanos(12345678000i64),
2237            component_url: Some(FlyStr::from("fake-url")),
2238            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2239            severity: Severity::Error,
2240        })
2241        .set_pid(123)
2242        .set_tid(456)
2243        .set_message("error message".to_string())
2244        .build();
2245
2246        assert_eq!(
2247            format!(
2248                "{}[00012.345678]{}{}[123][456][moniker] ERROR: error message{}",
2249                color::Fg(color::Cyan),
2250                style::Reset,
2251                color::Fg(color::Red),
2252                style::Reset
2253            ),
2254            LogTextPresenter::new(
2255                &error_data,
2256                LogTextDisplayOptions {
2257                    color: LogTextColor::BySeverity,
2258                    highlight_timestamp: true,
2259                    ..Default::default()
2260                }
2261            )
2262            .to_string()
2263        );
2264
2265        assert_eq!(
2266            "[00012.345678][123][456][moniker] ERROR: error message",
2267            LogTextPresenter::new(
2268                &error_data,
2269                LogTextDisplayOptions {
2270                    color: LogTextColor::None,
2271                    highlight_timestamp: true,
2272                    ..Default::default()
2273                }
2274            )
2275            .to_string()
2276        );
2277    }
2278
2279    #[fuchsia::test]
2280    fn display_for_logs_with_wall_time() {
2281        let data = LogsDataBuilder::new(BuilderArgs {
2282            timestamp: Timestamp::from_nanos(12345678000i64),
2283            component_url: Some(FlyStr::from("fake-url")),
2284            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2285            severity: Severity::Info,
2286        })
2287        .set_pid(123)
2288        .set_tid(456)
2289        .set_message("some message".to_string())
2290        .set_file("some_file.cc".to_string())
2291        .set_line(420)
2292        .add_tag("foo")
2293        .add_tag("bar")
2294        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2295        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2296        .build();
2297
2298        assert_eq!(
2299            "[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2300            LogTextPresenter::new(
2301                &data,
2302                LogTextDisplayOptions {
2303                    time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
2304                    ..Default::default()
2305                }
2306            )
2307            .to_string()
2308        );
2309
2310        assert_eq!(
2311            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2312            LogTextPresenter::new(
2313                &data,
2314                LogTextDisplayOptions {
2315                    time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
2316                    ..Default::default()
2317                }
2318            )
2319            .to_string(),
2320            "should fall back to monotonic if offset is 0"
2321        );
2322    }
2323
2324    #[fuchsia::test]
2325    fn display_for_logs_with_dropped_count() {
2326        let data = LogsDataBuilder::new(BuilderArgs {
2327            timestamp: Timestamp::from_nanos(12345678000i64),
2328            component_url: Some(FlyStr::from("fake-url")),
2329            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2330            severity: Severity::Info,
2331        })
2332        .set_dropped(5)
2333        .set_pid(123)
2334        .set_tid(456)
2335        .set_message("some message".to_string())
2336        .set_file("some_file.cc".to_string())
2337        .set_line(420)
2338        .add_tag("foo")
2339        .add_tag("bar")
2340        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2341        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2342        .build();
2343
2344        assert_eq!(
2345            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
2346            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2347        );
2348
2349        assert_eq!(
2350            format!(
2351                "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}",
2352                color::Fg(color::Yellow),
2353                style::Reset
2354            ),
2355            LogTextPresenter::new(
2356                &data,
2357                LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2358            )
2359            .to_string()
2360        );
2361    }
2362
2363    #[fuchsia::test]
2364    fn display_for_logs_with_rolled_count() {
2365        let data = LogsDataBuilder::new(BuilderArgs {
2366            timestamp: Timestamp::from_nanos(12345678000i64),
2367            component_url: Some(FlyStr::from("fake-url")),
2368            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2369            severity: Severity::Info,
2370        })
2371        .set_rolled_out(10)
2372        .set_pid(123)
2373        .set_tid(456)
2374        .set_message("some message".to_string())
2375        .set_file("some_file.cc".to_string())
2376        .set_line(420)
2377        .add_tag("foo")
2378        .add_tag("bar")
2379        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2380        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2381        .build();
2382
2383        assert_eq!(
2384            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
2385            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2386        );
2387
2388        assert_eq!(
2389            format!(
2390                "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}",
2391                color::Fg(color::Yellow),
2392                style::Reset
2393            ),
2394            LogTextPresenter::new(
2395                &data,
2396                LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2397            )
2398            .to_string()
2399        );
2400    }
2401
2402    #[fuchsia::test]
2403    fn display_for_logs_with_dropped_and_rolled_counts() {
2404        let data = LogsDataBuilder::new(BuilderArgs {
2405            timestamp: Timestamp::from_nanos(12345678000i64),
2406            component_url: Some(FlyStr::from("fake-url")),
2407            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2408            severity: Severity::Info,
2409        })
2410        .set_dropped(5)
2411        .set_rolled_out(10)
2412        .set_pid(123)
2413        .set_tid(456)
2414        .set_message("some message".to_string())
2415        .set_file("some_file.cc".to_string())
2416        .set_line(420)
2417        .add_tag("foo")
2418        .add_tag("bar")
2419        .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2420        .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2421        .build();
2422
2423        assert_eq!(
2424            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
2425            format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2426        );
2427
2428        assert_eq!(
2429            format!(
2430                "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}",
2431                color::Fg(color::Yellow),
2432                style::Reset
2433            ),
2434            LogTextPresenter::new(
2435                &data,
2436                LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2437            )
2438            .to_string()
2439        );
2440    }
2441
2442    #[fuchsia::test]
2443    fn display_for_logs_no_tags() {
2444        let data = LogsDataBuilder::new(BuilderArgs {
2445            timestamp: Timestamp::from_nanos(12345678000i64),
2446            component_url: Some(FlyStr::from("fake-url")),
2447            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2448            severity: Severity::Info,
2449        })
2450        .set_pid(123)
2451        .set_tid(456)
2452        .set_message("some message".to_string())
2453        .build();
2454
2455        assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{data}"))
2456    }
2457
2458    #[fuchsia::test]
2459    fn size_bytes_deserialize_backwards_compatibility() {
2460        let original_json = json!({
2461          "moniker": "a/b",
2462          "version": 1,
2463          "data_source": "Logs",
2464          "payload": {
2465            "root": {
2466              "message":{}
2467            }
2468          },
2469          "metadata": {
2470            "component_url": "url",
2471              "severity": "INFO",
2472              "tags": [],
2473
2474            "timestamp": 123,
2475          }
2476        });
2477        let expected_data = LogsDataBuilder::new(BuilderArgs {
2478            component_url: Some("url".into()),
2479            moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2480            severity: Severity::Info,
2481            timestamp: Timestamp::from_nanos(123),
2482        })
2483        .build();
2484        let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2485        assert_eq!(original_data, expected_data);
2486        // We skip deserializing the size_bytes
2487        assert_eq!(original_data.metadata.size_bytes, None);
2488    }
2489
2490    #[fuchsia::test]
2491    fn display_for_logs_with_null_terminator() {
2492        let data = LogsDataBuilder::new(BuilderArgs {
2493            timestamp: Timestamp::from_nanos(12345678000i64),
2494            component_url: Some(FlyStr::from("fake-url")),
2495            moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2496            severity: Severity::Info,
2497        })
2498        .set_pid(123)
2499        .set_tid(456)
2500        .set_message("some message\0garbage".to_string())
2501        .set_file("some_file.cc".to_string())
2502        .set_line(420)
2503        .add_tag("foo")
2504        .add_tag("bar")
2505        .build();
2506
2507        assert_eq!(
2508            "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message",
2509            format!("{data}")
2510        )
2511    }
2512
2513    #[fuchsia::test]
2514    fn dropped_deserialize_backwards_compatibility() {
2515        let original_json = json!({
2516          "moniker": "a/b",
2517          "version": 1,
2518          "data_source": "Logs",
2519          "payload": {
2520            "root": {
2521              "message":{}
2522            }
2523          },
2524          "metadata": {
2525            "dropped": 0,
2526            "component_url": "url",
2527              "severity": "INFO",
2528              "tags": [],
2529
2530            "timestamp": 123,
2531          }
2532        });
2533        let expected_data = LogsDataBuilder::new(BuilderArgs {
2534            component_url: Some("url".into()),
2535            moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2536            severity: Severity::Info,
2537            timestamp: Timestamp::from_nanos(123),
2538        })
2539        .build();
2540        let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2541        assert_eq!(original_data, expected_data);
2542        // We skip deserializing dropped
2543        assert_eq!(original_data.metadata.dropped, None);
2544    }
2545
2546    #[fuchsia::test]
2547    fn severity_aliases() {
2548        assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
2549        assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
2550    }
2551
2552    #[fuchsia::test]
2553    fn test_metadata_merge() {
2554        let mut meta = InspectMetadata {
2555            errors: Some(vec![InspectError { message: "error1".to_string() }]),
2556            name: InspectHandleName::name("test"),
2557            component_url: "fuchsia-pkg://test".into(),
2558            timestamp: Timestamp::from_nanos(100),
2559            escrowed: false,
2560        };
2561
2562        meta.merge(InspectMetadata {
2563            errors: Some(vec![InspectError { message: "error2".to_string() }]),
2564            name: InspectHandleName::name("test"),
2565            component_url: "fuchsia-pkg://test".into(),
2566            timestamp: Timestamp::from_nanos(200),
2567            escrowed: false,
2568        });
2569
2570        assert_eq!(
2571            meta,
2572            InspectMetadata {
2573                errors: Some(vec![
2574                    InspectError { message: "error1".to_string() },
2575                    InspectError { message: "error2".to_string() },
2576                ]),
2577                name: InspectHandleName::name("test"),
2578                component_url: "fuchsia-pkg://test".into(),
2579                timestamp: Timestamp::from_nanos(200),
2580                escrowed: false,
2581            }
2582        );
2583    }
2584
2585    #[fuchsia::test]
2586    fn test_metadata_merge_older_timestamp_noop() {
2587        let mut meta = InspectMetadata {
2588            errors: None,
2589            name: InspectHandleName::name("test"),
2590            component_url: TEST_URL.into(),
2591            timestamp: Timestamp::from_nanos(200),
2592            escrowed: false,
2593        };
2594        meta.merge(InspectMetadata {
2595            errors: None,
2596            name: InspectHandleName::name("test"),
2597            component_url: TEST_URL.into(),
2598            timestamp: Timestamp::from_nanos(100),
2599            escrowed: false,
2600        });
2601        assert_eq!(
2602            meta,
2603            InspectMetadata {
2604                errors: None,
2605                name: InspectHandleName::name("test"),
2606                component_url: TEST_URL.into(),
2607                timestamp: Timestamp::from_nanos(200),
2608                escrowed: false,
2609            }
2610        );
2611    }
2612
2613    fn new_test_data(moniker: &str, payload_val: Option<&str>, timestamp: i64) -> InspectData {
2614        let mut builder = InspectDataBuilder::new(
2615            moniker.try_into().unwrap(),
2616            TEST_URL,
2617            Timestamp::from_nanos(timestamp),
2618        );
2619        if let Some(val) = payload_val {
2620            builder = builder.with_hierarchy(hierarchy! { root: { "key": val } });
2621        }
2622        builder.build()
2623    }
2624
2625    #[fuchsia::test]
2626    fn test_data_merge() {
2627        let mut data = new_test_data("a/b/c", Some("val1"), 100);
2628        let mut other = new_test_data("a/b/c", Some("val2"), 200);
2629        other.metadata.errors = Some(vec![InspectError { message: "error".into() }]);
2630
2631        data.merge(other);
2632
2633        let expected_payload = hierarchy! { root: { "key": "val2" } };
2634        assert_eq!(data.payload, Some(expected_payload));
2635        assert_eq!(data.metadata.timestamp, Timestamp::from_nanos(200));
2636        assert_eq!(data.metadata.errors, Some(vec![InspectError { message: "error".into() }]));
2637    }
2638
2639    #[test_case(new_test_data("a/b/d", Some("v2"), 100); "different moniker")]
2640    #[test_case(
2641        {
2642            let mut d = new_test_data("a/b/c", Some("v2"), 100);
2643            d.version = 2;
2644            d
2645        }; "different version")]
2646    #[test_case(
2647        {
2648            let mut d = new_test_data("a/b/c", Some("v2"), 100);
2649            d.data_source = DataSource::Logs;
2650            d
2651        }; "different data source")]
2652    #[fuchsia::test]
2653    fn test_data_merge_noop(other: InspectData) {
2654        let mut data = new_test_data("a/b/c", Some("v1"), 100);
2655        let original = data.clone();
2656        data.merge(other);
2657        assert_eq!(data, original);
2658    }
2659
2660    #[test_case(None, Some("val2"), Some("val2") ; "none_with_some")]
2661    #[test_case(Some("val1"), None, Some("val1") ; "some_with_none")]
2662    #[test_case(Some("val1"), Some("val2"), Some("val2") ; "some_with_some")]
2663    #[fuchsia::test]
2664    fn test_data_merge_payloads(
2665        payload: Option<&str>,
2666        other_payload: Option<&str>,
2667        expected: Option<&str>,
2668    ) {
2669        let mut data = new_test_data("a/b/c", payload, 100);
2670        let other = new_test_data("a/b/c", other_payload, 100);
2671
2672        data.merge(other);
2673        assert_eq!(data, new_test_data("a/b/c", expected, 100));
2674    }
2675}