Skip to content

Commit

Permalink
subscriber: support NO_COLOR in fmt::Subscriber (#2647)
Browse files Browse the repository at this point in the history
## Motivation

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

## Solution

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
  • Loading branch information
dmlary authored Aug 17, 2023
1 parent 6c530d5 commit 81ab9d6
Showing 1 changed file with 87 additions and 2 deletions.
89 changes: 87 additions & 2 deletions tracing-subscriber/src/fmt/fmt_subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
};
use format::{FmtSpan, TimingDisplay};
use std::{
any::TypeId, cell::RefCell, fmt, io, marker::PhantomData, ops::Deref, ptr::NonNull,
any::TypeId, cell::RefCell, env, fmt, io, marker::PhantomData, ops::Deref, ptr::NonNull,
time::Instant,
};
use tracing_core::{
Expand Down Expand Up @@ -277,6 +277,15 @@ impl<C, N, E, W> Subscriber<C, N, E, W> {
/// 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
Expand All @@ -289,6 +298,9 @@ impl<C, N, E, W> Subscriber<C, N, E, W> {
/// 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 {
Expand Down Expand Up @@ -680,12 +692,16 @@ impl<C, N, E, W> Subscriber<C, N, E, W> {

impl<C> Default for Subscriber<C> {
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());

Subscriber {
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,
}
Expand Down Expand Up @@ -1505,4 +1521,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<String, env::VarError>);
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`.
}
}

0 comments on commit 81ab9d6

Please sign in to comment.