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::mem::transmute;
12use std::path::Path;
13
14/// The LogRingFile sink is a way to minimize the cost of logging, for debugging deadlock or race condition,
15/// when the problem cannot be reproduce with ordinary log (because disk I/O will slow down the
16/// execution and prevent the bug to occur).
17///
18/// # Usage
19///
20/// Enable feature `ringfile` in your Cargo.toml.
21///
22/// Replace the log setup with the following [recipe::ring_file()](crate::recipe::ring_file()) in your test case:
23///
24///(Set the level to Info or higher, to turn off other debugging logs.)
25///
26/// ``` rust
27/// use captains_log::*;
28/// // the recipe already register signal and dynamic=true, do not use test(),
29/// // because test() will clear the signal.
30/// recipe::ring_file("/tmp/ring.log", 1024*1024, Level::Info,
31///     signal_consts::SIGHUP).build().expect("log setup");
32/// ```
33///
34/// # Debugging deadlocks
35///
36/// Then add some high-level log to critical path in the code, try to reproduce the problem, and
37/// reduce the amount of log if the bug not occur.
38///
39/// On start-up, it will create a limited-size ring-buffer-like memory. The log content will be held within memory but
40/// not written to disk, old logs will be overwritten by new ones. Until specified signal arrives, the last
41/// part of log message will be dumped to the file, in time order.
42///
43/// Once your program hangs up completely, find your process PID and send a signal to it.
44///
45/// ``` shell
46/// kill -SIGHUP <pid>
47/// ```
48/// There will be messages printed to stdout:
49///
50/// ``` text
51/// RingFile: start dumping
52/// RingFile: dump complete
53/// ```
54/// The program will exit. Then you can inspect your log content on disk (for this example `/tmp/ring.log`).
55///
56/// A real-life debugging story can be found on <https://github.com/frostyplanet/crossfire-rs/issues/24>.
57///
58/// # Debugging assertions
59///
60/// When you debugging the reason of some unexpected assertions, it will automatically trigger the
61/// dump in our panic hook. If you want an explicit dump, you can call:
62/// ``` rust
63///    log::logger().flush();
64/// ```
65///
66/// # NOTE
67///
68/// The backend is provided by [RingFile crate](https://docs.rs/ring-file). To ensure low
69/// latency, the buffers are put in thread local. After the program hangs, or panic, because
70/// no more messages will be written to the buffer, log content can be safely copied from the buffer area to disk.
71///
72/// Be aware that it did not use mlock to prevent memory from being swapping. (Swapping might make the
73/// 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`.
74///
75/// The collected logs are from all the threads, including those exited. That means there might be
76/// very old contents mixed with newer contents. We suggest you log before thread exits. And also
77/// note that thread_id is reused after thread exits.
78#[derive(Hash)]
79pub struct LogRingFile {
80    pub file_path: Option<Box<Path>>,
81    pub level: Level,
82    pub format: LogFormat,
83    /// 0 < buf_size < i32::MAX, note this is the buffer size within each thread.
84    pub buf_size: i32,
85}
86
87impl LogRingFile {
88    pub fn new(
89        file_path: Option<Box<Path>>, buf_size: i32, max_level: Level, format: LogFormat,
90    ) -> Self {
91        assert!(buf_size > 0);
92        Self { buf_size, file_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        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
117pub(crate) struct LogSinkRingFile {
118    max_level: Level,
119    formatter: LogFormat,
120    ring: UnsafeCell<RingFile>,
121}
122
123unsafe impl Send for LogSinkRingFile {}
124unsafe impl Sync for LogSinkRingFile {}
125
126impl LogSinkRingFile {
127    fn new(config: &LogRingFile) -> Self {
128        Self {
129            max_level: config.level,
130            formatter: config.format.clone(),
131            ring: UnsafeCell::new(RingFile::new(
132                config.buf_size as usize,
133                config.file_path.clone(),
134            )),
135        }
136    }
137
138    #[inline(always)]
139    fn get_ring(&self) -> &RingFile {
140        unsafe { transmute(self.ring.get()) }
141    }
142
143    #[inline(always)]
144    fn get_ring_mut(&self) -> &mut RingFile {
145        unsafe { transmute(self.ring.get()) }
146    }
147
148    fn dump(&self) -> std::io::Result<()> {
149        println!("RingFile: start dumping");
150        if let Err(e) = self.get_ring().dump() {
151            println!("RingFile: dump error {:?}", e);
152            return Err(e);
153        }
154        println!("RingFile: dump complete");
155        Ok(())
156    }
157}
158
159impl LogSinkTrait for LogSinkRingFile {
160    fn open(&self) -> std::io::Result<()> {
161        println!("ringfile is on");
162        self.get_ring_mut().clear();
163        Ok(())
164    }
165
166    fn reopen(&self) -> std::io::Result<()> {
167        let _ = self.dump();
168        std::process::exit(-2);
169    }
170
171    #[inline(always)]
172    fn log(&self, now: &Timer, r: &Record) {
173        if r.level() <= self.max_level {
174            let (ts, content) = self.formatter.process_with_timestamp(now, r);
175            self.get_ring().write(ts, content);
176        }
177    }
178
179    /// Manually dump the log
180    #[inline(always)]
181    fn flush(&self) {
182        let _ = self.dump();
183    }
184}