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 let file = File::options().append(true).create(true).open(file_path)?;
208 let len = file.metadata()?.len();
209
210 Ok((file, len as usize))
211}
212
213fn floor_char_boundary(s: &str, mut index: usize) -> usize {
215 if index >= s.len() {
216 s.len()
217 } else {
218 loop {
219 if s.is_char_boundary(index) {
220 break;
221 } else {
222 index -= 1;
223 }
224 }
225 index
226 }
227}
228
229fn to_ymd(ts: &DateTime<Local>) -> (i64, u32, u32, u32) {
230 (ts.timestamp(), ts.year() as u32, ts.month(), ts.day())
231}
232
233impl Log for FileLogger {
234 fn enabled(&self, metadata: &Metadata) -> bool {
235 metadata.level() <= self.level && (self.target_filter)(metadata.target())
236 }
237
238 fn log(&self, record: &Record) {
239 if !self.enabled(record.metadata()) {
240 return;
241 }
242
243 let timestamp = Local::now();
244 let args = translate_args(record, timestamp);
245 let mut output = self.formatter.as_ref()(args);
246 output.push('\n');
247 self.buffered_write_entry(×tamp, &output);
248 }
249
250 fn flush(&self) {
251 let mut state = self.state.lock().unwrap();
252 self.flush_buf(&mut state);
253 }
254}
255
256#[cfg(test)]
257mod test {
258 use super::*;
259
260 #[test]
261 fn char_boundary() {
262 let org = "abcde";
263 let r = floor_char_boundary(org, 100);
264 assert_eq!(org, &org[..r]);
265
266 let org = "あいうえお";
267 let r = floor_char_boundary(org, 5);
268 assert_eq!(&org[..r], "あ");
269 let r = floor_char_boundary(org, 1);
270 assert_eq!(&org[..r], "");
271 }
272}