metrale_telemetry/
instrument.rs1use std::sync::atomic::{AtomicU64, Ordering};
17
18#[derive(Debug, Default)]
20pub struct Counter(AtomicU64);
21
22impl Counter {
23 pub const fn new() -> Self {
24 Self(AtomicU64::new(0))
25 }
26 #[inline]
27 pub fn add(&self, n: u64) {
28 self.0.fetch_add(n, Ordering::Relaxed);
29 }
30 pub fn get(&self) -> u64 {
31 self.0.load(Ordering::Relaxed)
32 }
33}
34
35#[derive(Debug, Default)]
37pub struct Gauge(AtomicU64);
38
39impl Gauge {
40 pub const fn new() -> Self {
41 Self(AtomicU64::new(0))
42 }
43 #[inline]
44 pub fn set(&self, v: u64) {
45 self.0.store(v, Ordering::Relaxed);
46 }
47 pub fn get(&self) -> u64 {
48 self.0.load(Ordering::Relaxed)
49 }
50}
51
52#[derive(Debug)]
55pub struct GaugeF64(AtomicU64);
56
57const UNSET: u64 = f64::NAN.to_bits();
58
59impl Default for GaugeF64 {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl GaugeF64 {
66 pub const fn new() -> Self {
67 Self(AtomicU64::new(UNSET))
68 }
69 #[inline]
70 pub fn set(&self, v: Option<f64>) {
71 self.0
72 .store(v.map_or(UNSET, f64::to_bits), Ordering::Relaxed);
73 }
74 pub fn get(&self) -> Option<f64> {
75 let v = f64::from_bits(self.0.load(Ordering::Relaxed));
76 (!v.is_nan()).then_some(v)
77 }
78}
79
80#[derive(Debug)]
85pub struct Histogram<const N: usize, const SHIFT: u32> {
86 buckets: [AtomicU64; N],
87 overflow: AtomicU64,
88 sum: AtomicU64,
89 count: AtomicU64,
90}
91
92impl<const N: usize, const SHIFT: u32> Default for Histogram<N, SHIFT> {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct HistogramSnapshot {
102 pub upper_bounds: Vec<u64>,
103 pub cumulative: Vec<u64>,
104 pub sum: u64,
105 pub count: u64,
106}
107
108impl<const N: usize, const SHIFT: u32> Histogram<N, SHIFT> {
109 pub const fn new() -> Self {
110 Self {
111 buckets: [const { AtomicU64::new(0) }; N],
112 overflow: AtomicU64::new(0),
113 sum: AtomicU64::new(0),
114 count: AtomicU64::new(0),
115 }
116 }
117
118 pub const fn bucket_of(v: u64) -> usize {
120 if v <= (1u64 << SHIFT) {
121 return 0;
122 }
123 let ceil_log2 = 64 - (v - 1).leading_zeros();
125 let i = (ceil_log2 - SHIFT) as usize;
126 if i < N { i } else { N }
127 }
128
129 pub const fn upper_bound(i: usize) -> u64 {
130 1u64 << (SHIFT + i as u32)
131 }
132
133 #[inline]
134 pub fn observe(&self, v: u64) {
135 let i = Self::bucket_of(v);
136 let slot = if i < N {
137 &self.buckets[i]
138 } else {
139 &self.overflow
140 };
141 slot.fetch_add(1, Ordering::Relaxed);
142 self.sum.fetch_add(v, Ordering::Relaxed);
143 self.count.fetch_add(1, Ordering::Relaxed);
144 }
145
146 pub fn count(&self) -> u64 {
147 self.count.load(Ordering::Relaxed)
148 }
149
150 pub fn snapshot(&self) -> HistogramSnapshot {
151 let mut running = 0;
152 let cumulative = self
153 .buckets
154 .iter()
155 .map(|b| {
156 running += b.load(Ordering::Relaxed);
157 running
158 })
159 .collect();
160 HistogramSnapshot {
161 upper_bounds: (0..N).map(Self::upper_bound).collect(),
162 cumulative,
163 sum: self.sum.load(Ordering::Relaxed),
164 count: self.count.load(Ordering::Relaxed),
165 }
166 }
167}
168
169pub type NanosHistogram = Histogram<27, 10>;
171pub type CountHistogram = Histogram<11, 0>;
173pub type MillijouleHistogram = Histogram<25, 0>;
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 type H = Histogram<4, 2>;
181
182 #[test]
183 fn values_land_in_the_first_bucket_whose_bound_covers_them() {
184 assert_eq!(H::bucket_of(0), 0);
186 assert_eq!(H::bucket_of(4), 0);
187 assert_eq!(H::bucket_of(5), 1);
188 assert_eq!(H::bucket_of(8), 1);
189 assert_eq!(H::bucket_of(9), 2);
190 assert_eq!(H::bucket_of(32), 3);
191 assert_eq!(H::bucket_of(33), 4, "past the last bound is +Inf");
192 assert_eq!(H::bucket_of(u64::MAX), 4);
193 }
194
195 #[test]
196 fn the_snapshot_is_cumulative_and_keeps_sum_and_count() {
197 let h = H::new();
198 for v in [1, 6, 7, 30, 1000] {
199 h.observe(v);
200 }
201 let s = h.snapshot();
202 assert_eq!(s.upper_bounds, vec![4, 8, 16, 32]);
203 assert_eq!(s.cumulative, vec![1, 3, 3, 4]);
204 assert_eq!(s.count, 5, "the +Inf observation counts in the total");
205 assert_eq!(s.sum, 1044);
206 }
207
208 #[test]
209 fn an_unset_float_gauge_is_none_not_zero() {
210 let g = GaugeF64::new();
211 assert_eq!(g.get(), None);
212 g.set(Some(0.25));
213 assert_eq!(g.get(), Some(0.25));
214 g.set(None);
215 assert_eq!(g.get(), None);
216 }
217
218 #[test]
219 fn the_nanos_histogram_spans_a_microsecond_to_a_minute() {
220 assert_eq!(NanosHistogram::upper_bound(0), 1024);
221 assert!(NanosHistogram::upper_bound(26) > 60_000_000_000);
222 }
223}