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}