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                unsafe {
142                    let _ = libc::write(
143                        file.as_raw_fd() as libc::c_int,
144                        buf.as_ptr() as *const libc::c_void,
145                        buf.len(),
146                    );
147                }
148            }
149        }
150    }
151
152    #[inline(always)]
153    fn flush(&self) {}
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::recipe;
160
161    #[test]
162    fn test_raw_file() {
163        let _file_sink = LogRawFile::new("/tmp", "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
164        let dir_path = Path::new("/tmp/test_dir");
165        if dir_path.is_dir() {
166            std::fs::remove_dir(&dir_path).expect("ok");
167        }
168        let _file_sink =
169            LogRawFile::new(&dir_path, "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
170        assert!(dir_path.is_dir());
171        std::fs::remove_dir(&dir_path).expect("ok");
172    }
173}