1use crate::config::global_xbp_paths;
8use crate::utils::find_xbp_config_upwards;
9use chrono::format::{DelayedFormat, StrftimeItems};
10use chrono::{DateTime, Local};
11use colored::{ColoredString, Colorize};
12use fs::ReadDir;
13use serde::{Deserialize, Serialize};
14use std::fs::{self, OpenOptions};
15use std::io::Write;
16use std::path::{Path, PathBuf};
17use std::sync::MutexGuard;
18use std::sync::{Arc, Mutex, OnceLock};
19use std::time::{Duration, Instant};
20static LOGGER: OnceLock<Arc<Mutex<Option<Arc<XbpLogger>>>>> = OnceLock::new();
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum LogLevel {
25 Info,
26 Warning,
27 Error,
28 Debug,
29 Success,
30}
31
32impl LogLevel {
33 fn to_string(&self) -> &'static str {
34 match self {
35 LogLevel::Info => "INFO",
36 LogLevel::Warning => "WARN",
37 LogLevel::Error => "ERROR",
38 LogLevel::Debug => "DEBUG",
39 LogLevel::Success => "SUCCESS",
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct LogEntry {
46 pub timestamp: DateTime<Local>,
47 pub level: LogLevel,
48 pub command: String,
49 pub message: String,
50 pub details: Option<String>,
51 pub duration_ms: Option<u64>,
52}
53
54pub struct XbpLogger {
55 log_dir: PathBuf,
56 debug_enabled: bool,
57 project_name: Option<String>,
58}
59
60impl XbpLogger {
61 pub async fn new(debug: bool) -> Result<Self, String> {
62 let log_dir: PathBuf = Self::determine_and_ensure_log_directory().await?;
63 let project_name: Option<String> = Self::detect_project_name();
64
65 let logger: XbpLogger = XbpLogger {
66 log_dir,
67 debug_enabled: debug,
68 project_name,
69 };
70
71 Ok(logger)
72 }
73
74 fn detect_project_name() -> Option<String> {
75 let current_dir: PathBuf = std::env::current_dir().ok()?;
76 let found: crate::utils::FoundXbpConfig = find_xbp_config_upwards(¤t_dir)?;
77 let content: String = fs::read_to_string(&found.config_path).ok()?;
78
79 let json = if found.kind == "yaml" {
80 serde_yaml::from_str::<serde_yaml::Value>(&content)
81 .ok()
82 .and_then(|v| serde_json::to_value(v).ok())?
83 } else {
84 serde_json::from_str::<serde_json::Value>(&content).ok()?
85 };
86
87 json.get("project_name")
88 .and_then(|v| v.as_str())
89 .map(|s| s.to_string())
90 }
91
92 pub async fn determine_and_ensure_log_directory() -> Result<PathBuf, String> {
93 let user_log_dir = global_xbp_paths()?.logs_dir;
94 Self::try_create_and_set_permissions(&user_log_dir, 0o700).await?;
95 Ok(user_log_dir)
96 }
97
98 #[cfg(unix)]
99 async fn try_create_and_set_permissions(path: &Path, mode: u32) -> Result<(), String> {
100 use std::os::unix::fs::PermissionsExt;
101
102 fs::create_dir_all(path)
103 .map_err(|e| format!("Failed to create log directory {}: {}", path.display(), e))?;
104
105 let permissions = std::fs::Permissions::from_mode(mode);
106 fs::set_permissions(path, permissions).map_err(|e| {
107 format!(
108 "Failed to set log directory permissions {}: {}",
109 path.display(),
110 e
111 )
112 })?;
113 Ok(())
114 }
115
116 #[cfg(not(unix))]
117 async fn try_create_and_set_permissions(path: &Path, _mode: u32) -> Result<(), String> {
118 fs::create_dir_all(path)
119 .map_err(|e| format!("Failed to create log directory {}: {}", path.display(), e))?;
120 Ok(())
121 }
122
123 pub async fn log_info(
125 &self,
126 command: &str,
127 message: &str,
128 details: Option<&str>,
129 ) -> Result<(), String> {
130 self.write_log(LogLevel::Info, command, message, details, None)
131 .await
132 }
133
134 pub async fn log_warning(
136 &self,
137 command: &str,
138 message: &str,
139 details: Option<&str>,
140 ) -> Result<(), String> {
141 self.write_log(LogLevel::Warning, command, message, details, None)
142 .await
143 }
144
145 pub async fn log_error(
147 &self,
148 command: &str,
149 message: &str,
150 details: Option<&str>,
151 ) -> Result<(), String> {
152 self.write_log(LogLevel::Error, command, message, details, None)
153 .await
154 }
155
156 pub async fn log_success(
158 &self,
159 command: &str,
160 message: &str,
161 details: Option<&str>,
162 ) -> Result<(), String> {
163 self.write_log(LogLevel::Success, command, message, details, None)
164 .await
165 }
166
167 pub async fn log_debug(
169 &self,
170 command: &str,
171 message: &str,
172 details: Option<&str>,
173 ) -> Result<(), String> {
174 if self.debug_enabled {
175 self.write_log(LogLevel::Debug, command, message, details, None)
176 .await
177 } else {
178 Ok(())
179 }
180 }
181
182 pub async fn log_timed(
184 &self,
185 level: LogLevel,
186 command: &str,
187 message: &str,
188 duration_ms: u64,
189 ) -> Result<(), String> {
190 self.write_log(level, command, message, None, Some(duration_ms))
191 .await
192 }
193
194 async fn write_log(
196 &self,
197 level: LogLevel,
198 command: &str,
199 message: &str,
200 details: Option<&str>,
201 duration_ms: Option<u64>,
202 ) -> Result<(), String> {
203 let entry: LogEntry = LogEntry {
204 timestamp: Local::now(),
205 level: level.clone(),
206 command: command.to_string(),
207 message: message.to_string(),
208 details: details.map(|s| s.to_string()),
209 duration_ms,
210 };
211
212 self.write_console(&entry);
214
215 self.write_file(&entry).await?;
217
218 crate::data::athena::persist_log_entry(&entry).await;
219
220 Ok(())
221 }
222
223 fn write_console(&self, entry: &LogEntry) {
224 use colored::Colorize;
225
226 let duration_str: String = if let Some(duration) = entry.duration_ms {
227 format!(" {}", format!("({}ms)", duration).dimmed())
228 } else {
229 String::new()
230 };
231
232 let level_colored: ColoredString = match entry.level {
233 LogLevel::Info => "INFO".cyan(),
234 LogLevel::Warning => "WARN".yellow(),
235 LogLevel::Error => "ERROR".red(),
236 LogLevel::Debug => "DEBUG".magenta(),
237 LogLevel::Success => "SUCCESS".green(),
238 };
239
240 let command_colored: ColoredString = entry.command.bright_blue();
241
242 let message_line: String = format!(
243 "{} {} {}{}",
244 level_colored, command_colored, entry.message, duration_str
245 );
246
247 match entry.level {
248 LogLevel::Warning | LogLevel::Error => eprintln!("{}", message_line),
249 _ => println!("{}", message_line),
250 }
251
252 if let Some(details) = &entry.details {
253 match entry.level {
254 LogLevel::Warning | LogLevel::Error => eprintln!(" {}", details.dimmed()),
255 _ => println!(" {}", details.dimmed()),
256 }
257 }
258 }
259
260 async fn write_file(&self, entry: &LogEntry) -> Result<(), String> {
262 let date_str: DelayedFormat<StrftimeItems<'_>> = entry.timestamp.format("%Y-%m-%d");
264 let general_log: PathBuf = self.log_dir.join(format!("xbp-{}.log", date_str));
265 let command_log: PathBuf = self
266 .log_dir
267 .join(format!("{}-{}.log", entry.command, date_str));
268
269 let duration_str: String = if let Some(duration) = entry.duration_ms {
271 format!(" duration={}ms", duration)
272 } else {
273 String::new()
274 };
275
276 let details_str: String = if let Some(details) = &entry.details {
277 format!(" details=\"{}\"", details.replace('"', "'"))
278 } else {
279 String::new()
280 };
281
282 let log_line: String = format!(
283 "{} level={} command={} message=\"{}\"{}{}\n",
284 entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
285 entry.level.to_string(),
286 entry.command,
287 entry.message.replace('"', "'"),
288 details_str,
289 duration_str
290 );
291
292 self.append_to_file(&general_log, &log_line).await?;
294 self.append_to_file(&command_log, &log_line).await?;
295
296 self.rotate_logs_if_needed(&general_log).await?;
298 self.rotate_logs_if_needed(&command_log).await?;
299
300 Ok(())
301 }
302
303 async fn append_to_file(&self, file_path: &Path, content: &str) -> Result<(), String> {
305 let mut file = OpenOptions::new()
306 .create(true)
307 .append(true)
308 .open(file_path)
309 .map_err(|e| format!("Failed to open log file {}: {}", file_path.display(), e))?;
310
311 file.write_all(content.as_bytes())
312 .map_err(|e| format!("Failed to write to log file: {}", e))?;
313
314 Ok(())
315 }
316
317 async fn rotate_logs_if_needed(&self, file_path: &Path) -> Result<(), String> {
319 if let Ok(metadata) = fs::metadata(file_path) {
320 const MAX_SIZE: u64 = 10 * 1024 * 1024; if metadata.len() > MAX_SIZE {
323 let rotated_path: PathBuf =
324 file_path.with_extension(format!("log.{}", Local::now().format("%H%M%S")));
325
326 fs::rename(file_path, &rotated_path)
327 .map_err(|e| format!("Failed to rotate log file: {}", e))?;
328
329 let entry: LogEntry = LogEntry {
331 timestamp: Local::now(),
332 level: LogLevel::Info,
333 command: "system".to_string(),
334 message: format!("Rotated log file to {}", rotated_path.display()),
335 details: None,
336 duration_ms: None,
337 };
338 self.write_console(&entry);
339 }
340 }
341
342 Ok(())
343 }
344
345 pub async fn log_command_execution(
347 &self,
348 command: &str,
349 cmd_args: &[&str],
350 start_time: Instant,
351 ) -> Result<(), String> {
352 let duration: Duration = start_time.elapsed();
353 let duration_ms: u64 = duration.as_millis() as u64;
354
355 let full_command: String = format!("{} {}", command, cmd_args.join(" "));
356
357 self.log_timed(
358 LogLevel::Info,
359 "command",
360 &format!("Executed: {}", full_command),
361 duration_ms,
362 )
363 .await
364 }
365
366 pub async fn log_process_output(
368 &self,
369 command: &str,
370 process_name: &str,
371 stdout: &str,
372 stderr: &str,
373 ) -> Result<(), String> {
374 if !stdout.is_empty() {
375 self.log_info(command, &format!("{} stdout", process_name), Some(stdout))
376 .await?;
377 }
378
379 if !stderr.is_empty() {
380 self.log_warning(command, &format!("{} stderr", process_name), Some(stderr))
381 .await?;
382 }
383
384 Ok(())
385 }
386
387 pub fn log_dir(&self) -> &Path {
389 &self.log_dir
390 }
391
392 pub fn get_project_name(&self) -> Option<String> {
393 self.project_name.clone()
394 }
395
396 pub async fn list_log_files(&self) -> Result<Vec<PathBuf>, String> {
398 let mut files: Vec<PathBuf> = Vec::new();
399
400 let entries: ReadDir = fs::read_dir(&self.log_dir)
401 .map_err(|e| format!("Failed to read log directory: {}", e))?;
402
403 for entry in entries {
404 let entry: fs::DirEntry =
405 entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
406 let path = entry.path();
407
408 if path.is_file() && path.extension().is_some_and(|ext| ext == "log") {
409 files.push(path);
410 }
411 }
412
413 files.sort();
414 Ok(files)
415 }
416
417 pub async fn read_recent_logs(
419 &self,
420 file_path: &Path,
421 lines: usize,
422 ) -> Result<Vec<String>, String> {
423 let content: String =
424 fs::read_to_string(file_path).map_err(|e| format!("Failed to read log file: {}", e))?;
425
426 let all_lines: Vec<&str> = content.lines().collect();
427 let recent_lines: &[&str] = if all_lines.len() > lines {
428 &all_lines[all_lines.len() - lines..]
429 } else {
430 &all_lines
431 };
432
433 Ok(recent_lines.iter().map(|s| s.to_string()).collect())
434 }
435}
436
437pub async fn init_logger(debug: bool) -> Result<(), String> {
439 let logger: Arc<XbpLogger> = Arc::new(XbpLogger::new(debug).await?);
440
441 let mutex: &Arc<Mutex<Option<Arc<XbpLogger>>>> =
442 LOGGER.get_or_init(|| Arc::new(Mutex::new(None)));
443 let mut guard: MutexGuard<'_, Option<Arc<XbpLogger>>> = mutex
444 .lock()
445 .map_err(|e| format!("Failed to lock logger: {}", e))?;
446 *guard = Some(logger);
447
448 Ok(())
449}
450
451pub fn get_logger() -> Option<Arc<XbpLogger>> {
453 LOGGER.get().and_then(|mutex| {
454 mutex
455 .lock()
456 .ok()
457 .and_then(|guard: MutexGuard<'_, Option<Arc<XbpLogger>>>| guard.as_ref().cloned())
458 })
459}
460
461pub async fn get_log_directory() -> Result<PathBuf, String> {
463 XbpLogger::determine_and_ensure_log_directory().await
464}
465
466pub async fn log_info(command: &str, message: &str, details: Option<&str>) -> Result<(), String> {
468 if let Some(logger) = get_logger() {
469 logger.log_info(command, message, details).await
470 } else {
471 Ok(())
472 }
473}
474
475pub async fn log_error(command: &str, message: &str, details: Option<&str>) -> Result<(), String> {
477 if let Some(logger) = get_logger() {
478 logger.log_error(command, message, details).await
479 } else {
480 Ok(())
481 }
482}
483
484pub async fn log_warn(command: &str, message: &str, details: Option<&str>) -> Result<(), String> {
486 if let Some(logger) = get_logger() {
487 logger.log_warning(command, message, details).await
488 } else {
489 Ok(())
490 }
491}
492
493pub async fn log_success(
495 command: &str,
496 message: &str,
497 details: Option<&str>,
498) -> Result<(), String> {
499 if let Some(logger) = get_logger() {
500 logger.log_success(command, message, details).await
501 } else {
502 Ok(())
503 }
504}
505
506pub async fn log_timed(
508 level: LogLevel,
509 command: &str,
510 message: &str,
511 duration_ms: u64,
512) -> Result<(), String> {
513 if let Some(logger) = get_logger() {
514 logger.log_timed(level, command, message, duration_ms).await
515 } else {
516 Ok(())
517 }
518}
519
520pub async fn log_process_output(
522 command: &str,
523 process_name: &str,
524 stdout: &str,
525 stderr: &str,
526) -> Result<(), String> {
527 if let Some(logger) = get_logger() {
528 logger
529 .log_process_output(command, process_name, stdout, stderr)
530 .await
531 } else {
532 Ok(())
533 }
534}
535
536pub async fn log_debug(command: &str, message: &str, details: Option<&str>) -> Result<(), String> {
538 if let Some(logger) = get_logger() {
539 logger.log_debug(command, message, details).await
540 } else {
541 Ok(())
542 }
543}
544
545pub fn get_project_name() -> Option<String> {
547 if let Some(logger) = get_logger() {
548 return logger.get_project_name();
549 }
550 None
551}
552
553pub fn get_prefix() -> String {
555 let xbp_prefix = format!("{} | ", "XBP".bright_magenta().bold());
556 if let Some(name) = get_project_name() {
557 format!("{}{} | ", xbp_prefix, name.bright_blue())
558 } else {
559 xbp_prefix
560 }
561}
562
563pub fn with_logger<F, R>(f: F)
565where
566 F: FnOnce(&XbpLogger) -> R,
567{
568 if let Some(logger) = get_logger() {
569 f(&logger);
570 }
571}
572
573#[macro_export]
575macro_rules! xbp_log {
576 (info, $command:expr, $message:expr) => {
577 $crate::logging::with_logger(|logger| {
578 tokio::task::block_in_place(|| {
579 tokio::runtime::Handle::current().block_on(async {
580 let _ = logger.log_info($command, $message, None).await;
581 })
582 })
583 });
584 };
585 (info, $command:expr, $message:expr, $details:expr) => {
586 $crate::logging::with_logger(|logger| {
587 tokio::task::block_in_place(|| {
588 tokio::runtime::Handle::current().block_on(async {
589 let _ = logger.log_info($command, $message, Some($details)).await;
590 })
591 })
592 });
593 };
594 (warning, $command:expr, $message:expr) => {
595 $crate::logging::with_logger(|logger| {
596 tokio::task::block_in_place(|| {
597 tokio::runtime::Handle::current().block_on(async {
598 let _ = logger.log_warning($command, $message, None).await;
599 })
600 })
601 });
602 };
603 (error, $command:expr, $message:expr) => {
604 $crate::logging::with_logger(|logger| {
605 tokio::task::block_in_place(|| {
606 tokio::runtime::Handle::current().block_on(async {
607 let _ = logger.log_error($command, $message, None).await;
608 })
609 })
610 });
611 };
612 (success, $command:expr, $message:expr) => {
613 $crate::logging::with_logger(|logger| {
614 tokio::task::block_in_place(|| {
615 tokio::runtime::Handle::current().block_on(async {
616 let _ = logger.log_success($command, $message, None).await;
617 })
618 })
619 });
620 };
621 (debug, $command:expr, $message:expr) => {
622 $crate::logging::with_logger(|logger| {
623 tokio::task::block_in_place(|| {
624 tokio::runtime::Handle::current().block_on(async {
625 let _ = logger.log_debug($command, $message, None).await;
626 })
627 })
628 });
629 };
630}