captains_log/
file_impl.rs

1use crate::{
2    config::{LogFormat, SinkConfigBuild, SinkConfigTrait},
3    log_impl::{LogSink, LogSinkTrait},
4    time::Timer,
5};
6use log::{Level, Record};
7use std::hash::{Hash, Hasher};
8use std::path::{Path, PathBuf};
9use std::{fs::OpenOptions, os::unix::prelude::*, sync::Arc};
10
11use arc_swap::ArcSwapOption;
12
13/// Config for file sink that supports atomic append from multiprocess.
14///
15/// Used when you want a reliable log regardless of crash or killed.
16/// For log rotation, you need system log-rotate service to notify with signal.
17///
18/// # Example
19///
20/// Source of [crate::recipe::raw_file_logger_custom()]
21///
22/// ``` rust
23/// use captains_log::*;
24/// use std::path::{self, Path, PathBuf};
25///
26/// pub fn raw_file_logger_custom<P: Into<PathBuf>>(
27///     file_path: P, max_level: Level, time_fmt: &'static str, format_func: FormatFunc,
28/// ) -> Builder {
29///     let format = LogFormat::new(time_fmt, format_func);
30///     let _file_path = file_path.into();
31///     let p = path::absolute(&_file_path).expect("path convert to absolute");
32///     let dir = p.parent().unwrap();
33///     let file_name = Path::new(p.file_name().unwrap());
34///     let file = LogRawFile::new(dir, file_name, max_level, format);
35///     return Builder::default().signal(signal_hook::consts::SIGUSR1).add_sink(file);
36/// }
37/// ```
38#[derive(Hash)]
39pub struct LogRawFile {
40    /// max log level in this file
41    pub level: Level,
42
43    pub format: LogFormat,
44
45    /// path: dir/name
46    pub file_path: Box<Path>,
47}
48
49impl LogRawFile {
50    /// Construct config for file sink,
51    /// will try to create dir if not exists.
52    ///
53    /// The type of `dir` and `file_name` can be &str / String / &OsStr / OsString / Path / PathBuf. They can be of
54    /// different types.
55    pub fn new<P1, P2>(dir: P1, file_name: P2, level: Level, format: LogFormat) -> Self
56    where
57        P1: Into<PathBuf>,
58        P2: Into<PathBuf>,
59    {
60        let dir_path: PathBuf = dir.into();
61        if !dir_path.exists() {
62            std::fs::create_dir(&dir_path).expect("create dir for log");
63        }
64        let file_path = dir_path.join(file_name.into()).into_boxed_path();
65        Self { level, format, file_path }
66    }
67}
68
69impl SinkConfigBuild for LogRawFile {
70    fn build(&self) -> LogSink {
71        LogSink::File(LogSinkFile::new(self))
72    }
73}
74
75impl SinkConfigTrait for LogRawFile {
76    fn get_level(&self) -> Level {
77        self.level
78    }
79
80    fn get_file_path(&self) -> Option<Box<Path>> {
81        Some(self.file_path.clone())
82    }
83
84    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
85        self.hash(hasher);
86        hasher.write(b"LogRawFile");
87    }
88}
89
90pub(crate) struct LogSinkFile {
91    max_level: Level,
92    path: Box<Path>,
93    // raw fd only valid before original File close, use ArcSwap to prevent drop while using.
94    f: ArcSwapOption<std::fs::File>,
95    formatter: LogFormat,
96}
97
98pub(crate) fn open_file(path: &Path) -> std::io::Result<std::fs::File> {
99    OpenOptions::new().append(true).create(true).open(path)
100}
101
102impl LogSinkFile {
103    fn new(config: &LogRawFile) -> Self {
104        Self {
105            path: config.file_path.clone(),
106            max_level: config.level,
107            formatter: config.format.clone(),
108            f: ArcSwapOption::new(None),
109        }
110    }
111}
112
113impl LogSinkTrait for LogSinkFile {
114    #[inline]
115    fn open(&self) -> std::io::Result<()> {
116        self.reopen()
117    }
118
119    fn reopen(&self) -> std::io::Result<()> {
120        match open_file(&self.path) {
121            Ok(f) => {
122                self.f.store(Some(Arc::new(f)));
123                Ok(())
124            }
125            Err(e) => {
126                eprintln!("open logfile {:#?} failed: {:?}", &self.path, e);
127                Err(e)
128            }
129        }
130    }
131
132    #[inline(always)]
133    fn log(&self, now: &Timer, r: &Record) {
134        if r.level() <= self.max_level {
135            // ArcSwap ensure file fd is not close during reopen for log rotation,
136            // in case of panic during write.
137            if let Some(file) = self.f.load_full() {
138                // Get a stable buffer,
139                // for concurrently write to file from multi process.
140                let buf = self.formatter.process(now, r);
141                let mut p = buf.as_ptr() as *const u8;
142                let mut l = buf.len();
143                loop {
144                    let r = unsafe {
145                        libc::write(file.as_raw_fd() as libc::c_int, p as *const libc::c_void, l)
146                    };
147                    if r == l as isize || r < 0 {
148                        // Ignore write error (disk err, space err), should not affect the program
149                        return;
150                    }
151                    // NOTE: If early return happens, means you are using a filesystem not
152                    // supporting atomic append
153                    l -= r as usize;
154                    p = unsafe { p.add(r as usize) };
155                }
156            }
157        }
158    }
159
160    #[inline(always)]
161    fn flush(&self) {}
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::recipe;
168
169    #[test]
170    fn test_raw_file() {
171        let _file_sink = LogRawFile::new("/tmp", "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
172        let dir_path = Path::new("/tmp/test_dir");
173        if dir_path.is_dir() {
174            std::fs::remove_dir(&dir_path).expect("ok");
175        }
176        let _file_sink =
177            LogRawFile::new(&dir_path, "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
178        assert!(dir_path.is_dir());
179        std::fs::remove_dir(&dir_path).expect("ok");
180    }
181}