customlog/
file.rs

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    /// For debug
32    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    /// Absolute path to the main log file
43    file_path: PathBuf,
44    dir_path: PathBuf,
45    file_name: String,
46    buf_size: usize,
47    rotate_opts: RotateOptions,
48
49    /// Lock for log
50    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    /// Flush if needed, then write to write_buf
103    fn buffered_write_entry(&self, ts: &DateTime<Local>, log_entry_str: &str) {
104        // lock
105        let mut state = self.state.lock().unwrap();
106
107        // rotate check
108        let mut rotate = false;
109        if let RotateSize::Enabled(size) = self.rotate_opts.size {
110            // if it would exceed the limit, rotate before write
111            // if log_entry_str is longer than the limit, rotate and write it.
112            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            // buf capacity in bytes
138            let rest = self.buf_size - state.write_buf.len();
139            // copy as many bytes as possible, but it must end at char boundary
140            let wsize = floor_char_boundary(data, rest);
141            let wdata = &data[..wsize];
142            data = &data[wsize..];
143            if wdata.is_empty() {
144                // write_buf full
145                // flush to file
146                self.flush_buf(&mut state);
147                debug_assert!(state.write_buf.is_empty());
148            } else {
149                // memcpy to write_buf
150                state.write_buf.push_str(wdata);
151                state.len += wdata.len();
152            }
153        }
154    }
155
156    /// Called when buffer becomes full and when log::flush() is called
157    fn flush_buf(&self, state: &mut FileLoggerState) {
158        // write
159        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        // "a.b.c" => ("a.b", "c")
166        // "a" => ("a", "")
167        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        // test ".1" .. ".(file_count - 1)"
175        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        // swap and close
197        std::mem::swap(&mut state.file, &mut new_file);
198        drop(new_file);
199        state.len = size;
200
201        Ok(())
202    }
203}
204
205/// Open with (create + append), return File and size.
206fn 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
213// str::floor_char_boundary() is unstable yet
214fn 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(&timestamp, &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}