diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0edfde0d7b..e41f3e4e56 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -269,10 +269,10 @@ jobs: - tracing steps: - uses: actions/checkout@v4 - - name: Install Rust 1.81 + - name: Install Rust 1.85 uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.81 + toolchain: 1.85 target: wasm32-unknown-unknown - name: install test runner for wasm uses: taiki-e/install-action@wasm-pack diff --git a/tracing-attributes/tests/ui/fail/async_instrument.stderr b/tracing-attributes/tests/ui/fail/async_instrument.stderr index fd10a91888..f10ec273eb 100644 --- a/tracing-attributes/tests/ui/fail/async_instrument.stderr +++ b/tracing-attributes/tests/ui/fail/async_instrument.stderr @@ -30,7 +30,7 @@ error[E0277]: `(&str,)` doesn't implement `std::fmt::Display` --> tests/ui/fail/async_instrument.rs:14:57 | 14 | async fn opaque_unsatisfied() -> impl std::fmt::Display { - | _________________________________________________________- + | _________________________________________________________^ 15 | | ("",) 16 | | } | | ^ diff --git a/tracing-subscriber/src/filter/env/field.rs b/tracing-subscriber/src/filter/env/field.rs index 45348bc41d..27ff409e18 100644 --- a/tracing-subscriber/src/filter/env/field.rs +++ b/tracing-subscriber/src/filter/env/field.rs @@ -169,6 +169,7 @@ impl Match { })? // TODO: validate field name .to_string(); + #[allow(clippy::result_large_err)] let value = parts .next() .map(|part| match regex { diff --git a/tracing-subscriber/src/fmt/fmt_layer.rs b/tracing-subscriber/src/fmt/fmt_layer.rs index d0233810e9..16034a5096 100644 --- a/tracing-subscriber/src/fmt/fmt_layer.rs +++ b/tracing-subscriber/src/fmt/fmt_layer.rs @@ -72,6 +72,7 @@ pub struct Layer< fmt_event: E, fmt_span: format::FmtSpanConfig, is_ansi: bool, + ansi_sanitization: bool, log_internal_errors: bool, _inner: PhantomData, } @@ -122,6 +123,7 @@ where fmt_span: self.fmt_span, make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -152,6 +154,7 @@ where fmt_span: self.fmt_span, make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -185,6 +188,7 @@ impl Layer { fmt_event: self.fmt_event, fmt_span: self.fmt_span, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, make_writer, _inner: self._inner, @@ -290,6 +294,7 @@ impl Layer { fmt_event: self.fmt_event, fmt_span: self.fmt_span, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, make_writer: TestWriter::default(), _inner: self._inner, @@ -340,6 +345,19 @@ impl Layer { } } + /// Sets whether ANSI control character sanitization is enabled. + /// + /// This defaults to `true` as a protective measure against terminal + /// injection attacks. If this is set to `false`, ANSI sanitization is + /// disabled and trusted ANSI control sequences in logged values are passed + /// through unchanged. + pub fn with_ansi_sanitization(self, ansi_sanitization: bool) -> Self { + Self { + ansi_sanitization, + ..self + } + } + /// Sets whether to write errors from [`FormatEvent`] to the writer. /// Defaults to true. /// @@ -386,6 +404,7 @@ impl Layer { fmt_event: self.fmt_event, fmt_span: self.fmt_span, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, make_writer: f(self.make_writer), _inner: self._inner, @@ -418,6 +437,7 @@ where fmt_span: self.fmt_span, make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -431,6 +451,7 @@ where fmt_span: self.fmt_span.without_time(), make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -560,6 +581,7 @@ where fmt_span: self.fmt_span, make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -575,6 +597,7 @@ where fmt_span: self.fmt_span, make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -606,6 +629,7 @@ where make_writer: self.make_writer, // always disable ANSI escapes in JSON mode! is_ansi: false, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -673,6 +697,7 @@ impl Layer { fmt_span: self.fmt_span, make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -704,6 +729,7 @@ impl Layer { fmt_span: self.fmt_span, make_writer: self.make_writer, is_ansi: self.is_ansi, + ansi_sanitization: self.ansi_sanitization, log_internal_errors: self.log_internal_errors, _inner: self._inner, } @@ -722,6 +748,7 @@ impl Default for Layer { fmt_span: format::FmtSpanConfig::default(), make_writer: io::stdout, is_ansi: ansi, + ansi_sanitization: true, log_internal_errors: false, _inner: PhantomData, } @@ -754,20 +781,32 @@ where /// without conflicting. /// /// [extensions]: crate::registry::Extensions -#[derive(Default)] pub struct FormattedFields { _format_fields: PhantomData, was_ansi: bool, + was_ansi_sanitized: bool, /// The formatted fields of a span. pub fields: String, } +impl Default for FormattedFields { + fn default() -> Self { + Self { + _format_fields: Default::default(), + was_ansi: Default::default(), + was_ansi_sanitized: true, + fields: Default::default(), + } + } +} + impl FormattedFields { /// Returns a new `FormattedFields`. pub fn new(fields: String) -> Self { Self { fields, was_ansi: false, + was_ansi_sanitized: true, _format_fields: PhantomData, } } @@ -777,7 +816,9 @@ impl FormattedFields { /// The returned [`format::Writer`] can be used with the /// [`FormatFields::format_fields`] method. pub fn as_writer(&mut self) -> format::Writer<'_> { - format::Writer::new(&mut self.fields).with_ansi(self.was_ansi) + format::Writer::new(&mut self.fields) + .with_ansi(self.was_ansi) + .with_ansi_sanitization(self.was_ansi_sanitized) } } @@ -787,6 +828,7 @@ impl fmt::Debug for FormattedFields { .field("fields", &self.fields) .field("formatter", &format_args!("{}", std::any::type_name::())) .field("was_ansi", &self.was_ansi) + .field("was_ansi_sanitized", &self.was_ansi_sanitized) .finish() } } @@ -835,12 +877,13 @@ where if extensions.get_mut::>().is_none() { let mut fields = FormattedFields::::new(String::new()); + fields.was_ansi = self.is_ansi; + fields.was_ansi_sanitized = self.ansi_sanitization; if self .fmt_fields - .format_fields(fields.as_writer().with_ansi(self.is_ansi), attrs) + .format_fields(fields.as_writer(), attrs) .is_ok() { - fields.was_ansi = self.is_ansi; extensions.insert(fields); } else { eprintln!( @@ -875,12 +918,13 @@ where } let mut fields = FormattedFields::::new(String::new()); + fields.was_ansi = self.is_ansi; + fields.was_ansi_sanitized = self.ansi_sanitization; if self .fmt_fields - .format_fields(fields.as_writer().with_ansi(self.is_ansi), values) + .format_fields(fields.as_writer(), values) .is_ok() { - fields.was_ansi = self.is_ansi; extensions.insert(fields); } } @@ -995,7 +1039,9 @@ where .fmt_event .format_event( &ctx, - format::Writer::new(&mut buf).with_ansi(self.is_ansi), + format::Writer::new(&mut buf) + .with_ansi(self.is_ansi) + .with_ansi_sanitization(self.ansi_sanitization), event, ) .is_ok() diff --git a/tracing-subscriber/src/fmt/format/escape.rs b/tracing-subscriber/src/fmt/format/escape.rs index 00837b04b3..3ae6d7c498 100644 --- a/tracing-subscriber/src/fmt/format/escape.rs +++ b/tracing-subscriber/src/fmt/format/escape.rs @@ -2,9 +2,17 @@ use std::fmt::{self, Write}; -/// A wrapper that implements `fmt::Debug` and `fmt::Display` and escapes ANSI sequences on-the-fly. -/// This avoids creating intermediate strings while providing security against terminal injection. -pub(super) struct Escape(pub(super) T); +/// A wrapper that conditionally escapes ANSI sequences when formatted. +pub(super) struct EscapeGuard { + pub(super) value: T, + pub(super) sanitize: bool, +} + +impl EscapeGuard { + pub(super) fn new(value: T, sanitize: bool) -> Self { + Self { value, sanitize } + } +} /// Helper struct that escapes ANSI sequences as characters are written struct EscapingWriter<'a, 'b> { @@ -17,18 +25,18 @@ impl<'a, 'b> fmt::Write for EscapingWriter<'a, 'b> { for ch in s.chars() { match ch { // C0 control characters that can be used in terminal escape sequences - '\x1b' => self.inner.write_str("\\x1b")?, // ESC - '\x07' => self.inner.write_str("\\x07")?, // BEL - '\x08' => self.inner.write_str("\\x08")?, // BS - '\x0c' => self.inner.write_str("\\x0c")?, // FF - '\x7f' => self.inner.write_str("\\x7f")?, // DEL - + '\x1b' => self.inner.write_str("\\x1b")?, // ESC + '\x07' => self.inner.write_str("\\x07")?, // BEL + '\x08' => self.inner.write_str("\\x08")?, // BS + '\x0c' => self.inner.write_str("\\x0c")?, // FF + '\x7f' => self.inner.write_str("\\x7f")?, // DEL + // C1 control characters (\x80-\x9f) - 8-bit control codes // These can be used as alternative escape sequences in some terminals ch if ch as u32 >= 0x80 && ch as u32 <= 0x9f => { write!(self.inner, "\\u{{{:x}}}", ch as u32)? - }, - + } + _ => self.inner.write_char(ch)?, } } @@ -36,16 +44,24 @@ impl<'a, 'b> fmt::Write for EscapingWriter<'a, 'b> { } } -impl fmt::Debug for Escape { +impl fmt::Debug for EscapeGuard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut escaping_writer = EscapingWriter { inner: f }; - write!(escaping_writer, "{:?}", self.0) + if self.sanitize { + let mut escaping_writer = EscapingWriter { inner: f }; + write!(escaping_writer, "{:?}", self.value) + } else { + write!(f, "{:?}", self.value) + } } } -impl fmt::Display for Escape { +impl fmt::Display for EscapeGuard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut escaping_writer = EscapingWriter { inner: f }; - write!(escaping_writer, "{}", self.0) + if self.sanitize { + let mut escaping_writer = EscapingWriter { inner: f }; + write!(escaping_writer, "{}", self.value) + } else { + write!(f, "{}", self.value) + } } -} \ No newline at end of file +} diff --git a/tracing-subscriber/src/fmt/format/mod.rs b/tracing-subscriber/src/fmt/format/mod.rs index bb7ff90df8..66ea74a3fe 100644 --- a/tracing-subscriber/src/fmt/format/mod.rs +++ b/tracing-subscriber/src/fmt/format/mod.rs @@ -49,7 +49,7 @@ use tracing_log::NormalizeEvent; use nu_ansi_term::{Color, Style}; mod escape; -use escape::Escape; +use escape::EscapeGuard; #[cfg(feature = "json")] mod json; @@ -310,6 +310,7 @@ pub struct Writer<'writer> { writer: &'writer mut dyn fmt::Write, // TODO(eliza): add ANSI support is_ansi: bool, + ansi_sanitization: bool, } /// A [`FormatFields`] implementation that formats fields by calling a function @@ -400,7 +401,7 @@ pub struct Full; /// span context, but other information is abbreviated. The [`Pretty`] logging /// format is an extra-verbose, multi-line human-readable logging format /// intended for use in development. -/// +/// /// [`FmtSubscriber`]: super::Subscriber #[derive(Debug, Clone)] pub struct Format { @@ -441,6 +442,7 @@ impl<'writer> Writer<'writer> { Self { writer: writer as &mut dyn fmt::Write, is_ansi: false, + ansi_sanitization: true, } } @@ -449,6 +451,13 @@ impl<'writer> Writer<'writer> { Self { is_ansi, ..self } } + pub(crate) fn with_ansi_sanitization(self, ansi_sanitization: bool) -> Self { + Self { + ansi_sanitization, + ..self + } + } + /// Return a new [`Writer`] that mutably borrows [`self`]. /// /// This can be used to temporarily borrow a [`Writer`] to pass a new [`Writer`] @@ -456,9 +465,11 @@ impl<'writer> Writer<'writer> { /// to still be used once that function returns. pub fn by_ref(&mut self) -> Writer<'_> { let is_ansi = self.is_ansi; + let ansi_sanitization = self.ansi_sanitization; Writer { writer: self as &mut dyn fmt::Write, is_ansi, + ansi_sanitization, } } @@ -531,6 +542,11 @@ impl<'writer> Writer<'writer> { self.is_ansi } + /// Returns `true` if ANSI escape codes should be sanitized. + pub fn sanitizes_ansi_escapes(&self) -> bool { + self.ansi_sanitization + } + pub(in crate::fmt::format) fn bold(&self) -> Style { #[cfg(feature = "ansi")] { @@ -587,6 +603,7 @@ impl fmt::Debug for Writer<'_> { f.debug_struct("Writer") .field("writer", &format_args!("<&mut dyn fmt::Write>")) .field("is_ansi", &self.is_ansi) + .field("ansi_sanitization", &self.ansi_sanitization) .finish() } } @@ -1257,23 +1274,24 @@ impl field::Visit for DefaultVisitor<'_> { } fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) { + let sanitize = self.writer.sanitizes_ansi_escapes(); if let Some(source) = value.source() { let italic = self.writer.italic(); self.record_debug( field, &format_args!( "{} {}{}{}{}", - Escape(&format_args!("{}", value)), + EscapeGuard::new(format_args!("{}", value), sanitize), italic.paint(field.name()), italic.paint(".sources"), self.writer.dimmed().paint("="), - ErrorSourceList(source) + ErrorSourceList::new(source, sanitize) ), ) } else { self.record_debug( field, - &format_args!("{}", Escape(&format_args!("{}", value))), + &format_args!("{}", EscapeGuard::new(format_args!("{}", value), sanitize)), ) } } @@ -1298,7 +1316,11 @@ impl field::Visit for DefaultVisitor<'_> { self.result = match name { "message" => { // Escape ANSI characters to prevent malicious patterns (e.g., terminal injection attacks) - write!(self.writer, "{:?}", Escape(value)) + write!( + self.writer, + "{:?}", + EscapeGuard::new(value, self.writer.sanitizes_ansi_escapes()) + ) } name if name.starts_with("r#") => write!( self.writer, @@ -1331,14 +1353,29 @@ impl crate::field::VisitFmt for DefaultVisitor<'_> { } /// Renders an error into a list of sources, *including* the error. -struct ErrorSourceList<'a>(&'a (dyn std::error::Error + 'static)); +struct ErrorSourceList<'a> { + error: &'a (dyn std::error::Error + 'static), + ansi_sanitization: bool, +} + +impl<'a> ErrorSourceList<'a> { + fn new(error: &'a (dyn std::error::Error + 'static), ansi_sanitization: bool) -> Self { + Self { + error, + ansi_sanitization, + } + } +} impl Display for ErrorSourceList<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut list = f.debug_list(); - let mut curr = Some(self.0); + let mut curr = Some(self.error); while let Some(curr_err) = curr { - list.entry(&Escape(&format_args!("{}", curr_err))); + list.entry(&EscapeGuard::new( + format_args!("{}", curr_err), + self.ansi_sanitization, + )); curr = curr_err.source(); } list.finish() @@ -1601,7 +1638,7 @@ impl fmt::Debug for FieldFnVisitor<'_, F> { /// Configures what points in the span lifecycle are logged as events. /// /// See also [`with_span_events`]. -/// +/// /// [`with_span_events`]: super::SubscriberBuilder::with_span_events #[derive(Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct FmtSpan(u8); diff --git a/tracing-subscriber/src/fmt/format/pretty.rs b/tracing-subscriber/src/fmt/format/pretty.rs index 9b89b6e60f..241509d979 100644 --- a/tracing-subscriber/src/fmt/format/pretty.rs +++ b/tracing-subscriber/src/fmt/format/pretty.rs @@ -451,21 +451,25 @@ impl field::Visit for PrettyVisitor<'_> { } fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) { + let sanitize = self.writer.sanitizes_ansi_escapes(); if let Some(source) = value.source() { let bold = self.bold(); self.record_debug( field, &format_args!( "{}, {}{}.sources{}: {}", - Escape(&format_args!("{}", value)), + EscapeGuard::new(format_args!("{}", value), sanitize), bold.prefix(), field, bold.infix(self.style), - ErrorSourceList(source), + ErrorSourceList::new(source, sanitize), ), ) } else { - self.record_debug(field, &Escape(&format_args!("{}", value))) + self.record_debug( + field, + &EscapeGuard::new(format_args!("{}", value), sanitize), + ) } } @@ -477,8 +481,12 @@ impl field::Visit for PrettyVisitor<'_> { match field.name() { "message" => { // Escape ANSI characters to prevent malicious patterns (e.g., terminal injection attacks) - self.write_padded(&format_args!("{}{:?}", self.style.prefix(), Escape(value))) - }, + self.write_padded(&format_args!( + "{}{:?}", + self.style.prefix(), + EscapeGuard::new(value, self.writer.sanitizes_ansi_escapes()) + )) + } // Skip fields that are actually log metadata that have already been handled #[cfg(feature = "tracing-log")] name if name.starts_with("log.") => self.result = Ok(()), diff --git a/tracing-subscriber/src/fmt/mod.rs b/tracing-subscriber/src/fmt/mod.rs index 99f383d00a..a1dc5fdad2 100644 --- a/tracing-subscriber/src/fmt/mod.rs +++ b/tracing-subscriber/src/fmt/mod.rs @@ -637,6 +637,22 @@ where } } + /// Sets whether ANSI control character sanitization is enabled. + /// + /// This defaults to `true` as a protective measure against terminal + /// injection attacks. If this is set to `false`, ANSI sanitization is + /// disabled and trusted ANSI control sequences in logged values are passed + /// through unchanged. + pub fn with_ansi_sanitization( + self, + ansi_sanitization: bool, + ) -> SubscriberBuilder, F, W> { + SubscriberBuilder { + inner: self.inner.with_ansi_sanitization(ansi_sanitization), + ..self + } + } + /// Sets whether to write errors from [`FormatEvent`] to the writer. /// Defaults to true. /// diff --git a/tracing-subscriber/src/fmt/time/datetime.rs b/tracing-subscriber/src/fmt/time/datetime.rs index 48a6d055a9..e1f28244eb 100644 --- a/tracing-subscriber/src/fmt/time/datetime.rs +++ b/tracing-subscriber/src/fmt/time/datetime.rs @@ -401,12 +401,18 @@ mod tests { case("1900-01-01T00:00:00.000000Z", -2208988800, 0); case("1899-12-31T23:59:59.000000Z", -2208988801, 0); - case("0000-01-01T00:00:00.000000Z", -62167219200, 0); - case("-0001-12-31T23:59:59.000000Z", -62167219201, 0); - - case("1234-05-06T07:08:09.000000Z", -23215049511, 0); - case("-1234-05-06T07:08:09.000000Z", -101097651111, 0); case("2345-06-07T08:09:01.000000Z", 11847456541, 0); - case("-2345-06-07T08:09:01.000000Z", -136154620259, 0); + + // Skipping pre-1601 dates on Windows: as of Rust 1.94, SystemTime + // subtraction panics when the result would be before the Windows + // FILETIME epoch (1601-01-01). See Rust 1.94.0 compatibility notes. + #[cfg(not(target_os = "windows"))] + { + case("1234-05-06T07:08:09.000000Z", -23215049511, 0); + case("0000-01-01T00:00:00.000000Z", -62167219200, 0); + case("-0001-12-31T23:59:59.000000Z", -62167219201, 0); + case("-1234-05-06T07:08:09.000000Z", -101097651111, 0); + case("-2345-06-07T08:09:01.000000Z", -136154620259, 0); + } } } diff --git a/tracing-subscriber/tests/ansi_escaping.rs b/tracing-subscriber/tests/ansi_escaping.rs index 120a44b588..030129da2b 100644 --- a/tracing-subscriber/tests/ansi_escaping.rs +++ b/tracing-subscriber/tests/ansi_escaping.rs @@ -39,8 +39,8 @@ impl<'a> MakeWriter<'a> for TestWriter { } } -/// Test that basic security expectations are met - this is a smoke test -/// for the ANSI escaping functionality using public APIs only +/// Test that ANSI escape sequences in error Display output are sanitized +/// when interpolated into the event message. #[test] fn test_error_ansi_escaping() { use std::fmt; @@ -68,17 +68,27 @@ fn test_error_ansi_escaping() { tracing::subscriber::with_default(subscriber, || { let malicious_error = MaliciousError("\x1b]0;PWNED\x07\x1b[2J\x08\x0c\x7f"); - // This demonstrates that errors are logged - the actual escaping - // is tested by our internal unit tests - tracing::error!(error = %malicious_error, "An error occurred"); + // Log the error as part of the message so it goes through the + // message sanitization path (not just Debug field formatting). + tracing::error!("An error occurred: {}", malicious_error); }); let output = writer.get_output(); - // Just verify that something was logged assert!( output.contains("An error occurred"), - "Error message should be logged" + "Error message should be logged: {}", + output + ); + assert!( + !output.contains('\x1b'), + "Output should not contain raw ESC characters: {}", + output + ); + assert!( + output.contains("\\x1b"), + "ESC should be escaped as \\x1b: {}", + output ); } @@ -279,3 +289,53 @@ fn test_c1_control_characters_escaping() { "Should contain escaped C1 characters" ); } + +/// Test that sanitization can be disabled via `with_ansi_sanitization(false)`, +/// allowing trusted ANSI sequences in messages to pass through. +#[test] +fn ansi_sanitization_can_be_disabled_for_messages() { + let writer = TestWriter::new(); + let subscriber = tracing_subscriber::fmt::Subscriber::builder() + .with_writer(writer.clone()) + .with_ansi(false) + .with_ansi_sanitization(false) + .without_time() + .with_target(false) + .with_level(false) + .finish(); + + tracing::subscriber::with_default(subscriber, || { + tracing::info!("Trusted color: \x1b[31mTEST\x1b[0m"); + }); + + let output = writer.get_output(); + + assert!( + output.contains("\x1b[31mTEST\x1b[0m"), + "ANSI message should pass through when sanitization is disabled" + ); +} + +#[cfg(feature = "ansi")] +#[test] +fn ansi_sanitization_can_be_disabled_for_pretty_messages() { + let writer = TestWriter::new(); + let subscriber = tracing_subscriber::fmt::Subscriber::builder() + .pretty() + .with_writer(writer.clone()) + .with_ansi(false) + .with_ansi_sanitization(false) + .without_time() + .with_target(false) + .finish(); + + tracing::subscriber::with_default(subscriber, || { + tracing::info!("Trusted color: \x1b[31mTEST\x1b[0m"); + }); + + let output = writer.get_output(); + assert!( + output.contains("\x1b[31mTEST\x1b[0m"), + "Pretty formatter message should pass through when sanitization is disabled" + ); +}