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; const 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 if self.old_path.exists() {
39 fs::remove_file(&self.old_path)?;
40 }
41 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 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 pub fn path(&self) -> &PathBuf {
112 &self.path
113 }
114
115 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 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
143use 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#[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}