detailer/
detailer.rs

1use std::{
2    fmt::{Arguments, Write},
3    sync::{atomic::AtomicUsize, Arc},
4    time::Instant,
5};
6
7/// An event or workflow detail logger.
8///
9/// When dropped or flush()ed it will output its accumulated input.
10pub struct Detailer {
11    level: log::LevelFilter,
12    accumulated: String,
13    current_indentation: Arc<AtomicUsize>,
14    start: Option<Instant>,
15    soft_limit: usize,
16}
17
18/// Configure the time logging prefix of detail lines
19pub enum TimingSetting {
20    /// Include timing info in line prefixes
21    WithTiming,
22    /// Do not include timing info in line prefixes
23    WithoutTiming,
24}
25
26/// Create a new root detailer. It will log as 1 expression upon
27/// being dropped or flushed.
28///
29/// ```rust
30/// use detailer::{Detailer, detail, new_detailer};
31/// let mut detailer = new_detailer!();
32///
33/// let mut detailer_at_debug = new_detailer!(Debug);
34///
35/// let mut detailer_at_debug_without_timing = new_detailer!(Debug, WithoutTiming);
36///
37/// let mut detailer_at_debug_without_timing_limited_length = new_detailer!(Debug, WithoutTiming, 1024);
38/// ```
39#[macro_export(local_inner_macros)]
40macro_rules! new_detailer {
41    // new_detailer!()
42    () => {
43        new_detailer!(Info)
44    };
45    // new_detailer!(Debug)
46    ($level:tt) => {
47        new_detailer!($level, WithTiming)
48    };
49    // new_detailer!(Debug, WithTiming)
50    // new_detailer!(Debug, WithoutTiming)
51    ($level:tt, $timing_setting:tt) => {
52        new_detailer!($level, $timing_setting, 4 * 1024)
53    };
54    // new_detailer!(Debug, WithTiming, 1024)
55    ($level:tt, $timing_setting:tt, $soft_limit:expr) => {
56        detailer::Detailer::new(
57            log::LevelFilter::$level,
58            detailer::TimingSetting::$timing_setting,
59            $soft_limit,
60        )
61    };
62}
63
64/// Add a detail line at info
65/// ```rust
66/// use detailer::{Detailer, detail, new_detailer};
67/// let mut detailer = new_detailer!();
68///
69/// detail!(detailer, "some info {}", 24);
70/// ```
71#[macro_export(local_inner_macros)]
72macro_rules! detail {
73    // detail!(detailer, "a {} event", "log")
74    ($detail_tracker:expr, $($arg:tt)+) => {
75        ($detail_tracker.log(
76            log::Level::Info,
77            core::format_args!($($arg)+))
78        );
79    };
80}
81
82/// Add a detail line at a specified level
83/// ```rust
84/// use detailer::{Detailer, detail_at, new_detailer};
85/// let mut detailer = new_detailer!();
86///
87/// detail_at!(detailer, Error, "yikes {}", 25);
88/// ```
89#[macro_export(local_inner_macros)]
90macro_rules! detail_at {
91    // detail!(detailer, Debug, "a {} event", "log")
92    ($detail_tracker:expr, $log_level:tt, $($arg:tt)+) => {
93        ($detail_tracker.log(
94            log::Level::$log_level,
95            core::format_args!($($arg)+))
96        );
97    };
98}
99
100/// Add a lexical scope indentation to the detail
101///
102/// You can go in multiple levels, but be aware that scopes
103/// bypass log level (other than Off) and will always show
104/// up in the output. Use them for clarity, but don't
105/// overuse them or your output might get hard to read.
106/// ```rust
107/// use detailer::{Detailer, scope, new_detailer, detail};
108/// let mut detailer = new_detailer!();
109///
110/// detail!(detailer, "not indented");
111/// {
112///     let _scope_1 = scope!(detailer, "expensive {} under this scope", "work");
113///     detail!(detailer, "indented");
114///     detail!(detailer, "also indented");
115/// }
116/// detail!(detailer, "not indented");
117/// ```
118#[macro_export(local_inner_macros)]
119macro_rules! scope {
120    // scope!(detailer, "scope {}", "log")
121    ($detail_tracker:expr, $($arg:tt)+) => {
122        ($detail_tracker.scope(
123            core::format_args!($($arg)+))
124        );
125    };
126}
127
128impl Detailer {
129    /// Create a new event Detailer logger.
130    ///
131    /// When dropped or flush()ed it will output its accumulated input.
132    pub fn new(level: log::LevelFilter, timing_setting: TimingSetting, limit: usize) -> Detailer {
133        Detailer {
134            level,
135            accumulated: Default::default(),
136            current_indentation: Default::default(),
137            start: match timing_setting {
138                TimingSetting::WithTiming => Some(Instant::now()),
139                TimingSetting::WithoutTiming => None,
140            },
141            soft_limit: limit,
142        }
143    }
144
145    /// See what's currently accumulated
146    pub fn peek(&self) -> &str {
147        &self.accumulated
148    }
149
150    /// Remove the contents and reset the timer (if enabled)
151    pub fn reset(&mut self) {
152        self.accumulated.clear();
153        if self.start.is_some() {
154            self.start = Some(Instant::now());
155        }
156    }
157
158    /// Output and clear the contents
159    pub fn flush(&mut self) {
160        let to_flush = self.accumulated.trim_end();
161        if !to_flush.is_empty() {
162            log::log!(
163                self.level.to_level().unwrap_or(log::Level::Info),
164                "{}",
165                to_flush
166            );
167        }
168        self.reset();
169    }
170
171    /// Indent output one more level as long as the scope guard exists
172    pub fn scope(&mut self, scope_name: Arguments) -> DetailScopeGuard {
173        if let Some(level) = self.level.to_level() {
174            self.log(level, scope_name);
175        }
176        DetailScopeGuard::new(self.current_indentation.clone())
177    }
178
179    /// log a line, if the level is enabled.
180    ///
181    /// ```
182    /// use detailer::{Detailer, new_detailer};
183    ///
184    /// let mut detailer = new_detailer!();
185    ///
186    /// detailer.log(log::Level::Warn, format_args!("yikes {}", 42));
187    /// ```
188    pub fn log(&mut self, level: log::Level, message: Arguments) {
189        if level <= self.level {
190            if self.soft_limit <= self.accumulated.len() {
191                log::warn!("truncated");
192                return;
193            }
194            let current_indentation = self
195                .current_indentation
196                .load(std::sync::atomic::Ordering::Relaxed);
197            if 0 < current_indentation {
198                let message = message.to_string();
199                let mut lines = message.split('\n');
200                if let Some(first_line) = lines.next() {
201                    if let Some(start) = &self.start {
202                        let elapsed = start.elapsed().as_micros() as u64;
203                        let _ = self.accumulated.write_fmt(format_args!("{elapsed:<6} "));
204                    }
205                    for _ in 0..current_indentation {
206                        let _ = self.accumulated.write_str("  ");
207                    }
208                    let _ = self.accumulated.write_fmt(format_args!("{first_line}\n"));
209                }
210                for line in lines {
211                    for _ in 0..current_indentation {
212                        let _ = self.accumulated.write_str("  ");
213                    }
214                    let _ = self.accumulated.write_fmt(format_args!("{line}\n"));
215                }
216            } else {
217                if let Some(start) = &self.start {
218                    let elapsed = start.elapsed().as_micros() as u64;
219                    let _ = self.accumulated.write_fmt(format_args!("{elapsed:<6} "));
220                }
221                let _ = self.accumulated.write_fmt(message);
222                let _ = self.accumulated.write_char('\n');
223            }
224        }
225    }
226
227    /// log a line
228    ///
229    /// ```
230    /// use detailer::{Detailer, new_detailer};
231    ///
232    /// let mut detailer = new_detailer!();
233    ///
234    /// detailer.info(format_args!("yikes {}", 42));
235    /// ```
236    pub fn info(&mut self, message: Arguments) {
237        self.log(log::Level::Info, message)
238    }
239
240    /// log a line
241    ///
242    /// ```
243    /// use detailer::{Detailer, new_detailer};
244    ///
245    /// let mut detailer = new_detailer!();
246    ///
247    /// detailer.trace(format_args!("yikes {}", 42));
248    /// ```
249    pub fn trace(&mut self, message: Arguments) {
250        self.log(log::Level::Trace, message)
251    }
252
253    /// log a line
254    ///
255    /// ```
256    /// use detailer::{Detailer, new_detailer};
257    ///
258    /// let mut detailer = new_detailer!();
259    ///
260    /// detailer.debug(format_args!("yikes {}", 42));
261    /// ```
262    pub fn debug(&mut self, message: Arguments) {
263        self.log(log::Level::Debug, message)
264    }
265
266    /// log a line
267    ///
268    /// ```
269    /// use detailer::{Detailer, new_detailer};
270    /// use log::{Level, LevelFilter};
271    ///
272    /// let mut detailer = new_detailer!();
273    ///
274    /// detailer.warn(format_args!("yikes {}", 42));
275    /// ```
276    pub fn warn(&mut self, message: Arguments) {
277        self.log(log::Level::Warn, message)
278    }
279
280    /// log a line
281    ///
282    /// ```
283    /// use detailer::{Detailer, new_detailer};
284    /// use log::{Level, LevelFilter};
285    ///
286    /// let mut detailer = new_detailer!();
287    ///
288    /// detailer.error(format_args!("yikes {}", 42));
289    /// ```
290    pub fn error(&mut self, message: Arguments) {
291        self.log(log::Level::Error, message)
292    }
293
294    /// Override the level for this detailer
295    ///
296    /// ```
297    /// use detailer::{Detailer, new_detailer};
298    /// use log::{Level, LevelFilter};
299    ///
300    /// let mut detailer = new_detailer!();
301    ///
302    /// detailer.debug(format_args!("{} logged", "not"));
303    /// detailer.level(log::LevelFilter::Debug);
304    /// detailer.debug(format_args!("{} logged", "is"));
305    /// ```
306    pub fn level(&mut self, level: log::LevelFilter) {
307        self.level = level;
308    }
309}
310
311impl Drop for Detailer {
312    fn drop(&mut self) {
313        if !self.accumulated.is_empty() {
314            self.log(
315                self.level.to_level().unwrap_or(log::Level::Info),
316                format_args!("dropped"),
317            );
318        }
319        self.flush()
320    }
321}
322
323impl Default for Detailer {
324    fn default() -> Self {
325        Self {
326            level: log::LevelFilter::Info,
327            accumulated: Default::default(),
328            current_indentation: Default::default(),
329            start: Some(Instant::now()),
330            soft_limit: 4 * 1024,
331        }
332    }
333}
334
335/// When a DetailScopeGuard is dropped, the level of indentation on following
336/// `detail!()`` messages for the detailer this scope was created for is
337/// immediately decremented.
338pub struct DetailScopeGuard {
339    level: Arc<AtomicUsize>,
340}
341
342impl DetailScopeGuard {
343    fn new(level: Arc<AtomicUsize>) -> Self {
344        level.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
345        Self { level }
346    }
347}
348
349impl Drop for DetailScopeGuard {
350    fn drop(&mut self) {
351        self.level
352            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
353    }
354}