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}