rate_log/
lib.rs

1//! # Rate Log
2//!
3//! A Rust library for rate-limited logging that prevents spam by tracking message frequency
4//! and duration. This crate helps reduce log noise by detecting repeated messages and only
5//! outputting warnings when configurable limits are exceeded.
6//!
7//! ## Features
8//!
9//! - **Count-based rate limiting**: Limit by number of repeated message occurrences
10//! - **Duration-based rate limiting**: Limit by accumulated time between repeated messages
11//! - **Unified tracking**: Always tracks both count and duration for comprehensive reporting
12//! - **Smart duration formatting**: Automatically formats durations in appropriate units (ms, s, m, h)
13//! - **Message deduplication**: Automatically resets counters when different messages are logged
14//! - **Zero-cost abstractions**: Minimal runtime overhead with compile-time optimizations
15//! - **Test-friendly**: Built-in output capture for unit testing
16//!
17//! ## Quick Start
18//!
19//! ```rust
20//! use rate_log::{RateLog, Limit};
21//! use std::time::Duration;
22//!
23//! // Create a rate limiter that allows up to 5 repeated messages
24//! let mut rate_log = RateLog::new(Limit::Rate(5));
25//!
26//! // First occurrence of any message is always printed immediately
27//! rate_log.log("This is a new message");  // Prints: "This is a new message"
28//!
29//! // Log the same message multiple times - no output until limit exceeded
30//! for i in 0..7 {
31//!     rate_log.log("This is a new message");
32//! }
33//! // After 5 repetitions, it will output: "Message: \"This is a new message\" repeat for 5 times in the past 10ms"
34//!
35//! // Different message gets printed immediately and resets counter
36//! rate_log.log("Different message");  // Prints: "Different message"
37//! ```
38//!
39//! ## Rate Limiting Types
40//!
41//! ### Count-based Limiting (`Limit::Rate`)
42//!
43//! Tracks the number of times the same message is logged consecutively:
44//!
45//! ```rust
46//! use rate_log::{RateLog, Limit};
47//!
48//! let mut logger = RateLog::new(Limit::Rate(3));
49//!
50//! logger.log("Error occurred");     // 1st occurrence - printed immediately: "Error occurred"
51//! logger.log("Error occurred");     // 2nd occurrence - counted silently
52//! logger.log("Error occurred");     // 3rd occurrence - counted silently
53//! logger.log("Error occurred");     // 4th occurrence - triggers: "Message: \"Error occurred\" repeat for 3 times in the past 15ms"
54//! logger.log("Different error");    // New message - printed immediately: "Different error"
55//! ```
56//!
57//! ### Duration-based Limiting (`Limit::Duration`)
58//!
59//! Accumulates the time elapsed between consecutive calls with the same message:
60//!
61//! ```rust
62//! use rate_log::{RateLog, Limit};
63//! use std::time::Duration;
64//! use std::thread;
65//!
66//! let mut logger = RateLog::new(Limit::Duration(Duration::from_secs(1)));
67//!
68//! logger.log("Periodic event");      // 1st occurrence - printed immediately: "Periodic event"
69//! thread::sleep(Duration::from_millis(300));
70//! logger.log("Periodic event");      // 300ms accumulated - silent
71//! thread::sleep(Duration::from_millis(800));
72//! logger.log("Periodic event");      // 1100ms total - triggers: "Message: \"Periodic event\" repeat for 2 times in the past 1s"
73//! ```
74//!
75//! ## Behavior
76//!
77//! - **New message printing**: Every new/different message is immediately printed to stdout
78//! - **Unified tracking**: Always tracks both message count and elapsed duration regardless of limit type
79//! - **Silent repetitions**: Repeated messages are counted silently until limit exceeded
80//! - **Smart duration formatting**: Automatically displays duration in appropriate units (ms, s, m, h) with whole numbers
81//! - **Comprehensive warnings**: Rate limit violations show both count and duration: "Message: \"text\" repeat for X times in the past Yms"
82//! - **Counter reset**: Switching to a different message resets all counters and prints the new message
83//!
84//! ## Use Cases
85//!
86//! - **Error logging**: Prevent log spam from repeated error conditions
87//! - **Debug output**: Control verbose debug message frequency
88//! - **Performance monitoring**: Rate-limit performance warnings
89//! - **Network logging**: Manage connection retry message frequency
90//! - **System monitoring**: Control repeated system state notifications
91
92use std::time::{Duration, Instant};
93
94/// Formats a duration into a human-readable string with at least two parts when possible.
95/// Shows hours and minutes for >= 1 hour, minutes and seconds for >= 1 minute,
96/// and single units for seconds and milliseconds.
97fn format_duration(duration: Duration) -> String {
98    let total_secs = duration.as_secs();
99    if total_secs >= 3600 {
100        let hours = total_secs / 3600;
101        let minutes = (total_secs % 3600) / 60;
102        format!("{}h{}m", hours, minutes)
103    } else if total_secs >= 60 {
104        let minutes = total_secs / 60;
105        let seconds = total_secs % 60;
106        format!("{}m{}s", minutes, seconds)
107    } else if total_secs >= 1 {
108        format!("{}s", total_secs)
109    } else {
110        format!("{}ms", duration.as_millis())
111    }
112}
113
114/// Defines the type and threshold for rate limiting.
115///
116/// `Limit` specifies how rate limiting should be applied - either by counting
117/// message occurrences or by measuring time duration between repeated messages.
118///
119/// # Examples
120///
121/// ```rust
122/// use rate_log::Limit;
123/// use std::time::Duration;
124///
125/// // Allow up to 10 repeated messages before triggering rate limit
126/// let count_limit = Limit::Rate(10);
127///
128/// // Allow up to 5 seconds of accumulated time between repeated messages
129/// let time_limit = Limit::Duration(Duration::from_secs(5));
130/// ```
131#[derive(Debug, PartialEq, PartialOrd)]
132pub enum Limit {
133    /// Count-based rate limiting.
134    ///
135    /// Triggers when the same message is repeated more than the specified number of times.
136    /// The counter resets when a different message is logged.
137    ///
138    /// # Example
139    /// ```rust
140    /// use rate_log::{RateLog, Limit};
141    ///
142    /// let mut logger = RateLog::new(Limit::Rate(3));
143    /// // Will trigger rate limit warning after 4th identical message
144    /// ```
145    Rate(u32),
146
147    /// Duration-based rate limiting.
148    ///
149    /// Triggers when the accumulated time between consecutive identical messages
150    /// exceeds the specified duration. Time is measured between actual calls,
151    /// providing real-world timing behavior.
152    ///
153    /// # Example
154    /// ```rust
155    /// use rate_log::{RateLog, Limit};
156    /// use std::time::Duration;
157    ///
158    /// let mut logger = RateLog::new(Limit::Duration(Duration::from_millis(500)));
159    /// // Will trigger if total elapsed time between identical messages > 500ms
160    /// ```
161    Duration(Duration),
162}
163
164#[derive(Debug)]
165struct State {
166    count: u32,
167    duration: Duration,
168    last_timestamp: Option<Instant>,
169}
170
171impl State {
172    fn new() -> Self {
173        State {
174            count: 0,
175            duration: Duration::from_secs(0),
176            last_timestamp: None,
177        }
178    }
179
180    fn reset(&mut self) {
181        self.count = 0;
182        self.duration = Duration::from_secs(0);
183        self.last_timestamp = None;
184    }
185
186    fn exceeds_limit(&self, limit: &Limit) -> bool {
187        match limit {
188            Limit::Rate(limit_count) => self.count >= *limit_count,
189            Limit::Duration(limit_duration) => self.duration >= *limit_duration,
190        }
191    }
192}
193
194/// A rate limiting logger that tracks message frequency and duration.
195///
196/// `RateLog` monitors how frequently the same message is logged and can enforce
197/// limits based on either count (number of occurrences) or time duration.
198/// It will output the message first time and then until the limits are exceeded.
199pub struct RateLog {
200    /// The maximum allowed limit for rate limiting.
201    /// This defines the threshold that triggers rate limit exceeded warnings.
202    /// For `Rate(n)`: maximum number of repeated messages allowed
203    /// For `Duration(d)`: maximum time duration allowed for repeated messages
204    limit: Limit,
205
206    /// The current tracking state containing count, duration, and timestamp.
207    /// Always tracks both message count and elapsed duration regardless of limit type,
208    /// enabling comprehensive rate limit reporting.
209    current: State,
210
211    /// The last message that was logged.
212    /// Used to detect when a different message is being logged, which resets
213    /// the rate limiting counters. Only identical messages contribute to rate limiting.
214    message: String,
215
216    /// Test-only field that captures output messages for verification in unit tests.
217    /// This field is only present when compiled with test configuration and allows
218    /// tests to verify the exact output without relying on stdout capture.
219    #[cfg(test)]
220    output: String,
221}
222
223impl RateLog {
224    /// Creates a new `RateLog` instance with the specified limit.
225    ///
226    /// The rate limiter starts with clean state - no previous messages tracked
227    /// and all counters at zero.
228    ///
229    /// # Arguments
230    ///
231    /// * `limit` - The rate limiting threshold to enforce
232    ///
233    /// # Examples
234    ///
235    /// ```rust
236    /// use rate_log::{RateLog, Limit};
237    /// use std::time::Duration;
238    ///
239    /// // Create count-based rate limiter
240    /// let count_limiter = RateLog::new(Limit::Rate(5));
241    ///
242    /// // Create duration-based rate limiter
243    /// let time_limiter = RateLog::new(Limit::Duration(Duration::from_secs(2)));
244    /// ```
245    pub fn new(limit: Limit) -> Self {
246        let current = State::new();
247
248        RateLog {
249            limit,
250            current,
251            message: String::new(),
252            #[cfg(test)]
253            output: String::new(),
254        }
255    }
256
257    /// Logs a message with rate limiting applied.
258    ///
259    /// This method immediately prints any new or different message to stdout, then tracks
260    /// repeated messages and enforces the configured rate limit. Repeated messages are
261    /// counted silently until the limit is exceeded.
262    ///
263    /// # Output Behavior
264    ///
265    /// - **New/different message**: Immediately printed to stdout and resets all counters
266    /// - **Repeated message**: Counted silently (no immediate output)
267    /// - **Limit exceeded**: Prints rate limit warning to stdout
268    ///
269    /// # Rate Limiting Behavior
270    ///
271    /// - **Count-based**: Increments counter for each repeated message
272    /// - **Duration-based**: Accumulates elapsed time between repeated messages
273    /// - **Message change**: Resets all tracking state and prints the new message
274    ///
275    /// # Arguments
276    ///
277    /// * `msg` - The message to log and track for rate limiting
278    ///
279    /// # Examples
280    ///
281    /// ```rust
282    /// use rate_log::{RateLog, Limit};
283    ///
284    /// let mut logger = RateLog::new(Limit::Rate(2));
285    ///
286    /// logger.log("Starting up");          // Prints: "Starting up"
287    /// logger.log("Error occurred");       // Prints: "Error occurred" (different message)
288    /// logger.log("Error occurred");       // Silent (1st repetition)
289    /// logger.log("Error occurred");       // Silent (2nd repetition)
290    /// logger.log("Error occurred");       // Prints: "Message: \"Error occurred\" repeat for 2 times in the past 15ms"
291    /// logger.log("Shutting down");        // Prints: "Shutting down" (different message)
292    /// ```
293    pub fn log(&mut self, msg: &str) {
294        let now = Instant::now();
295
296        if self.message != msg {
297            self.message = msg.to_string();
298            self.current.reset();
299
300            println!("{msg}");
301
302            #[cfg(test)]
303            {
304                self.output.push_str(msg);
305            }
306        } else {
307            self.current.count += 1;
308
309            if let Some(last_call) = self.current.last_timestamp {
310                let elapsed = now.duration_since(last_call);
311                self.current.duration += elapsed;
312            }
313
314            if self.current.exceeds_limit(&self.limit) {
315                let output = format!(
316                    "Message: \"{}\" repeat for {} times in the past {}",
317                    msg,
318                    self.current.count,
319                    format_duration(self.current.duration)
320                );
321
322                self.current.reset();
323
324                println!("{output}");
325
326                #[cfg(test)]
327                {
328                    self.output.push_str(&output);
329                }
330            }
331        }
332
333        self.current.last_timestamp = Some(now);
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_rate_log_exceed_time() {
343        let mut rate_log = RateLog::new(Limit::Rate(3));
344
345        // First call - should not exceed
346        rate_log.log("message1");
347        assert_eq!(rate_log.output, "message1");
348        rate_log.output.clear();
349
350        // Second call - should not exceed (current becomes 1, limit is 3)
351        rate_log.log("message1");
352        assert_eq!(rate_log.output, "");
353
354        // Third call - should not exceed (current becomes 2, limit is 3)
355        rate_log.log("message1");
356        assert_eq!(rate_log.output, "");
357
358        // Fourth call - should exceed (current becomes 3, limit is 3)
359        rate_log.log("message1");
360        assert_eq!(
361            rate_log.output,
362            "Message: \"message1\" repeat for 3 times in the past 0ms"
363        );
364        rate_log.output.clear();
365
366        // Fifth call - should not exceed (current becomes 1, limit is 3)
367        rate_log.log("message1");
368        assert_eq!(rate_log.output, "");
369
370        // Sixth call - should not exceed (current becomes 2, limit is 3)
371        rate_log.log("message1");
372        assert_eq!(rate_log.output, "");
373
374        // Seventh call - should exceed (current becomes 3, limit is 3)
375        rate_log.log("message1");
376        assert_eq!(
377            rate_log.output,
378            "Message: \"message1\" repeat for 3 times in the past 0ms"
379        );
380        rate_log.output.clear();
381    }
382
383    #[test]
384    fn test_rate_log_exceed_duration() {
385        use std::thread;
386
387        let mut rate_log = RateLog::new(Limit::Duration(Duration::from_millis(50)));
388
389        // First call
390        rate_log.log("message2");
391        assert_eq!(rate_log.output, "message2");
392        rate_log.output.clear();
393
394        // Second call after short delay - should not exceed
395        thread::sleep(Duration::from_millis(20));
396        rate_log.log("message2");
397        assert_eq!(rate_log.output, "");
398
399        // Third call after longer delay - should exceed the 50ms limit
400        thread::sleep(Duration::from_millis(40));
401        rate_log.log("message2");
402        assert_eq!(
403            rate_log.output,
404            "Message: \"message2\" repeat for 2 times in the past 60ms"
405        );
406        rate_log.output.clear();
407
408        rate_log.log("message2");
409        assert_eq!(rate_log.output, "");
410
411        thread::sleep(Duration::from_millis(50));
412        rate_log.log("message2");
413        assert_eq!(
414            rate_log.output,
415            "Message: \"message2\" repeat for 2 times in the past 50ms"
416        );
417        rate_log.output.clear();
418    }
419
420    #[test]
421    fn test_format_duration() {
422        // Test milliseconds (< 1 second)
423        let duration_ms = Duration::from_millis(500);
424        assert_eq!(format_duration(duration_ms), "500ms");
425
426        // Test seconds only (>= 1 second, < 1 minute)
427        let duration_s = Duration::from_secs(45);
428        assert_eq!(format_duration(duration_s), "45s");
429
430        // Test minutes and seconds (>= 1 minute, < 1 hour)
431        let duration_min = Duration::from_secs(3 * 60 + 25); // 3 minutes 25 seconds
432        assert_eq!(format_duration(duration_min), "3m25s");
433
434        // Test hours and minutes (>= 1 hour)
435        let duration_hour = Duration::from_secs(2 * 3600 + 45 * 60); // 2 hours 45 minutes
436        assert_eq!(format_duration(duration_hour), "2h45m");
437    }
438}