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