Skip to content

log: support multiple outputs with independent formats (ANSI stdout + plain file) #629

@TerraTech

Description

@TerraTech

Problem

In goa.design/clue/log (v1.2.3), logging configuration supports only a single output and a single formatter via:

log.Context(ctx,
    log.WithOutput(io.Writer),
    log.WithFormat(...),
)

This makes it impossible to achieve a very common production setup:

  • ANSI-colored logs to stdout (terminal)
  • Non-ANSI logs to a file (for grep / ingestion)

Using io.MultiWriter(os.Stdout, file) causes ANSI escape codes emitted by FormatTerminal to be written to the file as well.

There is currently no supported way to:

  • attach multiple outputs, and
  • configure different formats per output.

Expected behavior

A user should be able to express something equivalent to:

  • stdout → FormatTerminal (ANSI)
  • file → FormatText or FormatJSON (no ANSI)

without writing a custom io.Writer that strips ANSI codes.


Actual behavior

Only a single formatter is applied, so ANSI escapes always propagate to all outputs.

The only workaround in v1.2.3 is to implement a custom tee writer that:

  • writes raw bytes to stdout
  • strips ANSI before writing to file

This feels like something the logging library should handle.


Minimal repro

logFile, _ := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)

mw := io.MultiWriter(os.Stdout, logFile)

ctx := log.Context(
    context.Background(),
    log.WithOutput(mw),
)

log.Info(ctx, "hello world")

Result:

  • stdout: colored (expected)
  • app.log: contains ANSI escape sequences (unexpected)

Proposed API (one of many options)

Any of the following would solve this cleanly:

// Option A: multi-output with per-output format
log.WithOutputs(
    log.Output{Writer: os.Stdout, Format: log.FormatTerminal},
    log.Output{Writer: logFile,  Format: log.FormatText},
)
// Option B: helper for common case
log.WithTerminalAndFile(os.Stdout, logFile)
// Option C: explicit NO_COLOR / disable color on non-terminals
log.WithNoColorFileOutput(logFile)

Exact shape is flexible — the key need is separating output destinations from formatting.


Context

This pattern exists in other Go logging ecosystems:

  • zap (zapcore.NewTee)
  • slog (HandlerGroup)
  • go-kit/log (multi logger composition)

The lack of this capability in clue is surprising given its otherwise clean design.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions