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() -> 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#[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 #[cfg_attr(feature = "json_schema", schemars(with = "Option<String>"))]
357 pub component_url: Option<FlyStr>,
358
359 #[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
361 pub timestamp: Timestamp,
362
363 #[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 #[serde(skip_serializing_if = "Option::is_none")]
375 raw_severity: Option<u8>,
376
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub tags: Option<Vec<String>>,
380
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub pid: Option<u64>,
384
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub tid: Option<u64>,
388
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub file: Option<String>,
392
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub line: Option<u64>,
396
397 #[serde(skip)]
401 dropped: Option<u64>,
402
403 #[serde(skip)]
407 size_bytes: Option<usize>,
408}
409
410impl LogsMetadata {
411 pub fn component_url(&self) -> Option<&str> {
413 self.component_url.as_ref().map(|s| s.as_str())
414 }
415
416 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#[derive(Deserialize, Debug, Clone, PartialEq)]
427pub struct Data<D: DiagnosticsData> {
428 #[serde(default)]
430 pub data_source: DataSource,
432
433 #[serde(bound(
435 deserialize = "D::Metadata: DeserializeOwned",
436 serialize = "D::Metadata: Serialize"
437 ))]
438 pub metadata: D::Metadata,
439
440 #[serde(deserialize_with = "moniker_deserialize", serialize_with = "moniker_serialize")]
442 pub moniker: ExtendedMoniker,
443
444 pub payload: Option<DiagnosticsHierarchy<D::Key>>,
446
447 #[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 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 pub fn sort_payload(&mut self) {
532 if let Some(payload) = &mut self.payload {
533 payload.sort();
534 }
535 }
536
537 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 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 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 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#[derive(Debug, Error)]
591pub enum Error {
592 #[error(transparent)]
593 Internal(#[from] anyhow::Error),
594}
595
596pub type InspectData = Data<Inspect>;
598
599pub type LogsData = Data<Logs>;
601
602pub type LogsHierarchy = DiagnosticsHierarchy<LogsField>;
604
605pub type LogsProperty = Property<LogsField>;
607
608impl Data<Inspect> {
609 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
670pub struct LogsDataBuilder {
673 errors: Vec<LogError>,
675 msg: Option<String>,
677 tags: Vec<String>,
679 pid: Option<u64>,
681 tid: Option<u64>,
683 file: Option<String>,
685 line: Option<u64>,
687 args: BuilderArgs,
689 keys: Vec<Property<LogsField>>,
691 raw_severity: Option<u8>,
693}
694
695pub struct BuilderArgs {
697 pub moniker: ExtendedMoniker,
699 pub timestamp: Timestamp,
701 pub component_url: Option<FlyStr>,
703 pub severity: Severity,
705}
706
707impl LogsDataBuilder {
708 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 #[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 #[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 #[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 pub fn set_raw_severity(mut self, severity: u8) -> Self {
760 self.raw_severity = Some(severity);
761 self
762 }
763
764 #[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 pub fn set_severity(mut self, severity: Severity) -> Self {
786 self.args.severity = severity;
787 self.raw_severity = None;
788 self
789 }
790
791 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 pub fn set_severity(&mut self, severity: Severity) {
923 self.metadata.severity = severity;
924 self.metadata.raw_severity = None;
925 }
926
927 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 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 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 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 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 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 pub fn file_path(&self) -> Option<&str> {
1033 self.metadata.file.as_deref()
1034 }
1035
1036 pub fn line_number(&self) -> Option<&u64> {
1038 self.metadata.line.as_ref()
1039 }
1040
1041 pub fn pid(&self) -> Option<u64> {
1043 self.metadata.pid
1044 }
1045
1046 pub fn tid(&self) -> Option<u64> {
1048 self.metadata.tid
1049 }
1050
1051 pub fn tags(&self) -> Option<&Vec<String>> {
1053 self.metadata.tags.as_ref()
1054 }
1055
1056 pub fn severity(&self) -> Severity {
1058 self.metadata.severity
1059 }
1060
1061 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 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 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 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 self.component_name()
1097 }
1098
1099 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#[derive(Clone, Copy, Debug)]
1118pub struct LogTextDisplayOptions {
1119 pub show_moniker: bool,
1121
1122 pub show_full_moniker: bool,
1124
1125 pub prefer_url_component_name: bool,
1127
1128 pub show_metadata: bool,
1130
1131 pub show_tags: bool,
1133
1134 pub show_file: bool,
1136
1137 pub color: LogTextColor,
1139
1140 pub time_format: LogTimeDisplayFormat,
1142}
1143
1144impl Default for LogTextDisplayOptions {
1145 fn default() -> Self {
1146 Self {
1147 show_moniker: true,
1148 show_full_moniker: true,
1149 prefer_url_component_name: false,
1150 show_metadata: true,
1151 show_tags: true,
1152 show_file: true,
1153 color: Default::default(),
1154 time_format: Default::default(),
1155 }
1156 }
1157}
1158
1159#[derive(Clone, Copy, Debug, Default)]
1161pub enum LogTextColor {
1162 #[default]
1164 None,
1165
1166 BySeverity,
1168
1169 Highlight,
1171}
1172
1173impl LogTextColor {
1174 fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
1175 match self {
1176 LogTextColor::BySeverity => match severity {
1177 Severity::Fatal => {
1178 write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
1179 }
1180 Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
1181 Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
1182 Severity::Info => (),
1183 Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
1184 Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
1185 },
1186 LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
1187 LogTextColor::None => {}
1188 }
1189 Ok(())
1190 }
1191
1192 fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1193 if let LogTextColor::BySeverity = self {
1194 write!(f, "{}", color::Fg(color::Yellow))?;
1196 }
1197 Ok(())
1198 }
1199
1200 fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1201 match self {
1202 LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
1203 LogTextColor::None => {}
1204 };
1205 Ok(())
1206 }
1207}
1208
1209#[derive(Clone, Copy, Debug, PartialEq)]
1211pub enum Timezone {
1212 Local,
1214
1215 Utc,
1217}
1218
1219impl Timezone {
1220 fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
1221 const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
1222 match self {
1223 Timezone::Local => {
1224 Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1225 }
1226 Timezone::Utc => {
1227 Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1228 }
1229 }
1230 }
1231}
1232
1233#[derive(Clone, Copy, Debug, Default)]
1235pub enum LogTimeDisplayFormat {
1236 #[default]
1238 Original,
1239
1240 WallTime {
1242 tz: Timezone,
1244
1245 offset: i64,
1248 },
1249}
1250
1251impl LogTimeDisplayFormat {
1252 fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
1253 const NANOS_IN_SECOND: i64 = 1_000_000_000;
1254
1255 match self {
1256 Self::Original | Self::WallTime { offset: 0, .. } => {
1259 let time: Duration =
1260 Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
1261 write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
1262 }
1263 Self::WallTime { tz, offset } => {
1264 let adjusted = time.into_nanos() + offset;
1265 let seconds = adjusted / NANOS_IN_SECOND;
1266 let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
1267 let formatted = tz.format(seconds, rem_nanos);
1268 write!(f, "[{formatted}]")?;
1269 }
1270 }
1271 Ok(())
1272 }
1273}
1274
1275pub struct LogTextPresenter<'a> {
1277 log: &'a Data<Logs>,
1279
1280 options: LogTextDisplayOptions,
1282}
1283
1284impl<'a> LogTextPresenter<'a> {
1285 pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
1289 Self { log, options }
1290 }
1291}
1292
1293impl fmt::Display for Data<Logs> {
1294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1295 LogTextPresenter::new(self, Default::default()).fmt(f)
1296 }
1297}
1298
1299impl Deref for LogTextPresenter<'_> {
1300 type Target = Data<Logs>;
1301 fn deref(&self) -> &Self::Target {
1302 self.log
1303 }
1304}
1305
1306impl fmt::Display for LogTextPresenter<'_> {
1307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1308 self.options.color.begin_record(f, self.log.severity())?;
1309 self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1310
1311 if self.options.show_metadata {
1312 match self.pid() {
1313 Some(pid) => write!(f, "[{pid}]")?,
1314 None => write!(f, "[]")?,
1315 }
1316 match self.tid() {
1317 Some(tid) => write!(f, "[{tid}]")?,
1318 None => write!(f, "[]")?,
1319 }
1320 }
1321
1322 let moniker = if self.options.show_full_moniker {
1323 match &self.moniker {
1324 ExtendedMoniker::ComponentManager => {
1325 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1326 }
1327 ExtendedMoniker::ComponentInstance(instance) => {
1328 if instance.is_root() {
1329 Cow::Borrowed(ROOT_MONIKER_REPR)
1330 } else {
1331 Cow::Owned(instance.to_string())
1332 }
1333 }
1334 }
1335 } else {
1336 if self.options.prefer_url_component_name {
1337 self.component_name_by_url()
1338 } else {
1339 self.component_name()
1340 }
1341 };
1342 if self.options.show_moniker {
1343 write!(f, "[{moniker}]")?;
1344 }
1345
1346 if self.options.show_tags {
1347 match &self.metadata.tags {
1348 Some(tags) if !tags.is_empty() => {
1349 let mut filtered =
1350 tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
1351 if filtered.peek().is_some() {
1352 write!(f, "[{}]", filtered.join(","))?;
1353 }
1354 }
1355 _ => {}
1356 }
1357 }
1358
1359 write!(f, " {}:", self.metadata.severity)?;
1360
1361 if self.options.show_file {
1362 match (&self.metadata.file, &self.metadata.line) {
1363 (Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
1364 (Some(file), None) => write!(f, " [{file}]")?,
1365 _ => (),
1366 }
1367 }
1368
1369 if let Some(mut msg) = self.msg() {
1370 if let Some(nul) = msg.find("\0") {
1371 msg = &msg[0..nul];
1372 }
1373 write!(f, " {msg}")?;
1374 } else {
1375 write!(f, " <missing message>")?;
1376 }
1377 for kvp in self.payload_keys_strings() {
1378 write!(f, " {kvp}")?;
1379 }
1380
1381 let dropped = self.log.dropped_logs().unwrap_or_default();
1382 let rolled = self.log.rolled_out_logs().unwrap_or_default();
1383 if dropped != 0 || rolled != 0 {
1384 self.options.color.begin_lost_message_counts(f)?;
1385 if dropped != 0 {
1386 write!(f, " [dropped={dropped}]")?;
1387 }
1388 if rolled != 0 {
1389 write!(f, " [rolled={rolled}]")?;
1390 }
1391 }
1392
1393 self.options.color.end_record(f)?;
1394
1395 Ok(())
1396 }
1397}
1398
1399impl Eq for Data<Logs> {}
1400
1401impl PartialOrd for Data<Logs> {
1402 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1403 Some(self.cmp(other))
1404 }
1405}
1406
1407impl Ord for Data<Logs> {
1408 fn cmp(&self, other: &Self) -> Ordering {
1409 self.metadata.timestamp.cmp(&other.metadata.timestamp)
1410 }
1411}
1412
1413#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
1420pub enum LogsField {
1421 ProcessId,
1422 ThreadId,
1423 Dropped,
1424 Tag,
1425 Msg,
1426 MsgStructured,
1427 FilePath,
1428 LineNumber,
1429 Other(String),
1430}
1431
1432impl fmt::Display for LogsField {
1433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1434 match self {
1435 LogsField::ProcessId => write!(f, "pid"),
1436 LogsField::ThreadId => write!(f, "tid"),
1437 LogsField::Dropped => write!(f, "num_dropped"),
1438 LogsField::Tag => write!(f, "tag"),
1439 LogsField::Msg => write!(f, "message"),
1440 LogsField::MsgStructured => write!(f, "value"),
1441 LogsField::FilePath => write!(f, "file_path"),
1442 LogsField::LineNumber => write!(f, "line_number"),
1443 LogsField::Other(name) => write!(f, "{name}"),
1444 }
1445 }
1446}
1447
1448pub const PID_LABEL: &str = "pid";
1452pub const TID_LABEL: &str = "tid";
1454pub const DROPPED_LABEL: &str = "num_dropped";
1456pub const TAG_LABEL: &str = "tag";
1458pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
1460pub const MESSAGE_LABEL: &str = "message";
1462pub const FILE_PATH_LABEL: &str = "file";
1464pub const LINE_NUMBER_LABEL: &str = "line";
1466
1467impl AsRef<str> for LogsField {
1468 fn as_ref(&self) -> &str {
1469 match self {
1470 Self::ProcessId => PID_LABEL,
1471 Self::ThreadId => TID_LABEL,
1472 Self::Dropped => DROPPED_LABEL,
1473 Self::Tag => TAG_LABEL,
1474 Self::Msg => MESSAGE_LABEL,
1475 Self::FilePath => FILE_PATH_LABEL,
1476 Self::LineNumber => LINE_NUMBER_LABEL,
1477 Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
1478 Self::Other(str) => str.as_str(),
1479 }
1480 }
1481}
1482
1483impl<T> From<T> for LogsField
1484where
1485 T: Deref<Target = str>,
1487{
1488 fn from(s: T) -> Self {
1489 match s.as_ref() {
1490 PID_LABEL => Self::ProcessId,
1491 TID_LABEL => Self::ThreadId,
1492 DROPPED_LABEL => Self::Dropped,
1493 TAG_LABEL => Self::Tag,
1494 MESSAGE_LABEL => Self::Msg,
1495 FILE_PATH_LABEL => Self::FilePath,
1496 LINE_NUMBER_LABEL => Self::LineNumber,
1497 MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
1498 _ => Self::Other(s.to_string()),
1499 }
1500 }
1501}
1502
1503impl FromStr for LogsField {
1504 type Err = ();
1505 fn from_str(s: &str) -> Result<Self, Self::Err> {
1506 Ok(Self::from(s))
1507 }
1508}
1509
1510#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
1513#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
1514pub enum LogError {
1515 #[serde(rename = "dropped_logs")]
1518 DroppedLogs { count: u64 },
1519 #[serde(rename = "rolled_out_logs")]
1522 RolledOutLogs { count: u64 },
1523 #[serde(rename = "parse_record")]
1524 FailedToParseRecord(String),
1525 #[serde(rename = "other")]
1526 Other { message: String },
1527}
1528
1529const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
1530
1531impl MetadataError for LogError {
1532 fn dropped_payload() -> Self {
1533 Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
1534 }
1535
1536 fn message(&self) -> Option<&str> {
1537 match self {
1538 Self::FailedToParseRecord(msg) => Some(msg.as_str()),
1539 Self::Other { message } => Some(message.as_str()),
1540 _ => None,
1541 }
1542 }
1543}
1544
1545#[derive(Debug, PartialEq, Clone, Eq)]
1548pub struct InspectError {
1549 pub message: String,
1550}
1551
1552impl MetadataError for InspectError {
1553 fn dropped_payload() -> Self {
1554 Self { message: "Schema failed to fit component budget.".into() }
1555 }
1556
1557 fn message(&self) -> Option<&str> {
1558 Some(self.message.as_str())
1559 }
1560}
1561
1562impl fmt::Display for InspectError {
1563 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1564 write!(f, "{}", self.message)
1565 }
1566}
1567
1568impl Borrow<str> for InspectError {
1569 fn borrow(&self) -> &str {
1570 &self.message
1571 }
1572}
1573
1574impl Serialize for InspectError {
1575 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
1576 self.message.serialize(ser)
1577 }
1578}
1579
1580impl<'de> Deserialize<'de> for InspectError {
1581 fn deserialize<D>(de: D) -> Result<Self, D::Error>
1582 where
1583 D: Deserializer<'de>,
1584 {
1585 let message = String::deserialize(de)?;
1586 Ok(Self { message })
1587 }
1588}
1589
1590#[cfg(test)]
1591mod tests {
1592 use super::*;
1593 use diagnostics_hierarchy::hierarchy;
1594 use selectors::FastError;
1595 use serde_json::json;
1596 use test_case::test_case;
1597
1598 const TEST_URL: &str = "fuchsia-pkg://test";
1599
1600 #[fuchsia::test]
1601 fn test_canonical_json_inspect_formatting() {
1602 let mut hierarchy = hierarchy! {
1603 root: {
1604 x: "foo",
1605 }
1606 };
1607
1608 hierarchy.sort();
1609 let json_schema = InspectDataBuilder::new(
1610 "a/b/c/d".try_into().unwrap(),
1611 TEST_URL,
1612 Timestamp::from_nanos(123456i64),
1613 )
1614 .with_hierarchy(hierarchy)
1615 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1616 .build();
1617
1618 let result_json =
1619 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1620
1621 let expected_json = json!({
1622 "moniker": "a/b/c/d",
1623 "version": 1,
1624 "data_source": "Inspect",
1625 "payload": {
1626 "root": {
1627 "x": "foo"
1628 }
1629 },
1630 "metadata": {
1631 "component_url": TEST_URL,
1632 "filename": "test_file_plz_ignore.inspect",
1633 "timestamp": 123456,
1634 }
1635 });
1636
1637 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1638 }
1639
1640 #[fuchsia::test]
1641 fn test_errorful_json_inspect_formatting() {
1642 let json_schema = InspectDataBuilder::new(
1643 "a/b/c/d".try_into().unwrap(),
1644 TEST_URL,
1645 Timestamp::from_nanos(123456i64),
1646 )
1647 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1648 .with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
1649 .build();
1650
1651 let result_json =
1652 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1653
1654 let expected_json = json!({
1655 "moniker": "a/b/c/d",
1656 "version": 1,
1657 "data_source": "Inspect",
1658 "payload": null,
1659 "metadata": {
1660 "component_url": TEST_URL,
1661 "errors": ["too much fun being had."],
1662 "filename": "test_file_plz_ignore.inspect",
1663 "timestamp": 123456,
1664 }
1665 });
1666
1667 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1668 }
1669
1670 fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
1671 strings
1672 .iter()
1673 .map(|s| match selectors::parse_selector::<FastError>(s) {
1674 Ok(selector) => selector,
1675 Err(e) => panic!("Couldn't parse selector {s}: {e}"),
1676 })
1677 .collect::<Vec<_>>()
1678 }
1679
1680 #[fuchsia::test]
1681 fn test_filter_returns_none_on_empty_hierarchy() {
1682 let data = InspectDataBuilder::new(
1683 "a/b/c/d".try_into().unwrap(),
1684 TEST_URL,
1685 Timestamp::from_nanos(123456i64),
1686 )
1687 .build();
1688 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1689 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1690 }
1691
1692 #[fuchsia::test]
1693 fn test_filter_returns_none_on_selector_mismatch() {
1694 let mut hierarchy = hierarchy! {
1695 root: {
1696 x: "foo",
1697 }
1698 };
1699 hierarchy.sort();
1700 let data = InspectDataBuilder::new(
1701 "b/c/d/e".try_into().unwrap(),
1702 TEST_URL,
1703 Timestamp::from_nanos(123456i64),
1704 )
1705 .with_hierarchy(hierarchy)
1706 .build();
1707 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1708 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1709 }
1710
1711 #[fuchsia::test]
1712 fn test_filter_returns_none_on_data_mismatch() {
1713 let mut hierarchy = hierarchy! {
1714 root: {
1715 x: "foo",
1716 }
1717 };
1718 hierarchy.sort();
1719 let data = InspectDataBuilder::new(
1720 "a/b/c/d".try_into().unwrap(),
1721 TEST_URL,
1722 Timestamp::from_nanos(123456i64),
1723 )
1724 .with_hierarchy(hierarchy)
1725 .build();
1726 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1727
1728 assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
1729 }
1730
1731 #[fuchsia::test]
1732 fn test_filter_returns_matching_data() {
1733 let mut hierarchy = hierarchy! {
1734 root: {
1735 x: "foo",
1736 y: "bar",
1737 }
1738 };
1739 hierarchy.sort();
1740 let data = InspectDataBuilder::new(
1741 "a/b/c/d".try_into().unwrap(),
1742 TEST_URL,
1743 Timestamp::from_nanos(123456i64),
1744 )
1745 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1746 .with_hierarchy(hierarchy)
1747 .build();
1748 let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
1749
1750 let expected_json = json!({
1751 "moniker": "a/b/c/d",
1752 "version": 1,
1753 "data_source": "Inspect",
1754 "payload": {
1755 "root": {
1756 "x": "foo"
1757 }
1758 },
1759 "metadata": {
1760 "component_url": TEST_URL,
1761 "filename": "test_file_plz_ignore.inspect",
1762 "timestamp": 123456,
1763 }
1764 });
1765
1766 let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
1767 .expect("serialization should succeed.");
1768
1769 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1770 }
1771
1772 #[fuchsia::test]
1773 fn default_builder_test() {
1774 let builder = LogsDataBuilder::new(BuilderArgs {
1775 component_url: Some("url".into()),
1776 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1777 severity: Severity::Info,
1778 timestamp: Timestamp::from_nanos(0),
1779 });
1780 let expected_json = json!({
1782 "moniker": "moniker",
1783 "version": 1,
1784 "data_source": "Logs",
1785 "payload": {
1786 "root":
1787 {
1788 "message":{}
1789 }
1790 },
1791 "metadata": {
1792 "component_url": "url",
1793 "severity": "INFO",
1794 "tags": [],
1795
1796 "timestamp": 0,
1797 }
1798 });
1799 let result_json =
1800 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1801 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1802 }
1803
1804 #[fuchsia::test]
1805 fn regular_message_test() {
1806 let builder = LogsDataBuilder::new(BuilderArgs {
1807 component_url: Some("url".into()),
1808 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1809 severity: Severity::Info,
1810 timestamp: Timestamp::from_nanos(0),
1811 })
1812 .set_message("app")
1813 .set_file("test file.cc")
1814 .set_line(420)
1815 .set_pid(1001)
1816 .set_tid(200)
1817 .set_dropped(2)
1818 .add_tag("You're")
1819 .add_tag("IT!")
1820 .add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
1821 let expected_json = json!({
1823 "moniker": "moniker",
1824 "version": 1,
1825 "data_source": "Logs",
1826 "payload": {
1827 "root":
1828 {
1829 "keys":{
1830 "key":"value"
1831 },
1832 "message":{
1833 "value":"app"
1834 }
1835 }
1836 },
1837 "metadata": {
1838 "errors": [],
1839 "component_url": "url",
1840 "errors": [{"dropped_logs":{"count":2}}],
1841 "file": "test file.cc",
1842 "line": 420,
1843 "pid": 1001,
1844 "severity": "INFO",
1845 "tags": ["You're", "IT!"],
1846 "tid": 200,
1847
1848 "timestamp": 0,
1849 }
1850 });
1851 let result_json =
1852 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1853 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1854 }
1855
1856 #[fuchsia::test]
1857 fn display_for_logs() {
1858 let data = LogsDataBuilder::new(BuilderArgs {
1859 timestamp: Timestamp::from_nanos(12345678000i64),
1860 component_url: Some(FlyStr::from("fake-url")),
1861 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1862 severity: Severity::Info,
1863 })
1864 .set_pid(123)
1865 .set_tid(456)
1866 .set_message("some message".to_string())
1867 .set_file("some_file.cc".to_string())
1868 .set_line(420)
1869 .add_tag("foo")
1870 .add_tag("bar")
1871 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1872 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1873 .build();
1874
1875 assert_eq!(
1876 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1877 format!("{data}")
1878 )
1879 }
1880
1881 #[fuchsia::test]
1882 fn display_for_logs_with_duplicate_moniker() {
1883 let data = LogsDataBuilder::new(BuilderArgs {
1884 timestamp: Timestamp::from_nanos(12345678000i64),
1885 component_url: Some(FlyStr::from("fake-url")),
1886 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1887 severity: Severity::Info,
1888 })
1889 .set_pid(123)
1890 .set_tid(456)
1891 .set_message("some message".to_string())
1892 .set_file("some_file.cc".to_string())
1893 .set_line(420)
1894 .add_tag("moniker")
1895 .add_tag("bar")
1896 .add_tag("moniker")
1897 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1898 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1899 .build();
1900
1901 assert_eq!(
1902 "[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
1903 format!("{data}")
1904 )
1905 }
1906
1907 #[fuchsia::test]
1908 fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
1909 let data = LogsDataBuilder::new(BuilderArgs {
1910 timestamp: Timestamp::from_nanos(12345678000i64),
1911 component_url: Some(FlyStr::from("fake-url")),
1912 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1913 severity: Severity::Info,
1914 })
1915 .set_pid(123)
1916 .set_tid(456)
1917 .set_message("some message".to_string())
1918 .set_file("some_file.cc".to_string())
1919 .set_line(420)
1920 .add_tag("moniker")
1921 .add_tag("moniker")
1922 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1923 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1924 .build();
1925
1926 assert_eq!(
1927 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1928 format!("{data}")
1929 )
1930 }
1931
1932 #[fuchsia::test]
1933 fn test_component_name_by_url() {
1934 let data = LogsDataBuilder::new(BuilderArgs {
1935 timestamp: Timestamp::from_nanos(0),
1936 component_url: Some(FlyStr::from(
1937 "fuchsia-pkg://fuchsia.com/my-pkg#meta/my-component.cm",
1938 )),
1939 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1940 severity: Severity::Info,
1941 })
1942 .build();
1943 assert_eq!(data.component_name_by_url(), "my-component");
1944
1945 let data = LogsDataBuilder::new(BuilderArgs {
1946 timestamp: Timestamp::from_nanos(0),
1947 component_url: Some(FlyStr::from("fuchsia-pkg://fuchsia.com/my-pkg#meta/my-component")),
1948 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1949 severity: Severity::Info,
1950 })
1951 .build();
1952 assert_eq!(data.component_name_by_url(), "my-component");
1953
1954 let data = LogsDataBuilder::new(BuilderArgs {
1955 timestamp: Timestamp::from_nanos(0),
1956 component_url: Some(FlyStr::from("")),
1957 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1958 severity: Severity::Info,
1959 })
1960 .build();
1961 assert_eq!(data.component_name_by_url(), "moniker");
1962
1963 let data = LogsDataBuilder::new(BuilderArgs {
1964 timestamp: Timestamp::from_nanos(0),
1965 component_url: None,
1966 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1967 severity: Severity::Info,
1968 })
1969 .build();
1970 assert_eq!(data.component_name_by_url(), "moniker");
1971 }
1972
1973 #[fuchsia::test]
1974 fn display_for_logs_partial_moniker() {
1975 let data = LogsDataBuilder::new(BuilderArgs {
1976 timestamp: Timestamp::from_nanos(12345678000i64),
1977 component_url: Some(FlyStr::from("fake-url")),
1978 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1979 severity: Severity::Info,
1980 })
1981 .set_pid(123)
1982 .set_tid(456)
1983 .set_message("some message".to_string())
1984 .set_file("some_file.cc".to_string())
1985 .set_line(420)
1986 .add_tag("foo")
1987 .add_tag("bar")
1988 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1989 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1990 .build();
1991
1992 assert_eq!(
1993 "[00012.345678][123][456][fake-url][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1994 format!(
1995 "{}",
1996 LogTextPresenter::new(
1997 &data,
1998 LogTextDisplayOptions {
1999 show_full_moniker: false,
2000 prefer_url_component_name: true,
2001 ..Default::default()
2002 }
2003 )
2004 )
2005 )
2006 }
2007
2008 #[fuchsia::test]
2009 fn display_for_logs_exclude_metadata() {
2010 let data = LogsDataBuilder::new(BuilderArgs {
2011 timestamp: Timestamp::from_nanos(12345678000i64),
2012 component_url: Some(FlyStr::from("fake-url")),
2013 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2014 severity: Severity::Info,
2015 })
2016 .set_pid(123)
2017 .set_tid(456)
2018 .set_message("some message".to_string())
2019 .set_file("some_file.cc".to_string())
2020 .set_line(420)
2021 .add_tag("foo")
2022 .add_tag("bar")
2023 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2024 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2025 .build();
2026
2027 assert_eq!(
2028 "[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2029 format!(
2030 "{}",
2031 LogTextPresenter::new(
2032 &data,
2033 LogTextDisplayOptions { show_metadata: false, ..Default::default() }
2034 )
2035 )
2036 )
2037 }
2038
2039 #[fuchsia::test]
2040 fn display_for_logs_exclude_tags() {
2041 let data = LogsDataBuilder::new(BuilderArgs {
2042 timestamp: Timestamp::from_nanos(12345678000i64),
2043 component_url: Some(FlyStr::from("fake-url")),
2044 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2045 severity: Severity::Info,
2046 })
2047 .set_pid(123)
2048 .set_tid(456)
2049 .set_message("some message".to_string())
2050 .set_file("some_file.cc".to_string())
2051 .set_line(420)
2052 .add_tag("foo")
2053 .add_tag("bar")
2054 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2055 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2056 .build();
2057
2058 assert_eq!(
2059 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
2060 format!(
2061 "{}",
2062 LogTextPresenter::new(
2063 &data,
2064 LogTextDisplayOptions { show_tags: false, ..Default::default() }
2065 )
2066 )
2067 )
2068 }
2069
2070 #[fuchsia::test]
2071 fn display_for_logs_exclude_file() {
2072 let data = LogsDataBuilder::new(BuilderArgs {
2073 timestamp: Timestamp::from_nanos(12345678000i64),
2074 component_url: Some(FlyStr::from("fake-url")),
2075 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2076 severity: Severity::Info,
2077 })
2078 .set_pid(123)
2079 .set_tid(456)
2080 .set_message("some message".to_string())
2081 .set_file("some_file.cc".to_string())
2082 .set_line(420)
2083 .add_tag("foo")
2084 .add_tag("bar")
2085 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2086 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2087 .build();
2088
2089 assert_eq!(
2090 "[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
2091 format!(
2092 "{}",
2093 LogTextPresenter::new(
2094 &data,
2095 LogTextDisplayOptions { show_file: false, ..Default::default() }
2096 )
2097 )
2098 )
2099 }
2100
2101 #[fuchsia::test]
2102 fn display_for_logs_include_color_by_severity() {
2103 let data = LogsDataBuilder::new(BuilderArgs {
2104 timestamp: Timestamp::from_nanos(12345678000i64),
2105 component_url: Some(FlyStr::from("fake-url")),
2106 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2107 severity: Severity::Error,
2108 })
2109 .set_pid(123)
2110 .set_tid(456)
2111 .set_message("some message".to_string())
2112 .set_file("some_file.cc".to_string())
2113 .set_line(420)
2114 .add_tag("foo")
2115 .add_tag("bar")
2116 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2117 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2118 .build();
2119
2120 assert_eq!(
2121 format!(
2122 "{}[00012.345678][123][456][moniker][foo,bar] ERROR: [some_file.cc(420)] some message test=property value=test{}",
2123 color::Fg(color::Red),
2124 style::Reset
2125 ),
2126 format!(
2127 "{}",
2128 LogTextPresenter::new(
2129 &data,
2130 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2131 )
2132 )
2133 )
2134 }
2135
2136 #[fuchsia::test]
2137 fn display_for_logs_highlight_line() {
2138 let data = LogsDataBuilder::new(BuilderArgs {
2139 timestamp: Timestamp::from_nanos(12345678000i64),
2140 component_url: Some(FlyStr::from("fake-url")),
2141 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2142 severity: Severity::Info,
2143 })
2144 .set_pid(123)
2145 .set_tid(456)
2146 .set_message("some message".to_string())
2147 .set_file("some_file.cc".to_string())
2148 .set_line(420)
2149 .add_tag("foo")
2150 .add_tag("bar")
2151 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2152 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2153 .build();
2154
2155 assert_eq!(
2156 format!(
2157 "{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}",
2158 color::Fg(color::LightYellow),
2159 style::Reset
2160 ),
2161 LogTextPresenter::new(
2162 &data,
2163 LogTextDisplayOptions { color: LogTextColor::Highlight, ..Default::default() }
2164 )
2165 .to_string()
2166 )
2167 }
2168
2169 #[fuchsia::test]
2170 fn display_for_logs_with_wall_time() {
2171 let data = LogsDataBuilder::new(BuilderArgs {
2172 timestamp: Timestamp::from_nanos(12345678000i64),
2173 component_url: Some(FlyStr::from("fake-url")),
2174 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2175 severity: Severity::Info,
2176 })
2177 .set_pid(123)
2178 .set_tid(456)
2179 .set_message("some message".to_string())
2180 .set_file("some_file.cc".to_string())
2181 .set_line(420)
2182 .add_tag("foo")
2183 .add_tag("bar")
2184 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2185 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2186 .build();
2187
2188 assert_eq!(
2189 "[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2190 LogTextPresenter::new(
2191 &data,
2192 LogTextDisplayOptions {
2193 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
2194 ..Default::default()
2195 }
2196 )
2197 .to_string()
2198 );
2199
2200 assert_eq!(
2201 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2202 LogTextPresenter::new(
2203 &data,
2204 LogTextDisplayOptions {
2205 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
2206 ..Default::default()
2207 }
2208 )
2209 .to_string(),
2210 "should fall back to monotonic if offset is 0"
2211 );
2212 }
2213
2214 #[fuchsia::test]
2215 fn display_for_logs_with_dropped_count() {
2216 let data = LogsDataBuilder::new(BuilderArgs {
2217 timestamp: Timestamp::from_nanos(12345678000i64),
2218 component_url: Some(FlyStr::from("fake-url")),
2219 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2220 severity: Severity::Info,
2221 })
2222 .set_dropped(5)
2223 .set_pid(123)
2224 .set_tid(456)
2225 .set_message("some message".to_string())
2226 .set_file("some_file.cc".to_string())
2227 .set_line(420)
2228 .add_tag("foo")
2229 .add_tag("bar")
2230 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2231 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2232 .build();
2233
2234 assert_eq!(
2235 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
2236 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2237 );
2238
2239 assert_eq!(
2240 format!(
2241 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}",
2242 color::Fg(color::Yellow),
2243 style::Reset
2244 ),
2245 LogTextPresenter::new(
2246 &data,
2247 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2248 )
2249 .to_string()
2250 );
2251 }
2252
2253 #[fuchsia::test]
2254 fn display_for_logs_with_rolled_count() {
2255 let data = LogsDataBuilder::new(BuilderArgs {
2256 timestamp: Timestamp::from_nanos(12345678000i64),
2257 component_url: Some(FlyStr::from("fake-url")),
2258 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2259 severity: Severity::Info,
2260 })
2261 .set_rolled_out(10)
2262 .set_pid(123)
2263 .set_tid(456)
2264 .set_message("some message".to_string())
2265 .set_file("some_file.cc".to_string())
2266 .set_line(420)
2267 .add_tag("foo")
2268 .add_tag("bar")
2269 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2270 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2271 .build();
2272
2273 assert_eq!(
2274 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
2275 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2276 );
2277
2278 assert_eq!(
2279 format!(
2280 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}",
2281 color::Fg(color::Yellow),
2282 style::Reset
2283 ),
2284 LogTextPresenter::new(
2285 &data,
2286 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2287 )
2288 .to_string()
2289 );
2290 }
2291
2292 #[fuchsia::test]
2293 fn display_for_logs_with_dropped_and_rolled_counts() {
2294 let data = LogsDataBuilder::new(BuilderArgs {
2295 timestamp: Timestamp::from_nanos(12345678000i64),
2296 component_url: Some(FlyStr::from("fake-url")),
2297 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2298 severity: Severity::Info,
2299 })
2300 .set_dropped(5)
2301 .set_rolled_out(10)
2302 .set_pid(123)
2303 .set_tid(456)
2304 .set_message("some message".to_string())
2305 .set_file("some_file.cc".to_string())
2306 .set_line(420)
2307 .add_tag("foo")
2308 .add_tag("bar")
2309 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2310 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2311 .build();
2312
2313 assert_eq!(
2314 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
2315 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2316 );
2317
2318 assert_eq!(
2319 format!(
2320 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}",
2321 color::Fg(color::Yellow),
2322 style::Reset
2323 ),
2324 LogTextPresenter::new(
2325 &data,
2326 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2327 )
2328 .to_string()
2329 );
2330 }
2331
2332 #[fuchsia::test]
2333 fn display_for_logs_no_tags() {
2334 let data = LogsDataBuilder::new(BuilderArgs {
2335 timestamp: Timestamp::from_nanos(12345678000i64),
2336 component_url: Some(FlyStr::from("fake-url")),
2337 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2338 severity: Severity::Info,
2339 })
2340 .set_pid(123)
2341 .set_tid(456)
2342 .set_message("some message".to_string())
2343 .build();
2344
2345 assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{data}"))
2346 }
2347
2348 #[fuchsia::test]
2349 fn size_bytes_deserialize_backwards_compatibility() {
2350 let original_json = json!({
2351 "moniker": "a/b",
2352 "version": 1,
2353 "data_source": "Logs",
2354 "payload": {
2355 "root": {
2356 "message":{}
2357 }
2358 },
2359 "metadata": {
2360 "component_url": "url",
2361 "severity": "INFO",
2362 "tags": [],
2363
2364 "timestamp": 123,
2365 }
2366 });
2367 let expected_data = LogsDataBuilder::new(BuilderArgs {
2368 component_url: Some("url".into()),
2369 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2370 severity: Severity::Info,
2371 timestamp: Timestamp::from_nanos(123),
2372 })
2373 .build();
2374 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2375 assert_eq!(original_data, expected_data);
2376 assert_eq!(original_data.metadata.size_bytes, None);
2378 }
2379
2380 #[fuchsia::test]
2381 fn display_for_logs_with_null_terminator() {
2382 let data = LogsDataBuilder::new(BuilderArgs {
2383 timestamp: Timestamp::from_nanos(12345678000i64),
2384 component_url: Some(FlyStr::from("fake-url")),
2385 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2386 severity: Severity::Info,
2387 })
2388 .set_pid(123)
2389 .set_tid(456)
2390 .set_message("some message\0garbage".to_string())
2391 .set_file("some_file.cc".to_string())
2392 .set_line(420)
2393 .add_tag("foo")
2394 .add_tag("bar")
2395 .build();
2396
2397 assert_eq!(
2398 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message",
2399 format!("{data}")
2400 )
2401 }
2402
2403 #[fuchsia::test]
2404 fn dropped_deserialize_backwards_compatibility() {
2405 let original_json = json!({
2406 "moniker": "a/b",
2407 "version": 1,
2408 "data_source": "Logs",
2409 "payload": {
2410 "root": {
2411 "message":{}
2412 }
2413 },
2414 "metadata": {
2415 "dropped": 0,
2416 "component_url": "url",
2417 "severity": "INFO",
2418 "tags": [],
2419
2420 "timestamp": 123,
2421 }
2422 });
2423 let expected_data = LogsDataBuilder::new(BuilderArgs {
2424 component_url: Some("url".into()),
2425 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2426 severity: Severity::Info,
2427 timestamp: Timestamp::from_nanos(123),
2428 })
2429 .build();
2430 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2431 assert_eq!(original_data, expected_data);
2432 assert_eq!(original_data.metadata.dropped, None);
2434 }
2435
2436 #[fuchsia::test]
2437 fn severity_aliases() {
2438 assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
2439 assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
2440 }
2441
2442 #[fuchsia::test]
2443 fn test_metadata_merge() {
2444 let mut meta = InspectMetadata {
2445 errors: Some(vec![InspectError { message: "error1".to_string() }]),
2446 name: InspectHandleName::name("test"),
2447 component_url: "fuchsia-pkg://test".into(),
2448 timestamp: Timestamp::from_nanos(100),
2449 escrowed: false,
2450 };
2451
2452 meta.merge(InspectMetadata {
2453 errors: Some(vec![InspectError { message: "error2".to_string() }]),
2454 name: InspectHandleName::name("test"),
2455 component_url: "fuchsia-pkg://test".into(),
2456 timestamp: Timestamp::from_nanos(200),
2457 escrowed: false,
2458 });
2459
2460 assert_eq!(
2461 meta,
2462 InspectMetadata {
2463 errors: Some(vec![
2464 InspectError { message: "error1".to_string() },
2465 InspectError { message: "error2".to_string() },
2466 ]),
2467 name: InspectHandleName::name("test"),
2468 component_url: "fuchsia-pkg://test".into(),
2469 timestamp: Timestamp::from_nanos(200),
2470 escrowed: false,
2471 }
2472 );
2473 }
2474
2475 #[fuchsia::test]
2476 fn test_metadata_merge_older_timestamp_noop() {
2477 let mut meta = InspectMetadata {
2478 errors: None,
2479 name: InspectHandleName::name("test"),
2480 component_url: TEST_URL.into(),
2481 timestamp: Timestamp::from_nanos(200),
2482 escrowed: false,
2483 };
2484 meta.merge(InspectMetadata {
2485 errors: None,
2486 name: InspectHandleName::name("test"),
2487 component_url: TEST_URL.into(),
2488 timestamp: Timestamp::from_nanos(100),
2489 escrowed: false,
2490 });
2491 assert_eq!(
2492 meta,
2493 InspectMetadata {
2494 errors: None,
2495 name: InspectHandleName::name("test"),
2496 component_url: TEST_URL.into(),
2497 timestamp: Timestamp::from_nanos(200),
2498 escrowed: false,
2499 }
2500 );
2501 }
2502
2503 fn new_test_data(moniker: &str, payload_val: Option<&str>, timestamp: i64) -> InspectData {
2504 let mut builder = InspectDataBuilder::new(
2505 moniker.try_into().unwrap(),
2506 TEST_URL,
2507 Timestamp::from_nanos(timestamp),
2508 );
2509 if let Some(val) = payload_val {
2510 builder = builder.with_hierarchy(hierarchy! { root: { "key": val } });
2511 }
2512 builder.build()
2513 }
2514
2515 #[fuchsia::test]
2516 fn test_data_merge() {
2517 let mut data = new_test_data("a/b/c", Some("val1"), 100);
2518 let mut other = new_test_data("a/b/c", Some("val2"), 200);
2519 other.metadata.errors = Some(vec![InspectError { message: "error".into() }]);
2520
2521 data.merge(other);
2522
2523 let expected_payload = hierarchy! { root: { "key": "val2" } };
2524 assert_eq!(data.payload, Some(expected_payload));
2525 assert_eq!(data.metadata.timestamp, Timestamp::from_nanos(200));
2526 assert_eq!(data.metadata.errors, Some(vec![InspectError { message: "error".into() }]));
2527 }
2528
2529 #[test_case(new_test_data("a/b/d", Some("v2"), 100); "different moniker")]
2530 #[test_case(
2531 {
2532 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2533 d.version = 2;
2534 d
2535 }; "different version")]
2536 #[test_case(
2537 {
2538 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2539 d.data_source = DataSource::Logs;
2540 d
2541 }; "different data source")]
2542 #[fuchsia::test]
2543 fn test_data_merge_noop(other: InspectData) {
2544 let mut data = new_test_data("a/b/c", Some("v1"), 100);
2545 let original = data.clone();
2546 data.merge(other);
2547 assert_eq!(data, original);
2548 }
2549
2550 #[test_case(None, Some("val2"), Some("val2") ; "none_with_some")]
2551 #[test_case(Some("val1"), None, Some("val1") ; "some_with_none")]
2552 #[test_case(Some("val1"), Some("val2"), Some("val2") ; "some_with_some")]
2553 #[fuchsia::test]
2554 fn test_data_merge_payloads(
2555 payload: Option<&str>,
2556 other_payload: Option<&str>,
2557 expected: Option<&str>,
2558 ) {
2559 let mut data = new_test_data("a/b/c", payload, 100);
2560 let other = new_test_data("a/b/c", other_payload, 100);
2561
2562 data.merge(other);
2563 assert_eq!(data, new_test_data("a/b/c", expected, 100));
2564 }
2565}