Skip to content

Comments

fix: prevent ANSI escape sequences leaking into file log span fields#815

Merged
mergify[bot] merged 1 commit intosigp:unstablefrom
shane-moore:fix/file-logging-ansi
Feb 10, 2026
Merged

fix: prevent ANSI escape sequences leaking into file log span fields#815
mergify[bot] merged 1 commit intosigp:unstablefrom
shane-moore:fix/file-logging-ansi

Conversation

@shane-moore
Copy link
Member

Issue Addressed

Closes #812

Proposed Changes

  • Add a FileFields marker type that gives the file logging layer its own span field cache slot in
    tracing-subscriber's extensions type-map, preventing the console layer's ANSI-formatted cached fields
    from being reused by the file layer
  • Add a regression test verifying file layer output contains no ANSI escape sequences when two layers
    share a registry

Additional Info

  • tracing-subscriber caches formatted span fields per TypeId via FormattedFields<N>. Both the console
    and file layers were using the implicit default N = DefaultFields, sharing one cache slot. The console
    layer (ANSI-enabled by default) formatted first, caching italic/dimmed styled field names. The file layer
    found the cache populated and reused those ANSI-contaminated fields, even with .with_ansi(false)

    FileFields delegates to DefaultFields for formatting but has a distinct TypeId, so
    FormattedFields<FileFields> gets its own cache entry. The file layer's .with_ansi() setting now
    correctly controls its own field formatting

@claude-code-actions-sigp
Copy link

claude-code-actions-sigp bot commented Feb 10, 2026

Claude finished @shane-moore's task in 1m 59s —— View job


Code Review Complete

Tasks

  • Read core logging fix implementation
  • Review regression test
  • Analyze PR scope and organization
  • Check for security and performance issues
  • Provide detailed feedback

Critical Issue: Wrong Base Branch

This PR is targeting stable but should target unstable. According to CLAUDE.md:

Sigma Prime maintains two permanent branches:

  • stable: Always points to the latest stable release, ideal for most users
  • unstable: Used for development, contains the latest PRs, base branch for contributions

This PR contains 33 commits merging multiple unrelated PRs and features into stable, which violates the branching strategy. The stable branch should only receive merge commits from unstable for releases.

Action Required: Close this PR and create a new one targeting unstable with only the logging fix commit (a02fd41).


Review of the Logging Fix (Commit a02fd41)

The actual logging fix is well-implemented and addresses the root cause effectively. Here's my detailed review:

✅ Strengths

  1. Correct Root Cause Analysis: The fix properly identifies that tracing-subscriber caches formatted span fields by TypeId, and two layers using the same field formatter type share a cache slot.

  2. Minimal, Elegant Solution: The FileFields type is a perfect example of the zero-cost abstraction pattern. It has a distinct TypeId but delegates all formatting to DefaultFields, achieving cache separation with zero runtime overhead.

  3. Excellent Test Coverage: The regression test test_file_fields_no_ansi_leak_in_span_fields is comprehensive:

    • Tests the exact production scenario (two layers on one registry)
    • Verifies both positive (file has no ANSI) and negative (console has ANSI) cases
    • Clear AAA structure with detailed comments
    • Good helper abstractions (MockWriter, contains_ansi, shared_buffer)
  4. Clear Documentation: The doc comments on FileFields explain the "why" behind this seemingly empty type, which will help future maintainers.

Minor Observations

  1. Type Safety: Consider making FileFields a unit struct instead of a zero-sized type to make the intent more explicit:

    pub struct FileFields;  // Current
    // vs
    #[derive(Clone, Copy)]
    pub struct FileFields;  // Slightly more explicit

    However, the current implementation is perfectly valid.

  2. Test Assertions: The test has excellent assertion messages that would make debugging failures easy. Well done.

  3. Integration Point: The fix is applied correctly in anchor/src/main.rs:285 with .fmt_fields(FileFields) on the file layer only.

Security & Performance

  • No security concerns: This is purely a formatting fix with no security implications.
  • No performance impact: The FileFields type has zero runtime overhead (it's a marker type that delegates to DefaultFields).
  • No memory impact: Each layer gets its own cache slot, but this is the intended behavior and the cache size is negligible.

Code Quality

The code follows Rust best practices:

  • ✅ Proper use of the type system to solve the problem
  • ✅ Zero-cost abstraction
  • ✅ Clear documentation
  • ✅ Comprehensive testing
  • ✅ Minimal diff (3 files, ~150 lines including tests)

Summary

The logging fix itself is excellent and ready to merge. However, this PR cannot be merged in its current state because:

  1. It targets the wrong branch (stable instead of unstable)
  2. It contains 32 unrelated merged PRs (fork infrastructure, network refactoring, consensus changes, etc.)
  3. The PR has 137 changed files with 14,827 insertions and 3,847 deletions, when the actual fix is 3 files with ~150 lines

Recommended Action:

  1. Close this PR
  2. Create a new PR from a clean branch based on unstable
  3. Cherry-pick only commit a02fd41 (the logging fix)
  4. Update the PR to target unstable instead of stable

The logging fix code quality is excellent - it just needs to be in the right PR targeting the right branch.

@shane-moore
Copy link
Member Author

@claude

@shane-moore shane-moore changed the base branch from stable to unstable February 10, 2026 02:13
@claude-code-actions-sigp
Copy link

claude-code-actions-sigp bot commented Feb 10, 2026

Claude finished @shane-moore's task in 1m 1s —— View job


Code Review Complete

Tasks

  • Read changed files to understand the implementation
  • Review the code for correctness and best practices
  • Verify the fix addresses the root cause properly
  • Check test coverage
  • Provide comprehensive feedback

Summary

This PR effectively solves the ANSI escape sequence leak issue by introducing a separate cache slot for the file logging layer. The solution is clean, well-documented, and includes comprehensive regression testing. I have a few suggestions for consideration:

Detailed Review

Strengths

  1. Root Cause Analysis: The fix correctly identifies and addresses the core issue—tracing-subscriber caching formatted span fields by TypeId, causing cache sharing between layers.

  2. Minimal, Elegant Solution: The FileFields marker type is a clever, zero-overhead solution that creates a distinct cache slot without duplicating formatting logic.

  3. Excellent Documentation:

    • Clear inline comments explaining the problem and solution (lines 213-219 in format.rs)
    • Comprehensive test documentation explaining the setup and expected behavior (lines 254-262)
  4. Comprehensive Test Coverage: The test verifies:

    • File output contains no ANSI sequences
    • File output contains expected span fields
    • Console output still contains ANSI (confirming both layers work independently)
    • Test setup mirrors production configuration
  5. Correct Implementation: The MakeVisitor implementation properly delegates to DefaultFields while maintaining a distinct type identity.

🔍 Observations & Suggestions

  1. Test Helper Location (format.rs:335-351)

    The MockWriter helper is well-implemented but could be extracted to a test utilities module if it might be reused:

    // Consider moving to a test_utils module if reused
    struct MockWriter(Arc<Mutex<Vec<u8>>>);

    Impact: Low priority—current approach is fine for a single test.

  2. ANSI Detection Logic (format.rs:243-247)

    The contains_ansi function uses a simple byte sequence check. While sufficient for this test, consider if more comprehensive ANSI detection might be useful:

    // Current: checks for escape sequence prefix only
    fn contains_ansi(bytes: &[u8]) -> bool {
        bytes.windows(ANSI_ESCAPE.len()).any(|window| window == ANSI_ESCAPE)
    }

    Question: Should this also detect other ANSI sequences (e.g., \x1b], \x1b()?
    Impact: Current implementation is sufficient for the regression test.

  3. Test Assertions Ordering (format.rs:305-332)

    Consider grouping assertions by layer:

    // File layer assertions
    assert!(!file_output.is_empty(), "...");
    assert!(!contains_ansi(&file_output), "...");
    assert!(file_str.contains("some_field"), "...");
    
    // Console layer assertions  
    assert!(!console_output.is_empty(), "...");
    assert!(contains_ansi(&console_output), "...");

    Impact: Minor readability improvement.

  4. Public API Surface (lib.rs:8)

    FileFields is exported publicly. Consider if this should be public or if it's an internal implementation detail:

    pub use format::{AnchorFormatter, FileFields};

    Question: Do external consumers need direct access to FileFields?
    Impact: If it's only used in main.rs, could be kept as pub(crate) or within the crate boundary.

  5. Type Alias Documentation

    The PR description mentions "marker type" but doesn't explicitly document that FileFields should never be instantiated (it's zero-sized). Consider adding:

    /// Field formatter for the file logging layer.
    ///
    /// This is a zero-sized marker type that should never be instantiated.
    /// It exists solely to provide a distinct `TypeId` for the file layer's
    /// `FormattedFields<FileFields>` cache slot...
    pub struct FileFields;

🎯 Production Usage (main.rs:285)

The fix is correctly integrated in production code:

fmt::layer()
    .event_format(anchor_formatter_log)
    .fmt_fields(FileFields)  // ← Uses the new marker type
    .with_writer(file_logging_layer.non_blocking_writer)
    .with_ansi(file_logging_flags.logfile_color)

This ensures the file layer gets its own cache slot as intended.

Testing Quality

The regression test is excellent:

  • Uses realistic setup (console + file layers, shared registry)
  • Verifies both positive (console has ANSI) and negative (file doesn't have ANSI) cases
  • Includes helpful error messages with actual output for debugging
  • Tests the actual bug scenario, not just the fix

Recommendations

  1. Required: None—the PR is ready to merge as-is.

  2. Optional Improvements (can be done in follow-up):

    • Consider making FileFields pub(crate) if external access isn't needed
    • Add explicit "zero-sized marker type" documentation
    • Extract test helpers if they'll be reused

Verdict

✅ Approve with enthusiasm

This PR demonstrates:

  • Deep understanding of the tracing-subscriber internals
  • Minimal, elegant solution to a subtle caching bug
  • Excellent documentation and testing practices
  • Proper integration with production code

The fix is correct, well-tested, and ready for production. Great work!


/// distinct from the console layer's default `FormattedFields<DefaultFields>`.
/// Without this, the console layer (ANSI-enabled) caches span fields first,
/// and the file layer reuses those ANSI-contaminated fields.
pub struct FileFields;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent implementation. This is a textbook example of using Rust's type system to solve a caching problem. The distinct TypeId gives each layer its own FormattedFields<_> cache slot while delegating actual formatting logic to DefaultFields. Zero runtime overhead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks claude lol

Comment on lines +254 to +263
/// Verifies that the `FileFields` fix prevents ANSI escape sequences from
/// leaking into file layer output when two layers share a registry.
///
/// Setup mirrors production: a console layer (ANSI-enabled, `DefaultFields`)
/// and a file layer (ANSI-disabled, `FileFields`). Both are attached to the
/// same `Registry`. Because `FileFields` has a distinct `TypeId` from
/// `DefaultFields`, each layer gets its own `FormattedFields<_>` cache slot
/// in the span extensions. The file layer therefore never reads the
/// console layer's ANSI-contaminated cached fields.
///

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outstanding test documentation. These doc comments explain the production scenario, the fix mechanism, and the expected behavior. This will be invaluable for future maintainers who wonder "why does FileFields exist?"

Comment on lines +266 to +333
fn test_file_fields_no_ansi_leak_in_span_fields() {
// Arrange
let console_buf = shared_buffer();
let file_buf = shared_buffer();

let console_writer = console_buf.clone();
let file_writer = file_buf.clone();

// Console layer: ANSI enabled, uses default DefaultFields (implicit).
let console_layer =
fmt::layer()
.with_ansi(true)
.with_writer(move || -> Box<dyn std::io::Write> {
Box::new(MockWriter(console_writer.clone()))
});

// File layer: ANSI disabled, uses FileFields for a distinct cache slot.
let file_layer = fmt::layer()
.with_ansi(false)
.fmt_fields(FileFields)
.with_writer(move || -> Box<dyn std::io::Write> {
Box::new(MockWriter(file_writer.clone()))
});

let subscriber = tracing_subscriber::registry()
.with(console_layer)
.with(file_layer);

// Act
tracing::subscriber::with_default(subscriber, || {
let span = tracing::info_span!("test_span", some_field = "hello_world");
let _guard = span.enter();
tracing::info!("message inside span");
});

// Assert -- file buffer must not contain ANSI escape sequences
let file_output = file_buf.lock().unwrap();
let file_str = String::from_utf8_lossy(&file_output);

assert!(
!file_output.is_empty(),
"File layer should have captured output"
);
assert!(
!contains_ansi(&file_output),
"File layer output must not contain ANSI escape sequences.\n\
File output was:\n{file_str}"
);

// The file output should contain the span field value in plain text
assert!(
file_str.contains("some_field"),
"File layer output should contain the span field name 'some_field'.\n\
File output was:\n{file_str}"
);

// Assert -- console buffer SHOULD contain ANSI (confirms both layers work)
let console_output = console_buf.lock().unwrap();
assert!(
!console_output.is_empty(),
"Console layer should have captured output"
);
assert!(
contains_ansi(&console_output),
"Console layer output should contain ANSI escape sequences, \
confirming the console layer is ANSI-enabled."
);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured regression test. This test follows AAA pattern perfectly:

  • Arrange: Sets up production-like scenario with two layers
  • Act: Logs with span fields
  • Assert: Verifies file has no ANSI, console has ANSI

The assertion messages are particularly good - they include the actual output for debugging. The negative assertion (console SHOULD have ANSI) is a smart addition that verifies both layers are working.

Copy link
Member

@dknopik dknopik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, LGTM, thanks!

@mergify mergify bot added the queued label Feb 10, 2026
@mergify
Copy link

mergify bot commented Feb 10, 2026

Merge Queue Status

Rule: default


This pull request spent 10 minutes 20 seconds in the queue, including 9 minutes 11 seconds running CI.

Required conditions to merge

mergify bot added a commit that referenced this pull request Feb 10, 2026
@mergify mergify bot merged commit a1d463b into sigp:unstable Feb 10, 2026
26 of 28 checks passed
@mergify mergify bot removed the queued label Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ANSI sequences for the names of tracing span variables even if color logging is disabled

2 participants