Reference in New Issue
Block a user
Delete Branch "fix-log-stdout"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes issue #82.
Closes issue #97.
Two defects in
internal/log, fixed in one pass because both live inthe handler construction path.
USER-VISIBLE CHANGE:
--verboseand--debugoutput moves to stderrAll log output now goes to stderr. That includes everything from
--verboseand--debug, not just warnings and errors.vaultik snapshot list --verbose > out.txtused to capture thediagnostics along with the listing. It no longer does — they stay on the
terminal. Capturing both now needs
> out.txt 2> log.txt, or> out.txt 2>&1to interleave them. Anyone with a script or a cronwrapper that redirects only stdout and expects the log in the file will
see the change.
This is the intended contract rather than a side effect: stdout carries
the output that was asked for, stderr carries diagnostics.
README.mdgains a "stdout and stderr" section stating it, and the
--verbose/--debugflag entries point at it.#82: the logger wrote to stdout
Initializebuilt both handlers overos.Stdout. Every--jsonsubcommand writes its document to that same stream, and
WARN/ERRORare never suppressed by any flag, so a log record could land inside a
JSON document and break the parse.
Reproduced against
mainwith the trigger from the report — a configfile at mode 0644, which fires the insecure-permissions
WARNininternal/config:Same config, same command, this branch:
with the
WARNon stderr, andjson.loadaccepting stdout.The TTY/JSON format choice moved to
os.Stderralong with the sink. Ithas to follow the stream the records land on: testing stdout would
colorize records on a redirected stderr whenever stdout happened to be a
terminal, and emit JSON at a terminal in the reverse case.
--quietand--cronsemantics are unchanged. Level selection is nottouched,
ui.Writer's quiet handling is not touched, and the run aboveused
-q— the warning still fired, which is the documented behaviour(see issue #84):
UI.SetQuiet(true)suppresses Begin/Complete/Info/Notice/Detail/Progress/Banner and never Warning/Error.
The
snapshot_list.goworkaround is gonewarnWhileListingwas hand-rolling structured-log formatting(
fmt.Fprintf(&line, " %v=%v", ...)and akvPairSizeconstant) purelyto reach a writer that was not stdout, and the
jsonOutputparameterthreaded through
collectRemoteSnapshotsanddescribeRemoteOnlySnapshotsexisted only to pick between the twowriters. Both are deleted; those warnings go through
log.Warnin everymode.
reportJSONListingLimitslikewise moved fromfmt.Fprintf(v.Stderr, ...)tolog.Warnwith structured fields.warnRemoteListingFailedkeeps itsjsonOutputbranch, for a reasonthat is still true after this change and is now what the comment says:
table mode wants the prose
v.UI.Warningfline, andv.UIwrites tostdout, so
--jsonmode goes through the logger instead.The
listingWarningcollect-then-emit machinery stays, with arewritten rationale. Its original justification — the chosen writer is
not safe for concurrent use — is obsolete, since
sloghandlers are.What remains is ordering: emitting from the manifest-fetch workers would
order warnings by network timing, while collecting them and emitting in
key order after
group.Wait()makes two runs over the same damagedstore produce the same diagnostics in the same order. No comment
anywhere still refers to this as pending work.
#97:
TTYHandlerdiscardedWithAttrsandWithGroupBoth returned the receiver and dropped their argument while their doc
comments claimed otherwise. Because the handler is selected by TTY-ness,
attributes vanished on a terminal and were correct in CI — it failed
precisely when someone was debugging interactively.
WithAttrsretains the attributes and emits them on every subsequentrecord.
WithGroupis implemented rather than deferred: this format is oneline with nowhere to nest, so a group becomes a dotted key prefix
(
WithGroup("db").With("rows", 3)rendersdb.rows=3). Record-levelslog.KindGroupvalues flatten the same way. Empty attrs are droppedand an empty-key group is inlined, per the
slog.Handlercontract.the clone copies its slices rather than reslicing them, so two
concurrent derivations cannot append over each other's attribute. The
mutex became a
*sync.Mutexso handlers sharing a stream keep sharingone lock — a value mutex would have given every derived handler its
own and stopped serializing writes.
// Simplified for nowcomments are gone, and the doc commentsnow describe what the code does.
The tests fail against the unfixed handler
Verified by observation, not assumption:
internal/log/tty_handler.gowas reverted to its
mainversion with the new tests in place, andmake testreportedThe handler was then restored and the suite went green.
TestWithAttributesReachTTYOutputgoes through the exportedinternal/log.With, which is the reachable API the issue names. It is anin-package test because that is the only way to point the package logger
at a buffer;
//nolint:testpackagecarries the reason.TestTTYHandlerMatchesJSONHandlerAttributesfeeds both handlers the samederivation chain and the same record and compares the emitted attribute
sets, flattening
JSONHandler's nesting to the same dotted keys. That isthe drift guard the issue asks for.
Existing tests
Four
internal/vaultiktests asserted these warnings on the injectedv.Stderrbuffer. They now capture the process's real stderr through anew
captureProcessStderrhelper, since that is where the warnings go;the guarantees asserted are unchanged.
captureProcessStdoutkeepsrebuilding the logger on purpose — that is what would catch a logger
regressing back to stdout — and its doc comment no longer cites
issue #82 as the reason
no injectable sink exists.
Found while verifying, filed not fixed
Issue #106: the startup
banner is written to stdout by
internal/cli/entry.gobefore cobraparses anything, and
bannerSuppressedInArgsrecognizes only--quiet/-q/--cron, not--json. So every--jsondocument isstill preceded by two banner lines and a blank line unless
-qispassed. That is a different writer on a different path from the logger,
so it is filed rather than fixed here — but it means stdout is not fully
clean until that one lands too. The reproductions above use
-qforthat reason.
Verification
make check: green. Tests, lint, and fmt-check all pass; the onlylinter output is the
gomodguarddeprecation already tracked asissue #90.
script/cibuild: exit 0, 140s wall. The three check layers executedrather than replaying from cache (
make fmt-check2.4s,make lint40.4s,
make test58.5s under the freshCHECK_EPOCH); the 10CACHEDlayers are dependency and module layers. 14oklines, zero(cached)markers.file://destination, beforeand after, as shown above.
.golangci.ymlunchanged: sha256021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb.git tag -lis empty.Review: PASS
Independent review of
PR #107 against
issue #82 and
issue #97.
Blocking findings
None.
Verified
WithAttrs/WithGroupnever write tothe receiver;
clone()copies rather than reslicesattrs/groups,so concurrent derivation from one parent cannot interleave appends.
The
*sync.Mutexis load-bearing —TestTTYHandlerConcurrentDerivationhas 16 goroutines writing through derived handlers into one
bytes.Buffer, which is only safe because the pointer is shared. Theslog.Handlercontract corners are right:WithGroup("")andWithAttrs(nil)return the receiver, emptyAttrdropped, emptygroup dropped, empty-key group inlined,
KindGrouprecord valuesflattened. Nested groups, group-then-attrs, and attrs straddling a
group all match
slog.JSONHandlerunder the dotted-key flattening.Resolving
LogValueratWithAttrstime rather thanHandletime isa deviation from the naive reading but matches what stdlib's own
handlers do via
preformattedAttrs— correct, noted because itsurprises.
tty_handler.goreverted to
mainin a scratch worktree with the new tests in place:8 top-level
FAILs and 6 of 7TestTTYHandlerMatchesJSONHandlerAttributessubtests fail(
record_attributes_onlypasses, as it must — it exercises noderivation). Every other package stayed green. Matches the PR body
exactly.
internal/logtargets stdout anymore. Keying the TTY/JSON choice on
os.Stderris correct: aredirected stderr now gets jsonl even when stdout is a terminal, which
is the property that matters (see nit 1 on the doc text).
InitializesetsLevelWarnunder
--quiet/--cron, so thelog.Warncalls inreportJSONListingLimitsstill fire in the exact-q --jsonmode thereproduction uses — the truncation and unreadable-manifest counts are
not regressed from
PR #83's finding 2, and
the tests now assert the counts as
"unreadable":1/"omitted":1/"limit":1000rather than only the prose, which is stronger thanbefore.
warnRemoteListingFailed's--jsonbranch is still justified(
v.UIwrites to stdout). ThelistingWarningcollect-then-emitrationale holds: worker-order emission would vary with fetch timing,
and the rewritten comment says exactly that and explicitly retires the
obsolete concurrency-safety reason.
--quiet/--cronsemantics unchanged; level selection andui.Writeruntouched.--jsonstdout — the PR body, the TODOentry and the issue comment all name
issue #106 as the
remaining stdout contamination.
.golangci.ymlsha256 matches021cc83f…46bcb;Dockerfile,script/*,REPO_POLICIES.md,.gitea/,Makefilebyte-identical tomain. No test functiondeleted; 10 assertions removed, 16 added.
b7aee81(check / check, success in 2m34s).Mergeable against
main, no conflicts.script/cibuild: exit 0, 314s wall, freshCHECK_EPOCH; thecheck layers executed rather than replayed —
make fmt-check5.4s,make lint102.1s (0 issues.),make test132.1s — 14oklines,zero
(cached)markers; the 14CACHEDlayers are dependency/modulelayers. The only lint output is the
gomodguarddeprecation alreadytracked as issue #90.
-raceclean.diff or commit. No tags on origin. Naming, no-stutter, inclusive
terminology,
make fmtclean.Nits (non-blocking)
AGENTS.mdpolicy 9 now contradicts the code. Lines 86-87 stillread "If stdout is not a terminal, output the structured logs in
jsonl format." After this change the format follows stderr, so with
vaultik snapshot list --json | jqat a terminal, stdout is not aterminal and the logs are colorized rather than jsonl — a literal
deviation from the written rule.
internal/log/log.goargues thereinterpretation in a code comment, and I agree the policy's
operative content ("when the log destination is unwatched, make it
machine-readable") is faithfully preserved; keying on stdout after
moving the sink would put ANSI escapes on a redirected stderr, which
is plainly worse. Judgement call, stated plainly: I did not treat
this as an iron-rule failure, because the rule said "stdout" only
because that is where logs used to go. But
AGENTS.mdis what thenext agent will read, and it now says something false — it should get
the one-line amendment to "the log stream". Owner's call whether that
lands here or separately.
Vaultik.Stderrnow has zero production writers. AfterwarnWhileListingand thefmt.Fprintf(v.Stderr, …)calls wentaway, the field is only assigned in constructors and tests;
env.stderrinsnapshot_list_test.golikewise has no readers left.vaultik.godocuments the field as deliberately kept to "completethe standard triple", so this is a choice rather than an oversight —
flagging only so it is a known one.
TODO.mdline ~328 (a dated historical entry from earlier work)still says "still a local workaround pending issue #82". Read as an
immutable history record it is fine, and the new entry supersedes it;
noted only because the PR body claims nothing anywhere still cites
issue #82 as pending —
that is true of code comments, not of
TODO.mdhistory.bytesAttrKeyspecial-casing does not survive grouping. Anint64 attribute named
bytesunderWithGroup("x")has keyx.bytesby the timewriteAttrcompares, so it renders as a rawnumber rather than a human byte count. Unreachable today — nothing
in the tree calls
WithGroup— and arguably the more predictablebehaviour. Mentioned for the record.
issue #97 as
Closes [issue #97](…); Gitea's auto-close parser may not match akeyword followed by a markdown link, and the landing commit title
carries only
(closes #82). Worth confirmingissue #97 actually
closes on merge rather than assuming.