Skip to main content

git_iris/
logger.rs

1use parking_lot::Mutex;
2use std::fs::OpenOptions;
3use std::io::{self, Write};
4use tracing_subscriber::{
5    EnvFilter, Registry,
6    fmt::{self, format::FmtSpan},
7    layer::SubscriberExt,
8    util::SubscriberInitExt,
9};
10
11static LOGGING_ENABLED: std::sync::LazyLock<Mutex<bool>> =
12    std::sync::LazyLock::new(|| Mutex::new(false));
13static LOG_FILE: std::sync::LazyLock<Mutex<Option<std::fs::File>>> =
14    std::sync::LazyLock::new(|| Mutex::new(None));
15static LOG_TO_STDOUT: std::sync::LazyLock<Mutex<bool>> =
16    std::sync::LazyLock::new(|| Mutex::new(false));
17static VERBOSE_LOGGING: std::sync::LazyLock<Mutex<bool>> =
18    std::sync::LazyLock::new(|| Mutex::new(false));
19
20/// Custom writer that writes to both file and stdout/stderr
21#[derive(Clone)]
22struct UnifiedWriter;
23
24impl Write for UnifiedWriter {
25    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
26        // Write to file if configured
27        if let Some(file) = LOG_FILE.lock().as_mut() {
28            let _ = file.write_all(buf);
29            let _ = file.flush();
30        }
31
32        // Also write to stdout if enabled (for CLI debug mode)
33        if *LOG_TO_STDOUT.lock() {
34            let _ = io::stdout().write_all(buf);
35        }
36
37        Ok(buf.len())
38    }
39
40    fn flush(&mut self) -> io::Result<()> {
41        if let Some(file) = LOG_FILE.lock().as_mut() {
42            let _ = file.flush();
43        }
44        if *LOG_TO_STDOUT.lock() {
45            let _ = io::stdout().flush();
46        }
47        Ok(())
48    }
49}
50
51impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for UnifiedWriter {
52    type Writer = UnifiedWriter;
53
54    fn make_writer(&'a self) -> Self::Writer {
55        UnifiedWriter
56    }
57}
58
59/// Initialize unified logging system using tracing.
60///
61/// `debug` enables debug-level output for git-iris and rig crates. When false,
62/// only warnings and errors pass through the tracing filter. This must be called
63/// after CLI flags are parsed so `--log` can raise the level.
64///
65/// # Errors
66///
67/// Returns an error when the tracing subscriber cannot be initialized.
68pub fn init(debug: bool) -> Result<(), Box<dyn std::error::Error>> {
69    use std::sync::{Once, OnceLock};
70    static INIT: Once = Once::new();
71    static INIT_RESULT: OnceLock<Result<(), String>> = OnceLock::new();
72
73    INIT.call_once(|| {
74        // Check if we should enable verbose logging from environment
75        let verbose_from_env = std::env::var("GIT_IRIS_VERBOSE").is_ok()
76            || std::env::var("RUST_LOG").is_ok_and(|v| v.contains("debug") || v.contains("trace"));
77
78        let verbose = debug || verbose_from_env;
79
80        if verbose {
81            set_verbose_logging(true);
82            set_log_to_stdout(true);
83        }
84
85        // Enable logging to file only by default (stdout requires explicit --log flag)
86        enable_logging();
87
88        // Set up tracing subscriber with unified writer (for Rig logs)
89        let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
90            if verbose {
91                "git_iris=debug,iris=debug,rig=info,warn".into()
92            } else {
93                // Silent by default - no debug spam
94                "warn".into()
95            }
96        });
97
98        let fmt_layer = fmt::Layer::new()
99            .with_target(true)
100            .with_level(true)
101            .with_timer(fmt::time::ChronoUtc::rfc_3339())
102            .with_span_events(FmtSpan::CLOSE)
103            .with_writer(UnifiedWriter);
104
105        let result = Registry::default()
106            .with(env_filter)
107            .with(fmt_layer)
108            .try_init()
109            .map_err(|e| format!("Failed to initialize logging: {e}"));
110
111        let _ = INIT_RESULT.set(result);
112    });
113
114    match INIT_RESULT.get() {
115        Some(Ok(())) => Ok(()),
116        Some(Err(e)) => Err(e.clone().into()),
117        None => Err("Initialization failed unexpectedly".into()),
118    }
119}
120
121pub fn enable_logging() {
122    let mut logging_enabled = LOGGING_ENABLED.lock();
123    *logging_enabled = true;
124}
125
126pub fn disable_logging() {
127    let mut logging_enabled = LOGGING_ENABLED.lock();
128    *logging_enabled = false;
129}
130
131pub fn set_verbose_logging(enabled: bool) {
132    let mut verbose_logging = VERBOSE_LOGGING.lock();
133    *verbose_logging = enabled;
134
135    // Note: Verbose logging changes will take effect on next application restart
136    // or can be controlled via RUST_LOG environment variable before startup
137}
138
139/// Check if a log file is already configured
140#[must_use]
141pub fn has_log_file() -> bool {
142    LOG_FILE.lock().is_some()
143}
144
145/// Configure the log file destination.
146///
147/// # Errors
148///
149/// Returns an error when the log file cannot be opened for append.
150pub fn set_log_file(file_path: &str) -> std::io::Result<()> {
151    let file = OpenOptions::new()
152        .create(true)
153        .append(true)
154        .open(file_path)?;
155
156    let mut log_file = LOG_FILE.lock();
157    *log_file = Some(file);
158    Ok(())
159}
160
161pub fn set_log_to_stdout(enabled: bool) {
162    let mut log_to_stdout = LOG_TO_STDOUT.lock();
163    *log_to_stdout = enabled;
164}
165
166// All logging goes through tracing now
167#[macro_export]
168macro_rules! log_debug {
169    ($($arg:tt)*) => {
170        tracing::debug!($($arg)*)
171    };
172}
173
174#[macro_export]
175macro_rules! log_error {
176    ($($arg:tt)*) => {
177        tracing::error!($($arg)*)
178    };
179}
180
181#[macro_export]
182macro_rules! log_info {
183    ($($arg:tt)*) => {
184        tracing::info!($($arg)*)
185    };
186}
187
188#[macro_export]
189macro_rules! log_warn {
190    ($($arg:tt)*) => {
191        tracing::warn!($($arg)*)
192    };
193}
194
195// Tracing macros for enhanced logging (following Rig patterns)
196#[macro_export]
197macro_rules! trace_debug {
198    (target: $target:expr, $($arg:tt)*) => {
199        tracing::debug!(target: $target, $($arg)*)
200    };
201    ($($arg:tt)*) => {
202        tracing::debug!($($arg)*)
203    };
204}
205
206#[macro_export]
207macro_rules! trace_info {
208    (target: $target:expr, $($arg:tt)*) => {
209        tracing::info!(target: $target, $($arg)*)
210    };
211    ($($arg:tt)*) => {
212        tracing::info!($($arg)*)
213    };
214}
215
216#[macro_export]
217macro_rules! trace_warn {
218    (target: $target:expr, $($arg:tt)*) => {
219        tracing::warn!(target: $target, $($arg)*)
220    };
221    ($($arg:tt)*) => {
222        tracing::warn!($($arg)*)
223    };
224}
225
226#[macro_export]
227macro_rules! trace_error {
228    (target: $target:expr, $($arg:tt)*) => {
229        tracing::error!(target: $target, $($arg)*)
230    };
231    ($($arg:tt)*) => {
232        tracing::error!($($arg)*)
233    };
234}