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