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}