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