Skip to main content

log_debounce/
lib.rs

1//! # `log-debounce`
2//!
3//! Debounced logging macros for the `log` crate.
4//!
5//! ## Usage
6//!
7//! ```rust
8//! use log_debounce::{info_debounce, warn_once};
9//! use std::time::Duration;
10//!
11//! # let salinity = 35.2;
12//! info_debounce!(Duration::from_secs(60), "Measured current salinity as {:.1} ppt", salinity);
13//!
14//! warn_once!("Salinometer disconnected, no measurements are available");
15//! ```
16
17/// Logs a message at the info level, debounced by duration per callsite.
18///
19/// Subsequent calls to the same callsite within the specified duration will be silently dropped.
20/// Each unique location in your code where this macro is invoked maintains its own debounce state.
21///
22/// # Example
23///
24/// ```rust
25/// use log_debounce::info_debounce;
26/// use std::time::Duration;
27///
28/// # let temperature = 23.5;
29/// // Will log at most once per 30 seconds from this line
30/// info_debounce!(Duration::from_secs(30), "Temperature: {:.1}°C", temperature);
31/// ```
32#[macro_export]
33macro_rules! info_debounce {
34    ($duration:expr, $($arg:tt)*) => {{
35        use std::sync::LazyLock;
36        use std::sync::Mutex;
37        use std::time::Instant;
38
39        static LAST: LazyLock<Mutex<Option<Instant>>> =
40            LazyLock::new(|| Mutex::new(None));
41
42        if let Ok(mut last) = LAST.lock() {
43            let now = Instant::now();
44            let should_log = last
45                .map(|l| now.duration_since(l) >= $duration)
46                .unwrap_or(true);
47
48            if should_log {
49                *last = Some(now);
50                log::info!($($arg)*);
51            }
52        }
53    }};
54}
55
56/// Logs a message at the info level exactly once per callsite.
57///
58/// The first invocation logs the message; all subsequent calls are silently dropped.
59///
60/// # Example
61///
62/// ```rust
63/// use log_debounce::info_once;
64///
65/// // Logs only the first time this line is reached
66/// info_once!("Startup complete");
67/// ```
68#[macro_export]
69macro_rules! info_once {
70    ($($arg:tt)*) => {
71        $crate::info_debounce!(std::time::Duration::MAX, $($arg)*)
72    };
73}
74
75/// Logs a message at the warn level, debounced by duration per callsite.
76///
77/// Subsequent calls to the same callsite within the specified duration will be silently dropped.
78/// Each unique location in your code where this macro is invoked maintains its own debounce state.
79///
80/// # Example
81///
82/// ```rust
83/// use log_debounce::warn_debounce;
84/// use std::time::Duration;
85///
86/// # let queue_size = 1500;
87/// // Will log at most once per 5 minutes from this line
88/// warn_debounce!(Duration::from_secs(300), "Queue size high: {}", queue_size);
89/// ```
90#[macro_export]
91macro_rules! warn_debounce {
92    ($duration:expr, $($arg:tt)*) => {{
93        use std::sync::LazyLock;
94        use std::sync::Mutex;
95        use std::time::Instant;
96
97        static LAST: LazyLock<Mutex<Option<Instant>>> =
98            LazyLock::new(|| Mutex::new(None));
99
100        if let Ok(mut last) = LAST.lock() {
101            let now = Instant::now();
102            let should_log = last
103                .map(|l| now.duration_since(l) >= $duration)
104                .unwrap_or(true);
105
106            if should_log {
107                *last = Some(now);
108                log::warn!($($arg)*);
109            }
110        }
111    }};
112}
113
114/// Logs a message at the warn level exactly once per callsite.
115///
116/// The first invocation logs the message; all subsequent calls are silently dropped.
117///
118/// # Example
119///
120/// ```rust
121/// use log_debounce::warn_once;
122///
123/// // Logs only the first time this line is reached
124/// warn_once!("Deprecated configuration detected");
125/// ```
126#[macro_export]
127macro_rules! warn_once {
128    ($($arg:tt)*) => {
129        $crate::warn_debounce!(std::time::Duration::MAX, $($arg)*)
130    };
131}
132
133/// Logs a message at the error level, debounced by duration per callsite.
134///
135/// Subsequent calls to the same callsite within the specified duration will be silently dropped.
136/// Each unique location in your code where this macro is invoked maintains its own debounce state.
137///
138/// # Example
139///
140/// ```rust
141/// use log_debounce::error_debounce;
142/// use std::time::Duration;
143///
144/// # let error = "connection timeout";
145/// // Will log at most once per 10 seconds from this line
146/// error_debounce!(Duration::from_secs(10), "Database error: {}", error);
147/// ```
148#[macro_export]
149macro_rules! error_debounce {
150    ($duration:expr, $($arg:tt)*) => {{
151        use std::sync::LazyLock;
152        use std::sync::Mutex;
153        use std::time::Instant;
154
155        static LAST: LazyLock<Mutex<Option<Instant>>> =
156            LazyLock::new(|| Mutex::new(None));
157
158        if let Ok(mut last) = LAST.lock() {
159            let now = Instant::now();
160            let should_log = last
161                .map(|l| now.duration_since(l) >= $duration)
162                .unwrap_or(true);
163
164            if should_log {
165                *last = Some(now);
166                log::error!($($arg)*);
167            }
168        }
169    }};
170}
171
172/// Logs a message at the error level exactly once per callsite.
173///
174/// The first invocation logs the message; all subsequent calls are silently dropped.
175///
176/// # Example
177///
178/// ```rust
179/// use log_debounce::error_once;
180///
181/// // Logs only the first time this line is reached
182/// error_once!("Failed to load critical configuration");
183/// ```
184#[macro_export]
185macro_rules! error_once {
186    ($($arg:tt)*) => {
187        $crate::error_debounce!(std::time::Duration::MAX, $($arg)*)
188    };
189}
190
191/// Logs a message at the debug level, debounced by duration per callsite.
192///
193/// Subsequent calls to the same callsite within the specified duration will be silently dropped.
194/// Each unique location in your code where this macro is invoked maintains its own debounce state.
195///
196/// # Example
197///
198/// ```rust
199/// use log_debounce::debug_debounce;
200/// use std::time::Duration;
201///
202/// # let cache_hits = 42;
203/// // Will log at most once per 60 seconds from this line
204/// debug_debounce!(Duration::from_secs(60), "Cache hits: {}", cache_hits);
205/// ```
206#[macro_export]
207macro_rules! debug_debounce {
208    ($duration:expr, $($arg:tt)*) => {{
209        use std::sync::LazyLock;
210        use std::sync::Mutex;
211        use std::time::Instant;
212
213        static LAST: LazyLock<Mutex<Option<Instant>>> =
214            LazyLock::new(|| Mutex::new(None));
215
216        if let Ok(mut last) = LAST.lock() {
217            let now = Instant::now();
218            let should_log = last
219                .map(|l| now.duration_since(l) >= $duration)
220                .unwrap_or(true);
221
222            if should_log {
223                *last = Some(now);
224                log::debug!($($arg)*);
225            }
226        }
227    }};
228}
229
230/// Logs a message at the debug level exactly once per callsite.
231///
232/// The first invocation logs the message; all subsequent calls are silently dropped.
233///
234/// # Example
235///
236/// ```rust
237/// use log_debounce::debug_once;
238///
239/// // Logs only the first time this line is reached
240/// debug_once!("Entering hot path");
241/// ```
242#[macro_export]
243macro_rules! debug_once {
244    ($($arg:tt)*) => {
245        $crate::debug_debounce!(std::time::Duration::MAX, $($arg)*)
246    };
247}
248
249/// Logs a message at the trace level, debounced by duration per callsite.
250///
251/// Subsequent calls to the same callsite within the specified duration will be silently dropped.
252/// Each unique location in your code where this macro is invoked maintains its own debounce state.
253///
254/// # Example
255///
256/// ```rust
257/// use log_debounce::trace_debounce;
258/// use std::time::Duration;
259///
260/// # let iteration = 1000;
261/// // Will log at most once per 100ms from this line
262/// trace_debounce!(Duration::from_millis(100), "Iteration {}", iteration);
263/// ```
264#[macro_export]
265macro_rules! trace_debounce {
266    ($duration:expr, $($arg:tt)*) => {{
267        use std::sync::LazyLock;
268        use std::sync::Mutex;
269        use std::time::Instant;
270
271        static LAST: LazyLock<Mutex<Option<Instant>>> =
272            LazyLock::new(|| Mutex::new(None));
273
274        if let Ok(mut last) = LAST.lock() {
275            let now = Instant::now();
276            let should_log = last
277                .map(|l| now.duration_since(l) >= $duration)
278                .unwrap_or(true);
279
280            if should_log {
281                *last = Some(now);
282                log::trace!($($arg)*);
283            }
284        }
285    }};
286}
287
288/// Logs a message at the trace level exactly once per callsite.
289///
290/// The first invocation logs the message; all subsequent calls are silently dropped.
291///
292/// # Example
293///
294/// ```rust
295/// use log_debounce::trace_once;
296///
297/// // Logs only the first time this line is reached
298/// trace_once!("Function called for first time");
299/// ```
300#[macro_export]
301macro_rules! trace_once {
302    ($($arg:tt)*) => {
303        $crate::trace_debounce!(std::time::Duration::MAX, $($arg)*)
304    };
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use std::time::Duration;
311
312    #[test]
313    fn test_debounce_fires_first_time() {
314        info_debounce!(Duration::from_secs(1), "test message");
315        // if this compiles and runs, it works
316    }
317
318    #[test]
319    fn test_once_macro() {
320        info_once!("only once");
321        info_once!("only once");
322        // both should compile, second is dropped
323    }
324}