Skip to main content

feanor_tracing/
lib.rs

1#![feature(unboxed_closures)]
2#![feature(fn_traits)]
3
4#![doc = include_str!("../Readme.md")]
5
6use tracing::{Id, Level, Event, span, Metadata, Subscriber};
7use tracing::subscriber::Interest;
8use tracing::field::{Field, Visit};
9use std::ops::RangeInclusive;
10use std::fmt::{Display, Write};
11use std::sync::atomic::AtomicUsize;
12use core::*;
13
14#[cfg(feature = "print_colored")]
15use owo_colors::{AnsiColors, OwoColorize, Stream};
16
17mod core;
18
19///
20/// The palette of colors that is cycled through to distinguish second-level
21/// (i.e. depth-1) spans within a single line of output.
22///
23#[cfg(feature = "print_colored")]
24const SPAN_COLORS: [AnsiColors; 6] = [
25    AnsiColors::BrightBlue,
26    AnsiColors::BrightCyan,
27    AnsiColors::BrightMagenta,
28    AnsiColors::Yellow,
29    AnsiColors::BrightGreen,
30    AnsiColors::Red,
31];
32
33#[cfg(not(feature = "print_colored"))]
34#[derive(Copy, Clone)]
35struct AnsiColors;
36
37///
38/// Wraps `message` in the ANSI escape codes for the given color, if any.
39///
40/// This only emits escape codes when the output stream actually supports them; 
41/// in particular it respects whether stdout is a terminal as well as the
42/// `NO_COLOR` and `CLICOLOR` environment variables.
43///
44fn colorize(color: Option<AnsiColors>, message: String) -> String {
45    match color {
46        #[cfg(feature = "print_colored")]
47        Some(index) => {
48            message
49                .if_supports_color(Stream::Stdout, |text| text.color(index))
50                .to_string()
51        },
52        #[cfg(not(feature = "print_colored"))]
53        Some(_) => unreachable!(),
54        None => message
55    }
56}
57
58struct PrintToStdout;
59
60impl FnOnce<(String,)> for PrintToStdout {
61    type Output = ();
62
63    extern "rust-call" fn call_once(self, args: (String,)) {
64        self.call(args)
65    }
66}
67
68impl<'a> FnMut<(String,)> for PrintToStdout {
69    extern "rust-call" fn call_mut(&mut self, args: (String,)) {
70        self.call(args)
71    }
72}
73
74impl<'a> Fn<(String,)> for PrintToStdout {
75    extern "rust-call" fn call(&self, args: (String,)) {
76        // use print! here to interact with io capture during tests
77        print!("{}", args.0);
78        std::io::Write::flush(&mut std::io::stdout()).unwrap()
79    }
80}
81
82struct SpanData {
83    desc: FieldRecorder,
84    depth: usize,
85    metadata: &'static Metadata<'static>,
86    color: Option<AnsiColors>,
87    /// counter used to assign distinct colors to children; only set for
88    /// top-level spans
89    #[allow(unused)]
90    color_counter: Option<AtomicUsize>
91}
92
93impl SpanData {
94
95    #[cfg(feature = "print_colored")]
96    fn color_for_child(&self) -> Option<AnsiColors> {
97        if let Some(color_counter) = &self.color_counter {
98            debug_assert_eq!(0, self.depth);
99            Some(SPAN_COLORS[color_counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst) % SPAN_COLORS.len()])
100        } else {
101            self.color
102        }
103    }
104
105    #[cfg(not(feature = "print_colored"))]
106    fn color_for_child(&self) -> Option<AnsiColors> {
107        None
108    }
109}
110
111///
112/// A [`tracing::Subscriber`] that prints the events deemed most important to get a
113/// high-level overview of the executed algorithm to stdout.
114///
115/// Features:
116///  - Messages are only printed after the algorithm has spent a certain time in the
117///    current span, thus not printing anything for short spans
118///  - Messages are printed from the same threads as they are generated, which makes
119///    it compatible with IO capturing during tests
120///  - for each root span, all logged sub-spans and events are written in a single line,
121///    keeping the output compact.
122///  - to make the single line easier to read, every second-level span (and its
123///    sub-tree) is printed in a different color, cycling through a fixed palette.
124///    Colors are only emitted when the feature `print_colored` is active and the output
125///    stream supports them; Furthermore, they can be disabled via the the `NO_COLOR`
126///    and `CLICOLOR` environment variables.
127///
128pub struct DelayedLogger {
129    base: DelayedLoggerImpl<SpanData, PrintToStdout>,
130    levels: RangeInclusive<Level>,
131    max_depth: usize
132}
133
134impl DelayedLogger {
135
136    ///
137    /// Creates a new [`DelayedLogger`], which logs only events and spans
138    ///  - whose [`Level`] is within `levels`
139    ///  - which are part of a span that will be running for at least
140    ///    `silent_period` microseconds; any message queued at a time when this
141    ///    is not yet decided will be stored and possibly printed later
142    ///  - are part of a span of depth at most `max_depth` in the span tree
143    ///
144    pub fn new(max_depth: usize, silent_period: u64, levels: RangeInclusive<Level>) -> Self {
145        Self {
146            base: DelayedLoggerImpl::new(silent_period, PrintToStdout),
147            levels: levels,
148            max_depth: max_depth
149        }
150    }
151    
152    ///
153    /// Creates a new [`DelayedLogger`] using [`DelayedLogger::new()`] and sets it as
154    /// the global default. This will panic if a global default has already been set.
155    ///
156    pub fn init(max_depth: usize, silent_period: u64, levels: RangeInclusive<Level>) {
157        tracing::subscriber::set_global_default(Self::new(max_depth, silent_period, levels)).unwrap()
158    }
159
160    ///
161    /// Creates a new [`DelayedLogger`] using a test configuration and sets it as
162    /// the global default, unless a global default is already set.
163    ///
164    pub fn init_test() {
165        _ = tracing::subscriber::set_global_default(Self::new(4, 100000, Level::ERROR..=Level::TRACE));
166    }
167}
168
169#[derive(Clone)]
170struct FieldRecorder {
171    message: Option<String>,
172    fields: Option<String>
173}
174
175impl FieldRecorder {
176
177    fn new() -> Self {
178        Self { message: None, fields: None }
179    }
180
181    fn to_string(self) -> String {
182        match (self.message, self.fields) {
183            (Some(mut message), Some(fields)) => {
184                write!(&mut message, "({})", fields).unwrap();
185                message
186            },
187            (Some(mut message), None) => {
188                write!(&mut message, ".").unwrap();
189                message
190            },
191            (None, Some(fields)) => {
192                format!("({})", fields)
193            },
194            (None, None) => ".".to_owned()
195        }
196    }
197}
198
199impl Display for FieldRecorder {
200
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        if let Some(message) = &self.message {
203            write!(f, "{}", message)?;
204        }
205        if let Some(fields) = &self.fields {
206            write!(f, "({})", fields)
207        } else {
208            write!(f, ".")
209        }
210    }
211}
212
213impl Visit for FieldRecorder {
214
215    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
216        if field.name() == "message" {
217            self.message = Some(format!("{:?}", value));
218        } else {
219            if let Some(fields) = &mut self.fields {
220                write!(fields, ", {}={:?}", field.name(), value).unwrap();
221            } else {
222                self.fields = Some(format!("{}={:?}", field.name(), value));
223            }
224        }
225    }
226}
227
228impl Subscriber for DelayedLogger {
229
230    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
231        if self.levels.contains(metadata.level()) {
232            Interest::always()
233        } else {
234            Interest::never()
235        }
236    }
237
238    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
239        self.levels.contains(metadata.level())
240    }
241
242    fn current_span(&self) -> tracing_core::span::Current {
243        let mut result = tracing_core::span::Current::none();
244        if let Some(span) = self.base.span_stack().borrow().last().copied() {
245            self.base.span_data(span, |data, _, _| result = 
246                tracing_core::span::Current::new(Id::from_non_zero_u64(span), data.metadata)
247            );
248        }
249        return result;
250    }
251
252    fn new_span(&self, span: &span::Attributes<'_>) -> Id {
253        let mut fields = FieldRecorder::new();
254        span.record(&mut fields);
255        fields.message = Some(span.metadata().name().to_owned());
256
257        let id = if let Some(parent_id) = span.parent() {
258            let mut depth = 0;
259            let mut color = None;
260            self.base.span_data(parent_id.into_non_zero_u64(), |data, _, _| {
261                depth = data.depth + 1;
262                color = data.color_for_child();
263            });
264            self.base.create_span_with_parent(SpanData {
265                color_counter: if depth == 0 { Some(AtomicUsize::new(0)) } else { None },
266                desc: fields,
267                metadata: span.metadata(),
268                depth: depth,
269                color: color
270            }, Some(parent_id.into_non_zero_u64()))
271        } else {
272            self.base.create_span(|parent| {
273                let (depth, color) = if let Some((parent_data, _)) = parent {
274                    (parent_data.depth + 1, parent_data.color_for_child())
275                } else {
276                    (0, None)
277                };
278                SpanData {
279                    color_counter: if depth == 0 { Some(AtomicUsize::new(0)) } else { None },
280                    desc: fields,
281                    metadata: span.metadata(),
282                    depth: depth,
283                    color: color
284                }
285            })
286        };
287
288        return Id::from_non_zero_u64(id);
289    }
290
291    fn record(&self, _span: &Id, _values: &span::Record<'_>) {
292        unimplemented!()
293    }
294
295    fn record_follows_from(&self, _span: &Id, _follows: &Id) {
296        // we only care about parent spans currently
297    }
298
299    fn event(&self, event: &Event<'_>) {
300        if self.levels.contains(event.metadata().level()) {
301            let mut is_within_depth = false;
302            let mut color = None;
303            if let Some(span) = self.base.span_stack().borrow().last().copied() {
304                self.base.span_data(span, |data, _, _| if data.depth < self.max_depth {
305                    is_within_depth = true;
306                    color = data.color;
307                });
308                if is_within_depth {
309                    let mut fields = FieldRecorder::new();
310                    event.record(&mut fields);
311                    self.base.send_message(colorize(color, fields.to_string()), span);
312                }
313            } else {
314                let mut fields = FieldRecorder::new();
315                event.record(&mut fields);
316                PrintToStdout(fields.to_string())
317            }
318        }
319    }
320
321    fn enter(&self, span: &Id) {
322        self.base.enter(span.into_non_zero_u64());
323        let mut message = None;
324        self.base.span_data(span.into_non_zero_u64(), |data, _, _| if data.depth < self.max_depth {
325            message = Some(colorize(data.color, data.desc.clone().to_string()));
326        } else if data.depth == self.max_depth {
327            message = Some(colorize(data.color, format!("{}...", data.desc)));
328        });
329        if let Some(message) = message {
330            self.base.send_message(message, span.into_non_zero_u64());
331        }
332    }
333
334    fn exit(&self, span: &Id) {
335        let mut message = None;
336        self.base.span_data(span.into_non_zero_u64(), |data, _, running_time| if data.depth == 0 {
337            message = Some(format!("done({} us)\n", running_time.as_micros()));
338        } else if data.depth <= self.max_depth {
339            message = Some(colorize(data.color, format!("done({} us)", running_time.as_micros())));
340        });
341        if let Some(message) = message {
342            self.base.send_message(message, span.into_non_zero_u64());
343        }
344        self.base.exit(span.into_non_zero_u64())
345    }
346
347    fn clone_span(&self, id: &Id) -> Id {
348        self.base.clone_span(id.into_non_zero_u64());
349        return id.clone();
350    }
351
352    fn try_close(&self, id: Id) -> bool {
353        self.base.delete_span(id.into_non_zero_u64())
354    }
355}