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