Skip to content

feat(minimal,diagnostics): raise the log tail default to 32 MiB and make it a flag - #898

Merged
norrietaylor merged 2 commits into
mainfrom
feat/min-bug-log-tail-bytes
Jul 22, 2026
Merged

feat(minimal,diagnostics): raise the log tail default to 32 MiB and make it a flag#898
norrietaylor merged 2 commits into
mainfrom
feat/min-bug-log-tail-bytes

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 22, 2026

Copy link
Copy Markdown
Member

Why now

min bug's per-log tail has been pinned at LOG_TAIL_CAP (5 MiB) since the
command landed, applied per file by BundleWriter::add_file_tail. That was a
comfortable window while the daemons logged at INFO. Two things have changed:

  • #872 merged tonight and
    forwards the host's RUST_LOG to the guest daemon over the kernel cmdline.
    RUST_LOG=debug is 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.
  • There is no intra-day size rotation. The current day's file grows without
    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_CAP raised 5 MiB → 32 MiB, so an unflagged min bug gets the
    bigger window. Arithmetic below.
  • --log-tail-bytes on BugArgs, defaulting to LOG_TAIL_CAP rather than
    restating it, so the flag tracks the default automatically.
  • Threaded to collect::logs, which forwards it to both add_file_tail call
    sites — the rotated host logs (minimald.log*, minvmd.log*) and the
    provider-scoped run.log / boot.log. Those are the only two on main.
  • MAX_LOG_TAIL_BYTES (64 MiB) added to diagnostics::bundle, next to
    LOG_TAIL_CAP. It lives there 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 and a very large one is an unbounded read.
  • A const _: () = assert!(LOG_TAIL_CAP * 2 <= MAX_LOG_TAIL_BYTES) keeps the
    default 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=debug capture from this machine (18.4 MB,
168,963 records), re-enveloped into mlog's JSON-lines shipping format, since
the on-disk logs here predate Unit 4 and are still console-format:

raw zstd -19 ratio B/record
console format (as captured) 18,385,469 443,861 41.4x 109
mlog JSON-lines (shipping) 36,126,536 505,227 71.5x 214

What each cap buys, measuring the compressed size of an actual tail of that
length rather than assuming a ratio:

cap records kept wire bytes/file 5 files, wire
5 MiB 24,521 64,170 320,850
16 MiB 78,467 202,782 1,013,910
32 MiB 156,933 463,380 2,316,900
48 MiB 235,400 703,884 3,519,420
64 MiB 313,867 938,513 4,692,565

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_tail does
Vec::with_capacity(len.min(cap)) and read_to_end — the entire tail is
buffered in memory, so the cap is peak allocation per file (sequential, so
peak is one cap, not twelve). And collect::logs gathers 2 prefixes x 5
rotated files + 2 per provider:

cap 1 provider 3 providers
5 MiB 60 MiB extracted 80 MiB
16 MiB 192 MiB 256 MiB
32 MiB 384 MiB 512 MiB
48 MiB 576 MiB 768 MiB

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 — one
dependency, 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:

  1. Nothing on main truncates. Redaction::Truncated is declared in
    manifest.rs and has no constructor anywhere on main. The
    GUEST_BUNDLE_MAX_BYTES = 256 MiB constant lives on
    feat/diag-unit7-guest-fetch, not here.
  2. When it lands it will not bind. It caps the compressed nested guest
    bundle streamed over vsock, and the daemon bundles 5 files at its own
    log_tail_cap (which defaults to LOG_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.

0 is rejected too. On the wire contract 0 is 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 what 0 means
once 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-capped entry's
bytes is the cap, because add_file_tail reads exactly cap bytes when it
caps. 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_bytes field to Manifest so a non-default
cap is stated outright. It is deliberately not done here — it changes the bundle
schema and BundleWriter::finish's signature, both of which
feat/diag-unit6-daemon-bundle already rewrites, and this PR is meant to be a
flag 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 exactly cap bytes on a
capped file) rather than of the value. It stays pinned at two levels:
bundle::tests::add_file_tail_caps_large_files at the writer, and
a_custom_tail_cap_applies_to_every_log end-to-end through cmd_bug. Both use
explicit caps, so neither silently follows the default.

What has to wait for Units 6/7

On main today:

  • BugArgs has only --output. There is no --no-guest and no
    --guest-timeout-secs — those live on feat/diag-unit7-guest-fetch.
  • There is no DiagBundleRequest, no with_log_tail_bytes, and no guest
    request of any kind. crates/minimald/src/diag.rs does not exist. min bug
    is host-only.

So the guest-request half of this change is not implementable on main and
is left for Unit 7. When it lands, cmd_bug should pass args.log_tail_bytes
through DiagBundleRequest::with_log_tail_bytes — a setter that today has no
non-test caller anywhere.

One thing to fix when Unit 7 lands: its daemon-side handler defines its own
private MAX_LOG_TAIL_BYTES in crates/minimald/src/diag.rs and clamps
(requested.min(MAX_LOG_TAIL_BYTES)), which is the exact pattern this PR argues
against. 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_BYTES instead of
keeping a second copy of the number.

Verification

cargo test -p minimal does not build on macOS on main: minimal
dev-depends on minimald, which pulls hakoniwa → libcgroups → procfs, and
procfs' 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 path just test-cross documents for exactly this.

test diag::tests::a_cap_within_the_contract_is_accepted_verbatim ... ok
test diag::tests::a_zero_or_non_numeric_cap_is_rejected ... ok
test diag::tests::an_over_large_cap_is_rejected_not_clamped ... ok
test diag::tests::omitting_the_flag_uses_the_shared_default ... ok
test result: ok. 79 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

test a_custom_tail_cap_applies_to_every_log ... ok
test bug_without_daemon_still_produces_a_bundle ... ok
test incident_collectors_land_and_mask_macs ... FAILED
test logs_collects_newest_five_per_prefix_and_provider_logs ... FAILED
test planted_secrets_never_reach_the_bundle ... ok
test result: FAILED. 3 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out

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, so host/net/interfaces.txt is never captured and the net
collectors record errors. Verified by stashing this branch and running the same
target on clean main in the same container: identical two failures, identical
assertions. They are unrelated to the log tail.

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>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9f33589-c4d0-4302-aff4-4bfeadf93a98

📥 Commits

Reviewing files that changed from the base of the PR and between f03ced1 and dcb2ab1.

📒 Files selected for processing (5)
  • crates/diagnostics/src/bundle.rs
  • crates/diagnostics/src/lib.rs
  • crates/minimal/src/diag/collect.rs
  • crates/minimal/src/diag/mod.rs
  • crates/minimal/tests/bug.rs

Comment @coderabbitai help to get the list of available commands.

@norrietaylor

norrietaylor commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

@macroscope review

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Manual reviews triggered for commit cf8e81a:

All prior checks · these links stay valid even if you push more commits.

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Just FYI for future @mentions, I'm Macroscope-App, not Macroscope.

Review in progress. Results will be posted as check runs when complete.

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Approvability

Verdict: 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>
@norrietaylor norrietaylor changed the title feat(minimal): add --log-tail-bytes to min bug feat(minimal,diagnostics): raise the log tail default to 32 MiB and make it a flag Jul 22, 2026
@norrietaylor
norrietaylor merged commit 132369f into main Jul 22, 2026
29 checks passed
@norrietaylor
norrietaylor deleted the feat/min-bug-log-tail-bytes branch July 22, 2026 17:06
norrietaylor added a commit that referenced this pull request Jul 22, 2026
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>
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