1use std::sync::OnceLock;
10use std::time::Instant;
11
12pub fn enabled() -> bool {
14 static ON: OnceLock<bool> = OnceLock::new();
15 *ON.get_or_init(|| {
16 (std::env::var("ACTL_TRACE")
17 .map(|v| !v.is_empty() && v != "0")
18 .unwrap_or(false))
19 || file_path().is_some()
20 })
21}
22
23fn file_path() -> Option<&'static std::path::Path> {
26 static PATH: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
27 PATH.get_or_init(|| {
28 std::env::var_os("ACTL_TRACE_FILE")
29 .filter(|v| !v.is_empty())
30 .map(std::path::PathBuf::from)
31 })
32 .as_deref()
33}
34
35fn emit(line: &str) {
36 if enabled() {
37 eprintln!("{line}");
38 }
39 if let Some(path) = file_path() {
40 use std::io::Write;
41 if let Ok(mut file) = std::fs::OpenOptions::new()
42 .create(true)
43 .append(true)
44 .open(path)
45 {
46 let _ = writeln!(file, "{line}");
47 }
48 }
49}
50
51pub fn stage(stage: &str, started: Instant) {
53 if enabled() || file_path().is_some() {
54 emit(&format!(
55 "[actl-trace] {stage} {}ms",
56 started.elapsed().as_millis()
57 ));
58 }
59}
60
61pub struct TraceScope {
64 stage: &'static str,
65 started: Instant,
66}
67
68pub fn scope(stage: &'static str) -> TraceScope {
69 if enabled() || file_path().is_some() {
70 emit(&format!("[actl-trace] {stage} begin"));
71 }
72 TraceScope {
73 stage,
74 started: Instant::now(),
75 }
76}
77
78impl Drop for TraceScope {
79 fn drop(&mut self) {
80 stage(self.stage, self.started);
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn disabled_by_default_and_stage_is_silent_noop() {
90 let ok = std::panic::catch_unwind(|| {
92 stage("test.point", Instant::now());
93 let _t = scope("test.scope");
94 });
95 assert!(ok.is_ok());
96 }
97
98 #[test]
99 fn scope_drop_invokes_stage_without_panic() {
100 let _t = scope("test.lifetime");
101 drop(_t);
102 }
103}