use parking_lot::Mutex;
use std::fmt;
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DebugLevel {
Off = 0,
Error = 1,
Info = 2,
Debug = 3,
Trace = 4,
}
impl DebugLevel {
fn from_env() -> Self {
match std::env::var("DEBUG_LEVEL") {
Ok(val) => match val.trim().parse::<u8>() {
Ok(0) => DebugLevel::Off,
Ok(1) => DebugLevel::Error,
Ok(2) => DebugLevel::Info,
Ok(3) => DebugLevel::Debug,
Ok(4) => DebugLevel::Trace,
_ => DebugLevel::Off,
},
Err(_) => DebugLevel::Off,
}
}
}
struct DebugLogger {
level: DebugLevel,
file: Option<std::fs::File>,
}
impl DebugLogger {
fn new() -> Self {
let level = DebugLevel::from_env();
let log_path = std::env::temp_dir().join("par_term_debug.log");
if log_path
.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
{
let _ = std::fs::remove_file(&log_path);
}
#[cfg(unix)]
let file = {
use std::os::unix::fs::OpenOptionsExt;
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.custom_flags(libc::O_NOFOLLOW)
.open(&log_path)
.ok()
};
#[cfg(not(unix))]
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&log_path)
.ok();
#[cfg(unix)]
if file.is_some() {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&log_path, std::fs::Permissions::from_mode(0o600));
}
let mut logger = DebugLogger { level, file };
logger.write_raw(&format!(
"\n{}\npar-term log session started at {} (debug_level={:?}, rust_log={})\n{}\n",
"=".repeat(80),
get_timestamp(),
level,
std::env::var("RUST_LOG").unwrap_or_else(|_| "unset".to_string()),
"=".repeat(80)
));
logger
}
fn write_raw(&mut self, msg: &str) {
if let Some(ref mut file) = self.file {
let _ = file.write_all(msg.as_bytes());
let _ = file.flush();
}
}
fn log(&mut self, level: DebugLevel, category: &str, msg: &str) {
if level <= self.level {
let timestamp = get_timestamp();
let level_str = match level {
DebugLevel::Error => "ERROR",
DebugLevel::Info => "INFO ",
DebugLevel::Debug => "DEBUG",
DebugLevel::Trace => "TRACE",
DebugLevel::Off => return,
};
self.write_raw(&format!(
"[{}] [{}] [{}] {}\n",
timestamp, level_str, category, msg
));
}
}
fn log_record(&mut self, record: &log::Record) {
let timestamp = get_timestamp();
let level_str = match record.level() {
log::Level::Error => "ERROR",
log::Level::Warn => "WARN ",
log::Level::Info => "INFO ",
log::Level::Debug => "DEBUG",
log::Level::Trace => "TRACE",
};
self.write_raw(&format!(
"[{}] [{}] [{}] {}\n",
timestamp,
level_str,
record.target(),
record.args()
));
}
}
static LOGGER: OnceLock<Mutex<DebugLogger>> = OnceLock::new();
fn get_logger() -> &'static Mutex<DebugLogger> {
LOGGER.get_or_init(|| Mutex::new(DebugLogger::new()))
}
fn get_timestamp() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("SystemTime::now() is always after UNIX_EPOCH");
format!("{}.{:06}", now.as_secs(), now.subsec_micros())
}
pub fn log_path() -> std::path::PathBuf {
std::env::temp_dir().join("par_term_debug.log")
}
pub fn is_enabled(level: DebugLevel) -> bool {
let logger = get_logger().lock();
level <= logger.level
}
pub fn log(level: DebugLevel, category: &str, msg: &str) {
let mut logger = get_logger().lock();
logger.log(level, category, msg);
}
pub fn logf(level: DebugLevel, category: &str, args: fmt::Arguments) {
if is_enabled(level) {
log(level, category, &format!("{}", args));
}
}
struct LogCrateBridge {
max_level: log::LevelFilter,
mirror_stderr: bool,
module_filters: Vec<(&'static str, log::LevelFilter)>,
}
impl LogCrateBridge {
fn new() -> Self {
let rust_log_set = std::env::var("RUST_LOG").is_ok();
let max_level = if rust_log_set {
match std::env::var("RUST_LOG")
.unwrap_or_default()
.to_lowercase()
.as_str()
{
"trace" => log::LevelFilter::Trace,
"debug" => log::LevelFilter::Debug,
"info" => log::LevelFilter::Info,
"warn" => log::LevelFilter::Warn,
"error" => log::LevelFilter::Error,
"off" => log::LevelFilter::Off,
_ => log::LevelFilter::Info, }
} else {
log::LevelFilter::Info
};
LogCrateBridge {
max_level,
mirror_stderr: rust_log_set,
module_filters: vec![
("wgpu_core", log::LevelFilter::Warn),
("wgpu_hal", log::LevelFilter::Warn),
("naga", log::LevelFilter::Warn),
("rodio", log::LevelFilter::Error),
("cpal", log::LevelFilter::Error),
],
}
}
fn level_for_module(&self, target: &str) -> log::LevelFilter {
for (prefix, filter) in &self.module_filters {
if target.starts_with(prefix) {
return *filter;
}
}
self.max_level
}
}
impl log::Log for LogCrateBridge {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= self.level_for_module(metadata.target())
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
let mut logger = get_logger().lock();
logger.log_record(record);
drop(logger);
if self.mirror_stderr {
eprintln!(
"[{}] {}: {}",
record.level(),
record.target(),
record.args()
);
}
}
fn flush(&self) {}
}
pub fn init_log_bridge(level_override: Option<log::LevelFilter>) {
let _ = get_logger();
let bridge = LogCrateBridge::new();
let max_level = level_override.unwrap_or(bridge.max_level);
if log::set_boxed_logger(Box::new(bridge)).is_ok() {
log::set_max_level(max_level);
}
}
pub fn set_log_level(level: log::LevelFilter) {
log::set_max_level(level);
}
pub static TRY_LOCK_FAILURE_COUNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
static TRY_LOCK_LAST_REPORTED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[inline]
pub fn record_try_lock_failure(site: &str) {
let total = TRY_LOCK_FAILURE_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
logf(
DebugLevel::Debug,
"CONCURRENCY",
format_args!("try_lock() miss at '{}' (lifetime total: {})", site, total),
);
}
#[inline]
pub fn try_lock_failure_count() -> u64 {
TRY_LOCK_FAILURE_COUNT.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn maybe_log_try_lock_telemetry() {
let current = TRY_LOCK_FAILURE_COUNT.load(std::sync::atomic::Ordering::Relaxed);
let last = TRY_LOCK_LAST_REPORTED.load(std::sync::atomic::Ordering::Relaxed);
if current > last {
if TRY_LOCK_LAST_REPORTED
.compare_exchange(
last,
current,
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
)
.is_ok()
{
let new_since_last = current - last;
logf(
DebugLevel::Info,
"CONCURRENCY",
format_args!(
"try_lock telemetry: {} new failure(s) this interval, {} lifetime total",
new_since_last, current
),
);
}
}
}
#[macro_export]
macro_rules! debug_error {
($category:expr, $($arg:tt)*) => {
$crate::debug::logf($crate::debug::DebugLevel::Error, $category, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! debug_info {
($category:expr, $($arg:tt)*) => {
$crate::debug::logf($crate::debug::DebugLevel::Info, $category, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! debug_log {
($category:expr, $($arg:tt)*) => {
$crate::debug::logf($crate::debug::DebugLevel::Debug, $category, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! debug_trace {
($category:expr, $($arg:tt)*) => {
$crate::debug::logf($crate::debug::DebugLevel::Trace, $category, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! debug_and_log_warn {
($category:expr, $($arg:tt)*) => {{
$crate::debug::logf(
$crate::debug::DebugLevel::Error,
$category,
format_args!($($arg)*),
);
log::warn!($($arg)*);
}};
}
#[macro_export]
macro_rules! debug_and_log_error {
($category:expr, $($arg:tt)*) => {{
$crate::debug::logf(
$crate::debug::DebugLevel::Error,
$category,
format_args!($($arg)*),
);
log::error!($($arg)*);
}};
}