Log to stderr and stop discarding With attributes (closes #82)
All checks were successful
check / check (push) Successful in 4m20s
All checks were successful
check / check (push) Successful in 4m20s
Closes #97. internal/log attached both handlers to os.Stdout, so any record that was not suppressed landed in the middle of a --json document. WARN and ERROR are never suppressed, so this was not hypothetical: a config file with permissions looser than 0600 was enough to break `vaultik snapshot list --json | jq`. Both handlers now write to os.Stderr, and the TTY-vs-JSON format choice tests os.Stderr rather than os.Stdout - the format has to follow the stream the records land on, or a redirected stderr gets colorized whenever stdout happens to be a terminal. User-visible: --verbose and --debug output moves to stderr too, so `vaultik snapshot list -v > out.txt` no longer captures diagnostics. --quiet and --cron semantics are unchanged. TTYHandler.WithAttrs and WithGroup discarded their arguments and returned the receiver, while their doc comments claimed otherwise, so attributes passed through the exported log.With vanished. The effect was environment-dependent in the worst direction: handler choice is by TTY-ness, so attributes disappeared on a terminal - where a developer is debugging - and appeared correctly in CI. Both now return a new handler with copied state rather than mutating the receiver, since slog permits a handler to be shared and derived from concurrently. A test asserts the TTY and JSON handlers emit the same attribute set, which is the test that would have caught the original defect. The local workaround in snapshot_list.go is removed now that the logger no longer writes to stdout. The collect-then-emit machinery is kept, but for a different reason than it was added: emitting from the fetch workers would order warnings by network timing, whereas key-order emission after group.Wait() is deterministic run to run. Not yet complete: --json stdout still carries the startup banner, which internal/cli/entry.go writes before cobra parses and which bannerSuppressedInArgs does not recognise --json for. That is the remaining stdout contamination path and is tracked in #106.
This commit was merged in pull request #107.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
// Package log provides the application-wide structured logger: slog
|
||||
// with a colorized TTY handler on terminals and JSON output otherwise.
|
||||
// writing to stderr, with a colorized TTY handler when stderr is a
|
||||
// terminal and JSON output otherwise.
|
||||
//
|
||||
// Everything this package emits is a diagnostic, so it all goes to
|
||||
// stderr. stdout belongs to the output the user asked for.
|
||||
package log //nolint:revive,nolintlint // stdlib log unused here; see #76
|
||||
|
||||
import (
|
||||
@@ -69,13 +73,27 @@ func Initialize(cfg Config) {
|
||||
Level: level,
|
||||
}
|
||||
|
||||
// Check if stdout is a TTY.
|
||||
if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||
// Diagnostics go to stderr, never to stdout. stdout is reserved for
|
||||
// the output the user asked for: every --json subcommand writes its
|
||||
// document there, and WARN/ERROR are never suppressed, so a logger
|
||||
// on stdout puts log records inside that document and makes it
|
||||
// unparseable. A config file with group- or world-readable
|
||||
// permissions is enough to trigger it (see internal/config), so this
|
||||
// was not a theoretical collision.
|
||||
//
|
||||
// The format is chosen by the TTY-ness of the stream the records
|
||||
// actually land on. AGENTS.md policy 9 says "if stdout is not a
|
||||
// terminal, emit jsonl"; it says stdout because that is where logs
|
||||
// used to go, and the property it is really asking for is that
|
||||
// output nobody is watching be machine-readable. Testing stdout here
|
||||
// would colorize records on a redirected stderr whenever stdout
|
||||
// happened to be a terminal, and vice versa.
|
||||
if term.IsTerminal(int(os.Stderr.Fd())) {
|
||||
// Use colorized TTY handler
|
||||
logger = slog.New(NewTTYHandler(os.Stdout, opts))
|
||||
logger = slog.New(NewTTYHandler(os.Stderr, opts))
|
||||
} else {
|
||||
// Use JSON format for non-TTY output
|
||||
logger = slog.New(slog.NewJSONHandler(os.Stdout, opts))
|
||||
logger = slog.New(slog.NewJSONHandler(os.Stderr, opts))
|
||||
}
|
||||
|
||||
// Set as default logger
|
||||
|
||||
Reference in New Issue
Block a user