1use std::fs::{metadata, remove_file, File, OpenOptions};
2use std::io::{BufWriter, Write};
3use std::sync::RwLock;
4
5use anyhow::Result;
6use chrono::Local;
7use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
8use parking_lot::Mutex;
9
10use crate::common::{extract_lines, tilde, ACTION_LOG_PATH, NORMAL_LOG_PATH};
11
12static LAST_LOG_LINE: RwLock<String> = RwLock::new(String::new());
14
15static LAST_LOG_INFO: RwLock<String> = RwLock::new(String::new());
17
18const MAX_LOG_SIZE: u64 = 50_000;
21
22pub struct FMLogger {
26 normal_log: Mutex<BufWriter<std::fs::File>>,
27 action_log: Mutex<BufWriter<std::fs::File>>,
28}
29
30impl Default for FMLogger {
31 fn default() -> Self {
32 let normal_file = open_or_rotate(tilde(NORMAL_LOG_PATH).as_ref(), MAX_LOG_SIZE);
33 let action_file = open_or_rotate(tilde(ACTION_LOG_PATH).as_ref(), MAX_LOG_SIZE);
34 let normal_log = Mutex::new(BufWriter::new(normal_file));
35 let action_log = Mutex::new(BufWriter::new(action_file));
36 Self {
37 normal_log,
38 action_log,
39 }
40 }
41}
42
43impl FMLogger {
44 pub fn init(self) -> Result<(), SetLoggerError> {
45 log::set_boxed_logger(Box::new(self))?;
46 log::set_max_level(LevelFilter::Info);
47 log::info!("fm is starting with logs enabled");
48 Ok(())
49 }
50
51 fn write(&self, writer: &Mutex<BufWriter<File>>, record: &Record) {
52 let mut writer = writer.lock();
53 let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
54 let _ = writeln!(writer, "{timestamp} - {msg}", msg = record.args());
55 let _ = writer.flush();
56 }
57}
58
59impl log::Log for FMLogger {
60 fn enabled(&self, metadata: &Metadata) -> bool {
61 metadata.level() <= Level::Info
62 }
63
64 fn log(&self, record: &Record) {
65 if !self.enabled(record.metadata()) {
66 return;
67 }
68 if record.target() == "action" {
69 self.write(&self.action_log, record)
70 } else {
71 self.write(&self.normal_log, record)
72 }
73 }
74
75 fn flush(&self) {
76 let _ = self.normal_log.lock().flush();
77 let _ = self.action_log.lock().flush();
78 }
79}
80
81fn open_or_rotate(path: &str, max_size: u64) -> File {
82 if let Ok(meta) = metadata(path) {
83 if meta.len() > max_size {
84 let _ = remove_file(path);
85 }
86 }
87
88 OpenOptions::new()
89 .create(true)
90 .append(true)
91 .open(path)
92 .expect("cannot open log file")
93}
94
95pub fn read_log() -> Result<Vec<String>> {
97 let log_path = tilde(ACTION_LOG_PATH).to_string();
98 let content = std::fs::read_to_string(log_path)?;
99 Ok(extract_lines(content))
100}
101
102pub fn read_last_log_line() -> String {
105 let Ok(last_log_line) = LAST_LOG_LINE.read() else {
106 return "".to_owned();
107 };
108 last_log_line.to_owned()
109}
110
111fn write_last_log_line<S>(log: S)
114where
115 S: Into<String> + std::fmt::Display,
116{
117 let Ok(mut last_log_line) = LAST_LOG_LINE.write() else {
118 log::info!("Couldn't write to LAST_LOG_LINE");
119 return;
120 };
121 *last_log_line = log.to_string();
122}
123
124pub fn write_log_line<S>(log_line: S)
127where
128 S: Into<String> + std::fmt::Display,
129{
130 log::info!(target: "action", "{log_line}");
131 write_last_log_line(log_line);
132}
133
134#[macro_export]
139macro_rules! log_line {
140 ($($arg:tt)+) => (
141 $crate::io::write_log_line(
142 format!($($arg)+)
143 )
144 );
145}
146
147fn read_last_log_info() -> String {
150 let Ok(last_log_info) = LAST_LOG_INFO.read() else {
151 return "".to_owned();
152 };
153 last_log_info.to_owned()
154}
155
156fn write_last_log_info<S>(log: &S)
159where
160 S: Into<String> + std::fmt::Display,
161{
162 let Ok(mut last_log_info) = LAST_LOG_INFO.write() else {
163 log::info!("Couldn't write to LAST_LOG_LINE");
164 return;
165 };
166 *last_log_info = log.to_string();
167}
168
169pub fn write_log_info_once<S>(log_line: S)
172where
173 S: Into<String> + std::fmt::Display,
174{
175 if read_last_log_info() != log_line.to_string() {
176 write_last_log_info(&log_line);
177 log::info!("{log_line}");
178 }
179}
180
181#[macro_export]
186macro_rules! log_info {
187 ($($arg:tt)+) => {{
188 fn __log_info_dummy() {}
189 let function = {
190 let full = std::any::type_name_of_val(&__log_info_dummy);
191 full.trim_end_matches("::__log_info_dummy")
192 };
193
194 $crate::io::write_log_info_once(format!(
195 "{file}:{line}:{column} [{function}] - {content}",
196 file=file!(),
197 line=line!(),
198 column=column!(),
199 content=format_args!($($arg)+)
200 ))
201 }};
202}