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