Skip to content

Conversation

@raphael
Copy link
Member

@raphael raphael commented Jan 8, 2026

Problem

log.WithOutput(...) + log.WithFormat(...) configure one formatter for one writer.

A common production setup needs multiple destinations with independent formats:

  • stdout: human-friendly ANSI terminal logs
  • file: machine/grep-friendly plain text or JSON

Using io.MultiWriter(os.Stdout, file) tees already-formatted bytes, so ANSI escapes leak into the file.

Solution

Add a first-class multi-sink concept:

  • log.Output: pairs a Writer with a Format.
  • log.WithOutputs(...): configures one or more outputs; each log entry is formatted and written to every output.

This makes formatting a property of the destination, not of the global logger.

API

New

ctx := log.Context(ctx, log.WithOutputs(
    log.Output{Writer: os.Stdout, Format: log.FormatTerminal},
    log.Output{Writer: logfile,  Format: log.FormatJSON},
))

Backwards compatibility

This PR keeps the existing options:

  • log.WithOutput(w) mutates the first output writer.
  • log.WithFormat(f) mutates the first output format.

So existing code continues to compile and behave as before.

Examples

ANSI stdout + JSON file

logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
    // handle error
}

defaultCtx := log.Context(context.Background(), log.WithOutputs(
    log.Output{Writer: os.Stdout, Format: log.FormatTerminal},
    log.Output{Writer: logFile,  Format: log.FormatJSON},
))

log.Infof(defaultCtx, "hello")

Keep legacy single output configuration

ctx := log.Context(context.Background(),
    log.WithOutput(os.Stderr),
    log.WithFormat(log.FormatText),
)

Implementation notes

  • Internals now store outputs as []log.Output.
  • Writing fans out across outputs; behavior remains consistent with prior versions (write errors are not surfaced).

Tests

  • Updated existing tests/call sites to continue exercising legacy WithOutput/WithFormat through the first-output behavior.
  • Added tests for:
    • multi-output fanout (TestMultipleOutputs)
    • WithOutputs validation panics
    • deterministic coverage of the terminal default path in defaultOptions.

Test plan

  • go test ./...

Replace the single writer+format model with explicit outputs so each destination can format independently (e.g. terminal ANSI to stdout and JSON/text to a file).
@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.94%. Comparing base (a647621) to head (88dd73d).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #634      +/-   ##
==========================================
+ Coverage   86.73%   86.94%   +0.21%     
==========================================
  Files          39       39              
  Lines        1990     2007      +17     
==========================================
+ Hits         1726     1745      +19     
+ Misses        239      238       -1     
+ Partials       25       24       -1     
Flag Coverage Δ
micro 86.94% <100.00%> (+0.21%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Reintroduce the legacy single-output options on top of WithOutputs and keep write behavior consistent while adding a multi-output fanout test.
Add tests for WithOutputs validation and deterministic terminal detection, and make IsTerminal overridable in tests to fully cover defaultOptions.
@raphael raphael merged commit ab9388d into main Jan 11, 2026
7 checks passed
@raphael raphael deleted the log-multi-output branch January 11, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants