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}