1use chrono::{Local, TimeZone, Utc};
11use diagnostics_hierarchy::HierarchyMatcher;
12use fidl_fuchsia_diagnostics::{DataType, Selector};
13use fidl_fuchsia_inspect 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::{hierarchy, DiagnosticsHierarchy, Property};
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 {
83 Some(n.as_str())
84 } else {
85 None
86 }
87 }
88
89 pub fn as_filename(&self) -> Option<&str> {
91 if let Self::Filename(f) = self {
92 Some(f.as_str())
93 } else {
94 None
95 }
96 }
97}
98
99impl AsRef<str> for InspectHandleName {
100 fn as_ref(&self) -> &str {
101 match self {
102 Self::Filename(f) => f.as_str(),
103 Self::Name(n) => n.as_str(),
104 }
105 }
106}
107
108#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
110#[derive(Default, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
111pub enum DataSource {
112 #[default]
113 Unknown,
114 Inspect,
115 Logs,
116}
117
118pub trait MetadataError {
119 fn dropped_payload() -> Self;
120 fn message(&self) -> Option<&str>;
121}
122
123pub trait Metadata: DeserializeOwned + Serialize + Clone + Send {
124 type Error: Clone + MetadataError;
126
127 fn timestamp(&self) -> Timestamp;
129
130 fn errors(&self) -> Option<&[Self::Error]>;
132
133 fn set_errors(&mut self, errors: Vec<Self::Error>);
135
136 fn has_errors(&self) -> bool {
138 self.errors().map(|e| !e.is_empty()).unwrap_or_default()
139 }
140}
141
142pub trait DiagnosticsData {
144 type Metadata: Metadata;
146
147 type Key: AsRef<str> + Clone + DeserializeOwned + Eq + FromStr + Hash + Send + 'static;
149
150 const DATA_TYPE: DataType;
152}
153
154#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
156pub struct Inspect;
157
158impl DiagnosticsData for Inspect {
159 type Metadata = InspectMetadata;
160 type Key = String;
161 const DATA_TYPE: DataType = DataType::Inspect;
162}
163
164impl Metadata for InspectMetadata {
165 type Error = InspectError;
166
167 fn timestamp(&self) -> Timestamp {
168 self.timestamp
169 }
170
171 fn errors(&self) -> Option<&[Self::Error]> {
172 self.errors.as_deref()
173 }
174
175 fn set_errors(&mut self, errors: Vec<Self::Error>) {
176 self.errors = Some(errors);
177 }
178}
179
180#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
182pub struct Logs;
183
184impl DiagnosticsData for Logs {
185 type Metadata = LogsMetadata;
186 type Key = LogsField;
187 const DATA_TYPE: DataType = DataType::Logs;
188}
189
190impl Metadata for LogsMetadata {
191 type Error = LogError;
192
193 fn timestamp(&self) -> Timestamp {
194 self.timestamp
195 }
196
197 fn errors(&self) -> Option<&[Self::Error]> {
198 self.errors.as_deref()
199 }
200
201 fn set_errors(&mut self, errors: Vec<Self::Error>) {
202 self.errors = Some(errors);
203 }
204}
205
206pub fn serialize_timestamp<S>(timestamp: &Timestamp, serializer: S) -> Result<S::Ok, S::Error>
207where
208 S: Serializer,
209{
210 serializer.serialize_i64(timestamp.into_nanos())
211}
212
213pub fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<Timestamp, D::Error>
214where
215 D: Deserializer<'de>,
216{
217 let nanos = i64::deserialize(deserializer)?;
218 Ok(Timestamp::from_nanos(nanos))
219}
220
221#[cfg(target_os = "fuchsia")]
222mod zircon {
223 pub type Timestamp = zx::BootInstant;
224
225 pub fn unapply_mono_to_boot_offset(timestamp: Timestamp) -> zx::MonotonicInstant {
230 let mono_now = zx::MonotonicInstant::get();
231 let boot_now = zx::BootInstant::get();
232
233 let mono_to_boot_offset_nanos = boot_now.into_nanos() - mono_now.into_nanos();
234 zx::MonotonicInstant::from_nanos(timestamp.into_nanos() - mono_to_boot_offset_nanos)
235 }
236}
237
238#[cfg(target_os = "fuchsia")]
239pub use zircon::Timestamp;
240#[cfg(target_os = "fuchsia")]
241pub use zircon::unapply_mono_to_boot_offset;
242
243#[cfg(not(target_os = "fuchsia"))]
244mod host {
245 use serde::{Deserialize, Serialize};
246 use std::fmt;
247 use std::ops::Add;
248 use std::time::Duration;
249
250 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
251 pub struct Timestamp(i64);
252
253 impl Timestamp {
254 pub fn into_nanos(self) -> i64 {
256 self.0
257 }
258
259 pub fn from_nanos(nanos: i64) -> Self {
261 Self(nanos)
262 }
263 }
264
265 impl Add<Duration> for Timestamp {
266 type Output = Timestamp;
267 fn add(self, rhs: Duration) -> Self::Output {
268 Timestamp(self.0 + rhs.as_nanos() as i64)
269 }
270 }
271
272 impl fmt::Display for Timestamp {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 write!(f, "{}", self.0)
275 }
276 }
277}
278
279#[cfg(not(target_os = "fuchsia"))]
280pub use host::Timestamp;
281
282#[cfg(feature = "json_schema")]
283impl JsonSchema for Timestamp {
284 fn schema_name() -> String {
285 "integer".to_owned()
286 }
287
288 fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
289 i64::json_schema(generator)
290 }
291}
292
293#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
296pub struct InspectMetadata {
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub errors: Option<Vec<InspectError>>,
300
301 #[serde(flatten)]
303 pub name: InspectHandleName,
304
305 pub component_url: FlyStr,
307
308 #[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
310 pub timestamp: Timestamp,
311
312 #[serde(skip_serializing_if = "std::ops::Not::not")]
315 #[serde(default)]
316 pub escrowed: bool,
317}
318
319impl InspectMetadata {
320 pub fn component_url(&self) -> &str {
323 self.component_url.as_str()
324 }
325}
326
327#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
330#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
331pub struct LogsMetadata {
332 #[serde(skip_serializing_if = "Option::is_none")]
335 pub errors: Option<Vec<LogError>>,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub component_url: Option<FlyStr>,
340
341 #[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
343 pub timestamp: Timestamp,
344
345 #[serde(
349 serialize_with = "diagnostics_log_types_serde::severity::serialize",
350 deserialize_with = "diagnostics_log_types_serde::severity::deserialize"
351 )]
352 pub severity: Severity,
353
354 #[serde(skip_serializing_if = "Option::is_none")]
357 raw_severity: Option<u8>,
358
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub tags: Option<Vec<String>>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub pid: Option<u64>,
366
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub tid: Option<u64>,
370
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub file: Option<String>,
374
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub line: Option<u64>,
378
379 #[serde(skip)]
383 dropped: Option<u64>,
384
385 #[serde(skip)]
389 size_bytes: Option<usize>,
390}
391
392impl LogsMetadata {
393 pub fn component_url(&self) -> Option<&str> {
395 self.component_url.as_ref().map(|s| s.as_str())
396 }
397
398 pub fn raw_severity(&self) -> u8 {
400 match self.raw_severity {
401 Some(s) => s,
402 None => self.severity as u8,
403 }
404 }
405}
406
407#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
409pub struct Data<D: DiagnosticsData> {
410 #[serde(default)]
412 pub data_source: DataSource,
414
415 #[serde(bound(
417 deserialize = "D::Metadata: DeserializeOwned",
418 serialize = "D::Metadata: Serialize"
419 ))]
420 pub metadata: D::Metadata,
421
422 #[serde(deserialize_with = "moniker_deserialize", serialize_with = "moniker_serialize")]
424 pub moniker: ExtendedMoniker,
425
426 pub payload: Option<DiagnosticsHierarchy<D::Key>>,
428
429 #[serde(default)]
431 pub version: u64,
432}
433
434fn moniker_deserialize<'de, D>(deserializer: D) -> Result<ExtendedMoniker, D::Error>
435where
436 D: serde::Deserializer<'de>,
437{
438 let moniker_str = String::deserialize(deserializer)?;
439 ExtendedMoniker::parse_str(&moniker_str).map_err(serde::de::Error::custom)
440}
441
442fn moniker_serialize<S>(moniker: &ExtendedMoniker, s: S) -> Result<S::Ok, S::Error>
443where
444 S: Serializer,
445{
446 s.collect_str(moniker)
447}
448
449impl<D> Data<D>
450where
451 D: DiagnosticsData,
452{
453 pub fn drop_payload(&mut self) {
455 self.metadata.set_errors(vec![
456 <<D as DiagnosticsData>::Metadata as Metadata>::Error::dropped_payload(),
457 ]);
458 self.payload = None;
459 }
460
461 pub fn sort_payload(&mut self) {
463 if let Some(payload) = &mut self.payload {
464 payload.sort();
465 }
466 }
467
468 pub fn filter(mut self, selectors: &[Selector]) -> Result<Option<Self>, Error> {
471 let Some(hierarchy) = self.payload else {
472 return Ok(None);
473 };
474 let matching_selectors =
475 match self.moniker.match_against_selectors(selectors).collect::<Result<Vec<_>, _>>() {
476 Ok(selectors) if selectors.is_empty() => return Ok(None),
477 Ok(selectors) => selectors,
478 Err(e) => {
479 return Err(Error::Internal(e));
480 }
481 };
482
483 let matcher: HierarchyMatcher = match matching_selectors.try_into() {
485 Ok(hierarchy_matcher) => hierarchy_matcher,
486 Err(e) => {
487 return Err(Error::Internal(e.into()));
488 }
489 };
490
491 self.payload = match diagnostics_hierarchy::filter_hierarchy(hierarchy, &matcher) {
492 Some(hierarchy) => Some(hierarchy),
493 None => return Ok(None),
494 };
495 Ok(Some(self))
496 }
497}
498
499#[derive(Debug, Error)]
501pub enum Error {
502 #[error(transparent)]
503 Internal(#[from] anyhow::Error),
504}
505
506pub type InspectData = Data<Inspect>;
508
509pub type LogsData = Data<Logs>;
511
512pub type LogsHierarchy = DiagnosticsHierarchy<LogsField>;
514
515pub type LogsProperty = Property<LogsField>;
517
518impl Data<Inspect> {
519 pub fn name(&self) -> &str {
521 self.metadata.name.as_ref()
522 }
523}
524
525pub struct InspectDataBuilder {
526 data: Data<Inspect>,
527}
528
529impl InspectDataBuilder {
530 pub fn new(
531 moniker: ExtendedMoniker,
532 component_url: impl Into<FlyStr>,
533 timestamp: impl Into<Timestamp>,
534 ) -> Self {
535 Self {
536 data: Data {
537 data_source: DataSource::Inspect,
538 moniker,
539 payload: None,
540 version: 1,
541 metadata: InspectMetadata {
542 errors: None,
543 name: InspectHandleName::name(DEFAULT_TREE_NAME.clone()),
544 component_url: component_url.into(),
545 timestamp: timestamp.into(),
546 escrowed: false,
547 },
548 },
549 }
550 }
551
552 pub fn escrowed(mut self, escrowed: bool) -> Self {
553 self.data.metadata.escrowed = escrowed;
554 self
555 }
556
557 pub fn with_hierarchy(
558 mut self,
559 hierarchy: DiagnosticsHierarchy<<Inspect as DiagnosticsData>::Key>,
560 ) -> Self {
561 self.data.payload = Some(hierarchy);
562 self
563 }
564
565 pub fn with_errors(mut self, errors: Vec<InspectError>) -> Self {
566 self.data.metadata.errors = Some(errors);
567 self
568 }
569
570 pub fn with_name(mut self, name: InspectHandleName) -> Self {
571 self.data.metadata.name = name;
572 self
573 }
574
575 pub fn build(self) -> Data<Inspect> {
576 self.data
577 }
578}
579
580pub struct LogsDataBuilder {
583 errors: Vec<LogError>,
585 msg: Option<String>,
587 tags: Vec<String>,
589 pid: Option<u64>,
591 tid: Option<u64>,
593 file: Option<String>,
595 line: Option<u64>,
597 args: BuilderArgs,
599 keys: Vec<Property<LogsField>>,
601 raw_severity: Option<u8>,
603}
604
605pub struct BuilderArgs {
607 pub moniker: ExtendedMoniker,
609 pub timestamp: Timestamp,
611 pub component_url: Option<FlyStr>,
613 pub severity: Severity,
615}
616
617impl LogsDataBuilder {
618 pub fn new(args: BuilderArgs) -> Self {
620 LogsDataBuilder {
621 args,
622 errors: vec![],
623 msg: None,
624 file: None,
625 line: None,
626 pid: None,
627 tags: vec![],
628 tid: None,
629 keys: vec![],
630 raw_severity: None,
631 }
632 }
633
634 #[must_use = "You must call build on your builder to consume its result"]
636 pub fn set_moniker(mut self, value: ExtendedMoniker) -> Self {
637 self.args.moniker = value;
638 self
639 }
640
641 #[must_use = "You must call build on your builder to consume its result"]
643 pub fn set_url(mut self, value: Option<FlyStr>) -> Self {
644 self.args.component_url = value;
645 self
646 }
647
648 #[must_use = "You must call build on your builder to consume its result"]
653 pub fn set_dropped(mut self, value: u64) -> Self {
654 if value == 0 {
655 return self;
656 }
657 let val = self.errors.iter_mut().find_map(|error| {
658 if let LogError::DroppedLogs { count } = error {
659 Some(count)
660 } else {
661 None
662 }
663 });
664 if let Some(v) = val {
665 *v = value;
666 } else {
667 self.errors.push(LogError::DroppedLogs { count: value });
668 }
669 self
670 }
671
672 pub fn set_raw_severity(mut self, severity: u8) -> Self {
674 self.raw_severity = Some(severity);
675 self
676 }
677
678 #[must_use = "You must call build on your builder to consume its result"]
683 pub fn set_rolled_out(mut self, value: u64) -> Self {
684 if value == 0 {
685 return self;
686 }
687 let val = self.errors.iter_mut().find_map(|error| {
688 if let LogError::RolledOutLogs { count } = error {
689 Some(count)
690 } else {
691 None
692 }
693 });
694 if let Some(v) = val {
695 *v = value;
696 } else {
697 self.errors.push(LogError::RolledOutLogs { count: value });
698 }
699 self
700 }
701
702 pub fn set_severity(mut self, severity: Severity) -> Self {
704 self.args.severity = severity;
705 self.raw_severity = None;
706 self
707 }
708
709 #[must_use = "You must call build on your builder to consume its result"]
711 pub fn set_pid(mut self, value: u64) -> Self {
712 self.pid = Some(value);
713 self
714 }
715
716 #[must_use = "You must call build on your builder to consume its result"]
718 pub fn set_tid(mut self, value: u64) -> Self {
719 self.tid = Some(value);
720 self
721 }
722
723 pub fn build(self) -> LogsData {
725 let mut args = vec![];
726 if let Some(msg) = self.msg {
727 args.push(LogsProperty::String(LogsField::MsgStructured, msg));
728 }
729 let mut payload_fields = vec![DiagnosticsHierarchy::new("message", args, vec![])];
730 if !self.keys.is_empty() {
731 let val = DiagnosticsHierarchy::new("keys", self.keys, vec![]);
732 payload_fields.push(val);
733 }
734 let mut payload = LogsHierarchy::new("root", vec![], payload_fields);
735 payload.sort();
736 let (raw_severity, severity) =
737 self.raw_severity.map(Severity::parse_exact).unwrap_or((None, self.args.severity));
738 let mut ret = LogsData::for_logs(
739 self.args.moniker,
740 Some(payload),
741 self.args.timestamp,
742 self.args.component_url,
743 severity,
744 self.errors,
745 );
746 ret.metadata.raw_severity = raw_severity;
747 ret.metadata.file = self.file;
748 ret.metadata.line = self.line;
749 ret.metadata.pid = self.pid;
750 ret.metadata.tid = self.tid;
751 ret.metadata.tags = Some(self.tags);
752 ret
753 }
754
755 #[must_use = "You must call build on your builder to consume its result"]
757 pub fn add_error(mut self, error: LogError) -> Self {
758 self.errors.push(error);
759 self
760 }
761
762 #[must_use = "You must call build on your builder to consume its result"]
764 pub fn set_message(mut self, msg: impl Into<String>) -> Self {
765 self.msg = Some(msg.into());
766 self
767 }
768
769 #[must_use = "You must call build on your builder to consume its result"]
771 pub fn set_file(mut self, file: impl Into<String>) -> Self {
772 self.file = Some(file.into());
773 self
774 }
775
776 #[must_use = "You must call build on your builder to consume its result"]
778 pub fn set_line(mut self, line: u64) -> Self {
779 self.line = Some(line);
780 self
781 }
782
783 #[must_use = "You must call build on your builder to consume its result"]
785 pub fn add_key(mut self, kvp: Property<LogsField>) -> Self {
786 self.keys.push(kvp);
787 self
788 }
789
790 #[must_use = "You must call build on your builder to consume its result"]
792 pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
793 self.tags.push(tag.into());
794 self
795 }
796}
797
798impl Data<Logs> {
799 pub fn for_logs(
801 moniker: ExtendedMoniker,
802 payload: Option<LogsHierarchy>,
803 timestamp: impl Into<Timestamp>,
804 component_url: Option<FlyStr>,
805 severity: impl Into<Severity>,
806 errors: Vec<LogError>,
807 ) -> Self {
808 let errors = if errors.is_empty() { None } else { Some(errors) };
809
810 Data {
811 moniker,
812 version: SCHEMA_VERSION,
813 data_source: DataSource::Logs,
814 payload,
815 metadata: LogsMetadata {
816 timestamp: timestamp.into(),
817 component_url,
818 severity: severity.into(),
819 raw_severity: None,
820 errors,
821 file: None,
822 line: None,
823 pid: None,
824 tags: None,
825 tid: None,
826 dropped: None,
827 size_bytes: None,
828 },
829 }
830 }
831
832 pub fn set_raw_severity(&mut self, raw_severity: u8) {
835 self.metadata.raw_severity = Some(raw_severity);
836 self.metadata.severity = Severity::from(raw_severity);
837 }
838
839 pub fn set_severity(&mut self, severity: Severity) {
841 self.metadata.severity = severity;
842 self.metadata.raw_severity = None;
843 }
844
845 pub fn msg(&self) -> Option<&str> {
847 self.payload_message().as_ref().and_then(|p| {
848 p.properties.iter().find_map(|property| match property {
849 LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg.as_str()),
850 _ => None,
851 })
852 })
853 }
854
855 pub fn msg_mut(&mut self) -> Option<&mut String> {
857 self.payload_message_mut().and_then(|p| {
858 p.properties.iter_mut().find_map(|property| match property {
859 LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg),
860 _ => None,
861 })
862 })
863 }
864
865 pub fn payload_message(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
867 self.payload
868 .as_ref()
869 .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "message"))
870 }
871
872 pub fn payload_keys(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
874 self.payload
875 .as_ref()
876 .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "keys"))
877 }
878
879 pub fn metadata(&self) -> &LogsMetadata {
880 &self.metadata
881 }
882
883 pub fn payload_keys_strings(&self) -> Box<dyn Iterator<Item = String> + Send + '_> {
885 let maybe_iter = self.payload_keys().map(|p| {
886 Box::new(p.properties.iter().filter_map(|property| match property {
887 LogsProperty::String(LogsField::Tag, _tag) => None,
888 LogsProperty::String(LogsField::ProcessId, _tag) => None,
889 LogsProperty::String(LogsField::ThreadId, _tag) => None,
890 LogsProperty::String(LogsField::Dropped, _tag) => None,
891 LogsProperty::String(LogsField::Msg, _tag) => None,
892 LogsProperty::String(LogsField::FilePath, _tag) => None,
893 LogsProperty::String(LogsField::LineNumber, _tag) => None,
894 LogsProperty::String(
895 key @ (LogsField::Other(_) | LogsField::MsgStructured),
896 value,
897 ) => Some(format!("{key}={value}")),
898 LogsProperty::Bytes(key @ (LogsField::Other(_) | LogsField::MsgStructured), _) => {
899 Some(format!("{key} = <bytes>"))
900 }
901 LogsProperty::Int(
902 key @ (LogsField::Other(_) | LogsField::MsgStructured),
903 value,
904 ) => Some(format!("{key}={value}")),
905 LogsProperty::Uint(
906 key @ (LogsField::Other(_) | LogsField::MsgStructured),
907 value,
908 ) => Some(format!("{key}={value}")),
909 LogsProperty::Double(
910 key @ (LogsField::Other(_) | LogsField::MsgStructured),
911 value,
912 ) => Some(format!("{key}={value}")),
913 LogsProperty::Bool(
914 key @ (LogsField::Other(_) | LogsField::MsgStructured),
915 value,
916 ) => Some(format!("{key}={value}")),
917 LogsProperty::DoubleArray(
918 key @ (LogsField::Other(_) | LogsField::MsgStructured),
919 value,
920 ) => Some(format!("{key}={value:?}")),
921 LogsProperty::IntArray(
922 key @ (LogsField::Other(_) | LogsField::MsgStructured),
923 value,
924 ) => Some(format!("{key}={value:?}")),
925 LogsProperty::UintArray(
926 key @ (LogsField::Other(_) | LogsField::MsgStructured),
927 value,
928 ) => Some(format!("{key}={value:?}")),
929 LogsProperty::StringList(
930 key @ (LogsField::Other(_) | LogsField::MsgStructured),
931 value,
932 ) => Some(format!("{key}={value:?}")),
933 _ => None,
934 }))
935 });
936 match maybe_iter {
937 Some(i) => Box::new(i),
938 None => Box::new(std::iter::empty()),
939 }
940 }
941
942 pub fn payload_message_mut(&mut self) -> Option<&mut DiagnosticsHierarchy<LogsField>> {
944 self.payload.as_mut().and_then(|p| {
945 p.children.iter_mut().find(|property| property.name.as_str() == "message")
946 })
947 }
948
949 pub fn file_path(&self) -> Option<&str> {
951 self.metadata.file.as_deref()
952 }
953
954 pub fn line_number(&self) -> Option<&u64> {
956 self.metadata.line.as_ref()
957 }
958
959 pub fn pid(&self) -> Option<u64> {
961 self.metadata.pid
962 }
963
964 pub fn tid(&self) -> Option<u64> {
966 self.metadata.tid
967 }
968
969 pub fn tags(&self) -> Option<&Vec<String>> {
971 self.metadata.tags.as_ref()
972 }
973
974 pub fn severity(&self) -> Severity {
976 self.metadata.severity
977 }
978
979 pub fn dropped_logs(&self) -> Option<u64> {
981 self.metadata.errors.as_ref().and_then(|errors| {
982 errors.iter().find_map(|e| match e {
983 LogError::DroppedLogs { count } => Some(*count),
984 _ => None,
985 })
986 })
987 }
988
989 pub fn rolled_out_logs(&self) -> Option<u64> {
991 self.metadata.errors.as_ref().and_then(|errors| {
992 errors.iter().find_map(|e| match e {
993 LogError::RolledOutLogs { count } => Some(*count),
994 _ => None,
995 })
996 })
997 }
998
999 pub fn component_name(&self) -> Cow<'_, str> {
1001 match &self.moniker {
1002 ExtendedMoniker::ComponentManager => {
1003 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1004 }
1005 ExtendedMoniker::ComponentInstance(moniker) => {
1006 if moniker.is_root() {
1007 Cow::Borrowed(ROOT_MONIKER_REPR)
1008 } else {
1009 Cow::Owned(moniker.leaf().unwrap().to_string())
1010 }
1011 }
1012 }
1013 }
1014}
1015
1016#[derive(Clone, Copy, Debug)]
1018pub struct LogTextDisplayOptions {
1019 pub show_full_moniker: bool,
1021
1022 pub show_metadata: bool,
1024
1025 pub show_tags: bool,
1027
1028 pub show_file: bool,
1030
1031 pub color: LogTextColor,
1033
1034 pub time_format: LogTimeDisplayFormat,
1036}
1037
1038impl Default for LogTextDisplayOptions {
1039 fn default() -> Self {
1040 Self {
1041 show_full_moniker: true,
1042 show_metadata: true,
1043 show_tags: true,
1044 show_file: true,
1045 color: Default::default(),
1046 time_format: Default::default(),
1047 }
1048 }
1049}
1050
1051#[derive(Clone, Copy, Debug, Default)]
1053pub enum LogTextColor {
1054 #[default]
1056 None,
1057
1058 BySeverity,
1060
1061 Highlight,
1063}
1064
1065impl LogTextColor {
1066 fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
1067 match self {
1068 LogTextColor::BySeverity => match severity {
1069 Severity::Fatal => {
1070 write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
1071 }
1072 Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
1073 Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
1074 Severity::Info => (),
1075 Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
1076 Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
1077 },
1078 LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
1079 LogTextColor::None => {}
1080 }
1081 Ok(())
1082 }
1083
1084 fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1085 if let LogTextColor::BySeverity = self {
1086 write!(f, "{}", color::Fg(color::Yellow))?;
1088 }
1089 Ok(())
1090 }
1091
1092 fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1093 match self {
1094 LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
1095 LogTextColor::None => {}
1096 };
1097 Ok(())
1098 }
1099}
1100
1101#[derive(Clone, Copy, Debug, PartialEq)]
1103pub enum Timezone {
1104 Local,
1106
1107 Utc,
1109}
1110
1111impl Timezone {
1112 fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
1113 const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
1114 match self {
1115 Timezone::Local => {
1116 Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1117 }
1118 Timezone::Utc => {
1119 Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1120 }
1121 }
1122 }
1123}
1124
1125#[derive(Clone, Copy, Debug, Default)]
1127pub enum LogTimeDisplayFormat {
1128 #[default]
1130 Original,
1131
1132 WallTime {
1134 tz: Timezone,
1136
1137 offset: i64,
1140 },
1141}
1142
1143impl LogTimeDisplayFormat {
1144 fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
1145 const NANOS_IN_SECOND: i64 = 1_000_000_000;
1146
1147 match self {
1148 Self::Original | Self::WallTime { offset: 0, .. } => {
1151 let time: Duration =
1152 Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
1153 write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
1154 }
1155 Self::WallTime { tz, offset } => {
1156 let adjusted = time.into_nanos() + offset;
1157 let seconds = adjusted / NANOS_IN_SECOND;
1158 let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
1159 let formatted = tz.format(seconds, rem_nanos);
1160 write!(f, "[{formatted}]")?;
1161 }
1162 }
1163 Ok(())
1164 }
1165}
1166
1167pub struct LogTextPresenter<'a> {
1169 log: &'a Data<Logs>,
1171
1172 options: LogTextDisplayOptions,
1174}
1175
1176impl<'a> LogTextPresenter<'a> {
1177 pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
1181 Self { log, options }
1182 }
1183}
1184
1185impl fmt::Display for Data<Logs> {
1186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1187 LogTextPresenter::new(self, Default::default()).fmt(f)
1188 }
1189}
1190
1191impl Deref for LogTextPresenter<'_> {
1192 type Target = Data<Logs>;
1193 fn deref(&self) -> &Self::Target {
1194 self.log
1195 }
1196}
1197
1198impl fmt::Display for LogTextPresenter<'_> {
1199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1200 self.options.color.begin_record(f, self.log.severity())?;
1201 self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1202
1203 if self.options.show_metadata {
1204 match self.pid() {
1205 Some(pid) => write!(f, "[{pid}]")?,
1206 None => write!(f, "[]")?,
1207 }
1208 match self.tid() {
1209 Some(tid) => write!(f, "[{tid}]")?,
1210 None => write!(f, "[]")?,
1211 }
1212 }
1213
1214 let moniker = if self.options.show_full_moniker {
1215 match &self.moniker {
1216 ExtendedMoniker::ComponentManager => {
1217 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1218 }
1219 ExtendedMoniker::ComponentInstance(instance) => {
1220 if instance.is_root() {
1221 Cow::Borrowed(ROOT_MONIKER_REPR)
1222 } else {
1223 Cow::Owned(instance.to_string())
1224 }
1225 }
1226 }
1227 } else {
1228 self.component_name()
1229 };
1230 write!(f, "[{moniker}]")?;
1231
1232 if self.options.show_tags {
1233 match &self.metadata.tags {
1234 Some(tags) if !tags.is_empty() => {
1235 let mut filtered =
1236 tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
1237 if filtered.peek().is_some() {
1238 write!(f, "[{}]", filtered.join(","))?;
1239 }
1240 }
1241 _ => {}
1242 }
1243 }
1244
1245 write!(f, " {}:", self.metadata.severity)?;
1246
1247 if self.options.show_file {
1248 match (&self.metadata.file, &self.metadata.line) {
1249 (Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
1250 (Some(file), None) => write!(f, " [{file}]")?,
1251 _ => (),
1252 }
1253 }
1254
1255 if let Some(msg) = self.msg() {
1256 write!(f, " {msg}")?;
1257 } else {
1258 write!(f, " <missing message>")?;
1259 }
1260 for kvp in self.payload_keys_strings() {
1261 write!(f, " {kvp}")?;
1262 }
1263
1264 let dropped = self.log.dropped_logs().unwrap_or_default();
1265 let rolled = self.log.rolled_out_logs().unwrap_or_default();
1266 if dropped != 0 || rolled != 0 {
1267 self.options.color.begin_lost_message_counts(f)?;
1268 if dropped != 0 {
1269 write!(f, " [dropped={dropped}]")?;
1270 }
1271 if rolled != 0 {
1272 write!(f, " [rolled={rolled}]")?;
1273 }
1274 }
1275
1276 self.options.color.end_record(f)?;
1277
1278 Ok(())
1279 }
1280}
1281
1282impl Eq for Data<Logs> {}
1283
1284impl PartialOrd for Data<Logs> {
1285 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1286 Some(self.cmp(other))
1287 }
1288}
1289
1290impl Ord for Data<Logs> {
1291 fn cmp(&self, other: &Self) -> Ordering {
1292 self.metadata.timestamp.cmp(&other.metadata.timestamp)
1293 }
1294}
1295
1296#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
1303pub enum LogsField {
1304 ProcessId,
1305 ThreadId,
1306 Dropped,
1307 Tag,
1308 Msg,
1309 MsgStructured,
1310 FilePath,
1311 LineNumber,
1312 Other(String),
1313}
1314
1315impl fmt::Display for LogsField {
1316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1317 match self {
1318 LogsField::ProcessId => write!(f, "pid"),
1319 LogsField::ThreadId => write!(f, "tid"),
1320 LogsField::Dropped => write!(f, "num_dropped"),
1321 LogsField::Tag => write!(f, "tag"),
1322 LogsField::Msg => write!(f, "message"),
1323 LogsField::MsgStructured => write!(f, "value"),
1324 LogsField::FilePath => write!(f, "file_path"),
1325 LogsField::LineNumber => write!(f, "line_number"),
1326 LogsField::Other(name) => write!(f, "{name}"),
1327 }
1328 }
1329}
1330
1331pub const PID_LABEL: &str = "pid";
1335pub const TID_LABEL: &str = "tid";
1337pub const DROPPED_LABEL: &str = "num_dropped";
1339pub const TAG_LABEL: &str = "tag";
1341pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
1343pub const MESSAGE_LABEL: &str = "message";
1345pub const FILE_PATH_LABEL: &str = "file";
1347pub const LINE_NUMBER_LABEL: &str = "line";
1349
1350impl AsRef<str> for LogsField {
1351 fn as_ref(&self) -> &str {
1352 match self {
1353 Self::ProcessId => PID_LABEL,
1354 Self::ThreadId => TID_LABEL,
1355 Self::Dropped => DROPPED_LABEL,
1356 Self::Tag => TAG_LABEL,
1357 Self::Msg => MESSAGE_LABEL,
1358 Self::FilePath => FILE_PATH_LABEL,
1359 Self::LineNumber => LINE_NUMBER_LABEL,
1360 Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
1361 Self::Other(str) => str.as_str(),
1362 }
1363 }
1364}
1365
1366impl<T> From<T> for LogsField
1367where
1368 T: Deref<Target = str>,
1370{
1371 fn from(s: T) -> Self {
1372 match s.as_ref() {
1373 PID_LABEL => Self::ProcessId,
1374 TID_LABEL => Self::ThreadId,
1375 DROPPED_LABEL => Self::Dropped,
1376 TAG_LABEL => Self::Tag,
1377 MESSAGE_LABEL => Self::Msg,
1378 FILE_PATH_LABEL => Self::FilePath,
1379 LINE_NUMBER_LABEL => Self::LineNumber,
1380 MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
1381 _ => Self::Other(s.to_string()),
1382 }
1383 }
1384}
1385
1386impl FromStr for LogsField {
1387 type Err = ();
1388 fn from_str(s: &str) -> Result<Self, Self::Err> {
1389 Ok(Self::from(s))
1390 }
1391}
1392
1393#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
1396#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
1397pub enum LogError {
1398 #[serde(rename = "dropped_logs")]
1401 DroppedLogs { count: u64 },
1402 #[serde(rename = "rolled_out_logs")]
1405 RolledOutLogs { count: u64 },
1406 #[serde(rename = "parse_record")]
1407 FailedToParseRecord(String),
1408 #[serde(rename = "other")]
1409 Other { message: String },
1410}
1411
1412const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
1413
1414impl MetadataError for LogError {
1415 fn dropped_payload() -> Self {
1416 Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
1417 }
1418
1419 fn message(&self) -> Option<&str> {
1420 match self {
1421 Self::FailedToParseRecord(msg) => Some(msg.as_str()),
1422 Self::Other { message } => Some(message.as_str()),
1423 _ => None,
1424 }
1425 }
1426}
1427
1428#[derive(Debug, PartialEq, Clone, Eq)]
1431pub struct InspectError {
1432 pub message: String,
1433}
1434
1435impl MetadataError for InspectError {
1436 fn dropped_payload() -> Self {
1437 Self { message: "Schema failed to fit component budget.".into() }
1438 }
1439
1440 fn message(&self) -> Option<&str> {
1441 Some(self.message.as_str())
1442 }
1443}
1444
1445impl fmt::Display for InspectError {
1446 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1447 write!(f, "{}", self.message)
1448 }
1449}
1450
1451impl Borrow<str> for InspectError {
1452 fn borrow(&self) -> &str {
1453 &self.message
1454 }
1455}
1456
1457impl Serialize for InspectError {
1458 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
1459 self.message.serialize(ser)
1460 }
1461}
1462
1463impl<'de> Deserialize<'de> for InspectError {
1464 fn deserialize<D>(de: D) -> Result<Self, D::Error>
1465 where
1466 D: Deserializer<'de>,
1467 {
1468 let message = String::deserialize(de)?;
1469 Ok(Self { message })
1470 }
1471}
1472
1473#[cfg(test)]
1474mod tests {
1475 use super::*;
1476 use diagnostics_hierarchy::hierarchy;
1477 use selectors::FastError;
1478 use serde_json::json;
1479
1480 const TEST_URL: &str = "fuchsia-pkg://test";
1481
1482 #[fuchsia::test]
1483 fn test_canonical_json_inspect_formatting() {
1484 let mut hierarchy = hierarchy! {
1485 root: {
1486 x: "foo",
1487 }
1488 };
1489
1490 hierarchy.sort();
1491 let json_schema = InspectDataBuilder::new(
1492 "a/b/c/d".try_into().unwrap(),
1493 TEST_URL,
1494 Timestamp::from_nanos(123456i64),
1495 )
1496 .with_hierarchy(hierarchy)
1497 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1498 .build();
1499
1500 let result_json =
1501 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1502
1503 let expected_json = json!({
1504 "moniker": "a/b/c/d",
1505 "version": 1,
1506 "data_source": "Inspect",
1507 "payload": {
1508 "root": {
1509 "x": "foo"
1510 }
1511 },
1512 "metadata": {
1513 "component_url": TEST_URL,
1514 "filename": "test_file_plz_ignore.inspect",
1515 "timestamp": 123456,
1516 }
1517 });
1518
1519 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1520 }
1521
1522 #[fuchsia::test]
1523 fn test_errorful_json_inspect_formatting() {
1524 let json_schema = InspectDataBuilder::new(
1525 "a/b/c/d".try_into().unwrap(),
1526 TEST_URL,
1527 Timestamp::from_nanos(123456i64),
1528 )
1529 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1530 .with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
1531 .build();
1532
1533 let result_json =
1534 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1535
1536 let expected_json = json!({
1537 "moniker": "a/b/c/d",
1538 "version": 1,
1539 "data_source": "Inspect",
1540 "payload": null,
1541 "metadata": {
1542 "component_url": TEST_URL,
1543 "errors": ["too much fun being had."],
1544 "filename": "test_file_plz_ignore.inspect",
1545 "timestamp": 123456,
1546 }
1547 });
1548
1549 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1550 }
1551
1552 fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
1553 strings
1554 .iter()
1555 .map(|s| match selectors::parse_selector::<FastError>(s) {
1556 Ok(selector) => selector,
1557 Err(e) => panic!("Couldn't parse selector {s}: {e}"),
1558 })
1559 .collect::<Vec<_>>()
1560 }
1561
1562 #[fuchsia::test]
1563 fn test_filter_returns_none_on_empty_hierarchy() {
1564 let data = InspectDataBuilder::new(
1565 "a/b/c/d".try_into().unwrap(),
1566 TEST_URL,
1567 Timestamp::from_nanos(123456i64),
1568 )
1569 .build();
1570 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1571 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1572 }
1573
1574 #[fuchsia::test]
1575 fn test_filter_returns_none_on_selector_mismatch() {
1576 let mut hierarchy = hierarchy! {
1577 root: {
1578 x: "foo",
1579 }
1580 };
1581 hierarchy.sort();
1582 let data = InspectDataBuilder::new(
1583 "b/c/d/e".try_into().unwrap(),
1584 TEST_URL,
1585 Timestamp::from_nanos(123456i64),
1586 )
1587 .with_hierarchy(hierarchy)
1588 .build();
1589 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1590 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1591 }
1592
1593 #[fuchsia::test]
1594 fn test_filter_returns_none_on_data_mismatch() {
1595 let mut hierarchy = hierarchy! {
1596 root: {
1597 x: "foo",
1598 }
1599 };
1600 hierarchy.sort();
1601 let data = InspectDataBuilder::new(
1602 "a/b/c/d".try_into().unwrap(),
1603 TEST_URL,
1604 Timestamp::from_nanos(123456i64),
1605 )
1606 .with_hierarchy(hierarchy)
1607 .build();
1608 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1609
1610 assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
1611 }
1612
1613 #[fuchsia::test]
1614 fn test_filter_returns_matching_data() {
1615 let mut hierarchy = hierarchy! {
1616 root: {
1617 x: "foo",
1618 y: "bar",
1619 }
1620 };
1621 hierarchy.sort();
1622 let data = InspectDataBuilder::new(
1623 "a/b/c/d".try_into().unwrap(),
1624 TEST_URL,
1625 Timestamp::from_nanos(123456i64),
1626 )
1627 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1628 .with_hierarchy(hierarchy)
1629 .build();
1630 let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
1631
1632 let expected_json = json!({
1633 "moniker": "a/b/c/d",
1634 "version": 1,
1635 "data_source": "Inspect",
1636 "payload": {
1637 "root": {
1638 "x": "foo"
1639 }
1640 },
1641 "metadata": {
1642 "component_url": TEST_URL,
1643 "filename": "test_file_plz_ignore.inspect",
1644 "timestamp": 123456,
1645 }
1646 });
1647
1648 let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
1649 .expect("serialization should succeed.");
1650
1651 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1652 }
1653
1654 #[fuchsia::test]
1655 fn default_builder_test() {
1656 let builder = LogsDataBuilder::new(BuilderArgs {
1657 component_url: Some("url".into()),
1658 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1659 severity: Severity::Info,
1660 timestamp: Timestamp::from_nanos(0),
1661 });
1662 let expected_json = json!({
1664 "moniker": "moniker",
1665 "version": 1,
1666 "data_source": "Logs",
1667 "payload": {
1668 "root":
1669 {
1670 "message":{}
1671 }
1672 },
1673 "metadata": {
1674 "component_url": "url",
1675 "severity": "INFO",
1676 "tags": [],
1677
1678 "timestamp": 0,
1679 }
1680 });
1681 let result_json =
1682 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1683 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1684 }
1685
1686 #[fuchsia::test]
1687 fn regular_message_test() {
1688 let builder = LogsDataBuilder::new(BuilderArgs {
1689 component_url: Some("url".into()),
1690 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1691 severity: Severity::Info,
1692 timestamp: Timestamp::from_nanos(0),
1693 })
1694 .set_message("app")
1695 .set_file("test file.cc")
1696 .set_line(420)
1697 .set_pid(1001)
1698 .set_tid(200)
1699 .set_dropped(2)
1700 .add_tag("You're")
1701 .add_tag("IT!")
1702 .add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
1703 let expected_json = json!({
1705 "moniker": "moniker",
1706 "version": 1,
1707 "data_source": "Logs",
1708 "payload": {
1709 "root":
1710 {
1711 "keys":{
1712 "key":"value"
1713 },
1714 "message":{
1715 "value":"app"
1716 }
1717 }
1718 },
1719 "metadata": {
1720 "errors": [],
1721 "component_url": "url",
1722 "errors": [{"dropped_logs":{"count":2}}],
1723 "file": "test file.cc",
1724 "line": 420,
1725 "pid": 1001,
1726 "severity": "INFO",
1727 "tags": ["You're", "IT!"],
1728 "tid": 200,
1729
1730 "timestamp": 0,
1731 }
1732 });
1733 let result_json =
1734 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1735 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1736 }
1737
1738 #[fuchsia::test]
1739 fn display_for_logs() {
1740 let data = LogsDataBuilder::new(BuilderArgs {
1741 timestamp: Timestamp::from_nanos(12345678000i64),
1742 component_url: Some(FlyStr::from("fake-url")),
1743 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1744 severity: Severity::Info,
1745 })
1746 .set_pid(123)
1747 .set_tid(456)
1748 .set_message("some message".to_string())
1749 .set_file("some_file.cc".to_string())
1750 .set_line(420)
1751 .add_tag("foo")
1752 .add_tag("bar")
1753 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1754 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1755 .build();
1756
1757 assert_eq!(
1758 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1759 format!("{data}")
1760 )
1761 }
1762
1763 #[fuchsia::test]
1764 fn display_for_logs_with_duplicate_moniker() {
1765 let data = LogsDataBuilder::new(BuilderArgs {
1766 timestamp: Timestamp::from_nanos(12345678000i64),
1767 component_url: Some(FlyStr::from("fake-url")),
1768 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1769 severity: Severity::Info,
1770 })
1771 .set_pid(123)
1772 .set_tid(456)
1773 .set_message("some message".to_string())
1774 .set_file("some_file.cc".to_string())
1775 .set_line(420)
1776 .add_tag("moniker")
1777 .add_tag("bar")
1778 .add_tag("moniker")
1779 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1780 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1781 .build();
1782
1783 assert_eq!(
1784 "[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
1785 format!("{data}")
1786 )
1787 }
1788
1789 #[fuchsia::test]
1790 fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
1791 let data = LogsDataBuilder::new(BuilderArgs {
1792 timestamp: Timestamp::from_nanos(12345678000i64),
1793 component_url: Some(FlyStr::from("fake-url")),
1794 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1795 severity: Severity::Info,
1796 })
1797 .set_pid(123)
1798 .set_tid(456)
1799 .set_message("some message".to_string())
1800 .set_file("some_file.cc".to_string())
1801 .set_line(420)
1802 .add_tag("moniker")
1803 .add_tag("moniker")
1804 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1805 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1806 .build();
1807
1808 assert_eq!(
1809 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1810 format!("{data}")
1811 )
1812 }
1813
1814 #[fuchsia::test]
1815 fn display_for_logs_partial_moniker() {
1816 let data = LogsDataBuilder::new(BuilderArgs {
1817 timestamp: Timestamp::from_nanos(12345678000i64),
1818 component_url: Some(FlyStr::from("fake-url")),
1819 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1820 severity: Severity::Info,
1821 })
1822 .set_pid(123)
1823 .set_tid(456)
1824 .set_message("some message".to_string())
1825 .set_file("some_file.cc".to_string())
1826 .set_line(420)
1827 .add_tag("foo")
1828 .add_tag("bar")
1829 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1830 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1831 .build();
1832
1833 assert_eq!(
1834 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1835 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1836 show_full_moniker: false,
1837 ..Default::default()
1838 }))
1839 )
1840 }
1841
1842 #[fuchsia::test]
1843 fn display_for_logs_exclude_metadata() {
1844 let data = LogsDataBuilder::new(BuilderArgs {
1845 timestamp: Timestamp::from_nanos(12345678000i64),
1846 component_url: Some(FlyStr::from("fake-url")),
1847 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1848 severity: Severity::Info,
1849 })
1850 .set_pid(123)
1851 .set_tid(456)
1852 .set_message("some message".to_string())
1853 .set_file("some_file.cc".to_string())
1854 .set_line(420)
1855 .add_tag("foo")
1856 .add_tag("bar")
1857 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1858 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1859 .build();
1860
1861 assert_eq!(
1862 "[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1863 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1864 show_metadata: false,
1865 ..Default::default()
1866 }))
1867 )
1868 }
1869
1870 #[fuchsia::test]
1871 fn display_for_logs_exclude_tags() {
1872 let data = LogsDataBuilder::new(BuilderArgs {
1873 timestamp: Timestamp::from_nanos(12345678000i64),
1874 component_url: Some(FlyStr::from("fake-url")),
1875 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1876 severity: Severity::Info,
1877 })
1878 .set_pid(123)
1879 .set_tid(456)
1880 .set_message("some message".to_string())
1881 .set_file("some_file.cc".to_string())
1882 .set_line(420)
1883 .add_tag("foo")
1884 .add_tag("bar")
1885 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1886 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1887 .build();
1888
1889 assert_eq!(
1890 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1891 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1892 show_tags: false,
1893 ..Default::default()
1894 }))
1895 )
1896 }
1897
1898 #[fuchsia::test]
1899 fn display_for_logs_exclude_file() {
1900 let data = LogsDataBuilder::new(BuilderArgs {
1901 timestamp: Timestamp::from_nanos(12345678000i64),
1902 component_url: Some(FlyStr::from("fake-url")),
1903 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1904 severity: Severity::Info,
1905 })
1906 .set_pid(123)
1907 .set_tid(456)
1908 .set_message("some message".to_string())
1909 .set_file("some_file.cc".to_string())
1910 .set_line(420)
1911 .add_tag("foo")
1912 .add_tag("bar")
1913 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1914 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1915 .build();
1916
1917 assert_eq!(
1918 "[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
1919 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1920 show_file: false,
1921 ..Default::default()
1922 }))
1923 )
1924 }
1925
1926 #[fuchsia::test]
1927 fn display_for_logs_include_color_by_severity() {
1928 let data = LogsDataBuilder::new(BuilderArgs {
1929 timestamp: Timestamp::from_nanos(12345678000i64),
1930 component_url: Some(FlyStr::from("fake-url")),
1931 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1932 severity: Severity::Error,
1933 })
1934 .set_pid(123)
1935 .set_tid(456)
1936 .set_message("some message".to_string())
1937 .set_file("some_file.cc".to_string())
1938 .set_line(420)
1939 .add_tag("foo")
1940 .add_tag("bar")
1941 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1942 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1943 .build();
1944
1945 assert_eq!(
1946 format!("{}[00012.345678][123][456][moniker][foo,bar] ERROR: [some_file.cc(420)] some message test=property value=test{}", color::Fg(color::Red), style::Reset),
1947 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1948 color: LogTextColor::BySeverity,
1949 ..Default::default()
1950 }))
1951 )
1952 }
1953
1954 #[fuchsia::test]
1955 fn display_for_logs_highlight_line() {
1956 let data = LogsDataBuilder::new(BuilderArgs {
1957 timestamp: Timestamp::from_nanos(12345678000i64),
1958 component_url: Some(FlyStr::from("fake-url")),
1959 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1960 severity: Severity::Info,
1961 })
1962 .set_pid(123)
1963 .set_tid(456)
1964 .set_message("some message".to_string())
1965 .set_file("some_file.cc".to_string())
1966 .set_line(420)
1967 .add_tag("foo")
1968 .add_tag("bar")
1969 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1970 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1971 .build();
1972
1973 assert_eq!(
1974 format!("{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}", color::Fg(color::LightYellow), style::Reset),
1975 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
1976 color: LogTextColor::Highlight,
1977 ..Default::default()
1978 }))
1979 )
1980 }
1981
1982 #[fuchsia::test]
1983 fn display_for_logs_with_wall_time() {
1984 let data = LogsDataBuilder::new(BuilderArgs {
1985 timestamp: Timestamp::from_nanos(12345678000i64),
1986 component_url: Some(FlyStr::from("fake-url")),
1987 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1988 severity: Severity::Info,
1989 })
1990 .set_pid(123)
1991 .set_tid(456)
1992 .set_message("some message".to_string())
1993 .set_file("some_file.cc".to_string())
1994 .set_line(420)
1995 .add_tag("foo")
1996 .add_tag("bar")
1997 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1998 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1999 .build();
2000
2001 assert_eq!(
2002 "[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2003 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2004 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
2005 ..Default::default()
2006 }))
2007 );
2008
2009 assert_eq!(
2010 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2011 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2012 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
2013 ..Default::default()
2014 })),
2015 "should fall back to monotonic if offset is 0"
2016 );
2017 }
2018
2019 #[fuchsia::test]
2020 fn display_for_logs_with_dropped_count() {
2021 let data = LogsDataBuilder::new(BuilderArgs {
2022 timestamp: Timestamp::from_nanos(12345678000i64),
2023 component_url: Some(FlyStr::from("fake-url")),
2024 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2025 severity: Severity::Info,
2026 })
2027 .set_dropped(5)
2028 .set_pid(123)
2029 .set_tid(456)
2030 .set_message("some message".to_string())
2031 .set_file("some_file.cc".to_string())
2032 .set_line(420)
2033 .add_tag("foo")
2034 .add_tag("bar")
2035 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2036 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2037 .build();
2038
2039 assert_eq!(
2040 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
2041 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2042 );
2043
2044 assert_eq!(
2045 format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}", color::Fg(color::Yellow), style::Reset),
2046 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2047 color: LogTextColor::BySeverity,
2048 ..Default::default()
2049 })),
2050 );
2051 }
2052
2053 #[fuchsia::test]
2054 fn display_for_logs_with_rolled_count() {
2055 let data = LogsDataBuilder::new(BuilderArgs {
2056 timestamp: Timestamp::from_nanos(12345678000i64),
2057 component_url: Some(FlyStr::from("fake-url")),
2058 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2059 severity: Severity::Info,
2060 })
2061 .set_rolled_out(10)
2062 .set_pid(123)
2063 .set_tid(456)
2064 .set_message("some message".to_string())
2065 .set_file("some_file.cc".to_string())
2066 .set_line(420)
2067 .add_tag("foo")
2068 .add_tag("bar")
2069 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2070 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2071 .build();
2072
2073 assert_eq!(
2074 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
2075 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2076 );
2077
2078 assert_eq!(
2079 format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}", color::Fg(color::Yellow), style::Reset),
2080 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2081 color: LogTextColor::BySeverity,
2082 ..Default::default()
2083 })),
2084 );
2085 }
2086
2087 #[fuchsia::test]
2088 fn display_for_logs_with_dropped_and_rolled_counts() {
2089 let data = LogsDataBuilder::new(BuilderArgs {
2090 timestamp: Timestamp::from_nanos(12345678000i64),
2091 component_url: Some(FlyStr::from("fake-url")),
2092 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2093 severity: Severity::Info,
2094 })
2095 .set_dropped(5)
2096 .set_rolled_out(10)
2097 .set_pid(123)
2098 .set_tid(456)
2099 .set_message("some message".to_string())
2100 .set_file("some_file.cc".to_string())
2101 .set_line(420)
2102 .add_tag("foo")
2103 .add_tag("bar")
2104 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2105 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2106 .build();
2107
2108 assert_eq!(
2109 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
2110 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2111 );
2112
2113 assert_eq!(
2114 format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}", color::Fg(color::Yellow), style::Reset),
2115 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
2116 color: LogTextColor::BySeverity,
2117 ..Default::default()
2118 })),
2119 );
2120 }
2121
2122 #[fuchsia::test]
2123 fn display_for_logs_no_tags() {
2124 let data = LogsDataBuilder::new(BuilderArgs {
2125 timestamp: Timestamp::from_nanos(12345678000i64),
2126 component_url: Some(FlyStr::from("fake-url")),
2127 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2128 severity: Severity::Info,
2129 })
2130 .set_pid(123)
2131 .set_tid(456)
2132 .set_message("some message".to_string())
2133 .build();
2134
2135 assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{data}"))
2136 }
2137
2138 #[fuchsia::test]
2139 fn size_bytes_deserialize_backwards_compatibility() {
2140 let original_json = json!({
2141 "moniker": "a/b",
2142 "version": 1,
2143 "data_source": "Logs",
2144 "payload": {
2145 "root": {
2146 "message":{}
2147 }
2148 },
2149 "metadata": {
2150 "component_url": "url",
2151 "severity": "INFO",
2152 "tags": [],
2153
2154 "timestamp": 123,
2155 }
2156 });
2157 let expected_data = LogsDataBuilder::new(BuilderArgs {
2158 component_url: Some("url".into()),
2159 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2160 severity: Severity::Info,
2161 timestamp: Timestamp::from_nanos(123),
2162 })
2163 .build();
2164 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2165 assert_eq!(original_data, expected_data);
2166 assert_eq!(original_data.metadata.size_bytes, None);
2168 }
2169
2170 #[fuchsia::test]
2171 fn dropped_deserialize_backwards_compatibility() {
2172 let original_json = json!({
2173 "moniker": "a/b",
2174 "version": 1,
2175 "data_source": "Logs",
2176 "payload": {
2177 "root": {
2178 "message":{}
2179 }
2180 },
2181 "metadata": {
2182 "dropped": 0,
2183 "component_url": "url",
2184 "severity": "INFO",
2185 "tags": [],
2186
2187 "timestamp": 123,
2188 }
2189 });
2190 let expected_data = LogsDataBuilder::new(BuilderArgs {
2191 component_url: Some("url".into()),
2192 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2193 severity: Severity::Info,
2194 timestamp: Timestamp::from_nanos(123),
2195 })
2196 .build();
2197 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2198 assert_eq!(original_data, expected_data);
2199 assert_eq!(original_data.metadata.dropped, None);
2201 }
2202
2203 #[fuchsia::test]
2204 fn severity_aliases() {
2205 assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
2206 assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
2207 }
2208}