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///     let mut config = Builder::default().signal(signal_hook::consts::SIGUSR1).raw_file(file);
36///     // panic on debugging
37///     #[cfg(debug_assertions)]
38///     {
39///         config.continue_when_panic = false;
40///     }
41///     // do not panic on release
42///     #[cfg(not(debug_assertions))]
43///     {
44///         config.continue_when_panic = true;
45///     }
46///     return config;
47/// }
48/// ```
49#[derive(Hash)]
50pub struct LogRawFile {
51    /// max log level in this file
52    pub level: Level,
53
54    pub format: LogFormat,
55
56    /// path: dir/name
57    pub file_path: Box<Path>,
58}
59
60impl LogRawFile {
61    /// Construct config for file sink,
62    /// will try to create dir if not exists.
63    ///
64    /// The type of `dir` and `file_name` can be &str / String / &OsStr / OsString / Path / PathBuf. They can be of
65    /// different types.
66    pub fn new<P1, P2>(dir: P1, file_name: P2, level: Level, format: LogFormat) -> Self
67    where
68        P1: Into<PathBuf>,
69        P2: Into<PathBuf>,
70    {
71        let dir_path: PathBuf = dir.into();
72        if !dir_path.exists() {
73            std::fs::create_dir(&dir_path).expect("create dir for log");
74        }
75        let file_path = dir_path.join(file_name.into()).into_boxed_path();
76        Self { level, format, file_path }
77    }
78}
79
80impl SinkConfigTrait for LogRawFile {
81    fn get_level(&self) -> Level {
82        self.level
83    }
84
85    fn get_file_path(&self) -> Option<Box<Path>> {
86        Some(self.file_path.clone())
87    }
88
89    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
90        self.hash(hasher);
91        hasher.write(b"LogRawFile");
92    }
93
94    fn build(&self) -> LogSink {
95        LogSink::File(LogSinkFile::new(self))
96    }
97}
98
99pub(crate) struct LogSinkFile {
100    max_level: Level,
101    path: Box<Path>,
102    // raw fd only valid before original File close, use ArcSwap to prevent drop while using.
103    f: ArcSwapOption<std::fs::File>,
104    formatter: LogFormat,
105}
106
107pub(crate) fn open_file(path: &Path) -> std::io::Result<std::fs::File> {
108    OpenOptions::new().append(true).create(true).open(path)
109}
110
111impl LogSinkFile {
112    pub fn new(config: &LogRawFile) -> Self {
113        Self {
114            path: config.file_path.clone(),
115            max_level: config.level,
116            formatter: config.format.clone(),
117            f: ArcSwapOption::new(None),
118        }
119    }
120}
121
122impl LogSinkTrait for LogSinkFile {
123    fn reopen(&self) -> std::io::Result<()> {
124        match open_file(&self.path) {
125            Ok(f) => {
126                self.f.store(Some(Arc::new(f)));
127                Ok(())
128            }
129            Err(e) => {
130                eprintln!("open logfile {:#?} failed: {:?}", &self.path, e);
131                Err(e)
132            }
133        }
134    }
135
136    #[inline(always)]
137    fn log(&self, now: &Timer, r: &Record) {
138        if r.level() <= self.max_level {
139            // ArcSwap ensure file fd is not close during reopen for log rotation,
140            // in case of panic during write.
141            if let Some(file) = self.f.load_full() {
142                // Get a stable buffer,
143                // for concurrently write to file from multi process.
144                let buf = self.formatter.process(now, r);
145                unsafe {
146                    let _ = libc::write(
147                        file.as_raw_fd() as libc::c_int,
148                        buf.as_ptr() as *const libc::c_void,
149                        buf.len(),
150                    );
151                }
152            }
153        }
154    }
155
156    #[inline(always)]
157    fn flush(&self) {}
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::recipe;
164
165    #[test]
166    fn test_raw_file() {
167        let _file_sink = LogRawFile::new("/tmp", "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
168        let dir_path = Path::new("/tmp/test_dir");
169        if dir_path.is_dir() {
170            std::fs::remove_dir(&dir_path).expect("ok");
171        }
172        let _file_sink =
173            LogRawFile::new(&dir_path, "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
174        assert!(dir_path.is_dir());
175        std::fs::remove_dir(&dir_path).expect("ok");
176    }
177}