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::{AtomicBool, Ordering};
15
16/// The LogRingFile sink is a tool for debugging deadlock or race condition,
17/// when the problem cannot be reproduce with ordinary log (because disk I/O will slow down the
18/// execution and prevent the bug to occur).
19///
20/// # Usage
21///
22/// Enable feature `ringfile` in your Cargo.toml.
23///
24/// Replace the log setup with the following [recipe::ring_file()](crate::recipe::ring_file()) in your test case:
25///
26///(Set the level to Info or higher, to turn off other debugging logs.)
27///
28/// ``` rust
29/// use captains_log::*;
30/// // the recipe already register signal and dynamic=true, do not use test(),
31/// // because test() will clear the signal.
32/// recipe::ring_file("/tmp/ring.log", 512*1024*1024, Level::Info,
33/// signal_consts::SIGHUP).build().expect("log setup");
34/// ```
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///
55/// Then you can inspect your log content on disk (for this example `/tmp/ring.log`).
56///
57/// The backend is provided by [RingFile crate](https://docs.rs/ring-file). To ensure low
58/// latency, the buffer is protected by a spinlock instead of a mutex. After the program hangs, because
59/// no more messages will be written to the buffer, log content can be safely copied from the buffer area to disk.
60///
61/// Be aware that it did not use mlock to prevent memory from being swapping. (Swapping might make the
62/// 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`.
63///
64/// A real-life debugging story can be found on <https://github.com/frostyplanet/crossfire-rs/issues/24>.
65#[derive(Hash)]
66pub struct LogRingFile {
67 pub file_path: Box<Path>,
68 pub level: Level,
69 pub format: LogFormat,
70 /// 0 < buf_size < i32::MAX
71 pub buf_size: i32,
72}
73
74impl LogRingFile {
75 pub fn new<P: Into<PathBuf>>(
76 file_path: P, buf_size: i32, max_level: Level, format: LogFormat,
77 ) -> Self {
78 assert!(buf_size > 0);
79 Self { buf_size, file_path: file_path.into().into_boxed_path(), level: max_level, format }
80 }
81}
82
83impl SinkConfigBuild for LogRingFile {
84 fn build(&self) -> LogSink {
85 LogSink::RingFile(LogSinkRingFile::new(self))
86 }
87}
88
89impl SinkConfigTrait for LogRingFile {
90 fn get_level(&self) -> Level {
91 self.level
92 }
93
94 fn get_file_path(&self) -> Option<Box<Path>> {
95 Some(self.file_path.clone())
96 }
97
98 fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
99 self.hash(hasher);
100 hasher.write(b"LogRingFile");
101 }
102}
103
104pub(crate) struct LogSinkRingFile {
105 max_level: Level,
106 inner: UnsafeCell<RingFile>,
107 formatter: LogFormat,
108 /// In order to be fast, use a spin lock instead of Mutex
109 locked: AtomicBool,
110}
111
112unsafe impl Send for LogSinkRingFile {}
113unsafe impl Sync for LogSinkRingFile {}
114
115impl LogSinkRingFile {
116 fn new(config: &LogRingFile) -> Self {
117 Self {
118 max_level: config.level,
119 inner: UnsafeCell::new(RingFile::new(config.buf_size, config.file_path.to_path_buf())),
120 formatter: config.format.clone(),
121 locked: AtomicBool::new(false),
122 }
123 }
124
125 #[inline(always)]
126 fn get_inner(&self) -> &RingFile {
127 unsafe { transmute(self.inner.get()) }
128 }
129
130 #[inline(always)]
131 fn get_inner_mut(&self) -> &mut RingFile {
132 unsafe { transmute(self.inner.get()) }
133 }
134}
135
136impl LogSinkTrait for LogSinkRingFile {
137 fn open(&self) -> std::io::Result<()> {
138 println!("ringfile is on");
139 Ok(())
140 }
141
142 fn reopen(&self) -> std::io::Result<()> {
143 println!("RingFile: start dumping");
144 if let Err(e) = self.get_inner().dump() {
145 println!("RingFile: dump error {:?}", e);
146 Err(e)
147 } else {
148 println!("RingFile: dump complete");
149 Ok(())
150 }
151 }
152
153 #[inline(always)]
154 fn log(&self, now: &Timer, r: &Record) {
155 if r.level() <= self.max_level {
156 let buf = self.formatter.process(now, r);
157 while self
158 .locked
159 .compare_exchange_weak(false, true, Ordering::SeqCst, Ordering::Relaxed)
160 .is_err()
161 {
162 std::hint::spin_loop();
163 }
164 let _ = self.get_inner_mut().write_all(buf.as_bytes());
165 self.locked.store(false, Ordering::Release);
166 }
167 }
168
169 #[inline(always)]
170 fn flush(&self) {}
171}