1use super::{FormatArgs, translate_args};
2
3use chrono::{DateTime, Datelike, Local};
4use core::panic;
5use log::{LevelFilter, Log, Metadata, Record};
6use std::{
7 fs::File,
8 io::Write,
9 path::{Path, PathBuf},
10 sync::Mutex,
11};
12
13#[derive(Debug, Clone, Default)]
14pub struct RotateOptions {
15 pub size: RotateSize,
16 pub time: RotateTime,
17 pub file_count: u16,
18}
19
20#[derive(Debug, Clone, Default)]
21pub enum RotateSize {
22 #[default]
23 Disabled,
24 Enabled(usize),
25}
26
27#[derive(Debug, Clone, Default)]
28pub enum RotateTime {
29 #[default]
30 Disabled,
31 Second,
33 Day,
34 Month,
35 Year,
36}
37
38pub struct FileLogger {
39 level: LevelFilter,
40 target_filter: Box<dyn Fn(&str) -> bool + Send + Sync>,
41 formatter: Box<dyn Fn(FormatArgs) -> String + Send + Sync>,
42 file_path: PathBuf,
44 dir_path: PathBuf,
45 file_name: String,
46 buf_size: usize,
47 rotate_opts: RotateOptions,
48
49 state: Mutex<FileLoggerState>,
51}
52
53struct FileLoggerState {
54 file: File,
55 len: usize,
56 write_buf: String,
57 last_update: Option<(i64, u32, u32, u32)>,
58}
59
60impl FileLogger {
61 pub fn new_boxed<F1, F2>(
62 level: LevelFilter,
63 target_filter: F1,
64 formatter: F2,
65 file_path: impl AsRef<Path>,
66 buf_size: usize,
67 rotate_opts: RotateOptions,
68 ) -> Result<Box<dyn Log>, anyhow::Error>
69 where
70 F1: Fn(&str) -> bool + Send + Sync + 'static,
71 F2: Fn(FormatArgs) -> String + Send + Sync + 'static,
72 {
73 if buf_size < 8 {
74 panic!("buf_size < 8");
75 }
76
77 let (file, len) = open_new_or_append(&file_path)?;
78 let state = FileLoggerState {
79 file,
80 len,
81 write_buf: String::with_capacity(buf_size),
82 last_update: None,
83 };
84
85 let file_path = file_path.as_ref().canonicalize()?;
86 let dir_path = file_path.parent().unwrap().to_path_buf();
87 let file_name = file_path.file_name().unwrap().to_str().unwrap().to_string();
88
89 Ok(Box::new(Self {
90 level,
91 target_filter: Box::new(target_filter),
92 formatter: Box::new(formatter),
93 file_path,
94 dir_path,
95 file_name,
96 buf_size,
97 rotate_opts,
98 state: Mutex::new(state),
99 }))
100 }
101
102 fn buffered_write_entry(&self, ts: &DateTime<Local>, log_entry_str: &str) {
104 let mut state = self.state.lock().unwrap();
106
107 let mut rotate = false;
109 if let RotateSize::Enabled(size) = self.rotate_opts.size {
110 if state.len.saturating_add(log_entry_str.len()) > size {
113 rotate = true;
114 }
115 }
116 if let Some((lsec, ly, lm, ld)) = state.last_update {
117 let (sec, y, m, d) = to_ymd(ts);
118 rotate = match self.rotate_opts.time {
119 RotateTime::Year => y != ly,
120 RotateTime::Month => m != lm,
121 RotateTime::Day => d != ld,
122 RotateTime::Second => sec > lsec,
123 _ => false,
124 }
125 }
126 if rotate {
127 self.flush_buf(&mut state);
128 debug_assert!(state.write_buf.is_empty());
129 if let Err(e) = self.rotate(&mut state) {
130 eprintln!("Warning: log rotate failed");
131 eprintln!("{e:#}");
132 }
133 }
134
135 let mut data = log_entry_str;
136 while !data.is_empty() {
137 let rest = self.buf_size - state.write_buf.len();
139 let wsize = floor_char_boundary(data, rest);
141 let wdata = &data[..wsize];
142 data = &data[wsize..];
143 if wdata.is_empty() {
144 self.flush_buf(&mut state);
147 debug_assert!(state.write_buf.is_empty());
148 } else {
149 state.write_buf.push_str(wdata);
151 state.len += wdata.len();
152 }
153 }
154 }
155
156 fn flush_buf(&self, state: &mut FileLoggerState) {
158 state.file.write_all(state.write_buf.as_bytes()).unwrap();
160 state.write_buf.clear();
161 }
162
163 fn rotate(&self, state: &mut FileLoggerState) -> anyhow::Result<()> {
164 let main_name = self.file_name.as_str();
165 let (stem, ext) = if let Some(dotind) = main_name.rfind('.') {
168 (&main_name[..dotind], &main_name[dotind..])
169 } else {
170 (main_name, "")
171 };
172
173 let mut last_no = 0;
174 for i in 1..self.rotate_opts.file_count {
176 let archive_name = format!("{stem}.{i}{ext}");
177 let path = self.dir_path.join(archive_name);
178 if path.exists() {
179 last_no = i;
180 } else {
181 break;
182 }
183 }
184
185 for i in (0..=last_no).rev() {
186 let from = if i == 0 {
187 self.file_path.clone()
188 } else {
189 self.dir_path.join(format!("{stem}.{i}{ext}"))
190 };
191 let to = self.dir_path.join(format!("{stem}.{}{ext}", i + 1));
192 std::fs::rename(from, to)?;
193 }
194
195 let (mut new_file, size) = open_new_or_append(&self.file_path)?;
196 std::mem::swap(&mut state.file, &mut new_file);
198 drop(new_file);
199 state.len = size;
200
201 Ok(())
202 }
203}
204
205fn open_new_or_append(file_path: impl AsRef<Path>) -> Result<(File, usize), anyhow::Error> {
207 if let Some(dir) = file_path.as_ref().parent()
208 && dir != ""
209 {
210 std::fs::create_dir_all(dir)?;
211 }
212
213 let file = File::options().append(true).create(true).open(file_path)?;
214 let len = file.metadata()?.len();
215
216 Ok((file, len as usize))
217}
218
219fn floor_char_boundary(s: &str, mut index: usize) -> usize {
221 if index >= s.len() {
222 s.len()
223 } else {
224 loop {
225 if s.is_char_boundary(index) {
226 break;
227 } else {
228 index -= 1;
229 }
230 }
231 index
232 }
233}
234
235fn to_ymd(ts: &DateTime<Local>) -> (i64, u32, u32, u32) {
236 (ts.timestamp(), ts.year() as u32, ts.month(), ts.day())
237}
238
239impl Log for FileLogger {
240 fn enabled(&self, metadata: &Metadata) -> bool {
241 metadata.level() <= self.level && (self.target_filter)(metadata.target())
242 }
243
244 fn log(&self, record: &Record) {
245 if !self.enabled(record.metadata()) {
246 return;
247 }
248
249 let timestamp = Local::now();
250 let args = translate_args(record, timestamp);
251 let mut output = self.formatter.as_ref()(args);
252 output.push('\n');
253 self.buffered_write_entry(×tamp, &output);
254 }
255
256 fn flush(&self) {
257 let mut state = self.state.lock().unwrap();
258 self.flush_buf(&mut state);
259 }
260}
261
262#[cfg(test)]
263mod test {
264 use super::*;
265
266 #[test]
267 fn char_boundary() {
268 let org = "abcde";
269 let r = floor_char_boundary(org, 100);
270 assert_eq!(org, &org[..r]);
271
272 let org = "あいうえお";
273 let r = floor_char_boundary(org, 5);
274 assert_eq!(&org[..r], "あ");
275 let r = floor_char_boundary(org, 1);
276 assert_eq!(&org[..r], "");
277 }
278}