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