throttled_tracing/
lib.rs

1//! Periodic and throttled logging macros for the tracing ecosystem.
2//!
3//! This crate provides macros that extend `tracing` with rate-limited logging capabilities:
4//!
5//! - `*_once!` - Log only the first time the macro is reached
6//! - `*_every!(duration, ...)` - Log at most once per specified duration
7//! - `*_every_n!(n, ...)` - Log every N occurrences
8//! - `*_first_n!(n, ...)` - Log only the first N occurrences
9//! - `*_backoff!(initial, max, ...)` - Log with exponential backoff
10//! - `*_on_change!(value, ...)` - Log only when the tracked value changes
11//! - `*_once_per_value!(key, ...)` - Log once per unique key value
12//! - `*_sample!(probability, ...)` - Log with probability sampling
13//!
14//! # Examples
15//!
16//! ```rust
17//! use throttled_tracing::{info_once, debug_every, warn_every_n};
18//! use std::time::Duration;
19//!
20//! fn process_item(item: u32) {
21//!     // Only logs the first time this line is reached
22//!     info_once!("Processing started");
23//!
24//!     // Logs at most once per second
25//!     debug_every!(Duration::from_secs(1), "Processing item {}", item);
26//!
27//!     // Logs every 100th call
28//!     warn_every_n!(100, "Processed {} items so far", item);
29//! }
30//! ```
31//!
32//! ```rust
33//! use throttled_tracing::{error_backoff, info_on_change, debug_once_per_value, trace_sample};
34//! use std::time::Duration;
35//!
36//! fn handle_error(err: &str) {
37//!     // Exponential backoff: logs at 1s, 2s, 4s, 8s... up to 60s
38//!     error_backoff!(Duration::from_secs(1), Duration::from_secs(60), "Error: {}", err);
39//! }
40//!
41//! fn monitor_status(status: u32) {
42//!     // Only logs when status changes
43//!     info_on_change!(status, "Status changed to {}", status);
44//! }
45//!
46//! fn process_user(user_id: u64) {
47//!     // Logs once per unique user_id
48//!     debug_once_per_value!(user_id, "First time seeing user {}", user_id);
49//! }
50//!
51//! fn high_volume_operation() {
52//!     // Logs ~1% of calls
53//!     trace_sample!(0.01, "Sampled operation");
54//! }
55//! ```
56
57pub use parking_lot;
58pub use tracing;
59
60pub use fastrand;
61
62use parking_lot::Mutex;
63use std::collections::HashSet;
64use std::hash::{Hash, Hasher};
65use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
66use std::time::{Duration, Instant};
67
68/// Computes a hash of a value for change detection and deduplication.
69#[doc(hidden)]
70pub fn compute_hash<T: Hash>(value: &T) -> u64 {
71    use std::collections::hash_map::DefaultHasher;
72    let mut hasher = DefaultHasher::new();
73    value.hash(&mut hasher);
74    hasher.finish()
75}
76
77/// State for duration-based throttling.
78pub struct ThrottleState {
79    last_log: Mutex<Option<Instant>>,
80}
81
82impl Default for ThrottleState {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl ThrottleState {
89    pub const fn new() -> Self {
90        Self {
91            last_log: Mutex::new(None),
92        }
93    }
94
95    /// Returns true if enough time has passed since the last log.
96    pub fn should_log(&self, interval: Duration) -> bool {
97        let mut last = self.last_log.lock();
98        let now = Instant::now();
99
100        match *last {
101            None => {
102                *last = Some(now);
103                true
104            }
105            Some(prev) if now.duration_since(prev) >= interval => {
106                *last = Some(now);
107                true
108            }
109            _ => false,
110        }
111    }
112}
113
114/// State for count-based throttling.
115pub struct CountState {
116    count: AtomicUsize,
117}
118
119impl Default for CountState {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl CountState {
126    pub const fn new() -> Self {
127        Self {
128            count: AtomicUsize::new(0),
129        }
130    }
131
132    /// Returns true every N calls (1st call, N+1th call, 2N+1th call, etc.)
133    pub fn should_log(&self, n: usize) -> bool {
134        let count = self.count.fetch_add(1, Ordering::Relaxed);
135        count.is_multiple_of(n)
136    }
137}
138
139/// State for one-time logging.
140pub struct OnceState {
141    logged: AtomicBool,
142}
143
144impl Default for OnceState {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl OnceState {
151    pub const fn new() -> Self {
152        Self {
153            logged: AtomicBool::new(false),
154        }
155    }
156
157    /// Returns true only on the first call.
158    pub fn should_log(&self) -> bool {
159        !self.logged.swap(true, Ordering::Relaxed)
160    }
161}
162
163/// State for logging only the first N occurrences.
164pub struct FirstNState {
165    count: AtomicUsize,
166}
167
168impl Default for FirstNState {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl FirstNState {
175    pub const fn new() -> Self {
176        Self {
177            count: AtomicUsize::new(0),
178        }
179    }
180
181    /// Returns true for the first N calls, then false forever.
182    pub fn should_log(&self, n: usize) -> bool {
183        let count = self.count.fetch_add(1, Ordering::Relaxed);
184        count < n
185    }
186}
187
188/// State for exponential backoff logging.
189pub struct BackoffState {
190    state: Mutex<Option<BackoffInner>>,
191}
192
193struct BackoffInner {
194    last_log: Instant,
195    current_interval: Duration,
196}
197
198impl Default for BackoffState {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204impl BackoffState {
205    pub const fn new() -> Self {
206        Self {
207            state: Mutex::new(None),
208        }
209    }
210
211    /// Returns true with exponential backoff timing.
212    /// Starts at `initial` interval, doubles after each log, caps at `max`.
213    pub fn should_log(&self, initial: Duration, max: Duration) -> bool {
214        let mut state = self.state.lock();
215        let now = Instant::now();
216
217        match state.as_mut() {
218            None => {
219                *state = Some(BackoffInner {
220                    last_log: now,
221                    current_interval: initial,
222                });
223                true
224            }
225            Some(inner) if now.duration_since(inner.last_log) >= inner.current_interval => {
226                inner.last_log = now;
227                inner.current_interval = (inner.current_interval * 2).min(max);
228                true
229            }
230            _ => false,
231        }
232    }
233}
234
235/// State for logging only when a value changes.
236/// Uses hash comparison, so values must implement `Hash`.
237pub struct OnChangeState {
238    prev_hash: AtomicU64,
239    initialized: AtomicBool,
240}
241
242impl Default for OnChangeState {
243    fn default() -> Self {
244        Self::new()
245    }
246}
247
248impl OnChangeState {
249    pub const fn new() -> Self {
250        Self {
251            prev_hash: AtomicU64::new(0),
252            initialized: AtomicBool::new(false),
253        }
254    }
255
256    /// Returns true if the value's hash differs from the previous call.
257    /// Always returns true on the first call.
258    pub fn should_log<T: Hash>(&self, value: &T) -> bool {
259        let new_hash = compute_hash(value);
260
261        // First call: initialize and log
262        if !self.initialized.swap(true, Ordering::AcqRel) {
263            self.prev_hash.store(new_hash, Ordering::Release);
264            return true;
265        }
266
267        // Subsequent calls: compare and swap
268        let prev = self.prev_hash.swap(new_hash, Ordering::AcqRel);
269        prev != new_hash
270    }
271}
272
273/// State for logging once per unique value.
274/// Uses hash-based deduplication, so values must implement `Hash`.
275/// Note: Memory grows with each unique value seen.
276pub struct OncePerValueState {
277    seen: Mutex<HashSet<u64>>,
278}
279
280impl Default for OncePerValueState {
281    fn default() -> Self {
282        Self::new()
283    }
284}
285
286impl OncePerValueState {
287    pub fn new() -> Self {
288        Self {
289            seen: Mutex::new(HashSet::new()),
290        }
291    }
292
293    /// Returns true only the first time a particular value's hash is seen.
294    pub fn should_log<T: Hash>(&self, value: &T) -> bool {
295        let hash = compute_hash(value);
296        let mut seen = self.seen.lock();
297        seen.insert(hash)
298    }
299}
300
301/// State for probabilistic sampling.
302pub struct SampleState {
303    _private: (),
304}
305
306impl Default for SampleState {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312impl SampleState {
313    pub const fn new() -> Self {
314        Self { _private: () }
315    }
316
317    /// Returns true with the given probability (0.0 to 1.0).
318    pub fn should_log(&self, probability: f64) -> bool {
319        fastrand::f64() < probability
320    }
321}
322
323// ============================================================================
324// *_once! macros - log only the first time
325// ============================================================================
326
327/// Logs a message at the TRACE level only once.
328#[macro_export]
329macro_rules! trace_once {
330    ($($arg:tt)*) => {{
331        static STATE: $crate::OnceState = $crate::OnceState::new();
332        if STATE.should_log() {
333            $crate::tracing::trace!($($arg)*);
334        }
335    }};
336}
337
338/// Logs a message at the DEBUG level only once.
339#[macro_export]
340macro_rules! debug_once {
341    ($($arg:tt)*) => {{
342        static STATE: $crate::OnceState = $crate::OnceState::new();
343        if STATE.should_log() {
344            $crate::tracing::debug!($($arg)*);
345        }
346    }};
347}
348
349/// Logs a message at the INFO level only once.
350#[macro_export]
351macro_rules! info_once {
352    ($($arg:tt)*) => {{
353        static STATE: $crate::OnceState = $crate::OnceState::new();
354        if STATE.should_log() {
355            $crate::tracing::info!($($arg)*);
356        }
357    }};
358}
359
360/// Logs a message at the WARN level only once.
361#[macro_export]
362macro_rules! warn_once {
363    ($($arg:tt)*) => {{
364        static STATE: $crate::OnceState = $crate::OnceState::new();
365        if STATE.should_log() {
366            $crate::tracing::warn!($($arg)*);
367        }
368    }};
369}
370
371/// Logs a message at the ERROR level only once.
372#[macro_export]
373macro_rules! error_once {
374    ($($arg:tt)*) => {{
375        static STATE: $crate::OnceState = $crate::OnceState::new();
376        if STATE.should_log() {
377            $crate::tracing::error!($($arg)*);
378        }
379    }};
380}
381
382// ============================================================================
383// *_every! macros - log at most once per duration
384// ============================================================================
385
386/// Logs a message at the TRACE level at most once per specified duration.
387#[macro_export]
388macro_rules! trace_every {
389    ($duration:expr, $($arg:tt)*) => {{
390        static STATE: $crate::ThrottleState = $crate::ThrottleState::new();
391        if STATE.should_log($duration) {
392            $crate::tracing::trace!($($arg)*);
393        }
394    }};
395}
396
397/// Logs a message at the DEBUG level at most once per specified duration.
398#[macro_export]
399macro_rules! debug_every {
400    ($duration:expr, $($arg:tt)*) => {{
401        static STATE: $crate::ThrottleState = $crate::ThrottleState::new();
402        if STATE.should_log($duration) {
403            $crate::tracing::debug!($($arg)*);
404        }
405    }};
406}
407
408/// Logs a message at the INFO level at most once per specified duration.
409#[macro_export]
410macro_rules! info_every {
411    ($duration:expr, $($arg:tt)*) => {{
412        static STATE: $crate::ThrottleState = $crate::ThrottleState::new();
413        if STATE.should_log($duration) {
414            $crate::tracing::info!($($arg)*);
415        }
416    }};
417}
418
419/// Logs a message at the WARN level at most once per specified duration.
420#[macro_export]
421macro_rules! warn_every {
422    ($duration:expr, $($arg:tt)*) => {{
423        static STATE: $crate::ThrottleState = $crate::ThrottleState::new();
424        if STATE.should_log($duration) {
425            $crate::tracing::warn!($($arg)*);
426        }
427    }};
428}
429
430/// Logs a message at the ERROR level at most once per specified duration.
431#[macro_export]
432macro_rules! error_every {
433    ($duration:expr, $($arg:tt)*) => {{
434        static STATE: $crate::ThrottleState = $crate::ThrottleState::new();
435        if STATE.should_log($duration) {
436            $crate::tracing::error!($($arg)*);
437        }
438    }};
439}
440
441// ============================================================================
442// *_every_n! macros - log every N occurrences
443// ============================================================================
444
445/// Logs a message at the TRACE level every N occurrences.
446#[macro_export]
447macro_rules! trace_every_n {
448    ($n:expr, $($arg:tt)*) => {{
449        static STATE: $crate::CountState = $crate::CountState::new();
450        if STATE.should_log($n) {
451            $crate::tracing::trace!($($arg)*);
452        }
453    }};
454}
455
456/// Logs a message at the DEBUG level every N occurrences.
457#[macro_export]
458macro_rules! debug_every_n {
459    ($n:expr, $($arg:tt)*) => {{
460        static STATE: $crate::CountState = $crate::CountState::new();
461        if STATE.should_log($n) {
462            $crate::tracing::debug!($($arg)*);
463        }
464    }};
465}
466
467/// Logs a message at the INFO level every N occurrences.
468#[macro_export]
469macro_rules! info_every_n {
470    ($n:expr, $($arg:tt)*) => {{
471        static STATE: $crate::CountState = $crate::CountState::new();
472        if STATE.should_log($n) {
473            $crate::tracing::info!($($arg)*);
474        }
475    }};
476}
477
478/// Logs a message at the WARN level every N occurrences.
479#[macro_export]
480macro_rules! warn_every_n {
481    ($n:expr, $($arg:tt)*) => {{
482        static STATE: $crate::CountState = $crate::CountState::new();
483        if STATE.should_log($n) {
484            $crate::tracing::warn!($($arg)*);
485        }
486    }};
487}
488
489/// Logs a message at the ERROR level every N occurrences.
490#[macro_export]
491macro_rules! error_every_n {
492    ($n:expr, $($arg:tt)*) => {{
493        static STATE: $crate::CountState = $crate::CountState::new();
494        if STATE.should_log($n) {
495            $crate::tracing::error!($($arg)*);
496        }
497    }};
498}
499
500// ============================================================================
501// *_first_n! macros - log only the first N occurrences
502// ============================================================================
503
504/// Logs a message at the TRACE level only for the first N occurrences.
505#[macro_export]
506macro_rules! trace_first_n {
507    ($n:expr, $($arg:tt)*) => {{
508        static STATE: $crate::FirstNState = $crate::FirstNState::new();
509        if STATE.should_log($n) {
510            $crate::tracing::trace!($($arg)*);
511        }
512    }};
513}
514
515/// Logs a message at the DEBUG level only for the first N occurrences.
516#[macro_export]
517macro_rules! debug_first_n {
518    ($n:expr, $($arg:tt)*) => {{
519        static STATE: $crate::FirstNState = $crate::FirstNState::new();
520        if STATE.should_log($n) {
521            $crate::tracing::debug!($($arg)*);
522        }
523    }};
524}
525
526/// Logs a message at the INFO level only for the first N occurrences.
527#[macro_export]
528macro_rules! info_first_n {
529    ($n:expr, $($arg:tt)*) => {{
530        static STATE: $crate::FirstNState = $crate::FirstNState::new();
531        if STATE.should_log($n) {
532            $crate::tracing::info!($($arg)*);
533        }
534    }};
535}
536
537/// Logs a message at the WARN level only for the first N occurrences.
538#[macro_export]
539macro_rules! warn_first_n {
540    ($n:expr, $($arg:tt)*) => {{
541        static STATE: $crate::FirstNState = $crate::FirstNState::new();
542        if STATE.should_log($n) {
543            $crate::tracing::warn!($($arg)*);
544        }
545    }};
546}
547
548/// Logs a message at the ERROR level only for the first N occurrences.
549#[macro_export]
550macro_rules! error_first_n {
551    ($n:expr, $($arg:tt)*) => {{
552        static STATE: $crate::FirstNState = $crate::FirstNState::new();
553        if STATE.should_log($n) {
554            $crate::tracing::error!($($arg)*);
555        }
556    }};
557}
558
559// ============================================================================
560// *_backoff! macros - log with exponential backoff
561// ============================================================================
562
563/// Logs a message at the TRACE level with exponential backoff.
564/// Starts at `initial` interval, doubles after each log, caps at `max`.
565#[macro_export]
566macro_rules! trace_backoff {
567    ($initial:expr, $max:expr, $($arg:tt)*) => {{
568        static STATE: $crate::BackoffState = $crate::BackoffState::new();
569        if STATE.should_log($initial, $max) {
570            $crate::tracing::trace!($($arg)*);
571        }
572    }};
573}
574
575/// Logs a message at the DEBUG level with exponential backoff.
576/// Starts at `initial` interval, doubles after each log, caps at `max`.
577#[macro_export]
578macro_rules! debug_backoff {
579    ($initial:expr, $max:expr, $($arg:tt)*) => {{
580        static STATE: $crate::BackoffState = $crate::BackoffState::new();
581        if STATE.should_log($initial, $max) {
582            $crate::tracing::debug!($($arg)*);
583        }
584    }};
585}
586
587/// Logs a message at the INFO level with exponential backoff.
588/// Starts at `initial` interval, doubles after each log, caps at `max`.
589#[macro_export]
590macro_rules! info_backoff {
591    ($initial:expr, $max:expr, $($arg:tt)*) => {{
592        static STATE: $crate::BackoffState = $crate::BackoffState::new();
593        if STATE.should_log($initial, $max) {
594            $crate::tracing::info!($($arg)*);
595        }
596    }};
597}
598
599/// Logs a message at the WARN level with exponential backoff.
600/// Starts at `initial` interval, doubles after each log, caps at `max`.
601#[macro_export]
602macro_rules! warn_backoff {
603    ($initial:expr, $max:expr, $($arg:tt)*) => {{
604        static STATE: $crate::BackoffState = $crate::BackoffState::new();
605        if STATE.should_log($initial, $max) {
606            $crate::tracing::warn!($($arg)*);
607        }
608    }};
609}
610
611/// Logs a message at the ERROR level with exponential backoff.
612/// Starts at `initial` interval, doubles after each log, caps at `max`.
613#[macro_export]
614macro_rules! error_backoff {
615    ($initial:expr, $max:expr, $($arg:tt)*) => {{
616        static STATE: $crate::BackoffState = $crate::BackoffState::new();
617        if STATE.should_log($initial, $max) {
618            $crate::tracing::error!($($arg)*);
619        }
620    }};
621}
622
623// ============================================================================
624// *_on_change! macros - log only when value changes
625// ============================================================================
626
627/// Logs a message at the TRACE level only when the tracked value changes.
628#[macro_export]
629macro_rules! trace_on_change {
630    ($value:expr, $($arg:tt)*) => {{
631        static STATE: $crate::OnChangeState = $crate::OnChangeState::new();
632        if STATE.should_log(&$value) {
633            $crate::tracing::trace!($($arg)*);
634        }
635    }};
636}
637
638/// Logs a message at the DEBUG level only when the tracked value changes.
639#[macro_export]
640macro_rules! debug_on_change {
641    ($value:expr, $($arg:tt)*) => {{
642        static STATE: $crate::OnChangeState = $crate::OnChangeState::new();
643        if STATE.should_log(&$value) {
644            $crate::tracing::debug!($($arg)*);
645        }
646    }};
647}
648
649/// Logs a message at the INFO level only when the tracked value changes.
650#[macro_export]
651macro_rules! info_on_change {
652    ($value:expr, $($arg:tt)*) => {{
653        static STATE: $crate::OnChangeState = $crate::OnChangeState::new();
654        if STATE.should_log(&$value) {
655            $crate::tracing::info!($($arg)*);
656        }
657    }};
658}
659
660/// Logs a message at the WARN level only when the tracked value changes.
661#[macro_export]
662macro_rules! warn_on_change {
663    ($value:expr, $($arg:tt)*) => {{
664        static STATE: $crate::OnChangeState = $crate::OnChangeState::new();
665        if STATE.should_log(&$value) {
666            $crate::tracing::warn!($($arg)*);
667        }
668    }};
669}
670
671/// Logs a message at the ERROR level only when the tracked value changes.
672#[macro_export]
673macro_rules! error_on_change {
674    ($value:expr, $($arg:tt)*) => {{
675        static STATE: $crate::OnChangeState = $crate::OnChangeState::new();
676        if STATE.should_log(&$value) {
677            $crate::tracing::error!($($arg)*);
678        }
679    }};
680}
681
682// ============================================================================
683// *_once_per_value! macros - log once per unique value
684// ============================================================================
685
686/// Logs a message at the TRACE level once per unique value.
687#[macro_export]
688macro_rules! trace_once_per_value {
689    ($value:expr, $($arg:tt)*) => {{
690        static STATE: std::sync::OnceLock<$crate::OncePerValueState> = std::sync::OnceLock::new();
691        if STATE.get_or_init($crate::OncePerValueState::new).should_log(&$value) {
692            $crate::tracing::trace!($($arg)*);
693        }
694    }};
695}
696
697/// Logs a message at the DEBUG level once per unique value.
698#[macro_export]
699macro_rules! debug_once_per_value {
700    ($value:expr, $($arg:tt)*) => {{
701        static STATE: std::sync::OnceLock<$crate::OncePerValueState> = std::sync::OnceLock::new();
702        if STATE.get_or_init($crate::OncePerValueState::new).should_log(&$value) {
703            $crate::tracing::debug!($($arg)*);
704        }
705    }};
706}
707
708/// Logs a message at the INFO level once per unique value.
709#[macro_export]
710macro_rules! info_once_per_value {
711    ($value:expr, $($arg:tt)*) => {{
712        static STATE: std::sync::OnceLock<$crate::OncePerValueState> = std::sync::OnceLock::new();
713        if STATE.get_or_init($crate::OncePerValueState::new).should_log(&$value) {
714            $crate::tracing::info!($($arg)*);
715        }
716    }};
717}
718
719/// Logs a message at the WARN level once per unique value.
720#[macro_export]
721macro_rules! warn_once_per_value {
722    ($value:expr, $($arg:tt)*) => {{
723        static STATE: std::sync::OnceLock<$crate::OncePerValueState> = std::sync::OnceLock::new();
724        if STATE.get_or_init($crate::OncePerValueState::new).should_log(&$value) {
725            $crate::tracing::warn!($($arg)*);
726        }
727    }};
728}
729
730/// Logs a message at the ERROR level once per unique value.
731#[macro_export]
732macro_rules! error_once_per_value {
733    ($value:expr, $($arg:tt)*) => {{
734        static STATE: std::sync::OnceLock<$crate::OncePerValueState> = std::sync::OnceLock::new();
735        if STATE.get_or_init($crate::OncePerValueState::new).should_log(&$value) {
736            $crate::tracing::error!($($arg)*);
737        }
738    }};
739}
740
741// ============================================================================
742// *_sample! macros - log with probability sampling
743// ============================================================================
744
745/// Logs a message at the TRACE level with the given probability (0.0 to 1.0).
746#[macro_export]
747macro_rules! trace_sample {
748    ($probability:expr, $($arg:tt)*) => {{
749        static STATE: $crate::SampleState = $crate::SampleState::new();
750        if STATE.should_log($probability) {
751            $crate::tracing::trace!($($arg)*);
752        }
753    }};
754}
755
756/// Logs a message at the DEBUG level with the given probability (0.0 to 1.0).
757#[macro_export]
758macro_rules! debug_sample {
759    ($probability:expr, $($arg:tt)*) => {{
760        static STATE: $crate::SampleState = $crate::SampleState::new();
761        if STATE.should_log($probability) {
762            $crate::tracing::debug!($($arg)*);
763        }
764    }};
765}
766
767/// Logs a message at the INFO level with the given probability (0.0 to 1.0).
768#[macro_export]
769macro_rules! info_sample {
770    ($probability:expr, $($arg:tt)*) => {{
771        static STATE: $crate::SampleState = $crate::SampleState::new();
772        if STATE.should_log($probability) {
773            $crate::tracing::info!($($arg)*);
774        }
775    }};
776}
777
778/// Logs a message at the WARN level with the given probability (0.0 to 1.0).
779#[macro_export]
780macro_rules! warn_sample {
781    ($probability:expr, $($arg:tt)*) => {{
782        static STATE: $crate::SampleState = $crate::SampleState::new();
783        if STATE.should_log($probability) {
784            $crate::tracing::warn!($($arg)*);
785        }
786    }};
787}
788
789/// Logs a message at the ERROR level with the given probability (0.0 to 1.0).
790#[macro_export]
791macro_rules! error_sample {
792    ($probability:expr, $($arg:tt)*) => {{
793        static STATE: $crate::SampleState = $crate::SampleState::new();
794        if STATE.should_log($probability) {
795            $crate::tracing::error!($($arg)*);
796        }
797    }};
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803    use std::time::Duration;
804
805    #[test]
806    fn once_state_logs_only_first_time() {
807        let state = OnceState::new();
808        assert!(state.should_log());
809        assert!(!state.should_log());
810        assert!(!state.should_log());
811    }
812
813    #[test]
814    fn count_state_logs_every_n() {
815        let state = CountState::new();
816        // With n=3: logs on 0, 3, 6, 9...
817        assert!(state.should_log(3)); // count 0
818        assert!(!state.should_log(3)); // count 1
819        assert!(!state.should_log(3)); // count 2
820        assert!(state.should_log(3)); // count 3
821        assert!(!state.should_log(3)); // count 4
822        assert!(!state.should_log(3)); // count 5
823        assert!(state.should_log(3)); // count 6
824    }
825
826    #[test]
827    fn throttle_state_respects_duration() {
828        let state = ThrottleState::new();
829        let short_duration = Duration::from_millis(50);
830
831        // First call always logs
832        assert!(state.should_log(short_duration));
833
834        // Immediate second call should not log
835        assert!(!state.should_log(short_duration));
836
837        // Wait for duration to pass
838        std::thread::sleep(Duration::from_millis(60));
839
840        // Now it should log again
841        assert!(state.should_log(short_duration));
842    }
843
844    #[test]
845    fn once_macro_logs_once() {
846        for _ in 0..5 {
847            trace_once!("test message");
848            // We can't easily verify tracing output, but we can verify the macro compiles
849        }
850    }
851
852    #[test]
853    fn every_macro_compiles() {
854        for i in 0..5 {
855            debug_every!(Duration::from_secs(1), "iteration {}", i);
856        }
857    }
858
859    #[test]
860    fn every_n_macro_compiles() {
861        for i in 0..10 {
862            info_every_n!(3, "iteration {}", i);
863        }
864    }
865
866    #[test]
867    fn first_n_state_logs_first_n_only() {
868        let state = FirstNState::new();
869        // With n=3: logs on 0, 1, 2, then stops
870        assert!(state.should_log(3)); // count 0
871        assert!(state.should_log(3)); // count 1
872        assert!(state.should_log(3)); // count 2
873        assert!(!state.should_log(3)); // count 3
874        assert!(!state.should_log(3)); // count 4
875    }
876
877    #[test]
878    fn backoff_state_doubles_interval() {
879        let state = BackoffState::new();
880        let initial = Duration::from_millis(20);
881        let max = Duration::from_millis(100);
882
883        // First call always logs
884        assert!(state.should_log(initial, max));
885
886        // Immediate call should not log
887        assert!(!state.should_log(initial, max));
888
889        // Wait for initial interval
890        std::thread::sleep(Duration::from_millis(25));
891        assert!(state.should_log(initial, max));
892
893        // Now interval is 40ms, so 25ms wait shouldn't be enough
894        std::thread::sleep(Duration::from_millis(25));
895        assert!(!state.should_log(initial, max));
896
897        // Wait more to reach 40ms total
898        std::thread::sleep(Duration::from_millis(20));
899        assert!(state.should_log(initial, max));
900    }
901
902    #[test]
903    fn on_change_state_detects_changes() {
904        let state = OnChangeState::new();
905
906        // First call always logs
907        assert!(state.should_log(&42));
908
909        // Same value should not log
910        assert!(!state.should_log(&42));
911
912        // Different value should log
913        assert!(state.should_log(&100));
914
915        // Same new value should not log
916        assert!(!state.should_log(&100));
917
918        // Back to original value should log (it changed)
919        assert!(state.should_log(&42));
920    }
921
922    #[test]
923    fn once_per_value_state_logs_unique_values() {
924        let state = OncePerValueState::new();
925
926        // First time seeing each value
927        assert!(state.should_log(&"apple"));
928        assert!(state.should_log(&"banana"));
929        assert!(state.should_log(&"cherry"));
930
931        // Second time seeing values
932        assert!(!state.should_log(&"apple"));
933        assert!(!state.should_log(&"banana"));
934
935        // New value still logs
936        assert!(state.should_log(&"date"));
937    }
938
939    #[test]
940    fn sample_state_respects_probability() {
941        let state = SampleState::new();
942
943        // With probability 0, should never log
944        let mut logged = false;
945        for _ in 0..100 {
946            if state.should_log(0.0) {
947                logged = true;
948            }
949        }
950        assert!(!logged);
951
952        // With probability 1, should always log
953        for _ in 0..10 {
954            assert!(state.should_log(1.0));
955        }
956    }
957
958    #[test]
959    fn first_n_macro_compiles() {
960        for i in 0..10 {
961            info_first_n!(3, "iteration {}", i);
962        }
963    }
964
965    #[test]
966    fn backoff_macro_compiles() {
967        for i in 0..5 {
968            warn_backoff!(Duration::from_secs(1), Duration::from_secs(60), "iteration {}", i);
969        }
970    }
971
972    #[test]
973    fn on_change_macro_compiles() {
974        for i in 0..5 {
975            let value = i % 2;
976            debug_on_change!(value, "value changed to {}", value);
977        }
978    }
979
980    #[test]
981    fn once_per_value_macro_compiles() {
982        for i in 0..10 {
983            let key = i % 3;
984            info_once_per_value!(key, "first time seeing {}", key);
985        }
986    }
987
988    #[test]
989    fn sample_macro_compiles() {
990        for i in 0..10 {
991            trace_sample!(0.5, "sampled iteration {}", i);
992        }
993    }
994}