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}