bottom/src/utils/logging.rs
Clement Tsang 86b35bcd79
refactor: use clippy::unwrap_used (#1882)
* refactor: use clippy::unwrap_used

* fix log format use

* fix a bunch of macos ones

* oop

* allow schema script

* driveby schema bump
2025-11-22 23:26:02 -05:00

182 lines
4.6 KiB
Rust

#[cfg(feature = "logging")]
use std::sync::OnceLock;
#[cfg(feature = "logging")]
pub static OFFSET: OnceLock<time::UtcOffset> = OnceLock::new();
#[cfg(feature = "logging")]
pub fn init_logger(
min_level: log::LevelFilter, debug_file_name: Option<&std::ffi::OsStr>,
) -> anyhow::Result<()> {
let dispatch = fern::Dispatch::new()
.format(|out, message, record| {
let offset = OFFSET.get_or_init(|| {
time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC)
});
let offset_time = {
let utc = time::OffsetDateTime::now_utc();
utc.checked_to_offset(*offset).unwrap_or(utc)
};
out.finish(format_args!(
"{}[{}][{}] {}",
offset_time
.format(&time::macros::format_description!(
// The weird "[[[" is because we need to escape a bracket ("[[") to show
// one "[". See https://time-rs.github.io/book/api/format-description.html
"[[[year]-[month]-[day]][[[hour]:[minute]:[second][subsecond digits:9]]"
))
.expect("log formatting shouldn't fail"),
record.target(),
record.level(),
message
))
})
.level(min_level);
if let Some(debug_file_name) = debug_file_name {
dispatch.chain(fern::log_file(debug_file_name)?).apply()?;
} else {
dispatch.chain(std::io::stdout()).apply()?;
}
Ok(())
}
#[macro_export]
macro_rules! error {
($($x:tt)*) => {
#[cfg(feature = "logging")]
{
log::error!($($x)*);
}
};
}
#[macro_export]
macro_rules! warn {
($($x:tt)*) => {
#[cfg(feature = "logging")]
{
log::warn!($($x)*);
}
};
}
#[macro_export]
macro_rules! info {
($($x:tt)*) => {
#[cfg(feature = "logging")]
{
log::info!($($x)*);
}
};
}
#[macro_export]
macro_rules! debug {
($($x:tt)*) => {
#[cfg(feature = "logging")]
{
log::debug!($($x)*);
}
};
}
#[macro_export]
macro_rules! trace {
($($x:tt)*) => {
#[cfg(feature = "logging")]
{
log::trace!($($x)*);
}
};
}
#[macro_export]
macro_rules! log {
($($x:tt)*) => {
#[cfg(feature = "logging")]
{
log::log!(log::Level::Trace, $($x)*);
}
};
($level:expr, $($x:tt)*) => {
#[cfg(feature = "logging")]
{
log::log!($level, $($x)*);
}
};
}
#[macro_export]
macro_rules! info_every_n_secs {
($n:expr, $($x:tt)*) => {
#[cfg(feature = "logging")]
{
$crate::log_every_n_secs!(log::Level::Info, $n, $($x)*);
}
};
}
#[macro_export]
macro_rules! log_every_n_secs {
($level:expr, $n:expr, $($x:tt)*) => {
#[cfg(feature = "logging")]
{
static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let since_last_log = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("should be valid").as_secs();
if now - since_last_log > $n {
LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);
log::log!($level, $($x)*);
}
}
};
}
#[cfg(test)]
mod test {
#[cfg(feature = "logging")]
/// We do this to ensure that the test logger is only initialized _once_ for
/// things like the default test runner that run tests in the same process.
///
/// This doesn't do anything if you use something like nextest, which runs
/// a test-per-process, but that's fine.
fn init_test_logger() {
use std::sync::atomic::{AtomicBool, Ordering};
static LOG_INIT: AtomicBool = AtomicBool::new(false);
if LOG_INIT.load(Ordering::SeqCst) {
return;
}
LOG_INIT.store(true, Ordering::SeqCst);
super::init_logger(log::LevelFilter::Trace, None)
.expect("initializing the logger should succeed");
}
#[cfg(feature = "logging")]
#[test]
fn test_logging_macros() {
init_test_logger();
error!("This is an error.");
warn!("This is a warning.");
info!("This is an info.");
debug!("This is a debug.");
info!("This is a trace.");
}
#[cfg(feature = "logging")]
#[test]
fn test_log_every_macros() {
init_test_logger();
info_every_n_secs!(10, "This is an info every 10 seconds.");
}
}