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/// For log rotation, you need system log-rotate service to notify with signal.
15#[derive(Hash)]
16pub struct LogRawFile {
17    /// max log level in this file
18    pub level: Level,
19
20    pub format: LogFormat,
21
22    /// path: dir/name
23    pub file_path: Box<Path>,
24}
25
26impl LogRawFile {
27    /// Construct config for file sink,
28    /// will try to create dir if not exists.
29    ///
30    /// The type of `dir` and `file_name` can be &str / String / &OsStr / OsString / Path / PathBuf. They can be of
31    /// different types.
32    pub fn new<P1, P2>(dir: P1, file_name: P2, level: Level, format: LogFormat) -> Self
33    where
34        P1: Into<PathBuf>,
35        P2: Into<PathBuf>,
36    {
37        let dir_path: PathBuf = dir.into();
38        if !dir_path.exists() {
39            std::fs::create_dir(&dir_path).expect("create dir for log");
40        }
41        let file_path = dir_path.join(file_name.into()).into_boxed_path();
42        Self { level, format, file_path }
43    }
44}
45
46impl SinkConfigTrait for LogRawFile {
47    fn get_level(&self) -> Level {
48        self.level
49    }
50
51    fn get_file_path(&self) -> Option<Box<Path>> {
52        Some(self.file_path.clone())
53    }
54
55    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
56        self.hash(hasher);
57        hasher.write(b"LogRawFile");
58    }
59
60    fn build(&self) -> LogSink {
61        LogSink::File(LogSinkFile::new(self))
62    }
63}
64
65pub(crate) struct LogSinkFile {
66    max_level: Level,
67    path: Box<Path>,
68    // raw fd only valid before original File close, use ArcSwap to prevent drop while using.
69    f: ArcSwapOption<std::fs::File>,
70    formatter: LogFormat,
71}
72
73pub(crate) fn open_file(path: &Path) -> std::io::Result<std::fs::File> {
74    OpenOptions::new().append(true).create(true).open(path)
75}
76
77impl LogSinkFile {
78    pub fn new(config: &LogRawFile) -> Self {
79        Self {
80            path: config.file_path.clone(),
81            max_level: config.level,
82            formatter: config.format.clone(),
83            f: ArcSwapOption::new(None),
84        }
85    }
86}
87
88impl LogSinkTrait for LogSinkFile {
89    fn reopen(&self) -> std::io::Result<()> {
90        match open_file(&self.path) {
91            Ok(f) => {
92                self.f.store(Some(Arc::new(f)));
93                Ok(())
94            }
95            Err(e) => {
96                eprintln!("open logfile {:#?} failed: {:?}", &self.path, e);
97                Err(e)
98            }
99        }
100    }
101
102    #[inline(always)]
103    fn log(&self, now: &Timer, r: &Record) {
104        if r.level() <= self.max_level {
105            // ArcSwap ensure file fd is not close during reopen for log rotation,
106            // in case of panic during write.
107            if let Some(file) = self.f.load_full() {
108                // Get a stable buffer,
109                // for concurrently write to file from multi process.
110                let buf = self.formatter.process(now, r);
111                unsafe {
112                    let _ = libc::write(
113                        file.as_raw_fd() as libc::c_int,
114                        buf.as_ptr() as *const libc::c_void,
115                        buf.len(),
116                    );
117                }
118            }
119        }
120    }
121
122    #[inline(always)]
123    fn flush(&self) {}
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::recipe;
130
131    #[test]
132    fn test_raw_file() {
133        let _file_sink = LogRawFile::new("/tmp", "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
134        let dir_path = Path::new("/tmp/test_dir");
135        if dir_path.is_dir() {
136            std::fs::remove_dir(&dir_path).expect("ok");
137        }
138        let _file_sink =
139            LogRawFile::new(&dir_path, "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
140        assert!(dir_path.is_dir());
141        std::fs::remove_dir(&dir_path).expect("ok");
142    }
143}