From d66f129536ddee2485175c60e19a582b4864815d Mon Sep 17 00:00:00 2001 From: "David M. Lary" Date: Thu, 17 Aug 2023 11:56:29 -0500 Subject: [PATCH] subscriber: support `NO_COLOR` in `fmt::Subscriber` (#2647) It's necessary at times to be able to disable ANSI color output for rust utilities using `tracing`. The informal standard for this is the `NO_COLOR` environment variable described here: https://no-color.org/ Further details/discussion in #2388 This commit updates `fmt::Subscriber` to check the `NO_COLOR` environment variable when determining whether ANSI color output is enabled by default. As described in the spec, any non-empty value set for `NO_COLOR` will cause ANSI color support to be disabled by default. If the user manually overrides ANSI color support, such as by calling `with_ansi(true)`, this will still enable ANSI colors, even if `NO_COLOR` is set. The `NO_COLOR` env var only effects the default behavior. Fixes #2388 --- tracing-subscriber/src/fmt/fmt_layer.rs | 108 ++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 6 deletions(-) diff --git a/tracing-subscriber/src/fmt/fmt_layer.rs b/tracing-subscriber/src/fmt/fmt_layer.rs index e2f7cc8f98..959a4a3fc3 100644 --- a/tracing-subscriber/src/fmt/fmt_layer.rs +++ b/tracing-subscriber/src/fmt/fmt_layer.rs @@ -5,7 +5,9 @@ use crate::{ registry::{self, LookupSpan, SpanRef}, }; use format::{FmtSpan, TimingDisplay}; -use std::{any::TypeId, cell::RefCell, fmt, io, marker::PhantomData, ops::Deref, time::Instant}; +use std::{ + any::TypeId, cell::RefCell, env, fmt, io, marker::PhantomData, ops::Deref, time::Instant, +}; use tracing_core::{ field, span::{Attributes, Current, Id, Record}, @@ -276,6 +278,15 @@ impl Layer { /// Sets whether or not the formatter emits ANSI terminal escape codes /// for colors and other text formatting. /// + /// When the "ansi" crate feature flag is enabled, ANSI colors are enabled + /// by default unless the [`NO_COLOR`] environment variable is set to + /// a non-empty value. If the [`NO_COLOR`] environment variable is set to + /// any non-empty value, then ANSI colors will be suppressed by default. + /// The [`with_ansi`] and [`set_ansi`] methods can be used to forcibly + /// enable ANSI colors, overriding any [`NO_COLOR`] environment variable. + /// + /// [`NO_COLOR`]: https://no-color.org/ + /// /// Enabling ANSI escapes (calling `with_ansi(true)`) requires the "ansi" /// crate feature flag. Calling `with_ansi(true)` without the "ansi" /// feature flag enabled will panic if debug assertions are enabled, or @@ -288,6 +299,9 @@ impl Layer { /// ANSI escape codes can ensure that they are not used, regardless of /// whether or not other crates in the dependency graph enable the "ansi" /// feature flag. + /// + /// [`with_ansi`]: Subscriber::with_ansi + /// [`set_ansi`]: Subscriber::set_ansi pub fn with_ansi(self, ansi: bool) -> Self { #[cfg(not(feature = "ansi"))] if ansi { @@ -311,10 +325,10 @@ impl Layer { /// By default, `fmt::Layer` will write any `FormatEvent`-internal errors to /// the writer. These errors are unlikely and will only occur if there is a /// bug in the `FormatEvent` implementation or its dependencies. - /// + /// /// If writing to the writer fails, the error message is printed to stderr /// as a fallback. - /// + /// /// [`FormatEvent`]: crate::fmt::FormatEvent pub fn log_internal_errors(self, log_internal_errors: bool) -> Self { Self { @@ -677,12 +691,16 @@ impl Layer { impl Default for Layer { fn default() -> Self { + // only enable ANSI when the feature is enabled, and the NO_COLOR + // environment variable is unset or empty. + let ansi = cfg!(feature = "ansi") && env::var("NO_COLOR").map_or(true, |v| v.is_empty()); + Layer { fmt_fields: format::DefaultFields::default(), fmt_event: format::Format::default(), fmt_span: format::FmtSpanConfig::default(), make_writer: io::stdout, - is_ansi: cfg!(feature = "ansi"), + is_ansi: ansi, log_internal_errors: false, _inner: PhantomData, } @@ -1288,8 +1306,17 @@ mod test { let actual = sanitize_timings(make_writer.get_string()); // Only assert the start because the line number and callsite may change. - let expected = concat!("Unable to format the following event. Name: event ", file!(), ":"); - assert!(actual.as_str().starts_with(expected), "\nactual = {}\nshould start with expected = {}\n", actual, expected); + let expected = concat!( + "Unable to format the following event. Name: event ", + file!(), + ":" + ); + assert!( + actual.as_str().starts_with(expected), + "\nactual = {}\nshould start with expected = {}\n", + actual, + expected + ); } #[test] @@ -1491,4 +1518,73 @@ mod test { actual.as_str() ); } + + // Because we need to modify an environment variable for these test cases, + // we do them all in a single test. + #[cfg(feature = "ansi")] + #[test] + fn subscriber_no_color() { + const NO_COLOR: &str = "NO_COLOR"; + + // Restores the previous value of the `NO_COLOR` env variable when + // dropped. + // + // This is done in a `Drop` implementation, rather than just resetting + // the value at the end of the test, so that the previous value is + // restored even if the test panics. + struct RestoreEnvVar(Result); + impl Drop for RestoreEnvVar { + fn drop(&mut self) { + match self.0 { + Ok(ref var) => env::set_var(NO_COLOR, var), + Err(_) => env::remove_var(NO_COLOR), + } + } + } + + let _saved_no_color = RestoreEnvVar(env::var(NO_COLOR)); + + let cases: Vec<(Option<&str>, bool)> = vec![ + (Some("0"), false), // any non-empty value disables ansi + (Some("off"), false), // any non-empty value disables ansi + (Some("1"), false), + (Some(""), true), // empty value does not disable ansi + (None, true), + ]; + + for (var, ansi) in cases { + if let Some(value) = var { + env::set_var(NO_COLOR, value); + } else { + env::remove_var(NO_COLOR); + } + + let subscriber: Subscriber<()> = fmt::Subscriber::default(); + assert_eq!( + subscriber.is_ansi, ansi, + "NO_COLOR={:?}; Subscriber::default().is_ansi should be {}", + var, ansi + ); + + // with_ansi should override any `NO_COLOR` value + let subscriber: Subscriber<()> = fmt::Subscriber::default().with_ansi(true); + assert!( + subscriber.is_ansi, + "NO_COLOR={:?}; Subscriber::default().with_ansi(true).is_ansi should be true", + var + ); + + // set_ansi should override any `NO_COLOR` value + let mut subscriber: Subscriber<()> = fmt::Subscriber::default(); + subscriber.set_ansi(true); + assert!( + subscriber.is_ansi, + "NO_COLOR={:?}; subscriber.set_ansi(true); subscriber.is_ansi should be true", + var + ); + } + + // dropping `_saved_no_color` will restore the previous value of + // `NO_COLOR`. + } }