emit_term/
lib.rs

1/*!
2Emit diagnostic events to the console.
3
4This library implements a text-based format that's intended for direct end-user consumption, such as in interactive applications.
5
6# Getting started
7
8Add `emit` and `emit_term` to your `Cargo.toml`:
9
10```toml
11[dependencies.emit]
12version = "1.3.1"
13
14[dependencies.emit_term]
15version = "1.3.1"
16```
17
18Initialize `emit` using `emit_term`:
19
20```
21fn main() {
22    let rt = emit::setup()
23        .emit_to(emit_term::stdout())
24        .init();
25
26    // Your app code goes here
27
28    rt.blocking_flush(std::time::Duration::from_secs(30));
29}
30```
31
32`emit_term` uses a format optimized for human legibility, not for machine processing. You may also want to emit diagnostics to another location, such as OTLP through `emit_otlp` or a rolling file through `emit_file` for processing. You can use [`emit::Setup::and_emit_to`] to combine multiple emitters:
33
34```
35# fn some_other_emitter() -> impl emit::Emitter + Send + Sync + 'static {
36#    emit::emitter::from_fn(|_| {})
37# }
38fn main() {
39    let rt = emit::setup()
40        .emit_to(emit_term::stdout())
41        .and_emit_to(some_other_emitter())
42        .init();
43
44    // Your app code goes here
45
46    rt.blocking_flush(std::time::Duration::from_secs(30));
47}
48```
49
50## Configuration
51
52`emit_term` has a fixed format, but can be configured to force or disable color output instead of detect it.
53
54To disable colors, call [`Stdout::colored`] with the value `false`:
55
56```rust
57fn main() {
58    let rt = emit::setup()
59        // Disable colors
60        .emit_to(emit_term::stdout().colored(false))
61        .init();
62
63    // Your app code goes here
64
65    rt.blocking_flush(std::time::Duration::from_secs(5));
66}
67```
68
69To force colors, call [`Stdout::colored`] with the value `true`:
70
71```rust
72fn main() {
73    let rt = emit::setup()
74        // Force colors
75        .emit_to(emit_term::stdout().colored(true))
76        .init();
77
78    // Your app code goes here
79
80    rt.blocking_flush(std::time::Duration::from_secs(5));
81}
82```
83*/
84
85#![doc(html_logo_url = "https://raw.githubusercontent.com/emit-rs/emit/main/asset/logo.svg")]
86#![deny(missing_docs)]
87
88use std::{cell::RefCell, cmp, fmt, io::Write, iter, str, time::Duration};
89
90use emit::well_known::{
91    KEY_ERR, KEY_EVT_KIND, KEY_LVL, KEY_METRIC_VALUE, KEY_SPAN_ID, KEY_TRACE_ID,
92};
93use termcolor::{Buffer, BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
94
95/**
96Get an emitter that writes to `stdout`.
97
98Colors will be used if the terminal supports them.
99*/
100pub fn stdout() -> Stdout {
101    Stdout::new()
102}
103
104/**
105Get an emitter that writes to `stderr`.
106
107Colors will be used if the terminal supports them.
108*/
109pub fn stderr() -> Stderr {
110    Stderr::new()
111}
112
113/**
114An emitter that writes to `stdout`.
115
116Use [`stdout`] to construct an emitter and pass the result to [`emit::Setup::emit_to`] to configure `emit` to use it:
117
118```
119fn main() {
120    let rt = emit::setup()
121        .emit_to(emit_term::stdout())
122        .init();
123
124    // Your app code goes here
125
126    rt.blocking_flush(std::time::Duration::from_secs(30));
127}
128```
129*/
130pub struct Stdout {
131    writer: Writer,
132}
133
134impl Stdout {
135    /**
136    Get an emitter that writes to `stdout`.
137
138    Colors will be used if the terminal supports them.
139    */
140    pub fn new() -> Self {
141        Stdout {
142            writer: Writer {
143                writer: BufferWriter::stdout(ColorChoice::Auto),
144            },
145        }
146    }
147
148    /**
149    Whether to write using colors.
150
151    By default, colors will be used if the terminal supports them. You can explicitly enable or disable colors using this function. If `colored` is true then colors will always be used. If `colored` is false then colors will never be used.
152    */
153    pub fn colored(mut self, colored: bool) -> Self {
154        if colored {
155            self.writer = Writer {
156                writer: BufferWriter::stdout(ColorChoice::Always),
157            };
158        } else {
159            self.writer = Writer {
160                writer: BufferWriter::stdout(ColorChoice::Never),
161            };
162        }
163
164        self
165    }
166}
167
168impl emit::emitter::Emitter for Stdout {
169    fn emit<E: emit::event::ToEvent>(&self, evt: E) {
170        self.writer.emit(evt)
171    }
172
173    fn blocking_flush(&self, _: Duration) -> bool {
174        true
175    }
176}
177
178impl emit::runtime::InternalEmitter for Stdout {}
179
180/**
181An emitter that writes to `stderr`.
182
183Use [`stderr`] to construct an emitter and pass the result to [`emit::Setup::emit_to`] to configure `emit` to use it:
184
185```
186fn main() {
187    let rt = emit::setup()
188        .emit_to(emit_term::stderr())
189        .init();
190
191    // Your app code goes here
192
193    rt.blocking_flush(std::time::Duration::from_secs(30));
194}
195```
196*/
197pub struct Stderr {
198    writer: Writer,
199}
200
201impl Stderr {
202    /**
203    Get an emitter that writes to `stderr`.
204
205    Colors will be used if the terminal supports them.
206    */
207    pub fn new() -> Self {
208        Stderr {
209            writer: Writer {
210                writer: BufferWriter::stderr(ColorChoice::Auto),
211            },
212        }
213    }
214
215    /**
216    Whether to write using colors.
217
218    By default, colors will be used if the terminal supports them. You can explicitly enable or disable colors using this function. If `colored` is true then colors will always be used. If `colored` is false then colors will never be used.
219    */
220    pub fn colored(mut self, colored: bool) -> Self {
221        if colored {
222            self.writer = Writer {
223                writer: BufferWriter::stderr(ColorChoice::Always),
224            };
225        } else {
226            self.writer = Writer {
227                writer: BufferWriter::stderr(ColorChoice::Never),
228            };
229        }
230
231        self
232    }
233}
234
235impl emit::emitter::Emitter for Stderr {
236    fn emit<E: emit::event::ToEvent>(&self, evt: E) {
237        self.writer.emit(evt)
238    }
239
240    fn blocking_flush(&self, _: Duration) -> bool {
241        true
242    }
243}
244
245impl emit::runtime::InternalEmitter for Stderr {}
246
247struct Writer {
248    writer: BufferWriter,
249}
250
251impl Writer {
252    fn emit<E: emit::event::ToEvent>(&self, evt: E) {
253        let evt = evt.to_event();
254
255        with_shared_buf(&self.writer, |writer, buf| {
256            write_event(buf, evt);
257
258            let _ = writer.print(buf);
259        });
260    }
261}
262
263fn write_event(buf: &mut Buffer, evt: emit::event::Event<impl emit::props::Props>) {
264    if let Some(span_id) = evt.props().pull::<emit::SpanId, _>(KEY_SPAN_ID) {
265        if let Some(trace_id) = evt.props().pull::<emit::TraceId, _>(KEY_TRACE_ID) {
266            let trace_id_color = trace_id_color(&trace_id);
267
268            write_fg(buf, "▓", Color::Ansi256(trace_id_color));
269            write_plain(buf, " ");
270            write_plain(buf, hex_slice(&trace_id.to_hex(), 6));
271            write_plain(buf, " ");
272        } else {
273            write_plain(buf, "░      ");
274        }
275
276        let span_id_color = span_id_color(&span_id);
277
278        write_fg(buf, "▓", Color::Ansi256(span_id_color));
279        write_plain(buf, " ");
280        write_plain(buf, hex_slice(&span_id.to_hex(), 4));
281        write_plain(buf, " ");
282    }
283
284    if let Some(extent) = evt.extent() {
285        if let Some(len) = extent.len() {
286            write_timestamp(buf, *extent.as_point());
287            write_plain(buf, " ");
288            write_duration(buf, len);
289        } else if let Some(range) = extent.as_range() {
290            write_timestamp(buf, range.start);
291            write_plain(buf, "..");
292            write_timestamp(buf, range.end);
293        } else {
294            write_timestamp(buf, *extent.as_point());
295        }
296
297        write_plain(buf, " ");
298    }
299
300    let mut lvl = None;
301    if let Some(level) = evt.props().pull::<emit::Level, _>(KEY_LVL) {
302        lvl = level_color(&level).map(Color::Ansi256);
303
304        try_write_fg(buf, level, lvl);
305        write_plain(buf, " ");
306    }
307
308    if let Some(kind) = evt.props().get(KEY_EVT_KIND) {
309        write_fg(buf, kind, KIND);
310        write_plain(buf, " ");
311    }
312
313    let mut mdl = evt.mdl().segments();
314    if let (Some(first), last) = (mdl.next(), mdl.last()) {
315        write_fg(buf, first, MDL_FIRST);
316        write_plain(buf, " ");
317
318        if let Some(last) = last {
319            write_fg(buf, last, MDL_LAST);
320            write_plain(buf, " ");
321        }
322    }
323
324    let _ = evt.msg().write(TokenWriter { buf });
325    write_plain(buf, "\n");
326
327    if let Some(err) = evt.props().get(KEY_ERR) {
328        if let Some(err) = err.to_borrowed_error() {
329            write_plain(buf, "  ");
330            try_write_fg(buf, "err", lvl);
331            write_plain(buf, format_args!(": {err}\n"));
332
333            for cause in iter::successors(err.source(), |err| (*err).source()) {
334                write_plain(buf, "  ");
335                try_write_fg(buf, "caused by", lvl);
336                write_plain(buf, format_args!(": {cause}\n"));
337            }
338        }
339    }
340
341    if let Some(value) = evt.props().get(KEY_METRIC_VALUE) {
342        let buckets = value.to_f64_sequence().unwrap_or_default();
343
344        if !buckets.is_empty() {
345            write_timeseries(buf, &buckets);
346        }
347    }
348}
349
350fn write_timeseries(buf: &mut Buffer, buckets: &[f64]) {
351    const BLOCKS: [&'static str; 7] = ["▁", "▂", "▃", "▄", "▅", "▆", "▇"];
352
353    let mut bucket_min = f64::NAN;
354    let mut bucket_max = -f64::NAN;
355
356    for v in buckets {
357        bucket_min = cmp::min_by(*v, bucket_min, f64::total_cmp);
358        bucket_max = cmp::max_by(*v, bucket_max, f64::total_cmp);
359    }
360
361    for v in buckets {
362        let idx = (((v - bucket_min) / (bucket_max - bucket_min)) * ((BLOCKS.len() - 1) as f64))
363            .ceil() as usize;
364        let _ = buf.write(BLOCKS[idx].as_bytes());
365    }
366
367    let _ = buf.write(b"\n");
368}
369
370fn hex_slice<'a>(hex: &'a [u8], len: usize) -> impl fmt::Display + 'a {
371    struct HexSlice<'a>(&'a [u8], usize);
372
373    impl<'a> fmt::Display for HexSlice<'a> {
374        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
375            f.write_str(str::from_utf8(&self.0[..self.1]).unwrap())
376        }
377    }
378
379    HexSlice(hex, len)
380}
381
382struct LocalTime {
383    h: u8,
384    m: u8,
385    s: u8,
386    ms: u16,
387}
388
389fn local_ts(ts: emit::Timestamp) -> Option<LocalTime> {
390    #[cfg(test)]
391    {
392        // In tests it's easier just to use full RFC3339 timestamps
393        // since we don't know exactly what platforms `time` supports
394        let _ = ts;
395
396        None
397    }
398    #[cfg(not(test))]
399    {
400        // See: https://github.com/rust-lang/rust/issues/27970
401        //
402        // On Linux and OSX, this will fail to get the local offset in
403        // any multi-threaded program. It needs to be fixed in the standard
404        // library and propagated through libraries like `time`. Until then,
405        // you probably won't get local timestamps outside of Windows.
406        let local = time::OffsetDateTime::from_unix_timestamp_nanos(
407            ts.to_unix().as_nanos().try_into().ok()?,
408        )
409        .ok()?;
410        let local = local.checked_to_offset(time::UtcOffset::local_offset_at(local).ok()?)?;
411
412        let (h, m, s, ms) = local.time().as_hms_milli();
413
414        Some(LocalTime { h, m, s, ms })
415    }
416}
417
418fn write_timestamp(buf: &mut Buffer, ts: emit::Timestamp) {
419    if let Some(LocalTime { h, m, s, ms }) = local_ts(ts) {
420        write_plain(
421            buf,
422            format_args!("{:>02}:{:>02}:{:>02}.{:>03}", h, m, s, ms),
423        );
424    } else {
425        write_plain(buf, format_args!("{:.0}", ts));
426    }
427}
428
429struct FriendlyDuration {
430    pub value: u128,
431    pub unit: &'static str,
432}
433
434fn friendly_duration(duration: Duration) -> FriendlyDuration {
435    const NANOS_PER_MICRO: u128 = 1000;
436    const NANOS_PER_MILLI: u128 = NANOS_PER_MICRO * 1000;
437    const NANOS_PER_SEC: u128 = NANOS_PER_MILLI * 1000;
438    const NANOS_PER_MIN: u128 = NANOS_PER_SEC * 60;
439
440    let nanos = duration.as_nanos();
441
442    if nanos < NANOS_PER_MICRO * 2 {
443        FriendlyDuration {
444            value: nanos,
445            unit: "ns",
446        }
447    } else if nanos < NANOS_PER_MILLI * 2 {
448        FriendlyDuration {
449            value: nanos / NANOS_PER_MICRO,
450            unit: "μs",
451        }
452    } else if nanos < NANOS_PER_SEC * 2 {
453        FriendlyDuration {
454            value: nanos / NANOS_PER_MILLI,
455            unit: "ms",
456        }
457    } else if nanos < NANOS_PER_MIN * 2 {
458        FriendlyDuration {
459            value: nanos / NANOS_PER_SEC,
460            unit: "s",
461        }
462    } else {
463        FriendlyDuration {
464            value: nanos / NANOS_PER_MIN,
465            unit: "m",
466        }
467    }
468}
469
470fn write_duration(buf: &mut Buffer, duration: Duration) {
471    let FriendlyDuration { value, unit } = friendly_duration(duration);
472
473    write_fg(buf, value, NUMBER);
474    write_fg(buf, unit, TEXT);
475}
476
477struct TokenWriter<'a> {
478    buf: &'a mut Buffer,
479}
480
481impl<'a> sval_fmt::TokenWrite for TokenWriter<'a> {
482    fn write_text_quote(&mut self) -> fmt::Result {
483        Ok(())
484    }
485
486    fn write_text(&mut self, text: &str) -> fmt::Result {
487        self.write(text, TEXT);
488
489        Ok(())
490    }
491
492    fn write_number<N: fmt::Display>(&mut self, num: N) -> fmt::Result {
493        self.write(num, NUMBER);
494
495        Ok(())
496    }
497
498    fn write_atom<A: fmt::Display>(&mut self, atom: A) -> fmt::Result {
499        self.write(atom, ATOM);
500
501        Ok(())
502    }
503
504    fn write_ident(&mut self, ident: &str) -> fmt::Result {
505        self.write(ident, IDENT);
506
507        Ok(())
508    }
509
510    fn write_field(&mut self, field: &str) -> fmt::Result {
511        self.write(field, FIELD);
512
513        Ok(())
514    }
515}
516
517impl<'a> fmt::Write for TokenWriter<'a> {
518    fn write_str(&mut self, s: &str) -> fmt::Result {
519        write!(&mut self.buf, "{}", s).map_err(|_| fmt::Error)
520    }
521}
522
523impl<'a> emit::template::Write for TokenWriter<'a> {
524    fn write_hole_value(&mut self, _: &str, value: emit::Value) -> fmt::Result {
525        sval_fmt::stream_to_token_write(self, value)
526    }
527
528    fn write_hole_fmt(
529        &mut self,
530        _: &str,
531        value: emit::Value,
532        formatter: emit::template::Formatter,
533    ) -> fmt::Result {
534        use sval::Value as _;
535
536        match value.tag() {
537            Some(sval::tags::NUMBER) => self.write(formatter.apply(value), NUMBER),
538            _ => self.write(formatter.apply(value), TEXT),
539        }
540
541        Ok(())
542    }
543}
544
545const KIND: Color = Color::Ansi256(174);
546const MDL_FIRST: Color = Color::Ansi256(248);
547const MDL_LAST: Color = Color::Ansi256(244);
548
549const TEXT: Color = Color::Ansi256(69);
550const NUMBER: Color = Color::Ansi256(135);
551const ATOM: Color = Color::Ansi256(168);
552const IDENT: Color = Color::Ansi256(170);
553const FIELD: Color = Color::Ansi256(174);
554
555fn trace_id_color(trace_id: &emit::TraceId) -> u8 {
556    let mut hash = 0;
557
558    for b in trace_id.to_u128().to_le_bytes() {
559        hash ^= b;
560    }
561
562    hash
563}
564
565fn span_id_color(span_id: &emit::SpanId) -> u8 {
566    let mut hash = 0;
567
568    for b in span_id.to_u64().to_le_bytes() {
569        hash ^= b;
570    }
571
572    hash
573}
574
575fn level_color(level: &emit::Level) -> Option<u8> {
576    match level {
577        emit::Level::Debug => Some(244),
578        emit::Level::Info => None,
579        emit::Level::Warn => Some(202),
580        emit::Level::Error => Some(124),
581    }
582}
583
584fn write_fg(buf: &mut Buffer, v: impl fmt::Display, color: Color) {
585    let _ = buf.set_color(ColorSpec::new().set_fg(Some(color)));
586    let _ = write!(buf, "{}", v);
587    let _ = buf.reset();
588}
589
590fn try_write_fg(buf: &mut Buffer, v: impl fmt::Display, color: Option<Color>) {
591    if let Some(color) = color {
592        write_fg(buf, v, color);
593    } else {
594        write_plain(buf, v);
595    }
596}
597
598fn write_plain(buf: &mut Buffer, v: impl fmt::Display) {
599    let _ = write!(buf, "{}", v);
600}
601
602impl<'a> TokenWriter<'a> {
603    fn write(&mut self, v: impl fmt::Display, color: Color) {
604        write_fg(&mut *self.buf, v, color);
605    }
606}
607
608fn with_shared_buf(writer: &BufferWriter, with_buf: impl FnOnce(&BufferWriter, &mut Buffer)) {
609    thread_local! {
610        static STDOUT: RefCell<Option<Buffer>> = RefCell::new(None);
611    }
612
613    STDOUT.with(|buf| {
614        match buf.try_borrow_mut() {
615            // If there are no overlapping references then use the cached buffer
616            Ok(mut slot) => {
617                match &mut *slot {
618                    // If there's a cached buffer then clear it and print using it
619                    Some(buf) => {
620                        buf.clear();
621                        with_buf(&writer, buf);
622                    }
623                    // If there's no cached buffer then create one and use it
624                    // It'll be cached for future callers on this thread
625                    None => {
626                        let mut buf = writer.buffer();
627                        with_buf(&writer, &mut buf);
628
629                        *slot = Some(buf);
630                    }
631                }
632            }
633            // If there are overlapping references then just create a
634            // buffer on-demand to use
635            Err(_) => {
636                with_buf(&writer, &mut writer.buffer());
637            }
638        }
639    });
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    use std::str;
647
648    #[test]
649    fn write_log() {
650        let mut buf = Buffer::no_color();
651
652        write_event(
653            &mut buf,
654            emit::evt!(
655                extent: emit::Timestamp::try_from_str("2024-01-01T01:02:03.000Z").unwrap(),
656                "Hello, {user}",
657                user: "Rust",
658                extra: true,
659            ),
660        );
661
662        assert_eq!(
663            "2024-01-01T01:02:03Z emit_term tests Hello, Rust\n",
664            str::from_utf8(buf.as_slice()).unwrap()
665        );
666    }
667
668    #[test]
669    fn write_log_err() {
670        let mut buf = Buffer::no_color();
671
672        write_event(
673            &mut buf,
674            emit::evt!(
675                extent: emit::Timestamp::try_from_str("2024-01-01T01:02:03.000Z").unwrap(),
676                "An error",
677                lvl: "error",
678                err: std::io::Error::new(std::io::ErrorKind::Other, "Something went wrong"),
679            ),
680        );
681
682        assert_eq!(
683            "2024-01-01T01:02:03Z error emit_term tests An error\n  err: Something went wrong\n",
684            str::from_utf8(buf.as_slice()).unwrap()
685        );
686    }
687
688    #[test]
689    fn write_span() {
690        let mut buf = Buffer::no_color();
691
692        write_event(
693            &mut buf,
694            emit::evt!(
695                extent:
696                    emit::Timestamp::try_from_str("2024-01-01T01:02:03.000Z").unwrap()..
697                    emit::Timestamp::try_from_str("2024-01-01T01:02:04.000Z").unwrap(),
698                "Hello, {user}",
699                user: "Rust",
700                evt_kind: "span",
701                trace_id: "4bf92f3577b34da6a3ce929d0e0e4736",
702                span_id: "00f067aa0ba902b7",
703                extra: true,
704            ),
705        );
706
707        assert_eq!(
708            "▓ 4bf92f ▓ 00f0 2024-01-01T01:02:04Z 1000ms span emit_term tests Hello, Rust\n",
709            str::from_utf8(buf.as_slice()).unwrap()
710        );
711    }
712
713    #[test]
714    fn write_metric() {
715        let mut buf = Buffer::no_color();
716
717        write_event(
718            &mut buf,
719            emit::evt!(
720                extent: emit::Timestamp::try_from_str("2024-01-01T01:02:03.000Z").unwrap(),
721                "{metric_agg} of {metric_name} is {metric_value}",
722                user: "Rust",
723                evt_kind: "metric",
724                metric_name: "test",
725                metric_agg: "count",
726                metric_value: 42,
727            ),
728        );
729
730        assert_eq!(
731            "2024-01-01T01:02:03Z metric emit_term tests count of test is 42\n",
732            str::from_utf8(buf.as_slice()).unwrap()
733        );
734    }
735
736    #[test]
737    fn write_metric_timeseries() {
738        let mut buf = Buffer::no_color();
739
740        write_event(
741            &mut buf,
742            emit::evt!(
743                extent:
744                    emit::Timestamp::try_from_str("2024-01-01T01:02:00.000Z").unwrap()..
745                    emit::Timestamp::try_from_str("2024-01-01T01:02:10.000Z").unwrap(),
746                "{metric_agg} of {metric_name} is {metric_value}",
747                user: "Rust",
748                evt_kind: "metric",
749                metric_name: "test",
750                metric_agg: "count",
751                #[emit::as_value]
752                metric_value: [
753                    0,
754                    1,
755                    2,
756                    3,
757                    4,
758                    5,
759                    1,
760                    2,
761                    3,
762                    4,
763                    5,
764                ],
765            ),
766        );
767
768        assert_eq!("2024-01-01T01:02:10Z 10s metric emit_term tests count of test is [0, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n▁▃▄▅▆▇▃▄▅▆▇\n", str::from_utf8(buf.as_slice()).unwrap());
769    }
770}