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#[derive(Clone)]
27struct UnifiedWriter;
28
29impl Write for UnifiedWriter {
30 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
31 if let Some(file) = LOG_FILE.lock().as_mut() {
33 let _ = file.write_all(buf);
34 let _ = file.flush();
35 }
36
37 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 if metadata.target().starts_with("git_iris") {
72 return metadata.level() <= Level::Debug;
73 }
74
75 if metadata.target().starts_with("rig") {
77 return metadata.level() <= Level::Info;
78 }
79
80 let verbose_enabled = *VERBOSE_LOGGING.lock();
82 if !verbose_enabled {
83 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
133pub 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 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();
151
152 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 "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 let tracing_result = Registry::default()
172 .with(env_filter)
173 .with(fmt_layer)
174 .try_init();
175
176 let log_result = log::set_logger(&LOGGER).map(|()| log::set_max_level(LevelFilter::Debug));
178
179 let result = match tracing_result {
182 Ok(()) => Ok(()),
183 Err(tracing_err) => {
184 if log_result.is_ok() {
185 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 }
220
221pub 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#[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#[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}