1use std::fs::{self, File, OpenOptions};
2use std::io::{BufWriter, Write};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::Arc;
6use std::thread;
7use std::time::Duration;
8
9use chrono::Local;
10use crossbeam_channel::{bounded, Receiver, Sender};
11use log::{LevelFilter, Metadata, Record};
12
13use crate::cleaner;
14use crate::formatter;
15
16enum Msg {
19 Line(String),
20 Shutdown,
21}
22
23#[derive(Debug, Clone)]
44pub struct Config {
45 pub app_name: String,
50
51 pub log_dir: PathBuf,
53
54 pub ttl_hours: i64,
59
60 pub level: LevelFilter,
62
63 pub console: bool,
67}
68
69impl Default for Config {
70 fn default() -> Self {
71 Self {
72 app_name: "app".to_string(),
73 log_dir: std::env::temp_dir().join("app-logs"),
74 ttl_hours: 72,
75 level: LevelFilter::Info,
76 #[cfg(debug_assertions)]
77 console: true,
78 #[cfg(not(debug_assertions))]
79 console: false,
80 }
81 }
82}
83
84pub struct Logger {
91 sender: Sender<Msg>,
92 console: bool,
93 level: LevelFilter,
94 app_name: String,
95 shutdown_flag: Arc<AtomicBool>,
96}
97
98impl Logger {
99 pub fn new(config: Config) -> Self {
101 let log_dir = config.log_dir.clone();
102 let ttl_hours = config.ttl_hours;
103 let app_name = config.app_name.clone();
104
105 let (tx, rx): (Sender<Msg>, Receiver<Msg>) = bounded(8192);
106 let shutdown_flag = Arc::new(AtomicBool::new(false));
107 let shutdown_clone = Arc::clone(&shutdown_flag);
108 let app_name_thread = app_name.clone();
109
110 thread::Builder::new()
111 .name(format!("logger-nx/{}", app_name))
112 .spawn(move || {
113 writer_thread(rx, &log_dir, &app_name_thread, ttl_hours, shutdown_clone);
114 })
115 .expect("failed to spawn logger thread");
116
117 Self {
118 sender: tx,
119 console: config.console,
120 level: config.level,
121 app_name,
122 shutdown_flag,
123 }
124 }
125
126 pub fn shutdown(&self) {
130 self.shutdown_flag.store(true, Ordering::SeqCst);
131 let _ = self.sender.send(Msg::Shutdown);
132 thread::sleep(Duration::from_millis(2000));
133 }
134}
135
136impl log::Log for Logger {
137 fn enabled(&self, metadata: &Metadata) -> bool {
138 metadata.level() <= self.level
139 }
140
141 fn log(&self, record: &Record) {
142 if !self.enabled(record.metadata()) {
143 return;
144 }
145 let line = formatter::format_record(record, &self.app_name);
146 if self.console {
147 eprint!("{line}");
148 }
149 let _ = self.sender.try_send(Msg::Line(line));
152 }
153
154 fn flush(&self) {}
155}
156
157struct FileState {
160 writer: BufWriter<File>,
161 current_hour_tag: String, }
163
164fn current_hour_tag() -> String {
165 Local::now().format("%Y%m%d%H").to_string()
166}
167
168fn active_log_path(log_dir: &Path) -> PathBuf {
169 log_dir.join("app.log")
170}
171
172fn archive_log_path(log_dir: &Path, tag: &str) -> PathBuf {
173 log_dir.join(format!("app.log.{tag}"))
174}
175
176fn open_active(log_dir: &Path, _app_name: &str) -> std::io::Result<FileState> {
177 let path = active_log_path(log_dir);
178 let file = OpenOptions::new().create(true).append(true).open(&path)?;
179 Ok(FileState {
180 writer: BufWriter::with_capacity(64 * 1024, file),
181 current_hour_tag: current_hour_tag(),
182 })
183}
184
185fn rotate(log_dir: &Path, app_name: &str, state: &mut FileState) {
186 let _ = state.writer.flush();
187 let old_tag = state.current_hour_tag.clone();
188 let src = active_log_path(log_dir);
189 let dst = archive_log_path(log_dir, &old_tag);
190 let _ = fs::rename(&src, &dst);
191 match open_active(log_dir, app_name) {
192 Ok(new_state) => *state = new_state,
193 Err(e) => eprintln!("[logger-nx] Failed to open new log file: {e}"),
194 }
195}
196
197fn writer_thread(
198 rx: Receiver<Msg>,
199 log_dir: &Path,
200 app_name: &str,
201 ttl_hours: i64,
202 shutdown: Arc<AtomicBool>,
203) {
204 let _ = fs::create_dir_all(log_dir);
205
206 let mut state = match open_active(log_dir, app_name) {
207 Ok(s) => s,
208 Err(e) => {
209 eprintln!("[logger-nx] Cannot open log file: {e}");
210 return;
211 }
212 };
213
214 cleaner::cleanup(log_dir, app_name, ttl_hours);
217
218 let mut last_cleanup = std::time::Instant::now();
219 const CLEANUP_INTERVAL: Duration = Duration::from_secs(30 * 60);
220
221 for msg in &rx {
222 match msg {
223 Msg::Line(line) => {
224 let tag = current_hour_tag();
225 if tag != state.current_hour_tag {
226 rotate(log_dir, app_name, &mut state);
227 cleaner::cleanup(log_dir, app_name, ttl_hours);
228 last_cleanup = std::time::Instant::now();
229 }
230
231 if last_cleanup.elapsed() > CLEANUP_INTERVAL {
232 cleaner::cleanup(log_dir, app_name, ttl_hours);
233 last_cleanup = std::time::Instant::now();
234 }
235
236 let _ = state.writer.write_all(line.as_bytes());
237 let _ = state.writer.flush();
238 }
239 Msg::Shutdown => {
240 let _ = state.writer.flush();
241 break;
242 }
243 }
244
245 if shutdown.load(Ordering::Relaxed) {
246 let _ = state.writer.flush();
247 break;
248 }
249 }
250}