q_debug/
logging.rs

1use crate::fmt::Formatter;
2use chrono::prelude::*;
3use chrono::Duration;
4use std::fmt::Debug;
5use std::io::prelude::*;
6use std::thread;
7
8#[derive(Debug, PartialEq, Clone)]
9pub struct LogLocation {
10    pub file_path: String,
11    pub func_path: String,
12    pub lineno: u32,
13    pub thread_id: thread::ThreadId,
14}
15
16#[derive(Debug)]
17pub struct Logger<W: Write> {
18    formatter: Formatter,
19    pub header_interval: Duration,
20    sink: W,
21    prev: Option<(DateTime<Utc>, LogLocation)>,
22}
23
24impl<W: Write> Logger<W> {
25    pub fn new(sink: W) -> Logger<W> {
26        Logger {
27            formatter: Formatter {},
28            header_interval: Duration::seconds(2),
29            sink,
30            prev: None,
31        }
32    }
33
34    /// Return a header line for `loc` if a header should be included in it's
35    /// corresponding log line (see below) else `None`
36    ///
37    /// ## Header output semantics
38    ///
39    /// A log line should be preceeded by a header line IFF any of the following
40    /// conditions are met:
41    ///
42    /// - This is this first time logging with this `Logger`.
43    /// - The previous log occurred in a different module to `loc`.
44    /// - The previous log occurred in a different function to `loc`.
45    /// - The time duration between `now` and the previous log is greater than
46    ///   or equal to `self.header_interval`.
47    fn header(&self, now: DateTime<Utc>, loc: &LogLocation) -> Option<String> {
48        // FIXME: There is definitely a clearer way of implementing this!
49        let include_header = self.prev.as_ref().map_or(true, |(last_time, last_loc)| {
50            let elapsed = now.signed_duration_since(*last_time);
51            elapsed >= self.header_interval
52                || loc.file_path != last_loc.file_path
53                || loc.func_path != last_loc.func_path
54        });
55        if include_header {
56            Some(self.formatter.header(&now, loc))
57        } else {
58            None
59        }
60    }
61
62    pub fn q(&mut self, loc: LogLocation) {
63        let log_line = self.formatter.q();
64
65        self.write_log_line(loc, log_line);
66    }
67
68    pub fn q_literal<T: Debug>(&mut self, val: &T, loc: LogLocation) {
69        let log_line = self.formatter.q_literal(val);
70
71        self.write_log_line(loc, log_line);
72    }
73
74    pub fn q_expr<T: Debug>(&mut self, val: &T, expr: &str, loc: LogLocation) {
75        let log_line = self.formatter.q_expr(val, expr);
76
77        self.write_log_line(loc, log_line);
78    }
79
80    /// Write `log_line` to `self.sink` using `loc` to construct a header line
81    /// if necessary (see `Logger::header` for header semantics).
82    fn write_log_line<S: AsRef<str>>(&mut self, loc: LogLocation, log_line: S) {
83        let log_line = log_line.as_ref();
84        let now = Utc::now();
85
86        match self.header(now, &loc) {
87            Some(header) => writeln!(self.sink, "{}\n{}", header, log_line),
88            None => writeln!(self.sink, "{}", log_line),
89        }
90        .expect("Unable to write to log");
91
92        self.prev = Some((now, loc));
93    }
94}
95
96// TODO: Lots of repeated testing (smoke, logging and fmt) - how to resolve?
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn testing_logger() -> Logger<Vec<u8>> {
103        Logger::new(vec![])
104    }
105
106    macro_rules! ll {
107        ($filep: expr, $funcp:expr, $lineno: expr) => {
108            LogLocation {
109                file_path: String::from($filep),
110                func_path: String::from($funcp),
111                lineno: $lineno,
112                thread_id: thread::current().id(),
113            }
114        };
115    }
116
117    #[test]
118    fn test_header_returns_some_if_not_logged_previously() {
119        let logger = testing_logger();
120        let now = Utc.ymd(2020, 6, 22).and_hms(20, 5, 32);
121        let loc = ll!("src/lib.rs", "crate::foo::bar", 42);
122
123        // Sanity check
124        assert_eq!(logger.prev, None);
125
126        assert_eq!(
127            logger.header(now, &loc),
128            Some(format!(
129                "[20:05:32 {:?} src/lib.rs crate::foo::bar:42]",
130                loc.thread_id
131            ))
132        );
133    }
134
135    #[test]
136    fn test_header_returns_some_if_prev_function_differs() {
137        let now = Utc.ymd(2020, 6, 22).and_hms(20, 5, 32);
138        let prev_loc = ll!("src/lib.rs", "crate::foo::bar", 42);
139        let logger = {
140            let mut logger = testing_logger();
141            logger.prev = Some((now, prev_loc));
142            logger
143        };
144
145        let loc = ll!("src/lib.rs", "crate::foo::baz", 50);
146        assert_eq!(
147            logger.header(now, &loc),
148            Some(format!(
149                "[20:05:32 {:?} src/lib.rs crate::foo::baz:50]",
150                loc.thread_id
151            ))
152        );
153    }
154
155    #[test]
156    fn test_header_returns_some_if_prev_module_differs() {
157        let now = Utc.ymd(2020, 6, 22).and_hms(20, 5, 32);
158        let prev_loc = ll!("src/lib.rs", "crate::foo::bar", 42);
159        let logger = {
160            let mut logger = testing_logger();
161            logger.prev = Some((now, prev_loc));
162            logger
163        };
164        let loc = ll!("src/lib.rs", "crate::baz::bat", 42);
165
166        assert_eq!(
167            logger.header(now, &loc),
168            Some(format!(
169                "[20:05:32 {:?} src/lib.rs crate::baz::bat:42]",
170                loc.thread_id
171            ))
172        );
173    }
174
175    #[test]
176    fn test_header_returns_some_if_header_interval_elapsed() {
177        let prev_time = Utc.ymd(2020, 6, 22).and_hms(20, 5, 32);
178        let header_interval = Duration::seconds(2);
179        let loc = ll!("src/lib.rs", "crate::foo::bar", 42);
180        let logger = {
181            let mut logger = testing_logger();
182            logger.prev = Some((prev_time, loc.clone()));
183            logger.header_interval = header_interval;
184            logger
185        };
186
187        // 3 seconds after `prev_time`
188        // > `logger.header_interval` -> should return `Some`
189        let now = Utc.ymd(2020, 6, 22).and_hms(20, 5, 35);
190
191        assert_eq!(
192            logger.header(now, &loc),
193            Some(format!(
194                "[20:05:35 {:?} src/lib.rs crate::foo::bar:42]",
195                loc.thread_id
196            ))
197        );
198    }
199
200    #[test]
201    fn test_header_returns_none_if_header_interval_not_elapsed_and_prev_module_and_function_same() {
202        let prev_time = Utc.ymd(2020, 6, 22).and_hms(20, 5, 32);
203        let header_interval = Duration::seconds(2);
204        let loc = ll!("src/lib.rs", "crate::foo::bar", 42);
205        let logger = {
206            let mut logger = testing_logger();
207            logger.prev = Some((prev_time, loc.clone()));
208            logger.header_interval = header_interval;
209            logger
210        };
211
212        // 1 second after `prev_time`
213        // < `logger.header_interval` -> doesn't trigger header output
214        let now = Utc.ymd(2020, 6, 22).and_hms(20, 5, 33);
215
216        assert_eq!(
217            logger.header(now, &loc), // module/function same
218            None,
219        );
220    }
221}