captains_log/
file_impl.rs

1use crate::{
2    config::{LogFormat, 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).raw_file(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 SinkConfigTrait for LogRawFile {
70    fn get_level(&self) -> Level {
71        self.level
72    }
73
74    fn get_file_path(&self) -> Option<Box<Path>> {
75        Some(self.file_path.clone())
76    }
77
78    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
79        self.hash(hasher);
80        hasher.write(b"LogRawFile");
81    }
82
83    fn build(&self) -> LogSink {
84        LogSink::File(LogSinkFile::new(self))
85    }
86}
87
88pub(crate) struct LogSinkFile {
89    max_level: Level,
90    path: Box<Path>,
91    // raw fd only valid before original File close, use ArcSwap to prevent drop while using.
92    f: ArcSwapOption<std::fs::File>,
93    formatter: LogFormat,
94}
95
96pub(crate) fn open_file(path: &Path) -> std::io::Result<std::fs::File> {
97    OpenOptions::new().append(true).create(true).open(path)
98}
99
100impl LogSinkFile {
101    fn new(config: &LogRawFile) -> Self {
102        Self {
103            path: config.file_path.clone(),
104            max_level: config.level,
105            formatter: config.format.clone(),
106            f: ArcSwapOption::new(None),
107        }
108    }
109}
110
111impl LogSinkTrait for LogSinkFile {
112    fn reopen(&self) -> std::io::Result<()> {
113        match open_file(&self.path) {
114            Ok(f) => {
115                self.f.store(Some(Arc::new(f)));
116                Ok(())
117            }
118            Err(e) => {
119                eprintln!("open logfile {:#?} failed: {:?}", &self.path, e);
120                Err(e)
121            }
122        }
123    }
124
125    #[inline(always)]
126    fn log(&self, now: &Timer, r: &Record) {
127        if r.level() <= self.max_level {
128            // ArcSwap ensure file fd is not close during reopen for log rotation,
129            // in case of panic during write.
130            if let Some(file) = self.f.load_full() {
131                // Get a stable buffer,
132                // for concurrently write to file from multi process.
133                let buf = self.formatter.process(now, r);
134                unsafe {
135                    let _ = libc::write(
136                        file.as_raw_fd() as libc::c_int,
137                        buf.as_ptr() as *const libc::c_void,
138                        buf.len(),
139                    );
140                }
141            }
142        }
143    }
144
145    #[inline(always)]
146    fn flush(&self) {}
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::recipe;
153
154    #[test]
155    fn test_raw_file() {
156        let _file_sink = LogRawFile::new("/tmp", "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
157        let dir_path = Path::new("/tmp/test_dir");
158        if dir_path.is_dir() {
159            std::fs::remove_dir(&dir_path).expect("ok");
160        }
161        let _file_sink =
162            LogRawFile::new(&dir_path, "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
163        assert!(dir_path.is_dir());
164        std::fs::remove_dir(&dir_path).expect("ok");
165    }
166}