use std::sync::OnceLock;
use std::time::Instant;
pub fn enabled() -> bool {
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| {
(std::env::var("ACTL_TRACE")
.map(|v| !v.is_empty() && v != "0")
.unwrap_or(false))
|| file_path().is_some()
})
}
fn file_path() -> Option<&'static std::path::Path> {
static PATH: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
PATH.get_or_init(|| {
std::env::var_os("ACTL_TRACE_FILE")
.filter(|v| !v.is_empty())
.map(std::path::PathBuf::from)
})
.as_deref()
}
fn emit(line: &str) {
if enabled() {
eprintln!("{line}");
}
if let Some(path) = file_path() {
use std::io::Write;
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
let _ = writeln!(file, "{line}");
}
}
}
pub fn stage(stage: &str, started: Instant) {
if enabled() || file_path().is_some() {
emit(&format!(
"[actl-trace] {stage} {}ms",
started.elapsed().as_millis()
));
}
}
pub struct TraceScope {
stage: &'static str,
started: Instant,
}
pub fn scope(stage: &'static str) -> TraceScope {
if enabled() || file_path().is_some() {
emit(&format!("[actl-trace] {stage} begin"));
}
TraceScope {
stage,
started: Instant::now(),
}
}
impl Drop for TraceScope {
fn drop(&mut self) {
stage(self.stage, self.started);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_by_default_and_stage_is_silent_noop() {
let ok = std::panic::catch_unwind(|| {
stage("test.point", Instant::now());
let _t = scope("test.scope");
});
assert!(ok.is_ok());
}
#[test]
fn scope_drop_invokes_stage_without_panic() {
let _t = scope("test.lifetime");
drop(_t);
}
}