reminder_cli/
logger.rs

1use anyhow::Result;
2use chrono::Local;
3use std::fs::{self, File, OpenOptions};
4use std::io::{BufRead, BufReader, Write};
5use std::path::PathBuf;
6
7const MAX_LOG_SIZE: u64 = 1024 * 1024; // 1MB
8const LOG_FILE_NAME: &str = "reminder.log";
9const OLD_LOG_FILE_NAME: &str = "reminder.log.old";
10
11pub struct Logger {
12    path: PathBuf,
13    old_path: PathBuf,
14}
15
16impl Logger {
17    pub fn new() -> Result<Self> {
18        let data_dir = dirs::data_local_dir()
19            .ok_or_else(|| anyhow::anyhow!("Failed to get local data directory"))?
20            .join("reminder-cli");
21
22        fs::create_dir_all(&data_dir)?;
23
24        Ok(Self {
25            path: data_dir.join(LOG_FILE_NAME),
26            old_path: data_dir.join(OLD_LOG_FILE_NAME),
27        })
28    }
29
30    fn rotate_if_needed(&self) -> Result<()> {
31        if !self.path.exists() {
32            return Ok(());
33        }
34
35        let metadata = fs::metadata(&self.path)?;
36        if metadata.len() >= MAX_LOG_SIZE {
37            // Remove old log if exists
38            if self.old_path.exists() {
39                fs::remove_file(&self.old_path)?;
40            }
41            // Rename current log to old
42            fs::rename(&self.path, &self.old_path)?;
43        }
44
45        Ok(())
46    }
47
48    pub fn log(&self, level: LogLevel, message: &str) {
49        if let Err(e) = self.log_internal(level, message) {
50            eprintln!("Failed to write log: {}", e);
51        }
52    }
53
54    fn log_internal(&self, level: LogLevel, message: &str) -> Result<()> {
55        self.rotate_if_needed()?;
56
57        let mut file = OpenOptions::new()
58            .create(true)
59            .append(true)
60            .open(&self.path)?;
61
62        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
63        let level_str = match level {
64            LogLevel::Debug => "DEBUG",
65            LogLevel::Info => "INFO",
66            LogLevel::Warn => "WARN",
67            LogLevel::Error => "ERROR",
68        };
69
70        writeln!(file, "[{}] [{}] {}", timestamp, level_str, message)?;
71
72        Ok(())
73    }
74
75    pub fn info(&self, message: &str) {
76        self.log(LogLevel::Info, message);
77    }
78
79    pub fn warn(&self, message: &str) {
80        self.log(LogLevel::Warn, message);
81    }
82
83    pub fn error(&self, message: &str) {
84        self.log(LogLevel::Error, message);
85    }
86
87    pub fn debug(&self, message: &str) {
88        self.log(LogLevel::Debug, message);
89    }
90
91    /// Get the last N lines from the log file
92    pub fn tail(&self, lines: usize) -> Result<Vec<String>> {
93        if !self.path.exists() {
94            return Ok(Vec::new());
95        }
96
97        let file = File::open(&self.path)?;
98        let reader = BufReader::new(file);
99        let all_lines: Vec<String> = reader.lines().filter_map(|l| l.ok()).collect();
100
101        let start = if all_lines.len() > lines {
102            all_lines.len() - lines
103        } else {
104            0
105        };
106
107        Ok(all_lines[start..].to_vec())
108    }
109
110    /// Get log file path
111    pub fn path(&self) -> &PathBuf {
112        &self.path
113    }
114
115    /// Get log file size in bytes
116    pub fn size(&self) -> Result<u64> {
117        if !self.path.exists() {
118            return Ok(0);
119        }
120        Ok(fs::metadata(&self.path)?.len())
121    }
122
123    /// Clear all logs
124    pub fn clear(&self) -> Result<()> {
125        if self.path.exists() {
126            fs::remove_file(&self.path)?;
127        }
128        if self.old_path.exists() {
129            fs::remove_file(&self.old_path)?;
130        }
131        Ok(())
132    }
133}
134
135#[derive(Clone, Copy)]
136pub enum LogLevel {
137    Debug,
138    Info,
139    Warn,
140    Error,
141}
142
143// Global logger instance
144use std::sync::OnceLock;
145
146static LOGGER: OnceLock<Logger> = OnceLock::new();
147
148pub fn get_logger() -> &'static Logger {
149    LOGGER.get_or_init(|| Logger::new().expect("Failed to initialize logger"))
150}
151
152// Convenience macros
153#[macro_export]
154macro_rules! log_info {
155    ($($arg:tt)*) => {
156        $crate::logger::get_logger().info(&format!($($arg)*))
157    };
158}
159
160#[macro_export]
161macro_rules! log_warn {
162    ($($arg:tt)*) => {
163        $crate::logger::get_logger().warn(&format!($($arg)*))
164    };
165}
166
167#[macro_export]
168macro_rules! log_error {
169    ($($arg:tt)*) => {
170        $crate::logger::get_logger().error(&format!($($arg)*))
171    };
172}
173
174#[macro_export]
175macro_rules! log_debug {
176    ($($arg:tt)*) => {
177        $crate::logger::get_logger().debug(&format!($($arg)*))
178    };
179}