metricator/
lib.rs

1use monotonic_time_rs::Millis;
2use monotonic_time_rs::MillisDuration;
3/*
4 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/piot/metricator
5 * Licensed under the MIT License. See LICENSE in the project root for license information.
6 */
7use num_traits::Bounded;
8use num_traits::ToPrimitive;
9use std::cmp::PartialOrd;
10use std::fmt::Debug;
11use std::fmt::Display;
12use std::ops::{Add, Div};
13
14#[derive(Debug, PartialEq)]
15pub struct MinMaxAvg<T: Display> {
16    pub min: T,
17    pub avg: f32,
18    pub max: T,
19    pub unit: &'static str,
20}
21
22impl<T: Display> MinMaxAvg<T> {
23    pub const fn new(min: T, avg: f32, max: T) -> Self {
24        Self {
25            min,
26            avg,
27            max,
28            unit: "",
29        }
30    }
31
32    pub const fn with_unit(mut self, unit: &'static str) -> Self {
33        self.unit = unit;
34        self
35    }
36}
37
38impl<T: Display> Display for MinMaxAvg<T> {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(
41            f,
42            "min:{}{}, avg:{}{}, max:{}{}",
43            self.min, self.unit, self.avg, self.unit, self.max, self.unit,
44        )
45    }
46}
47
48/// Evaluating how many times something occurs every second.
49#[derive(Debug)]
50pub struct RateMetric {
51    count: u32,
52    last_calculated_at: Millis,
53    average: f32,
54    measurement_interval: MillisDuration,
55}
56
57impl RateMetric {
58    /// Creates a new `RateMetric` instance.
59    ///
60    /// # Arguments
61    ///
62    /// * `time` - The initial [`Millis`] from which time tracking starts.
63    ///
64    /// # Returns
65    ///
66    /// A `RateMetric` instance with an initialized count and time.
67    #[must_use] pub const fn new(time: Millis) -> Self {
68        Self {
69            count: 0,
70            last_calculated_at: time,
71            measurement_interval: MillisDuration::from_millis(500),
72            average: 0.0,
73        }
74    }
75
76    #[must_use] pub fn with_interval(time: Millis, measurement_interval: f32) -> Self {
77        Self {
78            count: 0,
79            last_calculated_at: time,
80            measurement_interval: MillisDuration::from_secs(measurement_interval)
81                .expect("measurement interval should be positive"),
82            average: 0.0,
83        }
84    }
85
86    /// Increments the internal event count by one.
87    ///
88    /// Call this method each time an event occurs that you want to track.
89    pub const fn increment(&mut self) {
90        self.count += 1;
91    }
92
93    /// Adds a specified number of events to the internal count.
94    ///
95    /// # Arguments
96    ///
97    /// * `count` - The number of events to add.
98    pub const fn add(&mut self, count: u32) {
99        self.count += count;
100    }
101
102    /// Updates the rate calculation based on the elapsed time since the last calculation.
103    ///
104    /// # Arguments
105    ///
106    /// * `time` - The current [`Millis`] representing the time at which the update is triggered.
107    ///
108    /// If the elapsed time since the last calculation is less than the measurement interval,
109    /// this method returns early without updating the rate.
110    pub fn update(&mut self, time: Millis) {
111        let elapsed_time = time - self.last_calculated_at;
112        if elapsed_time < self.measurement_interval {
113            return;
114        }
115
116        let rate = self.count as f32 / elapsed_time.as_secs();
117
118        // Reset the counter and start time for the next period
119        self.count = 0;
120        self.last_calculated_at = time;
121        self.average = rate;
122    }
123
124    #[must_use] pub const fn rate(&self) -> f32 {
125        self.average
126    }
127}
128
129/// Tracks minimum, maximum, and average values for numeric data (e.g., `i32`, `u32`, `f32`).
130#[derive(Debug)]
131pub struct AggregateMetric<T> {
132    sum: T,
133    count: u8,
134    max: T,
135    min: T,
136    threshold: u8,
137    max_ack: T,
138    min_ack: T,
139    avg: f32,
140    avg_is_set: bool,
141    unit: &'static str,
142}
143
144impl<T> AggregateMetric<T>
145where
146    T: Add<Output = T>
147        + Div<Output = T>
148        + Copy
149        + PartialOrd
150        + Default
151        + Debug
152        + Display
153        + Bounded
154        + ToPrimitive,
155{
156    /// Creates a new `AggregateMetric` instance with a given threshold.
157    pub fn new(threshold: u8) -> Result<Self, String> {
158        if threshold == 0 {
159            Err("threshold can not be zero".to_string())
160        } else {
161            Ok(Self {
162                sum: T::default(),
163                count: 0,
164                max: T::default(),
165                min: T::default(),
166                threshold,
167                max_ack: T::min_value(),
168                min_ack: T::max_value(),
169                avg: 0.0,
170                avg_is_set: false,
171                unit: "",
172            })
173        }
174    }
175
176    pub const fn with_unit(mut self, unit: &'static str) -> Self {
177        self.unit = unit;
178        self
179    }
180
181    /// Calculates the mean value, returning `None` if no values have been added.
182    pub const fn average(&self) -> Option<f32> {
183        if self.avg_is_set {
184            Some(self.avg)
185        } else {
186            None
187        }
188    }
189
190    /// Adds a value of type `T` to the metric.
191    pub fn add(&mut self, value: T) {
192        self.sum = self.sum + value;
193        self.count += 1;
194
195        // Update the max and min acknowledgments
196        if value > self.max_ack {
197            self.max_ack = value;
198        }
199        if value < self.min_ack {
200            self.min_ack = value;
201        }
202
203        // Check if the threshold is reached to calculate stats and reset counters
204        if self.count >= self.threshold {
205            let sum_f32 = self.sum.to_f32().unwrap_or(0.0);
206            let avg_f32 = sum_f32 / f32::from(self.count);
207
208            self.avg = avg_f32;
209
210            self.min = self.min_ack;
211            self.max = self.max_ack;
212            self.max_ack = T::min_value();
213            self.min_ack = T::max_value();
214            self.count = 0;
215            self.avg_is_set = true;
216            self.sum = T::default();
217        }
218    }
219
220    /// Returns the minimum, average, and maximum values as a tuple, if available.
221    pub const fn values(&self) -> Option<MinMaxAvg<T>> {
222        if self.avg_is_set {
223            Some(MinMaxAvg::new(self.min, self.avg, self.max).with_unit(self.unit))
224        } else {
225            None
226        }
227    }
228}