From 16ffe4e14f0e538d0a01e4a594100013c671887d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:36:53 +0000 Subject: [PATCH 1/6] Initial plan From def8899560762b4ca70873f4c87f76791592e3bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:56:34 +0000 Subject: [PATCH 2/6] feat: add independent format control for file and stderr logging with ANSI code handling Co-authored-by: josecelano <58816+josecelano@users.noreply.github.com> --- src/app.rs | 33 ++++-- src/logging.rs | 209 ++++++++++++++++++++++++++--------- tests/logging_integration.rs | 125 +++++++++++++++++++++ 3 files changed, 307 insertions(+), 60 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1d54198c..98f0f02c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,13 +16,27 @@ use torrust_tracker_deployer_lib::logging::{LogFormat, LogOutput, LoggingBuilder #[command(version)] #[allow(clippy::struct_field_names)] // CLI arguments intentionally share 'log_' prefix for clarity pub struct Cli { - /// Logging format (default: compact) + /// Format for file logging (default: compact, without ANSI codes) /// - /// - pretty: Pretty-printed output for development - /// - json: JSON output for production environments - /// - compact: Compact output for minimal verbosity + /// - pretty: Pretty-printed output for development (no ANSI in files) + /// - json: JSON output for production environments (no ANSI) + /// - compact: Compact output for minimal verbosity (no ANSI in files) + /// + /// Note: ANSI color codes are automatically disabled for file output + /// to ensure logs are easily parsed with standard text tools (grep, awk, sed). #[arg(long, value_enum, default_value = "compact", global = true)] - pub log_format: LogFormat, + pub log_file_format: LogFormat, + + /// Format for stderr logging (default: pretty, with ANSI codes) + /// + /// - pretty: Pretty-printed output with colors for development + /// - json: JSON output for machine processing + /// - compact: Compact output with colors for minimal verbosity + /// + /// Note: ANSI color codes are automatically enabled for stderr output + /// to provide colored terminal output for better readability. + #[arg(long, value_enum, default_value = "pretty", global = true)] + pub log_stderr_format: LogFormat, /// Log output mode (default: file-only for production) /// @@ -60,13 +74,15 @@ pub fn run() { let cli = Cli::parse(); // Clone values for logging before moving them - let log_format = cli.log_format.clone(); + let log_file_format = cli.log_file_format.clone(); + let log_stderr_format = cli.log_stderr_format.clone(); let log_output = cli.log_output; let log_dir = cli.log_dir.clone(); // Initialize logging FIRST before any other logic LoggingBuilder::new(&cli.log_dir) - .with_format(cli.log_format) + .with_file_format(cli.log_file_format) + .with_stderr_format(cli.log_stderr_format) .with_output(cli.log_output) .init(); @@ -75,7 +91,8 @@ pub fn run() { app = "torrust-tracker-deployer", version = env!("CARGO_PKG_VERSION"), log_dir = %log_dir.display(), - log_format = ?log_format, + log_file_format = ?log_file_format, + log_stderr_format = ?log_stderr_format, log_output = ?log_output, "Application started" ); diff --git a/src/logging.rs b/src/logging.rs index d05857c3..b1e0acd5 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -89,24 +89,35 @@ pub enum LogFormat { /// formats and output targets. It eliminates code duplication by centralizing /// layer creation and subscriber initialization. /// +/// Supports independent format control for file and stderr outputs, with +/// automatic ANSI code handling (disabled for files, enabled for stderr). +/// /// # Examples /// /// ```rust,no_run /// use std::path::Path; /// use torrust_tracker_deployer_lib::logging::{LogOutput, LogFormat, LoggingBuilder}; /// -/// // Basic usage with defaults (Compact format, FileAndStderr output) +/// // Basic usage with defaults (Compact file format, Pretty stderr format, FileAndStderr output) /// LoggingBuilder::new(Path::new("./data/logs")).init(); /// -/// // Custom configuration +/// // Custom configuration with independent formats +/// LoggingBuilder::new(Path::new("./data/logs")) +/// .with_file_format(LogFormat::Json) +/// .with_stderr_format(LogFormat::Pretty) +/// .with_output(LogOutput::FileAndStderr) +/// .init(); +/// +/// // Backward compatible with single format for both outputs /// LoggingBuilder::new(Path::new("./data/logs")) -/// .with_format(LogFormat::Json) +/// .with_format(LogFormat::Compact) /// .with_output(LogOutput::FileOnly) /// .init(); /// ``` pub struct LoggingBuilder { log_dir: std::path::PathBuf, - format: LogFormat, + file_format: LogFormat, + stderr_format: LogFormat, output: LogOutput, } @@ -114,7 +125,8 @@ impl LoggingBuilder { /// Create a new logging builder with default settings /// /// Default configuration: - /// - Format: `LogFormat::Compact` + /// - File Format: `LogFormat::Compact` (no ANSI codes) + /// - Stderr Format: `LogFormat::Pretty` (with ANSI codes) /// - Output: `LogOutput::FileAndStderr` /// /// # Arguments @@ -124,19 +136,52 @@ impl LoggingBuilder { pub fn new(log_dir: &Path) -> Self { Self { log_dir: log_dir.to_path_buf(), - format: LogFormat::Compact, + file_format: LogFormat::Compact, + stderr_format: LogFormat::Pretty, output: LogOutput::FileAndStderr, } } - /// Set the logging format + /// Set the logging format for both file and stderr outputs + /// + /// This is a convenience method for backward compatibility. + /// For independent format control, use `with_file_format()` and `with_stderr_format()`. /// /// # Arguments /// /// * `format` - The desired logging format (Pretty, Json, or Compact) #[must_use] pub fn with_format(mut self, format: LogFormat) -> Self { - self.format = format; + self.file_format = format.clone(); + self.stderr_format = format; + self + } + + /// Set the logging format for file output + /// + /// ANSI codes are automatically disabled for file output to ensure + /// logs are easily parsed with standard text tools (grep, awk, sed). + /// + /// # Arguments + /// + /// * `format` - The desired logging format for files (Pretty, Json, or Compact) + #[must_use] + pub fn with_file_format(mut self, format: LogFormat) -> Self { + self.file_format = format; + self + } + + /// Set the logging format for stderr output + /// + /// ANSI codes are automatically enabled for stderr output to provide + /// colored terminal output for better readability. + /// + /// # Arguments + /// + /// * `format` - The desired logging format for stderr (Pretty, Json, or Compact) + #[must_use] + pub fn with_stderr_format(mut self, format: LogFormat) -> Self { + self.stderr_format = format; self } @@ -165,7 +210,7 @@ impl LoggingBuilder { /// /// Both panics are intentional as logging is critical for observability. pub fn init(self) { - init_subscriber(&self.log_dir, self.output, &self.format); + init_subscriber(&self.log_dir, self.output, &self.file_format, &self.stderr_format); } } @@ -178,57 +223,113 @@ impl LoggingBuilder { /// This is the single source of truth for subscriber initialization. /// All public init functions delegate to this to eliminate duplication. /// +/// Automatically configures ANSI codes: +/// - File output: ANSI codes disabled (clean text for parsing) +/// - Stderr output: ANSI codes enabled (colored terminal output) +/// /// Note: We cannot extract the format-specific layer creation into a separate /// function because each format (Pretty, Json, Compact) creates a different /// concrete type, and Rust's type system requires all match arms to return /// the same type. Type erasure with boxed layers would work but adds runtime /// overhead for a one-time initialization cost. -fn init_subscriber(log_dir: &Path, output: LogOutput, format: &LogFormat) { +fn init_subscriber(log_dir: &Path, output: LogOutput, file_format: &LogFormat, stderr_format: &LogFormat) { let file_appender = create_log_file_appender(log_dir); let env_filter = create_env_filter(); - match (format, output) { - // Pretty format - (LogFormat::Pretty, LogOutput::FileOnly) => { - tracing_subscriber::registry() - .with(fmt::layer().pretty().with_writer(file_appender)) - .with(env_filter) - .init(); - } - (LogFormat::Pretty, LogOutput::FileAndStderr) => { - tracing_subscriber::registry() - .with(fmt::layer().pretty().with_writer(file_appender)) - .with(fmt::layer().pretty().with_writer(io::stderr)) - .with(env_filter) - .init(); - } - // JSON format - (LogFormat::Json, LogOutput::FileOnly) => { - tracing_subscriber::registry() - .with(fmt::layer().json().with_writer(file_appender)) - .with(env_filter) - .init(); + match output { + LogOutput::FileOnly => { + // File-only mode: single layer with ANSI disabled + match file_format { + LogFormat::Pretty => { + tracing_subscriber::registry() + .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) + .with(env_filter) + .init(); + } + LogFormat::Json => { + tracing_subscriber::registry() + .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) + .with(env_filter) + .init(); + } + LogFormat::Compact => { + tracing_subscriber::registry() + .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) + .with(env_filter) + .init(); + } + } } - (LogFormat::Json, LogOutput::FileAndStderr) => { - tracing_subscriber::registry() - .with(fmt::layer().json().with_writer(file_appender)) - .with(fmt::layer().json().with_writer(io::stderr)) - .with(env_filter) - .init(); - } - // Compact format - (LogFormat::Compact, LogOutput::FileOnly) => { - tracing_subscriber::registry() - .with(fmt::layer().compact().with_writer(file_appender)) - .with(env_filter) - .init(); - } - (LogFormat::Compact, LogOutput::FileAndStderr) => { - tracing_subscriber::registry() - .with(fmt::layer().compact().with_writer(file_appender)) - .with(fmt::layer().compact().with_writer(io::stderr)) - .with(env_filter) - .init(); + LogOutput::FileAndStderr => { + // Dual output mode: file layer (no ANSI) + stderr layer (with ANSI) + match (file_format, stderr_format) { + // Pretty file format combinations + (LogFormat::Pretty, LogFormat::Pretty) => { + tracing_subscriber::registry() + .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().pretty().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + (LogFormat::Pretty, LogFormat::Json) => { + tracing_subscriber::registry() + .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().json().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + (LogFormat::Pretty, LogFormat::Compact) => { + tracing_subscriber::registry() + .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().compact().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + // JSON file format combinations + (LogFormat::Json, LogFormat::Pretty) => { + tracing_subscriber::registry() + .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().pretty().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + (LogFormat::Json, LogFormat::Json) => { + tracing_subscriber::registry() + .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().json().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + (LogFormat::Json, LogFormat::Compact) => { + tracing_subscriber::registry() + .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().compact().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + // Compact file format combinations + (LogFormat::Compact, LogFormat::Pretty) => { + tracing_subscriber::registry() + .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().pretty().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + (LogFormat::Compact, LogFormat::Json) => { + tracing_subscriber::registry() + .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().json().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + (LogFormat::Compact, LogFormat::Compact) => { + tracing_subscriber::registry() + .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) + .with(fmt::layer().compact().with_ansi(true).with_writer(io::stderr)) + .with(env_filter) + .init(); + } + } } } } @@ -413,11 +514,15 @@ pub fn init_compact(log_dir: &Path, output: LogOutput) { /// This is a convenience wrapper around `LoggingBuilder` for backward compatibility. /// Consider using `LoggingBuilder` directly for more flexibility. /// +/// This function applies the same format to both file and stderr outputs. +/// For independent format control, use `LoggingBuilder` with `with_file_format()` +/// and `with_stderr_format()`. +/// /// # Arguments /// /// * `log_dir` - Directory where log files should be written (e.g., `./data/logs` for production) /// * `output` - Where to write logs (file only or file + stderr) -/// * `format` - The logging format to use +/// * `format` - The logging format to use for both outputs /// /// # Panics /// diff --git a/tests/logging_integration.rs b/tests/logging_integration.rs index 2fa85b87..655af1a7 100644 --- a/tests/logging_integration.rs +++ b/tests/logging_integration.rs @@ -431,3 +431,128 @@ fn it_should_create_log_directory_automatically() { "log file should be created inside the logs directory" ); } + +#[test] +fn it_should_not_include_ansi_codes_in_file_output_compact_format() { + let test = LoggingTest::new(); + + let output = test.run_test_logging("compact", "file-only"); + assert!( + output.success, + "test_logging binary should execute successfully" + ); + + // Read raw bytes from log file + let log_bytes = fs::read(&test.log_file_path).expect("Failed to read log file"); + + // Check for ANSI escape sequence marker (0x1B) + assert!( + !log_bytes.contains(&0x1B), + "log file should not contain ANSI escape sequences (0x1B) in compact format" + ); + + // Verify log content is still present + let log_content = String::from_utf8_lossy(&log_bytes); + assert!( + log_content.contains("INFO"), + "log file should still contain INFO level logs" + ); + assert!( + log_content.contains("WARN"), + "log file should still contain WARN level logs" + ); + assert!( + log_content.contains("ERROR"), + "log file should still contain ERROR level logs" + ); +} + +#[test] +fn it_should_not_include_ansi_codes_in_file_output_pretty_format() { + let test = LoggingTest::new(); + + let output = test.run_test_logging("pretty", "file-only"); + assert!( + output.success, + "test_logging binary should execute successfully" + ); + + // Read raw bytes from log file + let log_bytes = fs::read(&test.log_file_path).expect("Failed to read log file"); + + // Check for ANSI escape sequence marker (0x1B) + assert!( + !log_bytes.contains(&0x1B), + "log file should not contain ANSI escape sequences (0x1B) in pretty format" + ); + + // Verify log content is still present + let log_content = String::from_utf8_lossy(&log_bytes); + assert!( + log_content.contains("INFO"), + "log file should still contain INFO level logs" + ); + assert!( + log_content.contains("WARN"), + "log file should still contain WARN level logs" + ); + assert!( + log_content.contains("ERROR"), + "log file should still contain ERROR level logs" + ); +} + +#[test] +fn it_should_not_include_ansi_codes_in_file_output_json_format() { + let test = LoggingTest::new(); + + let output = test.run_test_logging("json", "file-only"); + assert!( + output.success, + "test_logging binary should execute successfully" + ); + + // Read raw bytes from log file + let log_bytes = fs::read(&test.log_file_path).expect("Failed to read log file"); + + // Check for ANSI escape sequence marker (0x1B) + assert!( + !log_bytes.contains(&0x1B), + "log file should not contain ANSI escape sequences (0x1B) in JSON format" + ); + + // Verify JSON structure is valid + let log_content = String::from_utf8_lossy(&log_bytes); + assert!( + log_content.contains(r#""level":"INFO"#) || log_content.contains(r#""level":"info"#), + "JSON logs should contain INFO level" + ); +} + +#[test] +fn it_should_not_include_ansi_codes_in_file_output_dual_mode() { + let test = LoggingTest::new(); + + // Run with file-and-stderr mode to test dual output + let output = test.run_test_logging("compact", "file-and-stderr"); + assert!( + output.success, + "test_logging binary should execute successfully" + ); + + // Read raw bytes from log file + let log_bytes = fs::read(&test.log_file_path).expect("Failed to read log file"); + + // File output should NOT contain ANSI codes + assert!( + !log_bytes.contains(&0x1B), + "log file should not contain ANSI escape sequences (0x1B) in file-and-stderr mode" + ); + + // Verify log content is still present in file + let log_content = String::from_utf8_lossy(&log_bytes); + assert!( + log_content.contains("INFO"), + "log file should still contain INFO level logs" + ); +} From 8b58149f6c7acf795f5305137e6c05102b469754 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:02:24 +0000 Subject: [PATCH 3/6] feat: update test_logging binary with independent format arguments and apply rustfmt Co-authored-by: josecelano <58816+josecelano@users.noreply.github.com> --- src/bin/test_logging.rs | 38 +++++++++-- src/logging.rs | 140 ++++++++++++++++++++++++++++++++++------ 2 files changed, 152 insertions(+), 26 deletions(-) diff --git a/src/bin/test_logging.rs b/src/bin/test_logging.rs index cd2b2ee0..86900fa1 100644 --- a/src/bin/test_logging.rs +++ b/src/bin/test_logging.rs @@ -38,9 +38,17 @@ use tracing::{debug, error, info, trace, warn}; #[command(name = "test_logging")] #[command(about = "Test binary for logging configuration validation")] struct Cli { - /// Logging format to use + /// Logging format to use (backward compatibility - applies to both file and stderr) #[arg(long, value_enum)] - format: LogFormat, + format: Option, + + /// Format for file logging (overrides --format for file output) + #[arg(long, value_enum)] + file_format: Option, + + /// Format for stderr logging (overrides --format for stderr output) + #[arg(long, value_enum)] + stderr_format: Option, /// Logging output target #[arg(long, value_enum)] @@ -55,10 +63,28 @@ fn main() { let cli = Cli::parse(); // Initialize logging with the specified configuration using the builder pattern - LoggingBuilder::new(&cli.log_dir) - .with_format(cli.format) - .with_output(cli.output) - .init(); + let mut builder = LoggingBuilder::new(&cli.log_dir).with_output(cli.output); + + // Handle format arguments (backward compatible) + match (cli.format, cli.file_format, cli.stderr_format) { + // If only --format is provided, use it for both file and stderr (backward compatibility) + (Some(format), None, None) => { + builder = builder.with_format(format); + } + // If file and/or stderr formats are provided, use them specifically + (_, file_fmt, stderr_fmt) => { + if let Some(fmt) = file_fmt { + builder = builder.with_file_format(fmt); + } + if let Some(fmt) = stderr_fmt { + builder = builder.with_stderr_format(fmt); + } + // If --format is also provided along with specific formats, --format is ignored + // (specific formats take precedence) + } + } + + builder.init(); // Emit one log message at each level for testing trace!("This is a TRACE level message"); diff --git a/src/logging.rs b/src/logging.rs index b1e0acd5..dadcf0f7 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -210,7 +210,12 @@ impl LoggingBuilder { /// /// Both panics are intentional as logging is critical for observability. pub fn init(self) { - init_subscriber(&self.log_dir, self.output, &self.file_format, &self.stderr_format); + init_subscriber( + &self.log_dir, + self.output, + &self.file_format, + &self.stderr_format, + ); } } @@ -232,7 +237,12 @@ impl LoggingBuilder { /// concrete type, and Rust's type system requires all match arms to return /// the same type. Type erasure with boxed layers would work but adds runtime /// overhead for a one-time initialization cost. -fn init_subscriber(log_dir: &Path, output: LogOutput, file_format: &LogFormat, stderr_format: &LogFormat) { +fn init_subscriber( + log_dir: &Path, + output: LogOutput, + file_format: &LogFormat, + stderr_format: &LogFormat, +) { let file_appender = create_log_file_appender(log_dir); let env_filter = create_env_filter(); @@ -242,19 +252,34 @@ fn init_subscriber(log_dir: &Path, output: LogOutput, file_format: &LogFormat, s match file_format { LogFormat::Pretty => { tracing_subscriber::registry() - .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) + .with( + fmt::layer() + .pretty() + .with_ansi(false) + .with_writer(file_appender), + ) .with(env_filter) .init(); } LogFormat::Json => { tracing_subscriber::registry() - .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) + .with( + fmt::layer() + .json() + .with_ansi(false) + .with_writer(file_appender), + ) .with(env_filter) .init(); } LogFormat::Compact => { tracing_subscriber::registry() - .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) + .with( + fmt::layer() + .compact() + .with_ansi(false) + .with_writer(file_appender), + ) .with(env_filter) .init(); } @@ -266,66 +291,141 @@ fn init_subscriber(log_dir: &Path, output: LogOutput, file_format: &LogFormat, s // Pretty file format combinations (LogFormat::Pretty, LogFormat::Pretty) => { tracing_subscriber::registry() - .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) - .with(fmt::layer().pretty().with_ansi(true).with_writer(io::stderr)) + .with( + fmt::layer() + .pretty() + .with_ansi(false) + .with_writer(file_appender), + ) + .with( + fmt::layer() + .pretty() + .with_ansi(true) + .with_writer(io::stderr), + ) .with(env_filter) .init(); } (LogFormat::Pretty, LogFormat::Json) => { tracing_subscriber::registry() - .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) + .with( + fmt::layer() + .pretty() + .with_ansi(false) + .with_writer(file_appender), + ) .with(fmt::layer().json().with_ansi(true).with_writer(io::stderr)) .with(env_filter) .init(); } (LogFormat::Pretty, LogFormat::Compact) => { tracing_subscriber::registry() - .with(fmt::layer().pretty().with_ansi(false).with_writer(file_appender)) - .with(fmt::layer().compact().with_ansi(true).with_writer(io::stderr)) + .with( + fmt::layer() + .pretty() + .with_ansi(false) + .with_writer(file_appender), + ) + .with( + fmt::layer() + .compact() + .with_ansi(true) + .with_writer(io::stderr), + ) .with(env_filter) .init(); } // JSON file format combinations (LogFormat::Json, LogFormat::Pretty) => { tracing_subscriber::registry() - .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) - .with(fmt::layer().pretty().with_ansi(true).with_writer(io::stderr)) + .with( + fmt::layer() + .json() + .with_ansi(false) + .with_writer(file_appender), + ) + .with( + fmt::layer() + .pretty() + .with_ansi(true) + .with_writer(io::stderr), + ) .with(env_filter) .init(); } (LogFormat::Json, LogFormat::Json) => { tracing_subscriber::registry() - .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) + .with( + fmt::layer() + .json() + .with_ansi(false) + .with_writer(file_appender), + ) .with(fmt::layer().json().with_ansi(true).with_writer(io::stderr)) .with(env_filter) .init(); } (LogFormat::Json, LogFormat::Compact) => { tracing_subscriber::registry() - .with(fmt::layer().json().with_ansi(false).with_writer(file_appender)) - .with(fmt::layer().compact().with_ansi(true).with_writer(io::stderr)) + .with( + fmt::layer() + .json() + .with_ansi(false) + .with_writer(file_appender), + ) + .with( + fmt::layer() + .compact() + .with_ansi(true) + .with_writer(io::stderr), + ) .with(env_filter) .init(); } // Compact file format combinations (LogFormat::Compact, LogFormat::Pretty) => { tracing_subscriber::registry() - .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) - .with(fmt::layer().pretty().with_ansi(true).with_writer(io::stderr)) + .with( + fmt::layer() + .compact() + .with_ansi(false) + .with_writer(file_appender), + ) + .with( + fmt::layer() + .pretty() + .with_ansi(true) + .with_writer(io::stderr), + ) .with(env_filter) .init(); } (LogFormat::Compact, LogFormat::Json) => { tracing_subscriber::registry() - .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) + .with( + fmt::layer() + .compact() + .with_ansi(false) + .with_writer(file_appender), + ) .with(fmt::layer().json().with_ansi(true).with_writer(io::stderr)) .with(env_filter) .init(); } (LogFormat::Compact, LogFormat::Compact) => { tracing_subscriber::registry() - .with(fmt::layer().compact().with_ansi(false).with_writer(file_appender)) - .with(fmt::layer().compact().with_ansi(true).with_writer(io::stderr)) + .with( + fmt::layer() + .compact() + .with_ansi(false) + .with_writer(file_appender), + ) + .with( + fmt::layer() + .compact() + .with_ansi(true) + .with_writer(io::stderr), + ) .with(env_filter) .init(); } From 2b0bec3058325d68ac19163547a8bd7b0865a08b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:08:24 +0000 Subject: [PATCH 4/6] docs: update logging documentation for independent format control Co-authored-by: josecelano <58816+josecelano@users.noreply.github.com> --- docs/contributing/logging-guide.md | 57 ++++++++--- docs/user-guide/logging.md | 146 ++++++++++++++++++++++------- 2 files changed, 160 insertions(+), 43 deletions(-) diff --git a/docs/contributing/logging-guide.md b/docs/contributing/logging-guide.md index 1de8cfa8..5e541433 100644 --- a/docs/contributing/logging-guide.md +++ b/docs/contributing/logging-guide.md @@ -19,7 +19,8 @@ pub fn run() { // Initialize logging FIRST before any other logic LoggingBuilder::new(&cli.log_dir) - .with_format(cli.log_format) + .with_file_format(cli.log_file_format) + .with_stderr_format(cli.log_stderr_format) .with_output(cli.log_output) .init(); @@ -28,7 +29,8 @@ pub fn run() { app = "torrust-tracker-deployer", version = env!("CARGO_PKG_VERSION"), log_dir = %cli.log_dir.display(), - log_format = ?cli.log_format, + log_file_format = ?cli.log_file_format, + log_stderr_format = ?cli.log_stderr_format, log_output = ?cli.log_output, "Application started" ); @@ -41,22 +43,31 @@ pub fn run() { ### User-Facing Configuration -Users can configure logging via CLI arguments: +Users can configure logging via CLI arguments with independent format control for file and stderr outputs: ```bash -# Default (production): file-only, compact format +# Default (production): file-only, compact format for files, pretty for stderr torrust-tracker-deployer -# Development: stderr output, pretty format -torrust-tracker-deployer --log-format pretty --log-output file-and-stderr +# Development: stderr output, pretty format for both +torrust-tracker-deployer --log-output file-and-stderr # Custom log directory torrust-tracker-deployer --log-dir /var/log/deployer -# JSON format for log aggregation -torrust-tracker-deployer --log-format json +# JSON format for files (log aggregation), pretty for stderr (debugging) +torrust-tracker-deployer --log-file-format json --log-stderr-format pretty --log-output file-and-stderr + +# Compact format for both file and stderr +torrust-tracker-deployer --log-file-format compact --log-stderr-format compact --log-output file-and-stderr ``` +**ANSI Code Handling:** +- File output: ANSI color codes are automatically **disabled** for clean, parseable logs +- Stderr output: ANSI color codes are automatically **enabled** for colored terminal output + +This ensures log files can be easily processed with standard text tools (grep, awk, sed) while maintaining colored output for real-time terminal viewing. + See [User Guide: Logging](../user-guide/logging.md) for complete user documentation. ## JSON Output Format @@ -125,23 +136,47 @@ fn main() { For CLI applications that want to support multiple formats: ```rust -use torrust_tracker_deploy::logging::{self, LogFormat}; +use torrust_tracker_deployer_lib::logging::{self, LogFormat, LogOutput, LoggingBuilder}; use clap::Parser; +use std::path::Path; #[derive(Parser)] struct Cli { + #[arg(long, default_value = "compact")] + log_file_format: LogFormat, + #[arg(long, default_value = "pretty")] - log_format: LogFormat, + log_stderr_format: LogFormat, + + #[arg(long, default_value = "file-only")] + log_output: LogOutput, } fn main() { let cli = Cli::parse(); - logging::init_with_format(&cli.log_format); + + // Use LoggingBuilder for independent format control + LoggingBuilder::new(Path::new("./data/logs")) + .with_file_format(cli.log_file_format) + .with_stderr_format(cli.log_stderr_format) + .with_output(cli.log_output) + .init(); // Your application code... } ``` +**Backward Compatibility:** +If you want to apply the same format to both file and stderr (old behavior), use `.with_format()`: + +```rust +// Apply same format to both outputs (backward compatible) +LoggingBuilder::new(Path::new("./data/logs")) + .with_format(cli.log_format) + .with_output(cli.log_output) + .init(); +``` + ## Span Hierarchy Examples When you execute operations, you'll see nested spans in your logs: diff --git a/docs/user-guide/logging.md b/docs/user-guide/logging.md index 20e437e2..20401101 100644 --- a/docs/user-guide/logging.md +++ b/docs/user-guide/logging.md @@ -11,6 +11,9 @@ The application provides comprehensive structured logging for observability and - **Always persistent**: Logs are always written to files for post-mortem analysis - **Configurable output**: Choose between file-only (production) or file+stderr (development) - **Multiple formats**: Pretty, JSON, or compact formatting to suit your needs +- **Independent format control**: Separate format settings for file and stderr outputs +- **ANSI-free files**: File logs automatically exclude ANSI color codes for clean parsing +- **Colored terminals**: Stderr output automatically includes ANSI colors for readability - **Environment-based filtering**: Use `RUST_LOG` to control log verbosity ## Quick Start @@ -26,8 +29,9 @@ torrust-tracker-deployer This configuration: - Writes logs to `./data/logs/log.txt` -- Uses **compact** format (space-efficient, readable) -- **File-only** output (no stderr pollution) +- Uses **compact** format for files (space-efficient, no ANSI codes) +- Uses **pretty** format for stderr (colored output, if enabled) +- **File-only** output (no stderr pollution by default) - **Info** level logging (controlled by `RUST_LOG`) ### Development Mode @@ -42,53 +46,91 @@ This shows log events in real-time on your terminal while still writing to the l ### Pretty Format for Debugging -For maximum readability during development: +For maximum readability during development with pretty format in both file and stderr: ```bash -torrust-tracker-deployer --log-format pretty --log-output file-and-stderr +torrust-tracker-deployer --log-file-format pretty --log-stderr-format pretty --log-output file-and-stderr +``` + +Or use the same format for both outputs (backward compatible): + +```bash +# JSON to files (for aggregation), pretty to terminal (for debugging) +torrust-tracker-deployer --log-file-format json --log-stderr-format pretty --log-output file-and-stderr ``` ## Configuration Options -### Log Format (`--log-format`) +### Independent Format Control + +The application supports independent format control for file and stderr outputs: + +- `--log-file-format`: Controls the format for log files (default: compact) +- `--log-stderr-format`: Controls the format for stderr output (default: pretty) -Controls how log entries are formatted: +**ANSI Code Handling:** -#### Compact (Default) +- **File output**: ANSI color codes are automatically **disabled** for clean, parseable logs +- **Stderr output**: ANSI color codes are automatically **enabled** for colored terminal display -Space-efficient single-line format, ideal for production: +This ensures log files can be easily processed with standard text tools (grep, awk, sed) while maintaining colored output for real-time terminal viewing. + +### Log File Format (`--log-file-format`) + +Controls how log entries are formatted in files: + +#### Compact (Default for Files) + +Space-efficient single-line format, ideal for production (no ANSI codes): ```bash -torrust-tracker-deployer --log-format compact +torrust-tracker-deployer --log-file-format compact ``` -Example output: +Example file output: ```text 2025-10-15T12:39:22.793955Z INFO torrust_tracker_deployer::app: Application started app="torrust-tracker-deployer" version="0.1.0" ``` -#### Pretty +✅ Benefits: +- Clean text (no ANSI escape codes) +- Easy to parse with grep, awk, sed +- Space-efficient for storage -Multi-line format with visual structure, ideal for development: +#### Pretty (Default for Stderr) + +Multi-line format with visual structure, ideal for development (with ANSI colors on stderr): ```bash -torrust-tracker-deployer --log-format pretty --log-output file-and-stderr +torrust-tracker-deployer --log-file-format pretty --log-output file-and-stderr ``` -Example output: +Example file output (no ANSI codes): ```text 2025-10-15T12:40:36.097921Z INFO torrust_tracker_deployer::app: Application started, app: "torrust-tracker-deployer", version: "0.1.0" at src/app.rs:69 ``` +Example stderr output (with ANSI colors - shown here without colors for documentation): + +```text + 2025-10-15T12:40:36.097921Z INFO torrust_tracker_deployer::app: Application started, app: "torrust-tracker-deployer", version: "0.1.0" + at src/app.rs:69 +``` + +✅ Benefits: +- Colored terminal output for stderr (enhanced readability) +- Clean file output without ANSI codes (easy parsing) +- Multi-line format shows more context + #### JSON -Machine-readable format for log aggregation systems: +Machine-readable format for log aggregation systems (no ANSI codes): ```bash -torrust-tracker-deployer --log-format json +torrust-tracker-deployer --log-file-format json ``` Example output: @@ -97,6 +139,26 @@ Example output: {"timestamp":"2025-10-15T12:42:34.178335Z","level":"INFO","fields":{"message":"Application started","app":"torrust-tracker-deployer","version":"0.1.0"},"target":"torrust_tracker_deployer::app"} ``` +✅ Benefits: +- Structured data (easy to parse programmatically) +- Compatible with log aggregation tools +- No ANSI codes by design + +### Log Stderr Format (`--log-stderr-format`) + +Controls how log entries are formatted on stderr (terminal output). Uses the same format options as file logging (compact, pretty, json), but automatically enables ANSI color codes for enhanced readability. + +```bash +# Pretty stderr format (default - with colors) +torrust-tracker-deployer --log-stderr-format pretty --log-output file-and-stderr + +# Compact stderr format (with colors) +torrust-tracker-deployer --log-stderr-format compact --log-output file-and-stderr + +# JSON stderr format (with ANSI codes for terminal) +torrust-tracker-deployer --log-stderr-format json --log-output file-and-stderr +``` + ### Log Output (`--log-output`) Controls where logs are written: @@ -227,37 +289,43 @@ torrust-tracker-deployer Real-time log visibility with readable format: ```bash -torrust-tracker-deployer --log-format pretty --log-output file-and-stderr +torrust-tracker-deployer --log-output file-and-stderr ``` -- Logs to `./data/logs/log.txt` and stderr -- Pretty format +- Logs to `./data/logs/log.txt` (compact, no ANSI) and stderr (pretty, with ANSI colors) - Info level (increase with RUST_LOG if needed) +Or specify both formats explicitly: + +```bash +torrust-tracker-deployer --log-file-format compact --log-stderr-format pretty --log-output file-and-stderr +``` + ### Scenario 3: Troubleshooting Issues Maximum verbosity for debugging: ```bash -RUST_LOG=debug torrust-tracker-deployer --log-format pretty --log-output file-and-stderr +RUST_LOG=debug torrust-tracker-deployer --log-file-format pretty --log-stderr-format pretty --log-output file-and-stderr ``` - Debug level logging -- Pretty format for readability -- Real-time visibility on terminal -- Persistent file for later analysis +- Pretty format for both file and stderr (file without ANSI, stderr with ANSI colors) +- Real-time visibility on terminal with colors +- Persistent file for later analysis (clean, no ANSI codes) ### Scenario 4: Log Aggregation -JSON format for external monitoring systems: +JSON format for external monitoring systems with pretty terminal output for debugging: ```bash -torrust-tracker-deployer --log-format json --log-dir /var/log/deployer +torrust-tracker-deployer --log-file-format json --log-stderr-format pretty --log-dir /var/log/deployer --log-output file-and-stderr ``` -- JSON format for machine parsing +- JSON format for files (machine parsing, log aggregation) +- Pretty format for stderr (colored terminal output for debugging) - Custom log directory -- File-only output (monitor the file externally) +- Both file and stderr output (file for aggregation, stderr for real-time monitoring) ### Scenario 5: CI/CD Pipeline @@ -357,7 +425,13 @@ torrust-tracker-deployer --log-dir /root/logs 2. **Use pretty format** for readability: ```bash - torrust-tracker-deployer --log-format pretty --log-output file-and-stderr + torrust-tracker-deployer --log-file-format pretty --log-stderr-format pretty --log-output file-and-stderr + ``` + + Or rely on defaults (compact for files, pretty for stderr): + + ```bash + torrust-tracker-deployer --log-output file-and-stderr ``` 3. **Increase verbosity** when debugging: @@ -374,10 +448,16 @@ torrust-tracker-deployer --log-dir /root/logs torrust-tracker-deployer ``` -2. **Consider JSON format** for log aggregation: +2. **Consider JSON format** for file output (log aggregation): ```bash - torrust-tracker-deployer --log-format json + torrust-tracker-deployer --log-file-format json + ``` + + Or combine JSON files with pretty stderr for debugging: + + ```bash + torrust-tracker-deployer --log-file-format json --log-stderr-format pretty --log-output file-and-stderr ``` 3. **Use absolute paths** for log directories: @@ -396,12 +476,14 @@ torrust-tracker-deployer --log-dir /root/logs torrust-tracker-deployer --log-output file-and-stderr ``` -2. **Use compact format** for space efficiency: +2. **Use compact format** for space efficiency (or rely on defaults): ```bash - torrust-tracker-deployer --log-format compact --log-output file-and-stderr + torrust-tracker-deployer --log-output file-and-stderr ``` + This uses compact format for files (default) and pretty for stderr (default). + 3. **Archive log files** as build artifacts ## Additional Resources From 04e6292e35397be425e7b70c9cbe35e6fa92c1e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:05:27 +0000 Subject: [PATCH 5/6] fix: add clippy allow attribute for too_many_lines in init_subscriber Co-authored-by: josecelano <58816+josecelano@users.noreply.github.com> --- src/logging.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/logging.rs b/src/logging.rs index dadcf0f7..97b6bd14 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -237,6 +237,7 @@ impl LoggingBuilder { /// concrete type, and Rust's type system requires all match arms to return /// the same type. Type erasure with boxed layers would work but adds runtime /// overhead for a one-time initialization cost. +#[allow(clippy::too_many_lines)] fn init_subscriber( log_dir: &Path, output: LogOutput, From 0d35398ee77896d50e8f91b7252635c7c969c866 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:55:28 +0000 Subject: [PATCH 6/6] docs: fix markdown linting errors by adding blank lines around lists Co-authored-by: josecelano <58816+josecelano@users.noreply.github.com> --- docs/contributing/logging-guide.md | 1 + docs/user-guide/logging.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docs/contributing/logging-guide.md b/docs/contributing/logging-guide.md index 5e541433..32729577 100644 --- a/docs/contributing/logging-guide.md +++ b/docs/contributing/logging-guide.md @@ -63,6 +63,7 @@ torrust-tracker-deployer --log-file-format compact --log-stderr-format compact - ``` **ANSI Code Handling:** + - File output: ANSI color codes are automatically **disabled** for clean, parseable logs - Stderr output: ANSI color codes are automatically **enabled** for colored terminal output diff --git a/docs/user-guide/logging.md b/docs/user-guide/logging.md index 20401101..e1d9de3c 100644 --- a/docs/user-guide/logging.md +++ b/docs/user-guide/logging.md @@ -94,6 +94,7 @@ Example file output: ``` ✅ Benefits: + - Clean text (no ANSI escape codes) - Easy to parse with grep, awk, sed - Space-efficient for storage @@ -121,6 +122,7 @@ Example stderr output (with ANSI colors - shown here without colors for document ``` ✅ Benefits: + - Colored terminal output for stderr (enhanced readability) - Clean file output without ANSI codes (easy parsing) - Multi-line format shows more context @@ -140,6 +142,7 @@ Example output: ``` ✅ Benefits: + - Structured data (easy to parse programmatically) - Compatible with log aggregation tools - No ANSI codes by design