tracing_glog/lib.rs
1//! `tracing-glog` is a [glog]-inspired formatter for [`tracing-subscriber`].
2//!
3//! `tracing-glog` is meant to be used with [`tracing-subscriber`]'s [`fmt::Subscriber`]
4//! and [`fmt::Layer`] to format events in a `glog`-inspired fashion. Similar to
5//! `tracing-subscriber`'s [`Full`] formatter, this formatter shows the span context before
6//! printing event data. Spans are displayed including their names and fields. The severity,
7//! time, PID, thread name, file, and line are also included.
8//!
9//! # Example Output
10//!
11//! <pre><font color="#26A269">I</font><font color="#8D8F8A">1201 01:13:04.724801 1025672 </font><b>main</b> [<b>yak_shave</b>] <b>examples/yak-shave.rs</b>:<b>34</b>] preparing to shave yaks, <b>number_of_yaks</b>: 3
12//! <font color="#26A269">I</font><font color="#8D8F8A">1201 01:13:04.724948 1025672 </font><b>main</b> [<b>yak_shave</b>] <b>examples/yak-shave.rs</b>:<b>75</b>] [<b>shaving_yaks</b>{<i><b>yaks</b></i>: 3}] shaving yaks
13//! <font color="#A2734C">W</font><font color="#8D8F8A">1201 01:13:04.725071 1025672 </font><b>main</b> [<b>yak_shave</b>] <b>examples/yak-shave.rs</b>:<b>56</b>] [<b>shaving_yaks</b>{<i><b>yaks</b></i>: 3}, <b>shave</b>{<i><b>yak</b></i>: 3}] could not locate yak
14//! <font color="#C01C28">E</font><font color="#8D8F8A">1201 01:13:04.725135 1025672 </font><b>main</b> [<b>yak_shave</b>] <b>examples/yak-shave.rs</b>:<b>85</b>] [<b>shaving_yaks</b>{<i><b>yaks</b></i>: 3}] failed to shave yak, <b>yak</b>: 3, <b>error</b>: out of cash
15//! <font color="#26A269">I</font><font color="#8D8F8A">1201 01:13:04.725195 1025672 </font><b>main</b> [<b>yak_shave</b>] <b>examples/yak-shave.rs</b>:<b>38</b>] yak shaving completed, <b>all_yaks_shaved</b>: false
16//! </pre>
17//!
18//! # Usage
19//!
20//! With [`fmt::Subscriber`]:
21//!
22//! ```
23//! use tracing_glog::{Glog, GlogFields};
24//!
25//! tracing_subscriber::fmt()
26//! .event_format(Glog::default())
27//! .fmt_fields(GlogFields::default())
28//! .init();
29//! ```
30//!
31//! With [`fmt::Layer`]:
32//!
33//! ```
34//! use tracing_subscriber::prelude::*;
35//! use tracing_subscriber::{fmt, Registry};
36//! use tracing_glog::{Glog, GlogFields};
37//!
38//! let fmt = fmt::Layer::default()
39//! .event_format(Glog::default())
40//! .fmt_fields(GlogFields::default());
41//!
42//! let subscriber = Registry::default().with(fmt);
43//! tracing::subscriber::set_global_default(subscriber).expect("Unable to set global subscriber");
44//! ```
45//!
46//! With [`UtcTime`]:
47//!
48//! ```
49//! use tracing_subscriber::prelude::*;
50//! use tracing_subscriber::{fmt, Registry};
51//! use tracing_glog::{Glog, GlogFields};
52//!
53//! let fmt = fmt::Layer::default()
54//! .event_format(Glog::default().with_timer(tracing_glog::UtcTime::default()))
55//! .fmt_fields(GlogFields::default());
56//!
57//! let subscriber = Registry::default().with(fmt);
58//! tracing::subscriber::set_global_default(subscriber).expect("Unable to set global subscriber");
59//! ```
60//!
61//! With [`LocalTime`]:
62//!
63//! ```
64//! use tracing_subscriber::prelude::*;
65//! use tracing_subscriber::{fmt, Registry};
66//! use tracing_glog::{Glog, GlogFields};
67//!
68//! let fmt = fmt::Layer::default()
69//! .event_format(Glog::default().with_timer(tracing_glog::LocalTime::default()))
70//! .fmt_fields(GlogFields::default());
71//!
72//! let subscriber = Registry::default().with(fmt);
73//! tracing::subscriber::set_global_default(subscriber).expect("Unable to set global subscriber");
74//! ```
75//!
76//! <div class="example-wrap" style="display:inline-block">
77//! <pre class="compile_fail" style="white-space:normal;font:inherit;">
78//! <strong>Warning</strong>: The <a href = "https://docs.rs/time/0.3/time/"><code>time</code>
79//! crate</a> must be compiled with <code>--cfg unsound_local_offset</code> in order to use
80//! `LocalTime`. When this cfg is not enabled, local timestamps cannot be recorded, and
81//! no events will be emitted.
82//!
83//! See the <a href="https://docs.rs/time/0.3.9/time/#feature-flags"><code>time</code>
84//! documentation</a> for more details.
85//! </pre></div>
86//!
87//! [glog]: https://github.com/google/glog
88//! [`tracing-subscriber`]: https://docs.rs/tracing-subscriber
89//! [`fmt::Subscriber`]: tracing_subscriber::fmt::Subscriber
90//! [`fmt::Layer`]: tracing_subscriber::fmt::Layer
91//! [`Full`]: tracing_subscriber::fmt::format::Full
92
93#[deny(rustdoc::broken_intra_doc_links)]
94mod format;
95
96#[cfg(feature = "time")]
97pub mod time_crate;
98
99#[cfg(feature = "ansi")]
100mod nu_ansi_term {
101 pub use ::nu_ansi_term::*;
102}
103
104#[cfg(not(feature = "ansi"))]
105mod nu_ansi_term {
106 // Minimal API shim for nu_ansi_term to avoid a pile of #[cfg(feature = "ansi")] directives.
107 #[derive(Copy, Clone)]
108 pub struct Style;
109
110 impl Style {
111 pub fn new() -> Self {
112 Style
113 }
114 pub fn bold(&self) -> Self {
115 Style
116 }
117 pub fn prefix(&self) -> &'static str {
118 ""
119 }
120 pub fn infix(&self, _: Style) -> &'static str {
121 ""
122 }
123 pub fn suffix(&self) -> &'static str {
124 ""
125 }
126 }
127}
128
129use crate::nu_ansi_term::Style;
130use format::FmtLevel;
131pub use format::{FormatLevelChars, LocalTime, UtcTime};
132use std::fmt;
133use tracing::{
134 field::{Field, Visit},
135 Subscriber,
136};
137#[cfg(feature = "tracing-log")]
138use tracing_log::NormalizeEvent;
139use tracing_subscriber::{
140 field::{MakeVisitor, VisitFmt, VisitOutput},
141 fmt::{
142 format::Writer, time::FormatTime, FmtContext, FormatEvent, FormatFields, FormattedFields,
143 },
144 registry::LookupSpan,
145};
146
147use crate::format::{FormatProcessData, FormatSpanFields};
148
149/// A [glog]-inspired span and event formatter.
150///
151/// [glog]: https://github.com/google/glog
152pub struct Glog<T = UtcTime> {
153 timer: T,
154 level_chars: &'static FormatLevelChars,
155 with_span_context: bool,
156 with_thread_names: bool,
157 with_target: bool,
158 with_span_names: bool,
159}
160
161impl<T> Glog<T> {
162 /// Use the given [timer] for span and event time stamps.
163 ///
164 /// `tracing-glog` provides two timers: [`LocalTime`] and [`UtcTime`].
165 /// [`UtcTime`] is the default timer.
166 ///
167 /// [timer]: tracing_subscriber::fmt::time::FormatTime
168 pub fn with_timer<T2>(self, timer: T2) -> Glog<T2>
169 where
170 T2: FormatTime,
171 {
172 Glog {
173 timer,
174 level_chars: self.level_chars,
175 with_thread_names: self.with_thread_names,
176 with_target: self.with_target,
177 with_span_context: self.with_span_context,
178 with_span_names: self.with_span_names,
179 }
180 }
181
182 pub fn with_thread_names(self, with_thread_names: bool) -> Glog<T> {
183 Glog {
184 with_thread_names,
185 ..self
186 }
187 }
188
189 pub fn with_target(self, with_target: bool) -> Glog<T> {
190 Glog {
191 with_target,
192 ..self
193 }
194 }
195
196 /// Sets whether or not the span name is included. Defaults to true.
197 ///
198 /// If span names are not included, then the fields from all spans are
199 /// printed as a single list of fields. This results is a more compact output.
200 ///
201 /// # Example Output
202 /// With `with_span_names` set to true:
203 /// <pre>
204 /// I0731 16:23:45.674465 990039 examples/tokio.rs:38] [parent_task{subtasks: 10, reason: "testing"}, subtask{number: 10}] polling subtask, number: 10
205 /// </pre>
206 ///
207 /// With `with_span_names` set to false:
208 /// <pre>
209 /// I0731 16:23:45.674465 990039 examples/tokio.rs:38] [subtasks: 10, reason: "testing", number: 10] polling subtask, number: 10
210 /// </pre>
211 pub fn with_span_names(self, with_span_names: bool) -> Glog<T> {
212 Glog {
213 with_span_names,
214 ..self
215 }
216 }
217
218 /// Sets whether or not the span context is included. Defaults to true.
219 ///
220 /// By default, formatters building atop of [`mod@tracing_subscriber::fmt`]
221 /// will include the span context as [`fmt::Layer`] and
222 /// [`fmt::Subscriber`] assume that span context is
223 /// is valuable and the _raison d’être_ for using `tracing` and, as such, do not provide a
224 /// toggle. However, users migrating from logging systems such
225 /// as [glog](https://github.com/google/glog) or folly's [`xlog`](https://github.com/facebook/folly/blob/main/folly/logging/xlog.h)
226 /// might find the span context to be overwhelming. Therefore, this formatter-level toggle
227 /// is availible in order to provide a smoother onboarding experience to [`tracing`].
228 ///
229 /// **Notice:** This is a relatively coarse toggle. In most circumstances, usage of
230 /// `tracing-subscriber`'s [`filter_fn`] is preferred to disable spans on a more
231 /// fine-grained basis.
232 ///
233 /// [`fmt::Layer`]: tracing_subscriber::fmt::Layer
234 /// [`fmt::Subscriber`]: tracing_subscriber::fmt::Subscriber
235 /// [per-layer filtering]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#per-layer-filtering
236 /// [`filter_fn`]: fn@tracing_subscriber::filter::filter_fn
237 /// [`tracing`]: mod@tracing
238 pub fn with_span_context(self, with_span_context: bool) -> Glog<T> {
239 Glog {
240 with_span_context,
241 ..self
242 }
243 }
244
245 /// Sets the characters to use to indicate the level for each event.
246 /// Defaults to the initial character of the level.
247 pub fn with_format_level_chars(self, level_chars: &'static FormatLevelChars) -> Glog<T> {
248 Glog {
249 level_chars,
250 ..self
251 }
252 }
253}
254
255impl Default for Glog<UtcTime> {
256 fn default() -> Self {
257 Glog {
258 timer: UtcTime::default(),
259 level_chars: &format::DEFAULT_FORMAT_LEVEL_CHARS,
260 with_thread_names: false,
261 with_target: false,
262 with_span_context: true,
263 with_span_names: true,
264 }
265 }
266}
267
268impl<S, N, T> FormatEvent<S, N> for Glog<T>
269where
270 S: Subscriber + for<'a> LookupSpan<'a>,
271 N: for<'a> FormatFields<'a> + 'static,
272 T: FormatTime,
273{
274 fn format_event(
275 &self,
276 ctx: &FmtContext<'_, S, N>,
277 mut writer: Writer<'_>,
278 event: &tracing::Event<'_>,
279 ) -> fmt::Result {
280 let level = *event.metadata().level();
281
282 // Convert log level to a single character representation.)
283 let level = FmtLevel::format_level(level, self.level_chars, writer.has_ansi_escapes());
284 write!(writer, "{level}")?;
285
286 // write the timestamp:
287 self.timer.format_time(&mut writer)?;
288
289 // get some process information
290 let pid = get_pid();
291 let thread = std::thread::current();
292 let thread_name = thread.name();
293
294 #[cfg(feature = "tracing-log")]
295 let normalized_meta = event.normalized_metadata();
296 #[cfg(feature = "tracing-log")]
297 let metadata = normalized_meta.as_ref().unwrap_or_else(|| event.metadata());
298 #[cfg(not(feature = "tracing-log"))]
299 let metadata = event.metadata();
300
301 let data = FormatProcessData {
302 pid,
303 thread_name,
304 with_thread_names: self.with_thread_names,
305 metadata,
306 with_target: self.with_target,
307 #[cfg(feature = "ansi")]
308 ansi: writer.has_ansi_escapes(),
309 };
310 write!(writer, "{data}] ")?;
311
312 if self.with_span_context {
313 // now, we're printing the span context into brackets of `[]`, which glog parsers ignore.
314 let leaf = ctx.lookup_current();
315
316 if let Some(leaf) = leaf {
317 let mut wrote_open_bracket = false;
318
319 // Write spans and fields of each span
320 let mut iter = leaf.scope().from_root();
321 let mut span = iter.next().expect(
322 "Unable to get the next item in the iterator; this should not be possible.",
323 );
324 loop {
325 let ext = span.extensions();
326 let fields = &ext
327 .get::<FormattedFields<N>>()
328 .expect("will never be `None`");
329
330 let fields = if !fields.is_empty() {
331 Some(fields.as_str())
332 } else {
333 None
334 };
335
336 if self.with_span_names || fields.is_some() {
337 if !wrote_open_bracket {
338 // Write the opening bracket once we know we need one
339 write!(writer, "[")?;
340 wrote_open_bracket = true;
341 }
342 let fields = FormatSpanFields::format_fields(
343 span.name(),
344 fields,
345 writer.has_ansi_escapes(),
346 self.with_span_names,
347 );
348 write!(writer, "{fields}")?;
349 }
350
351 drop(ext);
352 match iter.next() {
353 // if there's more, add a space.
354 Some(next) => {
355 if wrote_open_bracket {
356 write!(writer, ", ")?;
357 }
358 span = next;
359 }
360 // if there's nothing there, close.
361 None => break,
362 }
363 }
364 if wrote_open_bracket {
365 write!(writer, "] ")?;
366 }
367 }
368 }
369 ctx.field_format().format_fields(writer.by_ref(), event)?;
370 writeln!(writer)
371 }
372}
373
374#[derive(Clone)]
375struct FieldConfig {
376 should_quote_strings: bool,
377 use_whitespace_in_field: bool,
378}
379
380impl Default for FieldConfig {
381 fn default() -> Self {
382 Self {
383 should_quote_strings: true,
384 use_whitespace_in_field: true,
385 }
386 }
387}
388
389#[derive(Default)]
390pub struct GlogFields {
391 config: FieldConfig,
392}
393
394impl GlogFields {
395 /// Sets whether or not strings are wrapped in quotes.
396 ///
397 /// This is helpful for reducing line width at the cost of clarity when
398 /// using strings with whitespace as fields on Spans and Events.
399 pub fn should_quote_strings(mut self, value: bool) -> Self {
400 self.config.should_quote_strings = value;
401 self
402 }
403
404 /// Sets whether or not whitespace is added to printed fields.
405 ///
406 /// This defaults to `false`.
407 pub fn use_whitespace_in_field(mut self, value: bool) -> Self {
408 self.config.use_whitespace_in_field = value;
409 self
410 }
411
412 /// Sets the formatter to use compact options.
413 ///
414 /// Setting `.compat()` will set [`GlogFields::use_whitespace_in_field`]
415 /// and [`GlogFields::should_quote_strings`] to false.
416 pub fn compact(self) -> Self {
417 self.should_quote_strings(false)
418 .use_whitespace_in_field(false)
419 }
420}
421
422impl<'a> MakeVisitor<Writer<'a>> for GlogFields {
423 type Visitor = GlogVisitor<'a>;
424
425 #[inline]
426 fn make_visitor(&self, target: Writer<'a>) -> Self::Visitor {
427 GlogVisitor::new(target, self.config.clone())
428 }
429}
430
431#[doc(hidden)]
432pub struct GlogVisitor<'a> {
433 writer: Writer<'a>,
434 is_empty: bool,
435 style: Style,
436 result: fmt::Result,
437 config: FieldConfig,
438}
439
440impl<'a> GlogVisitor<'a> {
441 fn new(writer: Writer<'a>, config: FieldConfig) -> Self {
442 Self {
443 writer,
444 is_empty: true,
445 style: Style::new(),
446 result: Ok(()),
447 config,
448 }
449 }
450
451 fn write_padded(&mut self, value: &impl fmt::Debug) {
452 let padding = if self.is_empty {
453 self.is_empty = false;
454 ""
455 } else {
456 ", "
457 };
458 self.result = write!(self.writer, "{padding}{value:?}");
459 }
460
461 fn write_field(&mut self, name: &str, value: &dyn fmt::Debug) {
462 let bold = self.bold();
463 if self.config.use_whitespace_in_field {
464 self.write_padded(&format_args!(
465 "{}{}{}: {:?}",
466 bold.prefix(),
467 name,
468 bold.infix(self.style),
469 value,
470 ));
471 } else {
472 self.write_padded(&format_args!(
473 "{}{}{}:{:?}",
474 bold.prefix(),
475 name,
476 bold.infix(self.style),
477 value,
478 ));
479 }
480 }
481
482 fn bold(&self) -> Style {
483 if self.writer.has_ansi_escapes() {
484 self.style.bold()
485 } else {
486 Style::new()
487 }
488 }
489}
490
491impl<'a> Visit for GlogVisitor<'a> {
492 fn record_str(&mut self, field: &Field, value: &str) {
493 if self.result.is_err() {
494 return;
495 }
496
497 if field.name() == "message" {
498 self.record_debug(field, &format_args!("{value}"))
499 } else if self.config.should_quote_strings {
500 self.record_debug(field, &value)
501 } else {
502 self.record_debug(field, &format_args!("{value}"))
503 }
504 }
505
506 fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
507 if let Some(source) = value.source() {
508 self.record_debug(
509 field,
510 &format_args!("{}, {}.sources: {}", value, field, ErrorSourceList(source),),
511 )
512 } else {
513 self.record_debug(field, &format_args!("{value}"))
514 }
515 }
516
517 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
518 if self.result.is_err() {
519 return;
520 }
521
522 match field.name() {
523 "message" => self.write_padded(&format_args!("{}{:?}", self.style.prefix(), value,)),
524 // Skip fields that are actually log metadata that have already been handled
525 name if name.starts_with("log.") => self.result = Ok(()),
526 name if name.starts_with("r#") => self.write_field(&name[2..], value),
527 name => self.write_field(name, value),
528 };
529 }
530}
531
532impl<'a> VisitOutput<fmt::Result> for GlogVisitor<'a> {
533 fn finish(mut self) -> fmt::Result {
534 write!(&mut self.writer, "{}", self.style.suffix())?;
535 self.result
536 }
537}
538
539impl<'a> VisitFmt for GlogVisitor<'a> {
540 fn writer(&mut self) -> &mut dyn fmt::Write {
541 &mut self.writer
542 }
543}
544
545/// Renders an error into a list of sources, *including* the error
546struct ErrorSourceList<'a>(&'a (dyn std::error::Error + 'static));
547
548impl<'a> fmt::Display for ErrorSourceList<'a> {
549 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
550 let mut list = f.debug_list();
551 let mut curr = Some(self.0);
552 while let Some(curr_err) = curr {
553 list.entry(&format_args!("{curr_err}"));
554 curr = curr_err.source();
555 }
556 list.finish()
557 }
558}
559
560#[inline(always)]
561fn get_pid() -> u32 {
562 std::process::id()
563}