git_iris/
logger.rs

1use chrono::Local;
2use log::{Level, LevelFilter, Metadata, Record};
3use parking_lot::Mutex;
4use std::fs::OpenOptions;
5use std::io::{self, Write};
6use tracing_subscriber::{
7    EnvFilter, Registry,
8    fmt::{self, format::FmtSpan},
9    layer::SubscriberExt,
10    util::SubscriberInitExt,
11};
12
13struct GitIrisLogger;
14
15static LOGGER: GitIrisLogger = GitIrisLogger;
16static LOGGING_ENABLED: std::sync::LazyLock<Mutex<bool>> =
17    std::sync::LazyLock::new(|| Mutex::new(false));
18static LOG_FILE: std::sync::LazyLock<Mutex<Option<std::fs::File>>> =
19    std::sync::LazyLock::new(|| Mutex::new(None));
20static LOG_TO_STDOUT: std::sync::LazyLock<Mutex<bool>> =
21    std::sync::LazyLock::new(|| Mutex::new(false));
22static VERBOSE_LOGGING: std::sync::LazyLock<Mutex<bool>> =
23    std::sync::LazyLock::new(|| Mutex::new(false));
24
25/// Custom writer that writes to both file and stdout/stderr
26#[derive(Clone)]
27struct UnifiedWriter;
28
29impl Write for UnifiedWriter {
30    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
31        // Write to file if configured
32        if let Some(file) = LOG_FILE.lock().as_mut() {
33            let _ = file.write_all(buf);
34            let _ = file.flush();
35        }
36
37        // Also write to stdout if enabled (for CLI debug mode)
38        if *LOG_TO_STDOUT.lock() {
39            let _ = io::stdout().write_all(buf);
40        }
41
42        Ok(buf.len())
43    }
44
45    fn flush(&mut self) -> io::Result<()> {
46        if let Some(file) = LOG_FILE.lock().as_mut() {
47            let _ = file.flush();
48        }
49        if *LOG_TO_STDOUT.lock() {
50            let _ = io::stdout().flush();
51        }
52        Ok(())
53    }
54}
55
56impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for UnifiedWriter {
57    type Writer = UnifiedWriter;
58
59    fn make_writer(&'a self) -> Self::Writer {
60        UnifiedWriter
61    }
62}
63
64impl log::Log for GitIrisLogger {
65    fn enabled(&self, metadata: &Metadata) -> bool {
66        if !*LOGGING_ENABLED.lock() {
67            return false;
68        }
69
70        // Always allow our own logs
71        if metadata.target().starts_with("git_iris") {
72            return metadata.level() <= Level::Debug;
73        }
74
75        // Allow rig logs - they provide valuable LLM operation insights
76        if metadata.target().starts_with("rig") {
77            return metadata.level() <= Level::Info;
78        }
79
80        // Filter external library logs unless verbose logging is enabled
81        let verbose_enabled = *VERBOSE_LOGGING.lock();
82        if !verbose_enabled {
83            // Block common noisy external libraries
84            let target = metadata.target();
85            if target.starts_with("reqwest")
86                || target.starts_with("hyper")
87                || target.starts_with("h2")
88                || target.starts_with("rustls")
89                || target.starts_with("want")
90                || target.starts_with("mio")
91                || target.contains("anthropic")
92                || target.contains("openai")
93                || target.contains("completion")
94                || target.contains("connection")
95            {
96                return false;
97            }
98        }
99
100        metadata.level() <= Level::Debug
101    }
102
103    fn log(&self, record: &Record) {
104        if self.enabled(record.metadata()) {
105            let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
106            let target = if record.target().starts_with("rig") {
107                "🦀 rig"
108            } else {
109                record.target()
110            };
111            let message = format!(
112                "{} {} [{}] - {}\n",
113                timestamp,
114                record.level(),
115                target,
116                record.args()
117            );
118
119            if let Some(file) = LOG_FILE.lock().as_mut() {
120                let _ = file.write_all(message.as_bytes());
121                let _ = file.flush();
122            }
123
124            if *LOG_TO_STDOUT.lock() {
125                print!("{message}");
126            }
127        }
128    }
129
130    fn flush(&self) {}
131}
132
133/// Initialize unified logging system supporting both log and tracing
134pub fn init() -> Result<(), Box<dyn std::error::Error>> {
135    use std::sync::{Once, OnceLock};
136    static INIT: Once = Once::new();
137    static INIT_RESULT: OnceLock<Result<(), String>> = OnceLock::new();
138
139    INIT.call_once(|| {
140        // Check if we should enable verbose logging from environment
141        let verbose_from_env = std::env::var("GIT_IRIS_VERBOSE").is_ok()
142            || std::env::var("RUST_LOG").is_ok_and(|v| v.contains("debug") || v.contains("trace"));
143
144        if verbose_from_env {
145            set_verbose_logging(true);
146            set_log_to_stdout(true);
147        }
148
149        // Enable logging to file only by default (stdout requires explicit --log flag)
150        enable_logging();
151
152        // Set up tracing subscriber with unified writer (for Rig logs)
153        // Default: only warnings/errors. Debug requires RUST_LOG or --log flag
154        let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
155            if verbose_from_env {
156                "git_iris=debug,iris=debug,rig=info,warn".into()
157            } else {
158                // Silent by default - no debug spam
159                "warn".into()
160            }
161        });
162
163        let fmt_layer = fmt::Layer::new()
164            .with_target(true)
165            .with_level(true)
166            .with_timer(fmt::time::ChronoUtc::rfc_3339())
167            .with_span_events(FmtSpan::CLOSE)
168            .with_writer(UnifiedWriter);
169
170        // Try to initialize tracing subscriber
171        let tracing_result = Registry::default()
172            .with(env_filter)
173            .with(fmt_layer)
174            .try_init();
175
176        // Try to initialize the log system for backwards compatibility
177        let log_result = log::set_logger(&LOGGER).map(|()| log::set_max_level(LevelFilter::Debug));
178
179        // Tracing handles the log facade automatically via its compatibility layer,
180        // so log::set_logger() will typically fail (which is fine)
181        let result = match tracing_result {
182            Ok(()) => Ok(()),
183            Err(tracing_err) => {
184                if log_result.is_ok() {
185                    // Fallback to log-only (tracing already initialized elsewhere)
186                    Ok(())
187                } else {
188                    Err(format!("Failed to initialize logging: {tracing_err}"))
189                }
190            }
191        };
192
193        let _ = INIT_RESULT.set(result);
194    });
195
196    match INIT_RESULT.get() {
197        Some(Ok(())) => Ok(()),
198        Some(Err(e)) => Err(e.clone().into()),
199        None => Err("Initialization failed unexpectedly".into()),
200    }
201}
202
203pub fn enable_logging() {
204    let mut logging_enabled = LOGGING_ENABLED.lock();
205    *logging_enabled = true;
206}
207
208pub fn disable_logging() {
209    let mut logging_enabled = LOGGING_ENABLED.lock();
210    *logging_enabled = false;
211}
212
213pub fn set_verbose_logging(enabled: bool) {
214    let mut verbose_logging = VERBOSE_LOGGING.lock();
215    *verbose_logging = enabled;
216
217    // Note: Verbose logging changes will take effect on next application restart
218    // or can be controlled via RUST_LOG environment variable before startup
219}
220
221/// Check if a log file is already configured
222pub fn has_log_file() -> bool {
223    LOG_FILE.lock().is_some()
224}
225
226pub fn set_log_file(file_path: &str) -> std::io::Result<()> {
227    let file = OpenOptions::new()
228        .create(true)
229        .append(true)
230        .open(file_path)?;
231
232    let mut log_file = LOG_FILE.lock();
233    *log_file = Some(file);
234    Ok(())
235}
236
237pub fn set_log_to_stdout(enabled: bool) {
238    let mut log_to_stdout = LOG_TO_STDOUT.lock();
239    *log_to_stdout = enabled;
240}
241
242// Macros for git-iris logging (maintains compatibility)
243#[macro_export]
244macro_rules! log_debug {
245    ($($arg:tt)*) => {
246        log::debug!($($arg)*)
247    };
248}
249
250#[macro_export]
251macro_rules! log_error {
252    ($($arg:tt)*) => {
253        log::error!($($arg)*)
254    };
255}
256
257#[macro_export]
258macro_rules! log_info {
259    ($($arg:tt)*) => {
260        log::info!($($arg)*)
261    };
262}
263
264#[macro_export]
265macro_rules! log_warn {
266    ($($arg:tt)*) => {
267        log::warn!($($arg)*)
268    };
269}
270
271// New tracing macros for enhanced logging (following Rig patterns)
272#[macro_export]
273macro_rules! trace_debug {
274    (target: $target:expr, $($arg:tt)*) => {
275        tracing::debug!(target: $target, $($arg)*)
276    };
277    ($($arg:tt)*) => {
278        tracing::debug!($($arg)*)
279    };
280}
281
282#[macro_export]
283macro_rules! trace_info {
284    (target: $target:expr, $($arg:tt)*) => {
285        tracing::info!(target: $target, $($arg)*)
286    };
287    ($($arg:tt)*) => {
288        tracing::info!($($arg)*)
289    };
290}
291
292#[macro_export]
293macro_rules! trace_warn {
294    (target: $target:expr, $($arg:tt)*) => {
295        tracing::warn!(target: $target, $($arg)*)
296    };
297    ($($arg:tt)*) => {
298        tracing::warn!($($arg)*)
299    };
300}
301
302#[macro_export]
303macro_rules! trace_error {
304    (target: $target:expr, $($arg:tt)*) => {
305        tracing::error!(target: $target, $($arg)*)
306    };
307    ($($arg:tt)*) => {
308        tracing::error!($($arg)*)
309    };
310}