diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 00000000..dd1acb7c --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,104 @@ +use std::fmt; + +use tracing::Subscriber; +use tracing_subscriber::{ + fmt::{self as tracing_fmt, FmtContext, FormatEvent, FormatFields}, + registry::LookupSpan, +}; + +fn restore_ansi_sequences(input: &str, scratch: &mut String) -> bool { + if !input.contains("\\x") { + return false; + } + + scratch.clear(); + scratch.reserve(input.len()); + let bytes = input.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if i + 4 <= bytes.len() && bytes[i] == b'\\' && bytes[i + 1] == b'x' { + let code = &bytes[i + 2..i + 4]; + match code { + b"1b" | b"1B" => { + scratch.push('\x1b'); + i += 4; + continue; + } + b"07" => { + scratch.push('\x07'); + i += 4; + continue; + } + b"08" => { + scratch.push('\x08'); + i += 4; + continue; + } + b"0c" | b"0C" => { + scratch.push('\x0c'); + i += 4; + continue; + } + b"7f" | b"7F" => { + scratch.push('\x7f'); + i += 4; + continue; + } + _ => {} + } + } + scratch.push(bytes[i] as char); + i += 1; + } + true +} + +pub struct AnsiPreservingFormatter { + inner: F, +} + +struct AnsiEscapeRestorer<'a> { + writer: tracing_fmt::format::Writer<'a>, + scratch: String, +} + +impl<'a> fmt::Write for AnsiEscapeRestorer<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + if restore_ansi_sequences(s, &mut self.scratch) { + self.writer.write_str(&self.scratch) + } else { + self.writer.write_str(s) + } + } + + fn write_char(&mut self, c: char) -> fmt::Result { + self.writer.write_char(c) + } +} + +impl AnsiPreservingFormatter { + pub fn new(inner: F) -> Self { + Self { inner } + } +} + +impl FormatEvent for AnsiPreservingFormatter +where + S: Subscriber + for<'span> LookupSpan<'span>, + N: for<'a> FormatFields<'a> + 'static, + F: FormatEvent, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + writer: tracing_fmt::format::Writer<'_>, + event: &tracing::Event<'_>, + ) -> fmt::Result { + let mut adapter = AnsiEscapeRestorer { + writer, + scratch: String::new(), + }; + let proxy = tracing_fmt::format::Writer::new(&mut adapter); + self.inner.format_event(ctx, proxy, event) + } +} diff --git a/src/main.rs b/src/main.rs index 0e385aaf..4f96df66 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,14 @@ use fs_err as fs; use serde::Deserialize; use tracing::{error, instrument, warn, Level}; use tracing_error::ErrorLayer; +use tracing_subscriber::fmt::format::PrettyFields; use tracing_subscriber::{ - filter, fmt, layer::SubscriberExt, prelude::*, util::SubscriberInitExt, EnvFilter, + filter, + fmt::{self as tracing_fmt}, + layer::SubscriberExt, + prelude::*, + util::SubscriberInitExt, + EnvFilter, }; use kit::{ @@ -32,6 +38,9 @@ const STDERR_LOG_LEVEL_DEFAULT: &str = "error"; const FILE_LOG_LEVEL_DEFAULT: &str = "debug"; const RUST_LOG: &str = "RUST_LOG"; +mod logging; +use logging::AnsiPreservingFormatter; + #[derive(Debug, Deserialize)] struct Commit { sha: String, @@ -122,29 +131,35 @@ fn init_tracing(log_path: PathBuf) -> tracing_appender::non_blocking::WorkerGuar tracing_subscriber::registry() .with( - fmt::layer() - .without_time() + tracing_fmt::layer() + .event_format(AnsiPreservingFormatter::new( + tracing_fmt::format() + .without_time() + .with_level(false) + .with_target(false), + )) + .fmt_fields(PrettyFields::new().display_messages()) .with_writer(std::io::stdout) .with_ansi(true) - .with_level(false) - .with_target(false) - .fmt_fields(fmt::format::PrettyFields::new()) .with_filter(stdout_filter), ) .with( - fmt::layer() - .with_file(true) - .with_line_number(true) - .without_time() + tracing_fmt::layer() + .event_format(AnsiPreservingFormatter::new( + tracing_fmt::format() + .without_time() + .with_level(true) + .with_target(false) + .with_file(true) + .with_line_number(true), + )) + .fmt_fields(PrettyFields::new().display_messages()) .with_writer(std::io::stderr) .with_ansi(true) - .with_level(true) - .with_target(false) - .fmt_fields(fmt::format::PrettyFields::new()) .with_filter(stderr_filter), ) .with( - fmt::layer() + tracing_fmt::layer() .with_writer(non_blocking) .with_ansi(false) .json()