#[macro_export]
macro_rules! log_ratelim {
{
@impl activity_format: ( $act_fmt:literal $(, $act_arg:expr)* ) ;
result: ($result:expr ) ;
on_error: (Err($err_pat:pat), $err_level:ident $(, $err_fmt:literal $(, $err_arg:expr)* )? );
$( on_ok: (Ok($ok_pat:pat), $ok_level:ident, $ok_fmt:literal $(, $ok_arg:expr)* ); )?
} => {
#[allow(clippy::redundant_closure_call)]
(||{
use $crate::macro_prelude::*;
let Some(runtime) = rt_support() else {
match &$result {
#[allow(clippy::redundant_pattern)]
Err(the_error @ $err_pat) => {
tracing::event!(
tracing::Level::$err_level,
concat!($act_fmt, $(": ", $err_fmt, )? ": {}"),
$($act_arg,)*
$( $($err_arg, )* )?
the_error.report()
);
}
$(Ok($ok_pat) => {
tracing::event!(
tracing::Level::$ok_level,
$ok_fmt
$(, $ok_arg)*
);
})?
#[allow(unreachable_patterns)]
Ok(_) => {}
}
return;
};
struct Lg {
state: LogState,
last_activity: Option<Activity>,
}
impl Loggable for Lg {
fn flush(&mut self, summarizing: std::time::Duration) -> Activity {
let activity = self.state.activity();
match activity {
Activity::Active => {
tracing::event!(
tracing::Level::$err_level,
"{}",
self.state.display_problem(summarizing)
);
}
Activity::AppearsResolved => {
if self.last_activity != Some(Activity::AppearsResolved) {
tracing::event!(
tracing::Level::$err_level,
"{}",
self.state.display_recovery(summarizing)
);
}
}
Activity::Dormant => {}
}
self.state.reset();
self.last_activity = Some(activity);
activity
}
}
static LOGGERS: LazyLock<Mutex<WeakValueHashMap<String, Weak<RateLim<Lg>>>>> =
LazyLock::new(|| Mutex::new(WeakValueHashMap::new()));
let activity = format!($act_fmt $(, $act_arg)*);
let key = activity.clone();
match &$result {
#[allow(clippy::redundant_pattern)]
Err(the_error @ $err_pat) => {
let logger = LOGGERS
.lock()
.expect("poisoned lock")
.entry(key)
.or_insert_with(|| RateLim::new(Lg {
state: LogState::new(activity),
last_activity: None,
}));
logger.event(runtime, |lg| lg.state.note_fail(||
(
$crate::log_ratelim!{@first_nonempty
{ $( Some(format!($err_fmt $(, $err_arg)* )) )? }
{ None }
},
Some(Box::new(the_error.clone()))
)
));
}
Ok($crate::log_ratelim!{@first_nonempty { $($ok_pat)? } {_} }) => {
if let Some(logger) = LOGGERS
.lock()
.expect("poisoned lock")
.get(&key) {
logger.nonevent(|lg| lg.state.note_ok());
}
$(
tracing::event!(tracing::Level::$ok_level, $ok_fmt $(, $ok_arg )* );
)?
}
}
})()
};
{
$act_fmt:literal $(, $act_arg:expr )* $(,)? ;
$result:expr ;
Err($err_pat:pat) => $err_level:ident $(, $err_fmt:literal $(, $err_arg:expr)* )? $(,)?
$(; Ok($ok_pat:pat) => $ok_level:ident, $ok_fmt:literal $(, $ok_arg:expr )* $(,)? )?
$(;)?
} => {
$crate::log_ratelim!{
@impl
activity_format: ( $act_fmt $(, $act_arg)* );
result: ($result);
on_error: (Err($err_pat), $err_level $(, $err_fmt $(, $err_arg)* )? );
$( on_ok: ( Ok($ok_pat), $ok_level, $ok_fmt $(, $ok_arg)* ); )?
}
};
{
$act_fmt:literal $(, $act_arg:expr )* $(,)? ;
$result:expr
$(; Ok($ok_pat:pat) => $ok_level:ident, $ok_fmt:literal $(, $ok_arg:expr )* $(,)? )?
$(;)?
} => {
$crate::log_ratelim!{
@impl
activity_format: ( $act_fmt $(, $act_arg)* );
result: ($result);
on_error: (Err(_), WARN);
$( on_ok: ( Ok($ok_pat), $ok_level, $ok_fmt $(, $ok_arg)* ); )?
}
};
{ @first_nonempty { $($a:tt)+ } { $($b:tt)* }} => { $($a)+ };
{ @first_nonempty { } { $($b:tt)* } } => { $($b)+ };
}
#[cfg(test)]
mod test_syntax {
#![allow(dead_code)]
#[derive(Clone, Debug, thiserror::Error)]
enum MyErr {
#[error("it didn't work")]
DidntWork,
}
impl MyErr {
fn badness(&self) -> u8 {
3
}
}
fn various_syntaxes(friend: &str, r: &Result<u32, MyErr>) {
log_ratelim!(
"saying hi to {}", friend;
r;
);
log_ratelim!(
"saying hi to {}", friend;
r;
Err(_) => WARN;
);
log_ratelim!(
"saying hi to {}", friend;
r;
Err(e) => WARN, "badness={}", e.badness();
);
log_ratelim!(
"saying hi to {}", friend;
r;
Ok(v) => TRACE, "nothing bad happened; v={}", v;
);
log_ratelim!(
"saying hi to {}", friend;
r;
Ok(v) => TRACE, "nothing bad happened; v={}", v;
);
log_ratelim!(
"saying hi to {}", friend;
r;
Err(e) => WARN, "badness={}", e.badness();
Ok(v) => TRACE, "nothing bad happened; v={}", v;
);
}
}