captains_log/
ring.rs

1use crate::{
2    config::{LogFormat, SinkConfigBuild, SinkConfigTrait},
3    log_impl::{LogSink, LogSinkTrait},
4    time::Timer,
5};
6use log::*;
7use ring_file::*;
8
9use std::cell::UnsafeCell;
10use std::hash::{Hash, Hasher};
11use std::io::Write;
12use std::mem::transmute;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU8, Ordering};
15use std::time::Duration;
16
17/// The LogRingFile sink is a way to minimize the cost of logging, for debugging deadlock or race condition,
18/// when the problem cannot be reproduce with ordinary log (because disk I/O will slow down the
19/// execution and prevent the bug to occur).
20///
21/// # Usage
22///
23/// Enable feature `ringfile` in your Cargo.toml.
24///
25/// Replace the log setup with the following [recipe::ring_file()](crate::recipe::ring_file()) in your test case:
26///
27///(Set the level to Info or higher, to turn off other debugging logs.)
28///
29/// ``` rust
30/// use captains_log::*;
31/// // the recipe already register signal and dynamic=true, do not use test(),
32/// // because test() will clear the signal.
33/// recipe::ring_file("/tmp/ring.log", 512*1024*1024, Level::Info,
34///     signal_consts::SIGHUP).build().expect("log setup");
35/// ```
36///
37/// # Debugging deadlocks
38///
39/// Then add some high-level log to critical path in the code, try to reproduce the problem, and
40/// reduce the amount of log if the bug not occur.
41///
42/// On start-up, it will create a limited-size ring-buffer-like memory. The log content will be held within memory but
43/// not written to disk, old logs will be overwritten by new ones. Until specified signal arrives, the last
44/// part of log message will be dumped to the file, in time order.
45///
46/// Once your program hangs up completely, find your process PID and send a signal to it.
47///
48/// ``` shell
49/// kill -SIGHUP <pid>
50/// ```
51/// There will be messages printed to stdout:
52///
53/// ``` text
54/// RingFile: start dumping
55/// RingFile: dump complete
56/// ```
57/// Then you can inspect your log content on disk (for this example `/tmp/ring.log`).
58///
59/// A real-life debugging story can be found on <https://github.com/frostyplanet/crossfire-rs/issues/24>.
60///
61/// # Debugging assertions
62///
63/// When you debugging the reason of some unexpected assertions, it will automatically trigger the
64/// dump in our panic hook. If you want an explicit dump, you can call:
65/// ``` rust
66///    log::logger().flush();
67/// ```
68///
69/// # NOTE
70///
71/// The backend is provided by [RingFile crate](https://docs.rs/ring-file). To ensure low
72/// latency, the buffer is protected by a spinlock instead of a mutex. After the program hangs, because
73/// no more messages will be written to the buffer, log content can be safely copied from the buffer area to disk.
74///
75/// Be aware that it did not use mlock to prevent memory from being swapping. (Swapping might make the
76/// code slow to prevent bug reproduction). When your memory is not enough, use a smaller buf_size and turn off the swap with `swapoff -a`.
77///
78#[derive(Hash)]
79pub struct LogRingFile {
80    pub file_path: Box<Path>,
81    pub level: Level,
82    pub format: LogFormat,
83    /// 0 < buf_size < i32::MAX
84    pub buf_size: i32,
85}
86
87impl LogRingFile {
88    pub fn new<P: Into<PathBuf>>(
89        file_path: P, buf_size: i32, max_level: Level, format: LogFormat,
90    ) -> Self {
91        assert!(buf_size > 0);
92        Self { buf_size, file_path: file_path.into().into_boxed_path(), level: max_level, format }
93    }
94}
95
96impl SinkConfigBuild for LogRingFile {
97    fn build(&self) -> LogSink {
98        LogSink::RingFile(LogSinkRingFile::new(self))
99    }
100}
101
102impl SinkConfigTrait for LogRingFile {
103    fn get_level(&self) -> Level {
104        self.level
105    }
106
107    fn get_file_path(&self) -> Option<Box<Path>> {
108        Some(self.file_path.clone())
109    }
110
111    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
112        self.hash(hasher);
113        hasher.write(b"LogRingFile");
114    }
115}
116
117#[derive(Debug, PartialEq)]
118#[repr(u8)]
119enum RingFileState {
120    Unlock,
121    Lock,
122    Dump,
123}
124
125pub(crate) struct LogSinkRingFile {
126    max_level: Level,
127    inner: UnsafeCell<RingFile>,
128    formatter: LogFormat,
129    /// In order to be fast, use a spin lock instead of Mutex
130    locked: AtomicU8,
131}
132
133unsafe impl Send for LogSinkRingFile {}
134unsafe impl Sync for LogSinkRingFile {}
135
136impl LogSinkRingFile {
137    fn new(config: &LogRingFile) -> Self {
138        Self {
139            max_level: config.level,
140            inner: UnsafeCell::new(RingFile::new(config.buf_size, config.file_path.to_path_buf())),
141            formatter: config.format.clone(),
142            locked: AtomicU8::new(RingFileState::Unlock as u8),
143        }
144    }
145
146    #[inline(always)]
147    fn get_inner(&self) -> &RingFile {
148        unsafe { transmute(self.inner.get()) }
149    }
150
151    #[inline(always)]
152    fn get_inner_mut(&self) -> &mut RingFile {
153        unsafe { transmute(self.inner.get()) }
154    }
155
156    #[inline(always)]
157    fn try_lock(&self, state: RingFileState, target: RingFileState) -> Result<(), u8> {
158        match self.locked.compare_exchange_weak(
159            state as u8,
160            target as u8,
161            Ordering::Acquire,
162            Ordering::Relaxed,
163        ) {
164            Ok(_) => Ok(()),
165            Err(s) => Err(s),
166        }
167    }
168}
169
170impl LogSinkTrait for LogSinkRingFile {
171    fn open(&self) -> std::io::Result<()> {
172        println!("ringfile is on");
173        Ok(())
174    }
175
176    fn reopen(&self) -> std::io::Result<()> {
177        loop {
178            match self.try_lock(RingFileState::Unlock, RingFileState::Dump) {
179                Ok(_) => {
180                    println!("RingFile: start dumping");
181                    let r = self.get_inner().dump();
182                    self.locked.store(RingFileState::Unlock as u8, Ordering::Release);
183                    if let Err(e) = r {
184                        println!("RingFile: dump error {:?}", e);
185                        return Err(e);
186                    } else {
187                        println!("RingFile: dump complete");
188                        return Ok(());
189                    }
190                }
191                Err(s) => {
192                    if s == RingFileState::Dump as u8 {
193                        return Ok(());
194                    }
195                    std::hint::spin_loop();
196                }
197            }
198        }
199    }
200
201    #[inline(always)]
202    fn log(&self, now: &Timer, r: &Record) {
203        if r.level() <= self.max_level {
204            let buf = self.formatter.process(now, r);
205            loop {
206                match self.try_lock(RingFileState::Unlock, RingFileState::Lock) {
207                    Ok(_) => {
208                        let _ = self.get_inner_mut().write_all(buf.as_bytes());
209                        self.locked.store(RingFileState::Unlock as u8, Ordering::Release);
210                        return;
211                    }
212                    Err(s) => {
213                        if s == RingFileState::Dump as u8 {
214                            std::thread::sleep(Duration::from_millis(100));
215                        } else {
216                            std::hint::spin_loop();
217                        }
218                    }
219                }
220            }
221        }
222    }
223
224    /// Manually dump the log
225    #[inline(always)]
226    fn flush(&self) {
227        let _ = self.reopen();
228    }
229}