Skip to main content

zsh/
log.rs

1//! zshrs logging & profiling framework
2//!
3//! **Logging** (always on):
4//!   - File: $HOME/.cache/zshrs/zshrs.log
5//!   - Level: ZSHRS_LOG env var (default: info)
6//!   - Structured key=value fields, ISO timestamps, thread names, module paths
7//!
8//! **Profiling** (feature-gated, zero cost when off):
9//!   - `--features profiling`  → chrome://tracing JSON  → $HOME/.cache/zshrs/trace-{PID}.json
10//!   - `--features flamegraph` → folded stacks          → $HOME/.cache/zshrs/flame-{PID}.folded
11//!   - `--features prometheus` → metrics on :9090/metrics
12//!
13//! Call `zsh::log::init()` once at startup. Use `tracing::{info,debug,trace,warn,error}!`
14//! everywhere. Use `#[tracing::instrument]` or `zsh::log::span!` for timed sections.
15
16use std::path::PathBuf;
17use std::sync::OnceLock;
18use tracing_subscriber::prelude::*;
19
20/// Guards that must live for the duration of the process.
21/// Dropping any of these flushes and stops the associated writer.
22struct Guards {
23    #[cfg(feature = "profiling")]
24    _chrome: tracing_chrome::FlushGuard,
25    #[cfg(feature = "flamegraph")]
26    _flame: tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>,
27}
28
29static GUARDS: OnceLock<Guards> = OnceLock::new();
30
31/// Resolve log/profile output directory: $HOME/.cache/zshrs/
32pub fn log_dir() -> PathBuf {
33    dirs::home_dir()
34        .unwrap_or_else(|| PathBuf::from("/tmp"))
35        .join(".cache/zshrs")
36}
37
38/// Resolve full log path: $HOME/.cache/zshrs/zshrs.log
39pub fn log_path() -> PathBuf {
40    log_dir().join("zshrs.log")
41}
42
43/// Initialize logging + optional profiling subscribers.
44/// Safe to call multiple times — only the first call takes effect.
45///
46/// Env vars:
47///   ZSHRS_LOG=debug|trace|info|warn|error  (default: info)
48pub fn init() {
49    GUARDS.get_or_init(|| {
50        let dir = log_dir();
51        let _ = std::fs::create_dir_all(&dir);
52        let pid = std::process::id();
53
54        // --- File log layer (always on) ---
55        // Use a blocking Mutex<File> writer — log writes are microseconds and this
56        // guarantees data reaches disk even when std::process::exit() skips destructors.
57        let log_file = std::fs::OpenOptions::new()
58            .create(true)
59            .append(true)
60            .open(dir.join("zshrs.log"))
61            .unwrap_or_else(|_| {
62                std::fs::OpenOptions::new()
63                    .create(true)
64                    .append(true)
65                    .open("/tmp/zshrs.log")
66                    .expect("cannot open any log file")
67            });
68        let log_writer = std::sync::Mutex::new(log_file);
69
70        let env_filter = std::env::var("ZSHRS_LOG").unwrap_or_else(|_| "info".to_string());
71
72        let file_layer = tracing_subscriber::fmt::layer()
73            .with_writer(log_writer)
74            .with_ansi(false)
75            .with_target(true)
76            .with_thread_names(true)
77            .compact();
78
79        // --- Chrome tracing layer (--features profiling) ---
80        #[cfg(feature = "profiling")]
81        let (chrome_layer, chrome_guard) = {
82            let trace_path = dir.join(format!("trace-{}.json", pid));
83            let (layer, guard) = tracing_chrome::ChromeLayerBuilder::new()
84                .file(trace_path)
85                .include_args(true)
86                .build();
87            (Some(layer), guard)
88        };
89        #[cfg(not(feature = "profiling"))]
90        let chrome_layer: Option<tracing_subscriber::layer::Identity> = None;
91
92        // --- Flamegraph layer (--features flamegraph) ---
93        #[cfg(feature = "flamegraph")]
94        let (flame_layer, flame_guard) = {
95            let flame_path = dir.join(format!("flame-{}.folded", pid));
96            let file =
97                std::fs::File::create(&flame_path).expect("cannot create flamegraph output file");
98            let writer = std::io::BufWriter::new(file);
99            let (layer, guard) = tracing_flame::FlameLayer::with_writer(writer).build();
100            (Some(layer), guard)
101        };
102        #[cfg(not(feature = "flamegraph"))]
103        let flame_layer: Option<tracing_subscriber::layer::Identity> = None;
104
105        // --- Prometheus metrics (--features prometheus) ---
106        #[cfg(feature = "prometheus")]
107        {
108            // Spawn metrics HTTP server on :9090 in background
109            let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
110            if let Err(e) = builder.with_http_listener(([127, 0, 0, 1], 9090)).install() {
111                eprintln!("zshrs: failed to start prometheus exporter: {}", e);
112            }
113        }
114
115        // --- Assemble the subscriber registry ---
116        let subscriber = tracing_subscriber::registry()
117            .with(tracing_subscriber::EnvFilter::new(&env_filter))
118            .with(file_layer)
119            .with(chrome_layer)
120            .with(flame_layer);
121
122        let _ = tracing::subscriber::set_global_default(subscriber);
123
124        Guards {
125            #[cfg(feature = "profiling")]
126            _chrome: chrome_guard,
127            #[cfg(feature = "flamegraph")]
128            _flame: flame_guard,
129        }
130    });
131}
132
133/// Flush all log writers. Call before std::process::exit() to ensure
134/// buffered log data reaches disk — exit() doesn't run destructors.
135pub fn flush() {
136    // The WorkerGuard flushes on drop, but we can't drop a static.
137    // Instead, give the non-blocking writer time to drain its buffer.
138    // 50ms is more than enough for any reasonable log volume.
139    std::thread::sleep(std::time::Duration::from_millis(50));
140}
141
142/// Convenience: check if profiling features are compiled in
143pub fn profiling_enabled() -> bool {
144    cfg!(feature = "profiling")
145}
146
147pub fn flamegraph_enabled() -> bool {
148    cfg!(feature = "flamegraph")
149}
150
151pub fn prometheus_enabled() -> bool {
152    cfg!(feature = "prometheus")
153}