1use std::fmt::{self, Display, Write};
2use std::num::NonZero;
3use std::{cmp, iter};
4
5use foldhash::{HashMap, HashMapExt};
6use new_zealand::nz;
7
8use crate::{EventName, GLOBAL_REGISTRY, Magnitude, ObservationBagSnapshot, Observations};
9
10#[derive(Debug)]
17pub struct Report {
18 events: Box<[EventMetrics]>,
20}
21
22impl Report {
23 #[must_use]
47 pub fn collect() -> Self {
48 let mut event_name_to_merged_snapshot = HashMap::new();
50
51 GLOBAL_REGISTRY.inspect(|observation_bags| {
52 for (event_name, observation_bag) in observation_bags {
53 let snapshot = observation_bag.snapshot();
54
55 event_name_to_merged_snapshot
57 .entry(event_name.clone())
58 .and_modify(|existing_snapshot: &mut ObservationBagSnapshot| {
59 existing_snapshot.merge_from(&snapshot);
60 })
61 .or_insert(snapshot);
62 }
63 });
64
65 let mut events = event_name_to_merged_snapshot
67 .into_iter()
68 .map(|(event_name, snapshot)| EventMetrics::new(event_name, snapshot))
69 .collect::<Vec<_>>();
70
71 events.sort_by_key(|event_metrics| event_metrics.name().clone());
73
74 Self {
75 events: events.into_boxed_slice(),
76 }
77 }
78
79 pub fn events(&self) -> impl Iterator<Item = &EventMetrics> {
102 self.events.iter()
103 }
104
105 #[cfg(any(test, feature = "test-util"))]
110 #[must_use]
111 pub fn fake(events: Vec<EventMetrics>) -> Self {
112 Self {
113 events: events.into_boxed_slice(),
114 }
115 }
116}
117
118impl Display for Report {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 for event in &self.events {
121 writeln!(f, "{event}")?;
122 }
123
124 Ok(())
125 }
126}
127
128#[derive(Debug)]
132pub struct EventMetrics {
133 name: EventName,
134
135 count: u64,
136 sum: Magnitude,
137
138 mean: Magnitude,
140
141 histogram: Option<Histogram>,
143}
144
145impl EventMetrics {
146 pub(crate) fn new(name: EventName, snapshot: ObservationBagSnapshot) -> Self {
147 let count = snapshot.count;
148 let sum = snapshot.sum;
149
150 #[expect(
151 clippy::arithmetic_side_effects,
152 reason = "NonZero protects against division by zero"
153 )]
154 #[expect(
155 clippy::integer_division,
156 reason = "we accept that we lose the remainder - 100% precision not required"
157 )]
158 let mean = Magnitude::try_from(count)
159 .ok()
160 .and_then(NonZero::new)
161 .map_or(0, |count| sum / count.get());
162
163 let histogram = if snapshot.bucket_magnitudes.is_empty() {
164 None
165 } else {
166 let plus_infinity_bucket_count = snapshot
169 .count
170 .saturating_sub(snapshot.bucket_counts.iter().sum::<u64>());
171
172 Some(Histogram {
173 magnitudes: snapshot.bucket_magnitudes,
174 counts: snapshot.bucket_counts,
175 plus_infinity_bucket_count,
176 })
177 };
178
179 Self {
180 name,
181 count,
182 sum,
183 mean,
184 histogram,
185 }
186 }
187
188 #[must_use]
209 pub fn name(&self) -> &EventName {
210 &self.name
211 }
212
213 #[must_use]
235 pub fn count(&self) -> u64 {
236 self.count
237 }
238
239 #[must_use]
261 pub fn sum(&self) -> Magnitude {
262 self.sum
263 }
264
265 #[must_use]
289 pub fn mean(&self) -> Magnitude {
290 self.mean
291 }
292
293 #[must_use]
326 pub fn histogram(&self) -> Option<&Histogram> {
327 self.histogram.as_ref()
328 }
329
330 #[cfg(any(test, feature = "test-util"))]
335 #[must_use]
336 pub fn fake(
337 name: impl Into<EventName>,
338 count: u64,
339 sum: Magnitude,
340 histogram: Option<Histogram>,
341 ) -> Self {
342 #[expect(
343 clippy::arithmetic_side_effects,
344 reason = "NonZero protects against division by zero"
345 )]
346 #[expect(
347 clippy::integer_division,
348 reason = "we accept that we lose the remainder - 100% precision not required"
349 )]
350 let mean = Magnitude::try_from(count)
351 .ok()
352 .and_then(NonZero::new)
353 .map_or(0, |count| sum / count.get());
354
355 Self {
356 name: name.into(),
357 count,
358 sum,
359 mean,
360 histogram,
361 }
362 }
363}
364
365impl Display for EventMetrics {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 write!(f, "{}: ", self.name)?;
368
369 if self.count == 0 {
370 writeln!(f, "0")?;
372 return Ok(());
373 }
374
375 #[expect(
376 clippy::cast_possible_wrap,
377 reason = "intentional wrap - crate policy is that values out of safe range may be mangled"
378 )]
379 let count_as_magnitude = self.count as Magnitude;
380
381 if count_as_magnitude == self.sum && self.histogram.is_none() {
382 writeln!(f, "{} (counter)", self.count)?;
389 } else {
390 writeln!(f, "{}; sum {}; mean {}", self.count, self.sum, self.mean)?;
391 }
392
393 if let Some(histogram) = &self.histogram {
395 writeln!(f, "{histogram}")?;
396 }
397
398 Ok(())
399 }
400}
401
402#[derive(Debug)]
407pub struct Histogram {
408 magnitudes: &'static [Magnitude],
413
414 counts: Box<[u64]>,
415
416 plus_infinity_bucket_count: u64,
419}
420
421impl Histogram {
422 pub fn magnitudes(&self) -> impl Iterator<Item = Magnitude> {
431 self.magnitudes
432 .iter()
433 .copied()
434 .chain(iter::once(Magnitude::MAX))
435 }
436
437 pub fn counts(&self) -> impl Iterator<Item = u64> {
444 self.counts
445 .iter()
446 .copied()
447 .chain(iter::once(self.plus_infinity_bucket_count))
448 }
449
450 pub fn buckets(&self) -> impl Iterator<Item = (Magnitude, u64)> {
460 self.magnitudes().zip(self.counts())
461 }
462
463 #[cfg(any(test, feature = "test-util"))]
472 #[must_use]
473 pub fn fake(
474 magnitudes: &'static [Magnitude],
475 counts: Vec<u64>,
476 plus_infinity_count: u64,
477 ) -> Self {
478 Self {
479 magnitudes,
480 counts: counts.into_boxed_slice(),
481 plus_infinity_bucket_count: plus_infinity_count,
482 }
483 }
484}
485
486const HISTOGRAM_BAR_WIDTH_CHARS: u64 = 50;
496
497const HISTOGRAM_BAR_CHARS: &str =
501 "∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎";
502
503const HISTOGRAM_BAR_CHARS_LEN_BYTES: NonZero<usize> =
504 NonZero::new(HISTOGRAM_BAR_CHARS.len()).unwrap();
505
506const BYTES_PER_HISTOGRAM_BAR_CHAR: NonZero<usize> = nz!(3);
509
510impl Display for Histogram {
511 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512 let buckets = self.buckets().collect::<Vec<_>>();
513
514 let mut count_str = String::new();
518
519 let widest_count = buckets.iter().fold(0, |current, bucket| {
521 count_str.clear();
522 write!(&mut count_str, "{}", bucket.1)
523 .expect("we expect writing integer to String to be infallible");
524 cmp::max(current, count_str.len())
525 });
526
527 let mut upper_bound_str = String::new();
529
530 let widest_upper_bound = buckets.iter().fold(0, |current, bucket| {
532 upper_bound_str.clear();
533
534 if bucket.0 == Magnitude::MAX {
535 write!(&mut upper_bound_str, "+inf")
537 .expect("we expect writing integer to String to be infallible");
538 } else {
539 write!(&mut upper_bound_str, "{}", bucket.0)
540 .expect("we expect writing integer to String to be infallible");
541 }
542
543 cmp::max(current, upper_bound_str.len())
544 });
545
546 let histogram_scale = HistogramScale::new(self);
547
548 for (magnitude, count) in buckets {
549 upper_bound_str.clear();
550
551 if magnitude == Magnitude::MAX {
552 write!(&mut upper_bound_str, "+inf")?;
554 } else {
555 write!(&mut upper_bound_str, "{magnitude}")?;
557 }
558
559 let padding_needed = widest_upper_bound.saturating_sub(upper_bound_str.len());
560 for _ in 0..padding_needed {
561 upper_bound_str.insert(0, ' ');
562 }
563
564 count_str.clear();
565 write!(&mut count_str, "{count}")?;
566
567 let padding_needed = widest_count.saturating_sub(count_str.len());
568 for _ in 0..padding_needed {
569 count_str.insert(0, ' ');
570 }
571
572 write!(f, "value <= {upper_bound_str} [ {count_str} ]: ")?;
573 histogram_scale.write_bar(count, f)?;
574
575 writeln!(f)?;
576 }
577
578 Ok(())
579 }
580}
581
582#[derive(Debug)]
584struct HistogramScale {
585 count_per_char: NonZero<u64>,
588}
589
590impl HistogramScale {
591 fn new(snapshot: &Histogram) -> Self {
592 let max_count = snapshot
593 .counts()
594 .max()
595 .expect("a histogram always has at least one bucket by definition (+inf)");
596
597 #[expect(
602 clippy::integer_division,
603 reason = "we accept the loss of precision here - the bar might not always reach 100% of desired width or even overshoot it"
604 )]
605 let count_per_char = NonZero::new(cmp::max(max_count / HISTOGRAM_BAR_WIDTH_CHARS, 1))
606 .expect("guarded by max()");
607
608 Self { count_per_char }
609 }
610
611 fn write_bar(&self, count: u64, f: &mut impl Write) -> fmt::Result {
612 let histogram_bar_width = count
613 .checked_div(self.count_per_char.get())
614 .expect("division by zero impossible - divisor is NonZero");
615
616 let bar_width = usize::try_from(histogram_bar_width).expect("safe range");
620
621 let chars_in_constant = HISTOGRAM_BAR_CHARS_LEN_BYTES
622 .get()
623 .checked_div(BYTES_PER_HISTOGRAM_BAR_CHAR.get())
624 .expect("NonZero - cannot be zero");
625
626 let mut remaining = bar_width;
627
628 while remaining > 0 {
629 let chunk_size =
630 NonZero::new(remaining.min(chars_in_constant)).expect("guarded by loop condition");
631
632 let byte_end = chunk_size
634 .checked_mul(BYTES_PER_HISTOGRAM_BAR_CHAR)
635 .expect("we are seeking into a small constant value, overflow impossible");
636
637 #[expect(
638 clippy::string_slice,
639 reason = "safe slicing - ∎ characters have known UTF-8 encoding"
640 )]
641 f.write_str(&HISTOGRAM_BAR_CHARS[..byte_end.get()])?;
642
643 remaining = remaining
644 .checked_sub(chunk_size.get())
645 .expect("guarded by min() above");
646 }
647
648 Ok(())
649 }
650}
651
652#[cfg(test)]
653#[cfg_attr(coverage_nightly, coverage(off))]
654mod tests {
655 #![allow(clippy::indexing_slicing, reason = "panic is fine in tests")]
656
657 use super::*;
658
659 #[test]
660 fn histogram_properties_reflect_reality() {
661 let magnitudes = &[-5, 1, 10, 100];
662 let counts = &[66, 5, 3, 2];
663
664 let histogram = Histogram {
665 magnitudes,
666 counts: Vec::from(counts).into_boxed_slice(),
667 plus_infinity_bucket_count: 1,
668 };
669
670 assert_eq!(
671 histogram.magnitudes().collect::<Vec<_>>(),
672 magnitudes
673 .iter()
674 .copied()
675 .chain(iter::once(Magnitude::MAX))
676 .collect::<Vec<_>>()
677 );
678 assert_eq!(
679 histogram.counts().collect::<Vec<_>>(),
680 counts
681 .iter()
682 .copied()
683 .chain(iter::once(1))
684 .collect::<Vec<_>>()
685 );
686
687 let buckets: Vec<_> = histogram.buckets().collect();
688 assert_eq!(buckets.len(), 5);
689 assert_eq!(buckets[0], (-5, 66));
690 assert_eq!(buckets[1], (1, 5));
691 assert_eq!(buckets[2], (10, 3));
692 assert_eq!(buckets[3], (100, 2));
693 assert_eq!(buckets[4], (Magnitude::MAX, 1));
694 }
695
696 #[test]
697 fn histogram_display_contains_expected_information() {
698 let magnitudes = &[-5, 1, 10, 100];
699 let counts = &[666666, 5, 3, 2];
700
701 let histogram = Histogram {
702 magnitudes,
703 counts: Vec::from(counts).into_boxed_slice(),
704 plus_infinity_bucket_count: 1,
705 };
706
707 let mut output = String::new();
708 write!(&mut output, "{histogram}").unwrap();
709
710 println!("{output}");
711
712 assert!(output.contains("value <= -5 [ 666666 ]: "));
716 assert!(output.contains("value <= 1 [ 5 ]: "));
717 assert!(output.contains("value <= 10 [ 3 ]: "));
718 assert!(output.contains("value <= 100 [ 2 ]: "));
719 assert!(output.contains("value <= +inf [ 1 ]: "));
720
721 #[expect(clippy::cast_possible_truncation, reason = "safe range, tiny values")]
727 let max_acceptable_line_length = (HISTOGRAM_BAR_WIDTH_CHARS * 5) as usize;
728
729 for line in output.lines() {
730 assert!(
731 line.len() < max_acceptable_line_length,
732 "line is too long: {line}"
733 );
734 }
735 }
736
737 #[test]
738 fn event_properties_reflect_reality() {
739 let event_name = "test_event".to_string();
740 let count = 50;
741 let sum = Magnitude::from(1000);
742 let mean = Magnitude::from(20);
743
744 let histogram = Histogram {
745 magnitudes: &[1, 10, 100],
746 counts: vec![5, 3, 2].into_boxed_slice(),
747 plus_infinity_bucket_count: 1,
748 };
749
750 let event_metrics = EventMetrics {
751 name: event_name.clone().into(),
752 count,
753 sum,
754 mean,
755 histogram: Some(histogram),
756 };
757
758 assert_eq!(event_metrics.name(), &event_name);
759 assert_eq!(event_metrics.count(), count);
760 assert_eq!(event_metrics.sum(), sum);
761 assert_eq!(event_metrics.mean(), mean);
762 assert!(event_metrics.histogram().is_some());
763 }
764
765 #[test]
766 fn event_display_contains_expected_information() {
767 let event_name = "test_event".to_string();
768 let count = 50;
769 let sum = Magnitude::from(1000);
770 let mean = Magnitude::from(20);
771
772 let histogram = Histogram {
773 magnitudes: &[1, 10, 100],
774 counts: vec![5, 3, 2].into_boxed_slice(),
775 plus_infinity_bucket_count: 1,
776 };
777
778 let event_metrics = EventMetrics {
779 name: event_name.clone().into(),
780 count,
781 sum,
782 mean,
783 histogram: Some(histogram),
784 };
785
786 let mut output = String::new();
787 write!(&mut output, "{event_metrics}").unwrap();
788
789 println!("{output}");
790
791 assert!(output.contains(&event_name));
794 assert!(output.contains(&count.to_string()));
795 assert!(output.contains(&sum.to_string()));
796 assert!(output.contains(&mean.to_string()));
797 assert!(output.contains("value <= +inf [ 1 ]: "));
798 }
799
800 #[test]
801 fn report_properties_reflect_reality() {
802 let event1 = EventMetrics {
803 name: "event1".to_string().into(),
804 count: 10,
805 sum: Magnitude::from(100),
806 mean: Magnitude::from(10),
807 histogram: None,
808 };
809
810 let event2 = EventMetrics {
811 name: "event2".to_string().into(),
812 count: 5,
813 sum: Magnitude::from(50),
814 mean: Magnitude::from(10),
815 histogram: None,
816 };
817
818 let report = Report {
819 events: vec![event1, event2].into_boxed_slice(),
820 };
821
822 let events = report.events().collect::<Vec<_>>();
824
825 assert_eq!(events.len(), 2);
826 assert_eq!(events[0].name(), "event1");
827 assert_eq!(events[1].name(), "event2");
828 }
829
830 #[test]
831 fn report_display_contains_expected_events() {
832 let event1 = EventMetrics {
833 name: "event1".to_string().into(),
834 count: 10,
835 sum: Magnitude::from(100),
836 mean: Magnitude::from(10),
837 histogram: None,
838 };
839
840 let event2 = EventMetrics {
841 name: "event2".to_string().into(),
842 count: 5,
843 sum: Magnitude::from(50),
844 mean: Magnitude::from(10),
845 histogram: None,
846 };
847
848 let report = Report {
849 events: vec![event1, event2].into_boxed_slice(),
850 };
851
852 let mut output = String::new();
853 write!(&mut output, "{report}").unwrap();
854
855 println!("{output}");
856
857 assert!(output.contains("event1"));
859 assert!(output.contains("event2"));
860 }
861
862 #[test]
863 fn event_displayed_as_counter_if_unit_values_and_no_histogram() {
864 let counter = EventMetrics {
869 name: "test_event".to_string().into(),
870 count: 100,
871 sum: Magnitude::from(100),
872 mean: Magnitude::from(1),
873 histogram: None,
874 };
875
876 let not_counter = EventMetrics {
878 name: "test_event".to_string().into(),
879 count: 100,
880 sum: Magnitude::from(200),
881 mean: Magnitude::from(2),
882 histogram: None,
883 };
884
885 let also_not_counter = EventMetrics {
887 name: "test_event".to_string().into(),
888 count: 100,
889 sum: Magnitude::from(100),
890 mean: Magnitude::from(1),
891 histogram: Some(Histogram {
892 magnitudes: &[],
893 counts: Box::new([]),
894 plus_infinity_bucket_count: 100,
895 }),
896 };
897
898 let still_not_counter = EventMetrics {
900 name: "test_event".to_string().into(),
901 count: 100,
902 sum: Magnitude::from(200),
903 mean: Magnitude::from(2),
904 histogram: Some(Histogram {
905 magnitudes: &[],
906 counts: Box::new([]),
907 plus_infinity_bucket_count: 200,
908 }),
909 };
910
911 let mut output = String::new();
912
913 write!(&mut output, "{counter}").unwrap();
914 assert!(output.contains("100 (counter)"));
915 output.clear();
916
917 write!(&mut output, "{not_counter}").unwrap();
918 assert!(output.contains("100; sum 200; mean 2"));
919 output.clear();
920
921 write!(&mut output, "{also_not_counter}").unwrap();
922 assert!(output.contains("100; sum 100; mean 1"));
923 output.clear();
924
925 write!(&mut output, "{still_not_counter}").unwrap();
926 assert!(output.contains("100; sum 200; mean 2"));
927 }
928
929 #[test]
930 fn histogram_scale_zero() {
931 let histogram = Histogram {
933 magnitudes: &[1, 2, 3],
934 counts: Box::new([0, 0, 0]),
935 plus_infinity_bucket_count: 0,
936 };
937
938 let histogram_scale = HistogramScale::new(&histogram);
939
940 let mut output = String::new();
941 histogram_scale.write_bar(0, &mut output).unwrap();
942
943 assert_eq!(output, "");
944 }
945
946 #[test]
947 fn histogram_scale_small() {
948 let histogram = Histogram {
950 magnitudes: &[1, 2, 3],
951 counts: Box::new([1, 2, 3]),
952 plus_infinity_bucket_count: 0,
953 };
954
955 let histogram_scale = HistogramScale::new(&histogram);
956
957 let mut output = String::new();
958
959 histogram_scale.write_bar(0, &mut output).unwrap();
960 assert_eq!(output, "");
961 output.clear();
962
963 histogram_scale.write_bar(1, &mut output).unwrap();
964 assert_eq!(output, "∎");
965 output.clear();
966
967 histogram_scale.write_bar(2, &mut output).unwrap();
968 assert_eq!(output, "∎∎");
969 output.clear();
970
971 histogram_scale.write_bar(3, &mut output).unwrap();
972 assert_eq!(output, "∎∎∎");
973 }
974
975 #[test]
976 fn histogram_scale_just_over() {
977 let histogram = Histogram {
979 magnitudes: &[1, 2, 3],
980 counts: Box::new([
981 HISTOGRAM_BAR_WIDTH_CHARS + 1,
982 HISTOGRAM_BAR_WIDTH_CHARS + 1,
983 HISTOGRAM_BAR_WIDTH_CHARS + 1,
984 ]),
985 plus_infinity_bucket_count: 0,
986 };
987
988 let histogram_scale = HistogramScale::new(&histogram);
989
990 let mut output = String::new();
991
992 histogram_scale
993 .write_bar(HISTOGRAM_BAR_WIDTH_CHARS + 1, &mut output)
994 .unwrap();
995 assert_eq!(
997 output,
998 "∎".repeat(
999 usize::try_from(HISTOGRAM_BAR_WIDTH_CHARS + 1).expect("safe range, tiny value")
1000 )
1001 );
1002 }
1003
1004 #[test]
1005 fn histogram_scale_large_exact() {
1006 let histogram = Histogram {
1009 magnitudes: &[1, 2, 3],
1010 counts: Box::new([
1011 79,
1012 HISTOGRAM_BAR_WIDTH_CHARS * 100,
1013 HISTOGRAM_BAR_WIDTH_CHARS * 1000,
1014 ]),
1015 plus_infinity_bucket_count: 0,
1016 };
1017
1018 let histogram_scale = HistogramScale::new(&histogram);
1019
1020 let mut output = String::new();
1021
1022 histogram_scale.write_bar(0, &mut output).unwrap();
1023 assert_eq!(output, "");
1024 output.clear();
1025
1026 histogram_scale
1027 .write_bar(histogram_scale.count_per_char.get(), &mut output)
1028 .unwrap();
1029 assert_eq!(output, "∎");
1030 output.clear();
1031
1032 histogram_scale
1033 .write_bar(HISTOGRAM_BAR_WIDTH_CHARS * 1000, &mut output)
1034 .unwrap();
1035 assert_eq!(
1036 output,
1037 "∎".repeat(usize::try_from(HISTOGRAM_BAR_WIDTH_CHARS).expect("safe range, tiny value"))
1038 );
1039 }
1040
1041 #[test]
1042 fn histogram_scale_large_inexact() {
1043 let histogram = Histogram {
1046 magnitudes: &[1, 2, 3],
1047 counts: Box::new([
1048 79,
1049 HISTOGRAM_BAR_WIDTH_CHARS * 100,
1050 HISTOGRAM_BAR_WIDTH_CHARS * 1000,
1051 ]),
1052 plus_infinity_bucket_count: 0,
1053 };
1054
1055 let histogram_scale = HistogramScale::new(&histogram);
1056
1057 let mut output = String::new();
1058 histogram_scale.write_bar(3, &mut output).unwrap();
1059
1060 let mut output = String::new();
1061
1062 histogram_scale.write_bar(0, &mut output).unwrap();
1063 assert_eq!(output, "");
1064 output.clear();
1065
1066 histogram_scale
1067 .write_bar(histogram_scale.count_per_char.get() - 1, &mut output)
1068 .unwrap();
1069 assert_eq!(output, "");
1070 output.clear();
1071
1072 histogram_scale
1073 .write_bar(histogram_scale.count_per_char.get(), &mut output)
1074 .unwrap();
1075 assert_eq!(output, "∎");
1076 output.clear();
1077
1078 histogram_scale
1079 .write_bar(HISTOGRAM_BAR_WIDTH_CHARS * 1000 - 1, &mut output)
1080 .unwrap();
1081 assert_eq!(
1082 output,
1083 "∎".repeat(
1084 usize::try_from(HISTOGRAM_BAR_WIDTH_CHARS).expect("safe range, tiny value") - 1
1085 )
1086 );
1087 }
1088
1089 #[test]
1090 fn histogram_char_byte_count_is_correct() {
1091 assert_eq!("∎".len(), BYTES_PER_HISTOGRAM_BAR_CHAR.get());
1093
1094 let expected_chars = HISTOGRAM_BAR_CHARS.chars().count();
1096 let expected_bytes = expected_chars * BYTES_PER_HISTOGRAM_BAR_CHAR.get();
1097 assert_eq!(HISTOGRAM_BAR_CHARS.len(), expected_bytes);
1098 }
1099
1100 #[test]
1101 fn event_metrics_display_zero_count_reports_flat_zero() {
1102 let snapshot = ObservationBagSnapshot {
1105 count: 0,
1106 sum: 0,
1107 bucket_magnitudes: &[],
1108 bucket_counts: Box::new([]),
1109 };
1110
1111 let metrics = EventMetrics::new("zero_event".into(), snapshot);
1112
1113 let output = format!("{metrics}");
1114
1115 assert!(output.contains("zero_event: 0"));
1117
1118 assert_eq!(output.trim(), "zero_event: 0");
1120 }
1121
1122 #[test]
1123 fn event_metrics_new_zero_count_empty_buckets() {
1124 let snapshot = ObservationBagSnapshot {
1125 count: 0,
1126 sum: 0,
1127 bucket_magnitudes: &[],
1128 bucket_counts: Box::new([]),
1129 };
1130
1131 let metrics = EventMetrics::new("empty_event".into(), snapshot);
1132
1133 assert_eq!(metrics.name(), "empty_event");
1134 assert_eq!(metrics.count(), 0);
1135 assert_eq!(metrics.sum(), 0);
1136 assert_eq!(metrics.mean(), 0);
1137 assert!(metrics.histogram().is_none());
1138 }
1139
1140 #[test]
1141 fn event_metrics_new_non_zero_count_empty_buckets() {
1142 let snapshot = ObservationBagSnapshot {
1143 count: 10,
1144 sum: 100,
1145 bucket_magnitudes: &[],
1146 bucket_counts: Box::new([]),
1147 };
1148
1149 let metrics = EventMetrics::new("sum_event".into(), snapshot);
1150
1151 assert_eq!(metrics.name(), "sum_event");
1152 assert_eq!(metrics.count(), 10);
1153 assert_eq!(metrics.sum(), 100);
1154 assert_eq!(metrics.mean(), 10); assert!(metrics.histogram().is_none());
1156 }
1157
1158 #[test]
1159 fn event_metrics_new_non_zero_count_with_buckets() {
1160 let snapshot = ObservationBagSnapshot {
1165 count: 20,
1166 sum: 500,
1167 bucket_magnitudes: &[-10, 0, 10, 100],
1168 bucket_counts: vec![2, 3, 4, 5].into_boxed_slice(),
1169 };
1170
1171 let metrics = EventMetrics::new("histogram_event".into(), snapshot);
1172
1173 assert_eq!(metrics.name(), "histogram_event");
1174 assert_eq!(metrics.count(), 20);
1175 assert_eq!(metrics.sum(), 500);
1176 assert_eq!(metrics.mean(), 25); let histogram = metrics.histogram().expect("histogram should be present");
1179
1180 let magnitudes: Vec<_> = histogram.magnitudes().collect();
1182 assert_eq!(magnitudes, vec![-10, 0, 10, 100, Magnitude::MAX]);
1183
1184 let counts: Vec<_> = histogram.counts().collect();
1187 assert_eq!(counts, vec![2, 3, 4, 5, 6]);
1188
1189 let buckets: Vec<_> = histogram.buckets().collect();
1191 assert_eq!(buckets.len(), 5);
1192 assert_eq!(buckets[0], (-10, 2));
1193 assert_eq!(buckets[1], (0, 3));
1194 assert_eq!(buckets[2], (10, 4));
1195 assert_eq!(buckets[3], (100, 5));
1196 assert_eq!(buckets[4], (Magnitude::MAX, 6));
1197 }
1198
1199 #[test]
1200 fn event_metrics_fake_calculates_mean_correctly() {
1201 let metrics = EventMetrics::fake("test_event", 10, 100, None);
1203
1204 assert_eq!(metrics.name(), "test_event");
1205 assert_eq!(metrics.count(), 10);
1206 assert_eq!(metrics.sum(), 100);
1207 assert_eq!(metrics.mean(), 10); assert!(metrics.histogram().is_none());
1209 }
1210
1211 #[test]
1212 fn event_metrics_fake_calculates_mean_with_different_values() {
1213 let metrics = EventMetrics::fake("test_event", 25, 500, None);
1215
1216 assert_eq!(metrics.mean(), 20); }
1218
1219 #[test]
1220 fn event_metrics_fake_mean_zero_when_count_zero() {
1221 let metrics = EventMetrics::fake("test_event", 0, 100, None);
1223
1224 assert_eq!(metrics.count(), 0);
1225 assert_eq!(metrics.sum(), 100);
1226 assert_eq!(metrics.mean(), 0);
1227 }
1228
1229 #[test]
1230 fn event_metrics_fake_mean_handles_integer_division() {
1231 let metrics = EventMetrics::fake("test_event", 3, 10, None);
1236
1237 assert_eq!(metrics.mean(), 3); }
1239}