rate-log 0.3.0

A Rust library for rate-limited logging that prevents spam by tracking message frequency and duration.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//! # Rate Log
//!
//! A Rust library for rate-limited logging that prevents spam by tracking message frequency
//! and duration. This crate helps reduce log noise by detecting repeated messages and only
//! outputting warnings when configurable limits are exceeded.
//!
//! ## Features
//!
//! - **Count-based rate limiting**: Limit by number of repeated message occurrences
//! - **Duration-based rate limiting**: Limit by accumulated time between repeated messages
//! - **Unified tracking**: Always tracks both count and duration for comprehensive reporting
//! - **Smart duration formatting**: Automatically formats durations in appropriate units (ms, s, m, h)
//! - **Message deduplication**: Automatically resets counters when different messages are logged
//! - **Zero-cost abstractions**: Minimal runtime overhead with compile-time optimizations
//! - **Test-friendly**: Built-in output capture for unit testing
//!
//! ## Quick Start
//!
//! ```rust
//! use rate_log::{RateLog, Limit};
//! use std::time::Duration;
//!
//! // Create a rate limiter that allows up to 5 repeated messages
//! let mut rate_log = RateLog::new(Limit::Rate(5));
//!
//! // First occurrence of any message is always printed immediately
//! rate_log.log("This is a new message");  // Prints: "This is a new message"
//!
//! // Log the same message multiple times - no output until limit exceeded
//! for i in 0..7 {
//!     rate_log.log("This is a new message");
//! }
//! // After 5 repetitions, it will output: "Message: \"This is a new message\" repeat for 5 times in the past 10ms"
//!
//! // Different message gets printed immediately and resets counter
//! rate_log.log("Different message");  // Prints: "Different message"
//! ```
//!
//! ## Rate Limiting Types
//!
//! ### Count-based Limiting (`Limit::Rate`)
//!
//! Tracks the number of times the same message is logged consecutively:
//!
//! ```rust
//! use rate_log::{RateLog, Limit};
//!
//! let mut logger = RateLog::new(Limit::Rate(3));
//!
//! logger.log("Error occurred");     // 1st occurrence - printed immediately: "Error occurred"
//! logger.log("Error occurred");     // 2nd occurrence - counted silently
//! logger.log("Error occurred");     // 3rd occurrence - counted silently
//! logger.log("Error occurred");     // 4th occurrence - triggers: "Message: \"Error occurred\" repeat for 3 times in the past 15ms"
//! logger.log("Different error");    // New message - printed immediately: "Different error"
//! ```
//!
//! ### Duration-based Limiting (`Limit::Duration`)
//!
//! Accumulates the time elapsed between consecutive calls with the same message:
//!
//! ```rust
//! use rate_log::{RateLog, Limit};
//! use std::time::Duration;
//! use std::thread;
//!
//! let mut logger = RateLog::new(Limit::Duration(Duration::from_secs(1)));
//!
//! logger.log("Periodic event");      // 1st occurrence - printed immediately: "Periodic event"
//! thread::sleep(Duration::from_millis(300));
//! logger.log("Periodic event");      // 300ms accumulated - silent
//! thread::sleep(Duration::from_millis(800));
//! logger.log("Periodic event");      // 1100ms total - triggers: "Message: \"Periodic event\" repeat for 2 times in the past 1s"
//! ```
//!
//! ## Behavior
//!
//! - **New message printing**: Every new/different message is immediately printed to stdout
//! - **Unified tracking**: Always tracks both message count and elapsed duration regardless of limit type
//! - **Silent repetitions**: Repeated messages are counted silently until limit exceeded
//! - **Smart duration formatting**: Automatically displays duration in appropriate units (ms, s, m, h) with whole numbers
//! - **Comprehensive warnings**: Rate limit violations show both count and duration: "Message: \"text\" repeat for X times in the past Yms"
//! - **Counter reset**: Switching to a different message resets all counters and prints the new message
//!
//! ## Use Cases
//!
//! - **Error logging**: Prevent log spam from repeated error conditions
//! - **Debug output**: Control verbose debug message frequency
//! - **Performance monitoring**: Rate-limit performance warnings
//! - **Network logging**: Manage connection retry message frequency
//! - **System monitoring**: Control repeated system state notifications

use std::time::{Duration, Instant};

/// Formats a duration into a human-readable string with at least two parts when possible.
/// Shows hours and minutes for >= 1 hour, minutes and seconds for >= 1 minute,
/// and single units for seconds and milliseconds.
fn format_duration(duration: Duration) -> String {
    let total_secs = duration.as_secs();
    if total_secs >= 3600 {
        let hours = total_secs / 3600;
        let minutes = (total_secs % 3600) / 60;
        format!("{}h{}m", hours, minutes)
    } else if total_secs >= 60 {
        let minutes = total_secs / 60;
        let seconds = total_secs % 60;
        format!("{}m{}s", minutes, seconds)
    } else if total_secs >= 1 {
        format!("{}s", total_secs)
    } else {
        format!("{}ms", duration.as_millis())
    }
}

/// Defines the type and threshold for rate limiting.
///
/// `Limit` specifies how rate limiting should be applied - either by counting
/// message occurrences or by measuring time duration between repeated messages.
///
/// # Examples
///
/// ```rust
/// use rate_log::Limit;
/// use std::time::Duration;
///
/// // Allow up to 10 repeated messages before triggering rate limit
/// let count_limit = Limit::Rate(10);
///
/// // Allow up to 5 seconds of accumulated time between repeated messages
/// let time_limit = Limit::Duration(Duration::from_secs(5));
/// ```
#[derive(Debug, PartialEq, PartialOrd)]
pub enum Limit {
    /// Count-based rate limiting.
    ///
    /// Triggers when the same message is repeated more than the specified number of times.
    /// The counter resets when a different message is logged.
    ///
    /// # Example
    /// ```rust
    /// use rate_log::{RateLog, Limit};
    ///
    /// let mut logger = RateLog::new(Limit::Rate(3));
    /// // Will trigger rate limit warning after 4th identical message
    /// ```
    Rate(u32),

    /// Duration-based rate limiting.
    ///
    /// Triggers when the accumulated time between consecutive identical messages
    /// exceeds the specified duration. Time is measured between actual calls,
    /// providing real-world timing behavior.
    ///
    /// # Example
    /// ```rust
    /// use rate_log::{RateLog, Limit};
    /// use std::time::Duration;
    ///
    /// let mut logger = RateLog::new(Limit::Duration(Duration::from_millis(500)));
    /// // Will trigger if total elapsed time between identical messages > 500ms
    /// ```
    Duration(Duration),
}

#[derive(Debug)]
struct State {
    count: u32,
    duration: Duration,
    last_timestamp: Option<Instant>,
}

impl State {
    fn new() -> Self {
        State {
            count: 0,
            duration: Duration::from_secs(0),
            last_timestamp: None,
        }
    }

    fn reset(&mut self) {
        self.count = 0;
        self.duration = Duration::from_secs(0);
        self.last_timestamp = None;
    }

    fn exceeds_limit(&self, limit: &Limit) -> bool {
        match limit {
            Limit::Rate(limit_count) => self.count >= *limit_count,
            Limit::Duration(limit_duration) => self.duration >= *limit_duration,
        }
    }
}

/// A rate limiting logger that tracks message frequency and duration.
///
/// `RateLog` monitors how frequently the same message is logged and can enforce
/// limits based on either count (number of occurrences) or time duration.
/// It will output the message first time and then until the limits are exceeded.
pub struct RateLog {
    /// The maximum allowed limit for rate limiting.
    /// This defines the threshold that triggers rate limit exceeded warnings.
    /// For `Rate(n)`: maximum number of repeated messages allowed
    /// For `Duration(d)`: maximum time duration allowed for repeated messages
    limit: Limit,

    /// The current tracking state containing count, duration, and timestamp.
    /// Always tracks both message count and elapsed duration regardless of limit type,
    /// enabling comprehensive rate limit reporting.
    current: State,

    /// The last message that was logged.
    /// Used to detect when a different message is being logged, which resets
    /// the rate limiting counters. Only identical messages contribute to rate limiting.
    message: String,

    /// Test-only field that captures output messages for verification in unit tests.
    /// This field is only present when compiled with test configuration and allows
    /// tests to verify the exact output without relying on stdout capture.
    #[cfg(test)]
    output: String,
}

impl RateLog {
    /// Creates a new `RateLog` instance with the specified limit.
    ///
    /// The rate limiter starts with clean state - no previous messages tracked
    /// and all counters at zero.
    ///
    /// # Arguments
    ///
    /// * `limit` - The rate limiting threshold to enforce
    ///
    /// # Examples
    ///
    /// ```rust
    /// use rate_log::{RateLog, Limit};
    /// use std::time::Duration;
    ///
    /// // Create count-based rate limiter
    /// let count_limiter = RateLog::new(Limit::Rate(5));
    ///
    /// // Create duration-based rate limiter
    /// let time_limiter = RateLog::new(Limit::Duration(Duration::from_secs(2)));
    /// ```
    pub fn new(limit: Limit) -> Self {
        let current = State::new();

        RateLog {
            limit,
            current,
            message: String::new(),
            #[cfg(test)]
            output: String::new(),
        }
    }

    /// Logs a message with rate limiting applied.
    ///
    /// This method immediately prints any new or different message to stdout, then tracks
    /// repeated messages and enforces the configured rate limit. Repeated messages are
    /// counted silently until the limit is exceeded.
    ///
    /// # Output Behavior
    ///
    /// - **New/different message**: Immediately printed to stdout and resets all counters
    /// - **Repeated message**: Counted silently (no immediate output)
    /// - **Limit exceeded**: Prints rate limit warning to stdout
    ///
    /// # Rate Limiting Behavior
    ///
    /// - **Count-based**: Increments counter for each repeated message
    /// - **Duration-based**: Accumulates elapsed time between repeated messages
    /// - **Message change**: Resets all tracking state and prints the new message
    ///
    /// # Arguments
    ///
    /// * `msg` - The message to log and track for rate limiting
    ///
    /// # Examples
    ///
    /// ```rust
    /// use rate_log::{RateLog, Limit};
    ///
    /// let mut logger = RateLog::new(Limit::Rate(2));
    ///
    /// logger.log("Starting up");          // Prints: "Starting up"
    /// logger.log("Error occurred");       // Prints: "Error occurred" (different message)
    /// logger.log("Error occurred");       // Silent (1st repetition)
    /// logger.log("Error occurred");       // Silent (2nd repetition)
    /// logger.log("Error occurred");       // Prints: "Message: \"Error occurred\" repeat for 2 times in the past 15ms"
    /// logger.log("Shutting down");        // Prints: "Shutting down" (different message)
    /// ```
    pub fn log(&mut self, msg: &str) {
        let now = Instant::now();

        if self.message != msg {
            self.message = msg.to_string();
            self.current.reset();

            println!("{msg}");

            #[cfg(test)]
            {
                self.output.push_str(msg);
            }
        } else {
            self.current.count += 1;

            if let Some(last_call) = self.current.last_timestamp {
                let elapsed = now.duration_since(last_call);
                self.current.duration += elapsed;
            }

            if self.current.exceeds_limit(&self.limit) {
                let output = format!(
                    "Message: \"{}\" repeat for {} times in the past {}",
                    msg,
                    self.current.count,
                    format_duration(self.current.duration)
                );

                self.current.reset();

                println!("{output}");

                #[cfg(test)]
                {
                    self.output.push_str(&output);
                }
            }
        }

        self.current.last_timestamp = Some(now);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_rate_log_exceed_time() {
        let mut rate_log = RateLog::new(Limit::Rate(3));

        // First call - should not exceed
        rate_log.log("message1");
        assert_eq!(rate_log.output, "message1");
        rate_log.output.clear();

        // Second call - should not exceed (current becomes 1, limit is 3)
        rate_log.log("message1");
        assert_eq!(rate_log.output, "");

        // Third call - should not exceed (current becomes 2, limit is 3)
        rate_log.log("message1");
        assert_eq!(rate_log.output, "");

        // Fourth call - should exceed (current becomes 3, limit is 3)
        rate_log.log("message1");
        assert_eq!(
            rate_log.output,
            "Message: \"message1\" repeat for 3 times in the past 0ms"
        );
        rate_log.output.clear();

        // Fifth call - should not exceed (current becomes 1, limit is 3)
        rate_log.log("message1");
        assert_eq!(rate_log.output, "");

        // Sixth call - should not exceed (current becomes 2, limit is 3)
        rate_log.log("message1");
        assert_eq!(rate_log.output, "");

        // Seventh call - should exceed (current becomes 3, limit is 3)
        rate_log.log("message1");
        assert_eq!(
            rate_log.output,
            "Message: \"message1\" repeat for 3 times in the past 0ms"
        );
        rate_log.output.clear();
    }

    #[test]
    fn test_rate_log_exceed_duration() {
        use std::thread;

        let mut rate_log = RateLog::new(Limit::Duration(Duration::from_millis(50)));

        // First call
        rate_log.log("message2");
        assert_eq!(rate_log.output, "message2");
        rate_log.output.clear();

        // Second call after short delay - should not exceed
        thread::sleep(Duration::from_millis(20));
        rate_log.log("message2");
        assert_eq!(rate_log.output, "");

        // Third call after longer delay - should exceed the 50ms limit
        thread::sleep(Duration::from_millis(40));
        rate_log.log("message2");
        assert_eq!(
            rate_log.output,
            "Message: \"message2\" repeat for 2 times in the past 60ms"
        );
        rate_log.output.clear();

        rate_log.log("message2");
        assert_eq!(rate_log.output, "");

        thread::sleep(Duration::from_millis(50));
        rate_log.log("message2");
        assert_eq!(
            rate_log.output,
            "Message: \"message2\" repeat for 2 times in the past 50ms"
        );
        rate_log.output.clear();
    }

    #[test]
    fn test_format_duration() {
        // Test milliseconds (< 1 second)
        let duration_ms = Duration::from_millis(500);
        assert_eq!(format_duration(duration_ms), "500ms");

        // Test seconds only (>= 1 second, < 1 minute)
        let duration_s = Duration::from_secs(45);
        assert_eq!(format_duration(duration_s), "45s");

        // Test minutes and seconds (>= 1 minute, < 1 hour)
        let duration_min = Duration::from_secs(3 * 60 + 25); // 3 minutes 25 seconds
        assert_eq!(format_duration(duration_min), "3m25s");

        // Test hours and minutes (>= 1 hour)
        let duration_hour = Duration::from_secs(2 * 3600 + 45 * 60); // 2 hours 45 minutes
        assert_eq!(format_duration(duration_hour), "2h45m");
    }
}