|  | 
|  | 1 | +# Remove ANSI Color Codes from File Logging | 
|  | 2 | + | 
|  | 3 | +**Issue**: [#5](https://github.com/torrust/torrust-tracker-deployer/issues/5) (follow-up to #3) | 
|  | 4 | +**Parent Epic**: [#2](https://github.com/torrust/torrust-tracker-deployer/issues/2) - Scaffolding for main app | 
|  | 5 | +**Related**: | 
|  | 6 | + | 
|  | 7 | +- Issue [#3](https://github.com/torrust/torrust-tracker-deployer/issues/3) - Setup logging for production CLI | 
|  | 8 | +- [User Output vs Logging Separation](../research/UX/user-output-vs-logging-separation.md) | 
|  | 9 | +- [Development Principles](../development-principles.md) | 
|  | 10 | + | 
|  | 11 | +## Overview | 
|  | 12 | + | 
|  | 13 | +Remove ANSI color codes from log files when using `compact` and `pretty` formats, or implement independent format control for file vs stderr outputs. Currently, both `compact` and `pretty` formats write ANSI escape sequences to log files, making them harder to parse with standard text tools (grep, awk, editors). | 
|  | 14 | + | 
|  | 15 | +This is a follow-up refinement to Issue #3, discovered during PR review. The JSON format correctly omits ANSI codes, but the text-based formats include them regardless of output destination. | 
|  | 16 | + | 
|  | 17 | +## Problem Statement | 
|  | 18 | + | 
|  | 19 | +### Current Behavior | 
|  | 20 | + | 
|  | 21 | +When using `--log-format compact` or `--log-format pretty` with file logging: | 
|  | 22 | + | 
|  | 23 | +```bash | 
|  | 24 | +# Logs contain ANSI escape codes in files | 
|  | 25 | +$ torrust-tracker-deployer --log-format compact --log-dir /tmp/logs | 
|  | 26 | +$ hexdump -C /tmp/logs/log.txt | head -5 | 
|  | 27 | +00000000  1b 5b 32 6d 32 30 32 35  2d 31 30 2d 31 35 54 31  |.[2m2025-10-15T1| | 
|  | 28 | +00000010  35 3a 30 39 3a 33 31 2e  34 39 39 36 35 32 5a 1b  |5:09:31.499652Z.| | 
|  | 29 | +00000020  5b 30 6d 20 1b 5b 33 32  6d 20 49 4e 46 4f 1b 5b  |[0m .[32m INFO.[| | 
|  | 30 | +00000030  30 6d 20 1b 5b 32 6d 74  6f 72 72 75 73 74 5f 74  |0m .[2mtorrust_t| | 
|  | 31 | +``` | 
|  | 32 | +
 | 
|  | 33 | +ANSI codes present: | 
|  | 34 | +
 | 
|  | 35 | +- `\x1b[2m` - dim text | 
|  | 36 | +- `\x1b[32m` - green color | 
|  | 37 | +- `\x1b[0m` - reset formatting | 
|  | 38 | +
 | 
|  | 39 | +### Why This Is Problematic | 
|  | 40 | +
 | 
|  | 41 | +1. **Parsing difficulty**: Tools like `grep`, `awk`, `sed` see escape sequences as part of the text | 
|  | 42 | +2. **File size**: ANSI codes add ~15-20% overhead to log file size | 
|  | 43 | +3. **Readability**: Opening in plain text editors shows escape codes instead of text | 
|  | 44 | +4. **Processing**: Log aggregation tools may not handle ANSI codes correctly | 
|  | 45 | +5. **Searching**: Searching for patterns becomes harder with embedded codes | 
|  | 46 | +
 | 
|  | 47 | +### What Works Correctly | 
|  | 48 | +
 | 
|  | 49 | +✅ **JSON format**: No ANSI codes, clean machine-readable output | 
|  | 50 | +
 | 
|  | 51 | +```json | 
|  | 52 | +{ | 
|  | 53 | +  "timestamp": "2025-10-15T15:09:48.417627Z", | 
|  | 54 | +  "level": "INFO", | 
|  | 55 | +  "fields": { "message": "Application started" } | 
|  | 56 | +} | 
|  | 57 | +``` | 
|  | 58 | +
 | 
|  | 59 | +## Goals | 
|  | 60 | +
 | 
|  | 61 | +- [ ] Remove ANSI codes from log files when using `compact` format | 
|  | 62 | +- [ ] Remove ANSI codes from log files when using `pretty` format | 
|  | 63 | +- [ ] Keep ANSI codes for stderr output (development mode) | 
|  | 64 | +- [ ] Maintain JSON format as-is (already correct) | 
|  | 65 | +- [ ] Update documentation to reflect behavior | 
|  | 66 | +- [ ] Verify backwards compatibility with existing log files | 
|  | 67 | +
 | 
|  | 68 | +## Specifications | 
|  | 69 | +
 | 
|  | 70 | +### Option A: Independent Format Control (Recommended) | 
|  | 71 | +
 | 
|  | 72 | +Allow users to specify different formats for file and stderr, giving them full control over what format they want in each destination: | 
|  | 73 | +
 | 
|  | 74 | +```rust | 
|  | 75 | +#[derive(Parser)] | 
|  | 76 | +pub struct Cli { | 
|  | 77 | +    /// Format for file logging (default: compact without ANSI) | 
|  | 78 | +    #[arg(long, value_enum, default_value = "compact")] | 
|  | 79 | +    pub log_file_format: LogFormat, | 
|  | 80 | +
 | 
|  | 81 | +    /// Format for stderr logging (default: pretty with ANSI) | 
|  | 82 | +    #[arg(long, value_enum, default_value = "pretty")] | 
|  | 83 | +    pub log_stderr_format: LogFormat, | 
|  | 84 | +
 | 
|  | 85 | +    // ... other args | 
|  | 86 | +} | 
|  | 87 | +``` | 
|  | 88 | +
 | 
|  | 89 | +Example usage: | 
|  | 90 | +
 | 
|  | 91 | +```bash | 
|  | 92 | +# JSON to file, pretty to stderr (common for development) | 
|  | 93 | +torrust-tracker-deployer \ | 
|  | 94 | +    --log-file-format json \ | 
|  | 95 | +    --log-stderr-format pretty \ | 
|  | 96 | +    --log-output file-and-stderr | 
|  | 97 | +
 | 
|  | 98 | +# Compact to file, compact to stderr (production with real-time monitoring) | 
|  | 99 | +torrust-tracker-deployer \ | 
|  | 100 | +    --log-file-format compact \ | 
|  | 101 | +    --log-stderr-format compact \ | 
|  | 102 | +    --log-output file-and-stderr | 
|  | 103 | +
 | 
|  | 104 | +# JSON to file only (production default) | 
|  | 105 | +torrust-tracker-deployer --log-file-format json --log-output file-only | 
|  | 106 | +``` | 
|  | 107 | +
 | 
|  | 108 | +**Benefits:** | 
|  | 109 | +
 | 
|  | 110 | +- Users choose exactly what format they want for each destination | 
|  | 111 | +- No assumptions about user preferences (some may want compact in files, others JSON) | 
|  | 112 | +- ANSI codes automatically disabled for file formats (compact/pretty write without ANSI) | 
|  | 113 | +- ANSI codes automatically enabled for stderr formats (compact/pretty write with ANSI) | 
|  | 114 | +- Backwards compatible: can deprecate old `--log-format` in favor of new flags | 
|  | 115 | +
 | 
|  | 116 | +### Option B: Auto-detect and Force No-ANSI for Files (Simpler but Less Flexible) | 
|  | 117 | +
 | 
|  | 118 | +Automatically disable ANSI codes when writing to files, regardless of format chosen: | 
|  | 119 | +
 | 
|  | 120 | +```rust | 
|  | 121 | +// Pseudocode - actual implementation in src/logging.rs | 
|  | 122 | +match output_mode { | 
|  | 123 | +    LogOutput::FileOnly => { | 
|  | 124 | +        // File layer: no ANSI codes (forced) | 
|  | 125 | +        let file_layer = fmt::layer() | 
|  | 126 | +            .with_ansi(false)  // Always disable ANSI for files | 
|  | 127 | +            .with_format(self.format) | 
|  | 128 | +            .with_writer(file); | 
|  | 129 | +
 | 
|  | 130 | +        subscriber.with(file_layer) | 
|  | 131 | +    }, | 
|  | 132 | +    LogOutput::FileAndStderr => { | 
|  | 133 | +        // File layer: no ANSI codes (forced) | 
|  | 134 | +        let file_layer = fmt::layer() | 
|  | 135 | +            .with_ansi(false) | 
|  | 136 | +            .with_format(self.format) | 
|  | 137 | +            .with_writer(file); | 
|  | 138 | +
 | 
|  | 139 | +        // Stderr layer: with ANSI codes (forced) | 
|  | 140 | +        let stderr_layer = fmt::layer() | 
|  | 141 | +            .with_ansi(true) | 
|  | 142 | +            .with_format(self.format) | 
|  | 143 | +            .with_writer(stderr); | 
|  | 144 | +
 | 
|  | 145 | +        subscriber.with(file_layer).with(stderr_layer) | 
|  | 146 | +    } | 
|  | 147 | +} | 
|  | 148 | +``` | 
|  | 149 | +
 | 
|  | 150 | +**Drawbacks:** | 
|  | 151 | +
 | 
|  | 152 | +- Forces same format for both file and stderr | 
|  | 153 | +- Removes user choice (what if they want pretty in files with ANSI?) | 
|  | 154 | +- Less flexible for advanced use cases | 
|  | 155 | +
 | 
|  | 156 | +### Recommendation | 
|  | 157 | +
 | 
|  | 158 | +**Implement Option A** (independent format control): | 
|  | 159 | +
 | 
|  | 160 | +- Gives users full control over logging behavior | 
|  | 161 | +- Most flexible solution | 
|  | 162 | +- Better aligns with principle of not making assumptions | 
|  | 163 | +- Can support diverse production scenarios (JSON for aggregation, compact for grep, pretty for debugging) | 
|  | 164 | +
 | 
|  | 165 | +## Implementation Plan | 
|  | 166 | +
 | 
|  | 167 | +### Phase 1: Update CLI Arguments (1-2 hours) | 
|  | 168 | +
 | 
|  | 169 | +- [ ] Task 1.1: Add `--log-file-format` argument to `src/app.rs` | 
|  | 170 | +- [ ] Task 1.2: Add `--log-stderr-format` argument to `src/app.rs` | 
|  | 171 | +- [ ] Task 1.3: Deprecate old `--log-format` argument (or keep for backwards compat) | 
|  | 172 | +- [ ] Task 1.4: Update `LoggingBuilder` to accept separate file and stderr formats | 
|  | 173 | +- [ ] Task 1.5: Update help text to explain independent format control | 
|  | 174 | +
 | 
|  | 175 | +### Phase 2: Modify Logging Layer (1-2 hours) | 
|  | 176 | +
 | 
|  | 177 | +- [ ] Task 2.1: Update `src/logging.rs` to support two separate formats | 
|  | 178 | +- [ ] Task 2.2: Add `.with_ansi(false)` for file writers (all formats) | 
|  | 179 | +- [ ] Task 2.3: Add `.with_ansi(true)` for stderr writers (all formats) | 
|  | 180 | +- [ ] Task 2.4: Handle `FileOnly` mode with single format | 
|  | 181 | +- [ ] Task 2.5: Handle `FileAndStderr` mode with dual formats | 
|  | 182 | +- [ ] Task 2.6: Test all format combinations (compact, pretty, json × file-only, file-and-stderr) | 
|  | 183 | +
 | 
|  | 184 | +### Phase 3: Update Tests (45 minutes) | 
|  | 185 | +
 | 
|  | 186 | +- [ ] Task 3.1: Update `tests/logging_integration.rs` to verify no ANSI in files | 
|  | 187 | +- [ ] Task 3.2: Add test to verify ANSI codes present in stderr output | 
|  | 188 | +- [ ] Task 3.3: Test different format combinations (json file + pretty stderr, etc.) | 
|  | 189 | +- [ ] Task 3.4: Verify backwards compatibility if keeping old `--log-format` | 
|  | 190 | +- [ ] Task 3.5: Review E2E test binaries (`src/bin/e2e_*.rs`) - verify if they need updates or if defaults are acceptable | 
|  | 191 | +
 | 
|  | 192 | +### Phase 4: Documentation Updates (45 minutes) | 
|  | 193 | +
 | 
|  | 194 | +- [ ] Task 4.1: Update `docs/user-guide/logging.md` to explain independent format control | 
|  | 195 | +- [ ] Task 4.2: Add examples showing different format combinations | 
|  | 196 | +- [ ] Task 4.3: Add note about ANSI codes (files clean, stderr colored) | 
|  | 197 | +- [ ] Task 4.4: Update `README.md` with new argument examples | 
|  | 198 | +- [ ] Task 4.5: Update `docs/contributing/logging-guide.md` for contributors | 
|  | 199 | +- [ ] Task 4.6: Document migration path from old `--log-format` (if deprecated) | 
|  | 200 | +
 | 
|  | 201 | +### Phase 5: Verification (30 minutes) | 
|  | 202 | +
 | 
|  | 203 | +- [ ] Task 5.1: Manual testing with all format combinations | 
|  | 204 | +- [ ] Task 5.2: Verify file sizes reduced (no ANSI overhead in files) | 
|  | 205 | +- [ ] Task 5.3: Test grep/awk/sed work cleanly on log files | 
|  | 206 | +- [ ] Task 5.4: Verify stderr colors display correctly in terminal | 
|  | 207 | +- [ ] Task 5.5: Test backwards compatibility (if keeping old args) | 
|  | 208 | +- [ ] Task 5.6: Run full test suite (`./scripts/pre-commit.sh`) | 
|  | 209 | +
 | 
|  | 210 | +## Technical Details | 
|  | 211 | +
 | 
|  | 212 | +### Current Implementation Location | 
|  | 213 | +
 | 
|  | 214 | +The logging configuration is in `src/logging.rs`, specifically in the `LoggingBuilder` implementation: | 
|  | 215 | +
 | 
|  | 216 | +```rust | 
|  | 217 | +// Current code (approximate location) | 
|  | 218 | +pub fn init(self) { | 
|  | 219 | +    // ... setup code ... | 
|  | 220 | +
 | 
|  | 221 | +    let file_layer = fmt::layer() | 
|  | 222 | +        .with_format(self.format) | 
|  | 223 | +        .with_writer(file); | 
|  | 224 | +
 | 
|  | 225 | +    // Need to add: .with_ansi(false) here for files | 
|  | 226 | +} | 
|  | 227 | +``` | 
|  | 228 | +
 | 
|  | 229 | +### Key Files to Modify | 
|  | 230 | +
 | 
|  | 231 | +1. **`src/app.rs`**: Add new CLI arguments (`--log-file-format`, `--log-stderr-format`) | 
|  | 232 | +2. **`src/logging.rs`**: Main implementation (add `.with_ansi()` calls, support dual formats) | 
|  | 233 | +3. **`src/bin/e2e_*.rs`**: E2E test binaries (verify logging setup, may need updates) | 
|  | 234 | +4. **`tests/logging_integration.rs`**: Add/update tests for ANSI detection | 
|  | 235 | +5. **`docs/user-guide/logging.md`**: Document clean file output behavior | 
|  | 236 | +6. **`docs/contributing/logging-guide.md`**: Update contributor guide | 
|  | 237 | +
 | 
|  | 238 | +### Backward Compatibility | 
|  | 239 | +
 | 
|  | 240 | +**Breaking changes** (requires major version bump or deprecation period): | 
|  | 241 | +
 | 
|  | 242 | +- Old `--log-format` argument replaced with `--log-file-format` and `--log-stderr-format` | 
|  | 243 | +- **Migration path**: Keep `--log-format` for backwards compatibility, applies to both outputs if new args not specified | 
|  | 244 | +
 | 
|  | 245 | +**Recommended approach** (backwards compatible): | 
|  | 246 | +
 | 
|  | 247 | +```rust | 
|  | 248 | +// If new args not provided, fall back to old --log-format for both | 
|  | 249 | +let file_format = cli.log_file_format.unwrap_or(cli.log_format); | 
|  | 250 | +let stderr_format = cli.log_stderr_format.unwrap_or(cli.log_format); | 
|  | 251 | +``` | 
|  | 252 | +
 | 
|  | 253 | +**No data breaking changes**: | 
|  | 254 | +
 | 
|  | 255 | +- Existing log files with ANSI codes remain valid | 
|  | 256 | +- New log files will be cleaner and easier to process | 
|  | 257 | +- JSON format unchanged | 
|  | 258 | +- E2E test binaries continue to work | 
|  | 259 | +
 | 
|  | 260 | +## Acceptance Criteria | 
|  | 261 | +
 | 
|  | 262 | +- [ ] Compact format files contain no ANSI escape codes | 
|  | 263 | +- [ ] Pretty format files contain no ANSI escape codes | 
|  | 264 | +- [ ] JSON format files remain unchanged (no ANSI codes) | 
|  | 265 | +- [ ] Stderr output shows colors when using `--log-output file-and-stderr` | 
|  | 266 | +- [ ] File-only mode produces clean, grep-friendly logs | 
|  | 267 | +- [ ] Log file sizes reduced (less ANSI overhead) | 
|  | 268 | +- [ ] Documentation updated to explain ANSI behavior | 
|  | 269 | +- [ ] Integration tests verify ANSI presence/absence | 
|  | 270 | +- [ ] All existing tests pass (`./scripts/pre-commit.sh`) | 
|  | 271 | +- [ ] Manual testing confirms grep/awk work cleanly on log files | 
|  | 272 | +
 | 
|  | 273 | +## Example Outputs | 
|  | 274 | +
 | 
|  | 275 | +### Before (Current - with ANSI codes in files) | 
|  | 276 | +
 | 
|  | 277 | +```bash | 
|  | 278 | +$ torrust-tracker-deployer --log-format compact | 
|  | 279 | +$ cat data/logs/log.txt | 
|  | 280 | +^[[2m2025-10-15T15:09:31.499652Z^[[0m ^[[32m INFO^[[0m ^[[2mtorrust_tracker_deployer::app^[[0m... | 
|  | 281 | +# Hard to grep, includes escape sequences | 
|  | 282 | +``` | 
|  | 283 | +
 | 
|  | 284 | +### After (Fixed - no ANSI codes in files) | 
|  | 285 | +
 | 
|  | 286 | +```bash | 
|  | 287 | +$ torrust-tracker-deployer --log-format compact | 
|  | 288 | +$ cat data/logs/log.txt | 
|  | 289 | +2025-10-15T15:09:31.499652Z  INFO torrust_tracker_deployer::app: Application started... | 
|  | 290 | +# Clean, grep-friendly output | 
|  | 291 | +
 | 
|  | 292 | +$ grep "Application started" data/logs/log.txt | 
|  | 293 | +2025-10-15T15:09:31.499652Z  INFO torrust_tracker_deployer::app: Application started... | 
|  | 294 | +# Works perfectly! | 
|  | 295 | +``` | 
|  | 296 | +
 | 
|  | 297 | +### Stderr Still Has Colors (Development Mode) | 
|  | 298 | +
 | 
|  | 299 | +```bash | 
|  | 300 | +$ torrust-tracker-deployer --log-format pretty --log-output file-and-stderr | 
|  | 301 | +  2025-10-15T15:09:31.499652Z  INFO torrust_tracker_deployer::app: Application started... | 
|  | 302 | +# Terminal shows colored output for real-time monitoring | 
|  | 303 | +# But file remains clean for post-mortem analysis | 
|  | 304 | +``` | 
|  | 305 | +
 | 
|  | 306 | +## Related Documentation | 
|  | 307 | +
 | 
|  | 308 | +- [User Output vs Logging Separation](../research/UX/user-output-vs-logging-separation.md) - Design principles | 
|  | 309 | +- [Development Principles](../development-principles.md) - Observability and user-friendliness | 
|  | 310 | +- [Logging User Guide](../user-guide/logging.md) - End-user documentation | 
|  | 311 | +- [Logging Contributor Guide](../contributing/logging-guide.md) - Developer documentation | 
|  | 312 | +- Issue #3 - Parent issue that introduced logging CLI | 
|  | 313 | +- [tracing-subscriber documentation](https://docs.rs/tracing-subscriber/) - Upstream library reference | 
|  | 314 | +
 | 
|  | 315 | +## Notes | 
|  | 316 | +
 | 
|  | 317 | +### Why This Wasn't Caught Earlier | 
|  | 318 | +
 | 
|  | 319 | +- The implementation in Issue #3 correctly followed standard Rust logging patterns | 
|  | 320 | +- ANSI codes in files is common behavior for many Rust logging libraries | 
|  | 321 | +- The focus was on getting logging infrastructure working (achieved successfully) | 
|  | 322 | +- This is a refinement, not a bug - JSON format works correctly for production | 
|  | 323 | +
 | 
|  | 324 | +### Production Workaround (Until Fixed) | 
|  | 325 | +
 | 
|  | 326 | +Users who need clean log files immediately can: | 
|  | 327 | +
 | 
|  | 328 | +```bash | 
|  | 329 | +# Option 1: Use JSON format (already clean) | 
|  | 330 | +torrust-tracker-deployer --log-format json | 
|  | 331 | +
 | 
|  | 332 | +# Option 2: Strip ANSI codes with sed (post-processing) | 
|  | 333 | +sed 's/\x1b\[[0-9;]*m//g' data/logs/log.txt > data/logs/log-clean.txt | 
|  | 334 | +``` | 
|  | 335 | +
 | 
|  | 336 | +### E2E Test Binaries Consideration | 
|  | 337 | +
 | 
|  | 338 | +The E2E test binaries (`src/bin/e2e_provision_tests.rs`, `src/bin/e2e_config_tests.rs`, etc.) currently initialize logging directly: | 
|  | 339 | +
 | 
|  | 340 | +```rust | 
|  | 341 | +// Example from e2e_provision_tests.rs | 
|  | 342 | +LoggingBuilder::new(std::path::Path::new("./data/logs")) | 
|  | 343 | +    .with_format(LogFormat::Compact) | 
|  | 344 | +    .with_output(LogOutput::FileAndStderr) | 
|  | 345 | +    .init(); | 
|  | 346 | +``` | 
|  | 347 | +
 | 
|  | 348 | +**Action needed**: Review whether E2E tests should: | 
|  | 349 | +
 | 
|  | 350 | +- **Option 1**: Keep current behavior (compact format for both file and stderr) - likely acceptable since they need file+stderr for CI visibility | 
|  | 351 | +- **Option 2**: Update to use new dual format API once available | 
|  | 352 | +- **Option 3**: Use JSON for files, pretty for stderr (better for debugging) | 
|  | 353 | +
 | 
|  | 354 | +**Recommendation**: Start with Option 1 (no changes needed) unless testing reveals issues with ANSI codes in E2E log files affecting CI/CD tooling. | 
|  | 355 | +
 | 
|  | 356 | +### Future Enhancements (Out of Scope) | 
|  | 357 | +
 | 
|  | 358 | +- Automatic log rotation based on size | 
|  | 359 | +- Compression of old log files | 
|  | 360 | +- Integration with log aggregation services (CloudWatch, DataDog, etc.) | 
|  | 361 | +- Separate verbosity control for file vs stderr | 
|  | 362 | +- Custom format templates | 
|  | 363 | +
 | 
|  | 364 | +## Estimated Effort | 
|  | 365 | +
 | 
|  | 366 | +**Total**: 5-6 hours | 
|  | 367 | +
 | 
|  | 368 | +- Phase 1: 1-2 hours (CLI arguments update) | 
|  | 369 | +- Phase 2: 1-2 hours (logging layer modifications) | 
|  | 370 | +- Phase 3: 45 minutes (tests) | 
|  | 371 | +- Phase 4: 45 minutes (documentation) | 
|  | 372 | +- Phase 5: 30 minutes (verification) | 
|  | 373 | +- Buffer: 1 hour | 
|  | 374 | +
 | 
|  | 375 | +This is a moderate refactoring that adds new functionality while maintaining backwards compatibility. | 
0 commit comments