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 whole numbers only.
95/// Automatically chooses the most appropriate unit (hours, minutes, seconds, or milliseconds).
96fn format_duration(duration: Duration) -> String {
97    let total_secs = duration.as_secs();
98    if total_secs >= 3600 {
99        format!("{}h", total_secs / 3600)
100    } else if total_secs >= 60 {
101        format!("{}m", total_secs / 60)
102    } else if total_secs >= 1 {
103        format!("{}s", total_secs)
104    } else {
105        format!("{}ms", duration.as_millis())
106    }
107}
108
109/// Defines the type and threshold for rate limiting.
110///
111/// `Limit` specifies how rate limiting should be applied - either by counting
112/// message occurrences or by measuring time duration between repeated messages.
113///
114/// # Examples
115///
116/// ```rust
117/// use rate_log::Limit;
118/// use std::time::Duration;
119///
120/// // Allow up to 10 repeated messages before triggering rate limit
121/// let count_limit = Limit::Rate(10);
122///
123/// // Allow up to 5 seconds of accumulated time between repeated messages
124/// let time_limit = Limit::Duration(Duration::from_secs(5));
125/// ```
126#[derive(Debug, PartialEq, PartialOrd)]
127pub enum Limit {
128    /// Count-based rate limiting.
129    ///
130    /// Triggers when the same message is repeated more than the specified number of times.
131    /// The counter resets when a different message is logged.
132    ///
133    /// # Example
134    /// ```rust
135    /// use rate_log::{RateLog, Limit};
136    ///
137    /// let mut logger = RateLog::new(Limit::Rate(3));
138    /// // Will trigger rate limit warning after 4th identical message
139    /// ```
140    Rate(u32),
141
142    /// Duration-based rate limiting.
143    ///
144    /// Triggers when the accumulated time between consecutive identical messages
145    /// exceeds the specified duration. Time is measured between actual calls,
146    /// providing real-world timing behavior.
147    ///
148    /// # Example
149    /// ```rust
150    /// use rate_log::{RateLog, Limit};
151    /// use std::time::Duration;
152    ///
153    /// let mut logger = RateLog::new(Limit::Duration(Duration::from_millis(500)));
154    /// // Will trigger if total elapsed time between identical messages > 500ms
155    /// ```
156    Duration(Duration),
157}
158
159#[derive(Debug)]
160struct State {
161    count: u32,
162    duration: Duration,
163    last_timestamp: Option<Instant>,
164}
165
166impl State {
167    fn new() -> Self {
168        State {
169            count: 0,
170            duration: Duration::from_secs(0),
171            last_timestamp: None,
172        }
173    }
174
175    fn reset(&mut self) {
176        self.count = 0;
177        self.duration = Duration::from_secs(0);
178        self.last_timestamp = None;
179    }
180
181    fn exceeds_limit(&self, limit: &Limit) -> bool {
182        match limit {
183            Limit::Rate(limit_count) => self.count >= *limit_count,
184            Limit::Duration(limit_duration) => self.duration >= *limit_duration,
185        }
186    }
187}
188
189/// A rate limiting logger that tracks message frequency and duration.
190///
191/// `RateLog` monitors how frequently the same message is logged and can enforce
192/// limits based on either count (number of occurrences) or time duration.
193/// It will output the message first time and then until the limits are exceeded.
194pub struct RateLog {
195    /// The maximum allowed limit for rate limiting.
196    /// This defines the threshold that triggers rate limit exceeded warnings.
197    /// For `Rate(n)`: maximum number of repeated messages allowed
198    /// For `Duration(d)`: maximum time duration allowed for repeated messages
199    limit: Limit,
200
201    /// The current tracking state containing count, duration, and timestamp.
202    /// Always tracks both message count and elapsed duration regardless of limit type,
203    /// enabling comprehensive rate limit reporting.
204    current: State,
205
206    /// The last message that was logged.
207    /// Used to detect when a different message is being logged, which resets
208    /// the rate limiting counters. Only identical messages contribute to rate limiting.
209    message: String,
210
211    /// Test-only field that captures output messages for verification in unit tests.
212    /// This field is only present when compiled with test configuration and allows
213    /// tests to verify the exact output without relying on stdout capture.
214    #[cfg(test)]
215    output: String,
216}
217
218impl RateLog {
219    /// Creates a new `RateLog` instance with the specified limit.
220    ///
221    /// The rate limiter starts with clean state - no previous messages tracked
222    /// and all counters at zero.
223    ///
224    /// # Arguments
225    ///
226    /// * `limit` - The rate limiting threshold to enforce
227    ///
228    /// # Examples
229    ///
230    /// ```rust
231    /// use rate_log::{RateLog, Limit};
232    /// use std::time::Duration;
233    ///
234    /// // Create count-based rate limiter
235    /// let count_limiter = RateLog::new(Limit::Rate(5));
236    ///
237    /// // Create duration-based rate limiter
238    /// let time_limiter = RateLog::new(Limit::Duration(Duration::from_secs(2)));
239    /// ```
240    pub fn new(limit: Limit) -> Self {
241        let current = State::new();
242
243        RateLog {
244            limit,
245            current,
246            message: String::new(),
247            #[cfg(test)]
248            output: String::new(),
249        }
250    }
251
252    /// Logs a message with rate limiting applied.
253    ///
254    /// This method immediately prints any new or different message to stdout, then tracks
255    /// repeated messages and enforces the configured rate limit. Repeated messages are
256    /// counted silently until the limit is exceeded.
257    ///
258    /// # Output Behavior
259    ///
260    /// - **New/different message**: Immediately printed to stdout and resets all counters
261    /// - **Repeated message**: Counted silently (no immediate output)
262    /// - **Limit exceeded**: Prints rate limit warning to stdout
263    ///
264    /// # Rate Limiting Behavior
265    ///
266    /// - **Count-based**: Increments counter for each repeated message
267    /// - **Duration-based**: Accumulates elapsed time between repeated messages
268    /// - **Message change**: Resets all tracking state and prints the new message
269    ///
270    /// # Arguments
271    ///
272    /// * `msg` - The message to log and track for rate limiting
273    ///
274    /// # Examples
275    ///
276    /// ```rust
277    /// use rate_log::{RateLog, Limit};
278    ///
279    /// let mut logger = RateLog::new(Limit::Rate(2));
280    ///
281    /// logger.log("Starting up");          // Prints: "Starting up"
282    /// logger.log("Error occurred");       // Prints: "Error occurred" (different message)
283    /// logger.log("Error occurred");       // Silent (1st repetition)
284    /// logger.log("Error occurred");       // Silent (2nd repetition)
285    /// logger.log("Error occurred");       // Prints: "Message: \"Error occurred\" repeat for 2 times in the past 15ms"
286    /// logger.log("Shutting down");        // Prints: "Shutting down" (different message)
287    /// ```
288    pub fn log(&mut self, msg: &str) {
289        let now = Instant::now();
290
291        if self.message != msg {
292            self.message = msg.to_string();
293            self.current.reset();
294
295            println!("{msg}");
296
297            #[cfg(test)]
298            {
299                self.output.push_str(msg);
300            }
301        } else {
302            self.current.count += 1;
303
304            if let Some(last_call) = self.current.last_timestamp {
305                let elapsed = now.duration_since(last_call);
306                self.current.duration += elapsed;
307            }
308
309            if self.current.exceeds_limit(&self.limit) {
310                let output = format!(
311                    "Message: \"{}\" repeat for {} times in the past {}",
312                    msg,
313                    self.current.count,
314                    format_duration(self.current.duration)
315                );
316                println!("{output}");
317
318                self.current.reset();
319
320                println!("{output}");
321
322                #[cfg(test)]
323                {
324                    self.output.push_str(&output);
325                }
326            }
327        }
328
329        self.current.last_timestamp = Some(now);
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_rate_log_exceed_time() {
339        let mut rate_log = RateLog::new(Limit::Rate(3));
340
341        // First call - should not exceed
342        rate_log.log("message1");
343        assert_eq!(rate_log.output, "message1");
344        rate_log.output.clear();
345
346        // Second call - should not exceed (current becomes 1, limit is 3)
347        rate_log.log("message1");
348        assert_eq!(rate_log.output, "");
349
350        // Third call - should not exceed (current becomes 2, limit is 3)
351        rate_log.log("message1");
352        assert_eq!(rate_log.output, "");
353
354        // Fourth call - should exceed (current becomes 3, limit is 3)
355        rate_log.log("message1");
356        assert_eq!(
357            rate_log.output,
358            "Message: \"message1\" repeat for 3 times in the past 0ms"
359        );
360        rate_log.output.clear();
361
362        // Fifth call - should not exceed (current becomes 1, limit is 3)
363        rate_log.log("message1");
364        assert_eq!(rate_log.output, "");
365
366        // Sixth call - should not exceed (current becomes 2, limit is 3)
367        rate_log.log("message1");
368        assert_eq!(rate_log.output, "");
369
370        // Seventh call - should exceed (current becomes 3, limit is 3)
371        rate_log.log("message1");
372        assert_eq!(
373            rate_log.output,
374            "Message: \"message1\" repeat for 3 times in the past 0ms"
375        );
376        rate_log.output.clear();
377    }
378
379    #[test]
380    fn test_rate_log_exceed_duration() {
381        use std::thread;
382
383        let mut rate_log = RateLog::new(Limit::Duration(Duration::from_millis(50)));
384
385        // First call
386        rate_log.log("message2");
387        assert_eq!(rate_log.output, "message2");
388        rate_log.output.clear();
389
390        // Second call after short delay - should not exceed
391        thread::sleep(Duration::from_millis(20));
392        rate_log.log("message2");
393        assert_eq!(rate_log.output, "");
394
395        // Third call after longer delay - should exceed the 50ms limit
396        thread::sleep(Duration::from_millis(40));
397        rate_log.log("message2");
398        assert_eq!(
399            rate_log.output,
400            "Message: \"message2\" repeat for 2 times in the past 60ms"
401        );
402        rate_log.output.clear();
403
404        rate_log.log("message2");
405        assert_eq!(rate_log.output, "");
406
407        thread::sleep(Duration::from_millis(50));
408        rate_log.log("message2");
409        assert_eq!(
410            rate_log.output,
411            "Message: \"message2\" repeat for 2 times in the past 50ms"
412        );
413        rate_log.output.clear();
414    }
415}