feat(minimal,diagnostics): raise the log tail default to 32 MiB and make it a flag - #898
Conversation
The diagnostic bundle's per-log tail has been fixed at LOG_TAIL_CAP (5 MiB) since `min bug` landed. That was a comfortable window while the daemons logged at INFO, but #872 now forwards the host's RUST_LOG to the guest daemon over the kernel cmdline, and RUST_LOG=debug is one of the documented workarounds on #869 — so the user most likely to be capturing a bundle is the one whose guest daemon is at DEBUG. There is no intra-day size rotation either, so the current day's file grows without bound while the tail stays fixed, and the captured window can begin well after the incident. The flag defaults to LOG_TAIL_CAP, so an unflagged `min bug` passes the same value to the same call sites and its output is unchanged. Values above the writer's ceiling are rejected at parse time rather than clamped: a clamp reports success while capturing less than was asked for, and the resulting short log then reads as a fact about the system instead of a fact about the flag. MAX_LOG_TAIL_BYTES lives in `diagnostics::bundle` because the reason for the ceiling lives there — add_file_tail seeks -(cap as i64) from the end, so a cap past i64::MAX is a nonsense seek. The guest-side half waits for the daemon bundle RPC: there is no DiagBundleRequest on main, so `min bug` is host-only here. Refs: #869 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 5 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
Comment |
|
@macroscope review |
|
Manual reviews triggered for commit All prior checks · these links stay valid even if you push more commits. |
|
Just FYI for future @mentions, I'm Review in progress. Results will be posted as check runs when complete. |
ApprovabilityVerdict: Would Approve Adds a configurable --log-tail-bytes flag to the diagnostic command with proper validation and comprehensive tests. The author owns all changed files and the changes are additive with no security or runtime risk implications. Macroscope would have approved this PR. Enable approvability here. |
5 MiB was chosen when the daemons logged at INFO. #872 now forwards the host's RUST_LOG into the guest, and RUST_LOG=debug is a documented workaround on #869, so the bundle most likely to be captured is the one whose daemon is at DEBUG. Measured on a real RUST_LOG=debug capture from this machine, re-enveloped into mlog's JSON-lines shipping format: - 214 B/record, so 5 MiB held ~24.5k records and 32 MiB holds ~157k - log text compresses ~70x in the bundle's zstd stream, so five rotated files cost ~2.2 MiB of archive at the new cap, up from ~0.3 MiB The wire cost is therefore negligible; the cost that binds is uncompressed. add_file_tail buffers a whole tail in memory, so the cap is also peak allocation per file, and a bundle collects up to ~16 log files, bounding extraction near 512 MiB. Going higher buys linear history for linear extraction cost with no categorical win: the same capture produced 36 MB of records in eight seconds, nearly all of it russh chatter rather than minimal's own records, so no per-file cap holds a DEBUG incident. 32 MiB also keeps 2x headroom under MAX_LOG_TAIL_BYTES so --log-tail-bytes stays useful in both directions; a const assertion now enforces that rather than leaving it to be noticed in review. Refs: #869 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The merge of origin/main (log_tail_bytes, #898) into this branch updated the logs() signature but missed the two Linux-gated unit-test call sites, which macOS cargo check never compiles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Why now
min bug's per-log tail has been pinned atLOG_TAIL_CAP(5 MiB) since thecommand landed, applied per file by
BundleWriter::add_file_tail. That was acomfortable window while the daemons logged at INFO. Two things have changed:
forwards the host's
RUST_LOGto the guest daemon over the kernel cmdline.RUST_LOG=debugis one of the documented workarounds on#869 — so the user most
likely to be capturing a bundle is now precisely the one whose guest daemon
is logging at DEBUG, where 5 MiB is a very short window.
bound while the tail stays fixed, so a 5 MiB tail of a large file can begin
well after the incident being diagnosed.
The result is a bundle that looks complete and silently omits the interesting
part. This PR both raises the default and makes it adjustable.
What this plumbs
LOG_TAIL_CAPraised 5 MiB → 32 MiB, so an unflaggedmin buggets thebigger window. Arithmetic below.
--log-tail-bytesonBugArgs, defaulting toLOG_TAIL_CAPrather thanrestating it, so the flag tracks the default automatically.
collect::logs, which forwards it to bothadd_file_tailcallsites — the rotated host logs (
minimald.log*,minvmd.log*) and theprovider-scoped
run.log/boot.log. Those are the only two onmain.MAX_LOG_TAIL_BYTES(64 MiB) added todiagnostics::bundle, next toLOG_TAIL_CAP. It lives there because the reason for the ceiling livesthere:
add_file_tailseeks-(cap as i64)from the end, so a cap pasti64::MAXis a nonsense seek and a very large one is an unbounded read.const _: () = assert!(LOG_TAIL_CAP * 2 <= MAX_LOG_TAIL_BYTES)keeps thedefault from creeping up until the flag is downward-only. Compile-time, so
it cannot land silently.
Picking 32 MiB
Measured on a real
RUST_LOG=debugcapture from this machine (18.4 MB,168,963 records), re-enveloped into
mlog's JSON-lines shipping format, sincethe on-disk logs here predate Unit 4 and are still console-format:
What each cap buys, measuring the compressed size of an actual tail of that
length rather than assuming a ratio:
Wire cost is not the constraint. Log text compresses ~70x, so the whole
increase costs about 2 MiB of archive. Nobody notices that in a bundle they
are uploading once.
The constraint that binds is uncompressed.
add_file_taildoesVec::with_capacity(len.min(cap))andread_to_end— the entire tail isbuffered in memory, so the cap is peak allocation per file (sequential, so
peak is one cap, not twelve). And
collect::logsgathers 2 prefixes x 5rotated files + 2 per provider:
Why not higher. The tempting argument for 48 MiB is that the measured
session is 36.1 MB in shipping format, so 32 MiB does not hold all of it. That
argument does not survive the rate: those 18.4 MB arrived in 8 seconds
(22,316 records/s), and 168,962 of the 168,963 records are
russh— onedependency, not minimal's own code. At that rate 32 MiB is ~7s of history and
48 MiB is ~11s. Neither "holds the incident", so stretching the default buys
linear history for linear extraction cost and no categorical win, while
pushing a normal bundle toward 768 MiB extracted and halving the flag's upward
headroom.
Why not lower. 16 MiB would be defensible on extraction size alone, but
32 MiB is still only ~512 MiB extracted worst case, and it doubles the
retained history for ~1.3 MiB more archive. Steady-state INFO on this machine
measures 61–852 KiB/day, so at INFO both values are effectively "weeks"; the
difference only matters at DEBUG, which is exactly the case #872 makes likely.
So the evidence supports the 32 MiB candidate — though for a different reason
than "6x the current": it is the largest value whose uncompressed footprint
still looks like a normal bundle.
Truncation check
The premise that a 256 MiB client-side content limit might be pushed into
truncation does not apply, for two independent reasons:
maintruncates.Redaction::Truncatedis declared inmanifest.rsand has no constructor anywhere onmain. TheGUEST_BUNDLE_MAX_BYTES = 256 MiBconstant lives onfeat/diag-unit7-guest-fetch, not here.bundle streamed over vsock, and the daemon bundles 5 files at its own
log_tail_cap(which defaults toLOG_TAIL_CAP). At 32 MiB that is~2.3 MiB compressed — 0.9% of the cap, ~110x headroom. Even at the
64 MiB ceiling it is ~4.7 MiB.
Validate, don't clamp
Over-large values are rejected at parse time with an error naming the ceiling,
rather than clamped to it. A clamp returns success while quietly capturing less
than was asked for, so the resulting short log reads as a fact about the system
instead of a fact about the flag — the same trap as
SO_VM_SOCKETS_BUFFER_SIZE,which returns success while changing nothing when the value exceeds an unrelated
maximum.
0is rejected too. On the wire contract0is the "use the daemon default"sentinel; taken literally on the host side it would bundle empty files labelled
tail-capped. Rejecting it keeps the two from disagreeing about what0meansonce the guest path lands.
Manifest honesty
The manifest does not record which cap was applied, and this PR does not add
it. Today a reader can only recover the cap indirectly: a
tail-cappedentry'sbytesis the cap, becauseadd_file_tailreads exactlycapbytes when itcaps. That is an inference, not a record, and it says nothing at all when no
file happened to exceed the cap.
Recommendation: add a
log_tail_bytesfield toManifestso a non-defaultcap is stated outright. It is deliberately not done here — it changes the bundle
schema and
BundleWriter::finish's signature, both of whichfeat/diag-unit6-daemon-bundlealready rewrites, and this PR is meant to be aflag rather than a collector change.
The equality is unaffected by the raised default, because it is a property of
add_file_tail(take(cap).read_to_end(...)yields exactlycapbytes on acapped file) rather than of the value. It stays pinned at two levels:
bundle::tests::add_file_tail_caps_large_filesat the writer, anda_custom_tail_cap_applies_to_every_logend-to-end throughcmd_bug. Both useexplicit caps, so neither silently follows the default.
What has to wait for Units 6/7
On
maintoday:BugArgshas only--output. There is no--no-guestand no--guest-timeout-secs— those live onfeat/diag-unit7-guest-fetch.DiagBundleRequest, nowith_log_tail_bytes, and no guestrequest of any kind.
crates/minimald/src/diag.rsdoes not exist.min bugis host-only.
So the guest-request half of this change is not implementable on
mainandis left for Unit 7. When it lands,
cmd_bugshould passargs.log_tail_bytesthrough
DiagBundleRequest::with_log_tail_bytes— a setter that today has nonon-test caller anywhere.
One thing to fix when Unit 7 lands: its daemon-side handler defines its own
private
MAX_LOG_TAIL_BYTESincrates/minimald/src/diag.rsand clamps(
requested.min(MAX_LOG_TAIL_BYTES)), which is the exact pattern this PR arguesagainst. The daemon does need a hard ceiling — it cannot trust a remote caller —
but it should reject the request rather than silently serve a shorter tail than
was asked for, and it should import
diagnostics::MAX_LOG_TAIL_BYTESinstead ofkeeping a second copy of the number.
Verification
cargo test -p minimaldoes not build on macOS onmain:minimaldev-depends on
minimald, which pullshakoniwa → libcgroups → procfs, andprocfs' build script refuses any non-Linux target. Confirmed pre-existing by
stashing this branch and reproducing on a clean tree. Tests were therefore run
on Linux via
cross, the pathjust test-crossdocuments for exactly this.diagnostics: 43 passed, 0 failed.cross clippy -p minimal -p diagnostics --all-targets -- -D warnings: exit 0.The two failures are pre-existing and environmental — the cross container
has no
ip/ss, sohost/net/interfaces.txtis never captured and the netcollectors record errors. Verified by stashing this branch and running the same
target on clean
mainin the same container: identical two failures, identicalassertions. They are unrelated to the log tail.