feat(logging): report file-sink health on control.status - #237
feat(logging): report file-sink health on control.status#237MichaelTaylor3d wants to merge 2 commits into
Conversation
`dig-logging` 0.1.4 made `init` all-or-nothing: when the rolling file
appender could not be built it returned `Err` and the stderr layer was
never installed either, so the process ran with NO tracing subscriber at
all. A user running `dig-node run` interactively on a host where the
machine log dir belongs to the service account got silence, which reads
as a dead subsystem rather than a broken one.
`dig-logging` 0.2.0 (already declared on main) degrades to console-only
logging instead and reports the reason via `LogGuard::file_error()`.
This wires that signal through:
- `logging::{health, file_error, log_dir, initialized}` expose the guard
state; `health` is pure in its inputs so both arms are testable
without a process-global subscriber.
- `control.status` gains a `logging` object (`initialized`, `dir`,
`file_logging`, `file_error`). A node serving while writing nothing to
disk no longer reports healthy file logging.
- `tests/logging_degraded.rs` proves the property end to end. The
fixture is a path whose PARENT is a regular file, so `create_dir_all`
cannot succeed on any platform -- it does not depend on running
unprivileged, on ACLs, or on a read-only mount, the three things that
make a permission fixture pass for the wrong reason.
- SPEC.md records the degraded contract (SS20.1).
Additive only: a new JSON field on an existing method, no signature
changed. `logging::init`'s two callers (entrypoint.rs, win_service.rs)
are untouched.
Co-Authored-By: Claude <noreply@anthropic.com>
39b2343 to
bafb54a
Compare
Lane progress — resume-readyBranch: Done
Section 2.4b dependency auditEvery One deliberate non-bump, reported rather than done: Remains
Exact next actionRun the gate round against Then, and only then, |
CI note —
|
Correction to the CI-anomaly note above: there is no anomaly.The earlier note reported Measured directly against the check-run record on this tip: At 23:44Z that is 15 minutes elapsed, not 85 — inside the stated baseline. The larger figure was Consequence: do not file the cache-key ticket. Its entire premise was the 4x slowdown, and the Worth carrying: an elapsed time is only a measurement of the job if it is taken from the job's own |
CI is fully green — and the long-run hypothesis is confirmed
That outcome settles the question raised in the previous comment. A cold-cache rebuild finishes; a hang does not. So the delay was real compute, not a stuck test — consistent with the version bump changing All five required checks now pass, asserted BY NAME from branch protection rather than read off the rollup: This PR is still a DRAFT, and that is deliberate
The gate round has still never run on it. Marking it ready would signal the opposite — a ready PR with green checks and no unresolved threads is exactly the state a well-behaved sibling lane squash-merges on sight. Undrafting before a gate verdict returns has already caused a bad release in this repo. Undrafting is the orchestrator's call, after the gate returns, not a side effect of CI going green. Suggested gate tier: one correctness gateThe diff is additive and touches nothing custody-, crypto- or peer-input-shaped:
Possible follow-up, not filedIf the cache-key hypothesis holds generally, every version-bumping PR in this repo pays a ~90-minute CI tax — and under this contract every PR bumps a version. Excluding the version line from the cache key would be a cheap, broad win. Not filed as a ticket from this lane (inflow discipline); flagging it for whoever wants to measure it properly. |
Closing measurement: 18m07s. There is no cache tax. Do not file that ticket.
18 minutes 7 seconds — the bottom of the stated 18-23 minute baseline. This matters because the figure was reported three times, escalating each time: first as A hypothesis restated often enough acquires the grammar of a finding without acquiring its The cargo-cache-key ticket is withdrawn on this evidence. If a version-bumping PR here ever does show PR state5/5 required checks SUCCESS, asserted by name from branch protection. Staying a DRAFT: exit 0 is the |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CORRECTNESS GATE — verdict: CHANGES-REQUIRED
Head reviewed: bafb54a8ccb0d0316f196f301c1d49f57f2c8aa4 (resolved from gh pr view 237 --json headRefOid).
Single leg (§1.10): additive diff, no custody/crypto/peer-input surface.
1. Title vs diff — MATCHES. No finding.
git diff origin/main...bafb54a8 (base 26e640d) is 6 files / +181 -8, and every hunk is file-sink health reporting plus its version bump. Verified the dep-bump half really is gone rather than silently pinned backwards:
origin/main:crates/dig-node-service/Cargo.toml:175andbafb54a8:...:175are bothdig-logging = "0.2".Cargo.lockon both refs resolvesdig-logging 0.2.0. The PR's one-line lock change is thedig-node-serviceversion field, nothing else.- crates.io index (
curl -sH 'User-Agent: dig-loop' https://index.crates.io/di/g-/dig-logging) latest = 0.2.0. No regression, already at latest.
SemVer 0.164.0 -> 0.165.0 minor is right: a new field on an existing control method, nothing removed or renamed.
2. GATING — the SPEC clause this PR adds is not enforceable by the code it describes
See the inline thread on SPEC.md. dig-logging 0.2.0 computes file_error once, at init; it is never revised. So the added normative MUST over-claims, and file_logging: true means "the sink opened at start-up", not "the sink is writing".
3. Non-gating (resolved by me, not blocking)
dig-node-control-interfacedeclarescontrol.statuswith an untyped result plus a prose summary (src/method.rs:538) that does not enumeratelogging— but it already omitswallet_mtlsonmain, so this is pre-existing drift, not introduced here. Not a declared-but-unserved mismatch: the method is declared and served.tests/logging_degraded.rsremoves its blocking fixture file only on the success path; a failing assertion leaves a stray file intemp_dir(). Cosmetic.
Test-vacuity check — PASSES
Counted distinct VALUES per field, not tests. file_logging appears as true (logging.rs health_reports_file_logging_on_when_the_sink_is_live), as false-with-a-reason (health_reports_file_logging_off_and_names_the_reason), and as false-never-installed (health_never_claims_file_logging_when_logging_was_never_installed). A constant in either direction fails one of the three, so the nearest wrong implementation (file_logging = initialized) is caught.
The integration test would fail on the unadopted crate for the right reason: under 0.1.x this input returned Err, no subscriber was installed, and LevelFilter::current() would be OFF — which is exactly what line ~52 asserts against. The fixture (a path whose parent is a regular file) does not depend on privilege, ACLs or a read-only mount, so it cannot pass for the wrong reason under an elevated runner.
Coverage notes / what I did NOT do
- Blast radius by grep + direct read, not gitnexus — the only index is the superproject's, which §2.0 forbids a lane from querying, and a per-worktree
analyzewas not worth the 10-minute bound for a 6-file diff.health/file_error/log_dir/initializedare all new; their only callers arecontrol.rs:981-987and the tests.init's signature is unchanged. - §2.4b: I verified
dig-loggingonly (at latest). I did not sweep everydig-*/chia-*dep ofdig-node-service; stating that rather than implying a clean sweep. - I read everything from git objects. No shared checkout was mutated.
|
Lane claim: working the blocking gate finding on this PR (born-false normative clause at Scope: restate the clause as a start-up verdict, correct the same over-claim in the Branch |
…ive one
The SPEC clause added by this PR ("a node serving while writing nothing to
disk MUST NOT report healthy file logging") is not enforceable by the code
that ships alongside it. dig-logging 0.2.0 computes `file_error` once during
`init` (src/init.rs:91, moved into the guard at :139) and exposes a private
field with a read-only accessor and no mutator (:41/:55), so after a
post-init sink failure -- log dir deleted, volume full, rotation failure --
`file_error()` stays `None` and `control.status` keeps reporting
`logging.file_logging: true`.
Correct the claim to what the code can actually assert: file-sink health is
determined at logger initialization and reported as of that point. The
`logging` object stays -- the capability is real and worth reporting; only
the strength of the claim was wrong. The doc-comments on `logging::health`
and `logging::file_error` carried the same over-claim and now say plainly
that a post-init failure is not detected, and the control.status method
table flags the field as a start-up verdict where a reader meets it.
No runtime behaviour change.
Live file-sink health is tracked as DIG-Network/dig-logging#7.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Fixed in The clause no longer asserts something the code cannot enforce. Same over-claim corrected at its two other readers: the doc-comments on Runtime behaviour is unchanged. Real post-init sink-health detection -- a mutator or periodic revalidation so |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
RE-GATE — verdict: PASS
Head reviewed: 193e7119cd95e9bb505d16ff0d924eb1a0793bc9 (resolved from gh pr view 237 --json headRefOid; previous gate read bafb54a8).
Scope: docs/SPEC only, as briefed. git diff bafb54a8..193e7119 touches exactly two files — SPEC.md and doc-comments in crates/dig-node-service/src/logging.rs. No executable line changed; health()'s body, control.rs and the tests are byte-identical.
The blocking finding is genuinely fixed, not softened into vagueness
The replacement clause (SPEC.md:6710-6718) is still falsifiable in both directions:
- The positive half — "
logging.file_logging: trueasserts only that the rolling JSONL sink OPENED SUCCESSFULLY at start-up, andlogging.file_errornames the reason it did not" — is exactly whattests/logging_degraded.rsalready proves, and whathealth()'s three-value unit coverage discriminates. - The negative half — "a sink failure that occurs AFTER initialization ... is NOT detected" — is a testable negative, not a hedge: delete the log directory after
initand assertfile_loggingstaystrue. A vague sentence would not admit that test. - It names the exact condition that would let the clause be widened again ("once
dig-loggingcan revisefile_errorafter init"), so the boundary is stated rather than left implicit.
SPEC.md:1592 flags it at the point of use ("a START-UP verdict; see §20.1"), so a reader of the method table cannot reach the field without the caveat.
The over-claiming "nearest wrong implementation" note is corrected
logging.rs — was "which is precisely the lie a degraded file sink makes possible"; now "ignoring file_error entirely, which is the lie a start-up sink failure would then tell". That is the accurate statement of what the tests discriminate.
file_error() and health() both now open on the start-up framing and name the three undetected modes (directory deleted, volume full, rotation failure). No doc-comment in the diff still reads as current-state.
Swept beyond where I pointed — no other surface promises live file-logging health
git grep -nE 'file_logging|file_error' 193e7119outsidelogging.rs/SPEC.mdreturns onlycontrol.rs:987(the call site) and the test's own assertions. No README, runbook or other SPEC section.git grep -niE 'log(ging)?[^.]{0,30}health|healthy (file )?logging' 193e7119 -- '*.md'returns one unrelated hit (SPEC.md:1359, the/healthendpoint pointer).dig-node-control-interfacementions neitherfile_loggingnor logging health, so nothing there to correct.
The child ticket cannot be closed by a start-up-only test
DIG-Network/dig-logging#7, Evidence section, verbatim: "a LogGuard whose sink opened successfully reports file_error() == None, then reports Some(...) after the sink is broken at runtime ... i.e. the value genuinely changes within one process. A test that only exercises the start-up-failure path does not close this; that path already passes today." That is the right bar and it explicitly forecloses the vacuous version. It also cites init.rs:41/55/91/139 and links back to this PR and the parent epic.
Non-gating (not blocking, no action required)
control.rs:981-983— the call-site comment still frames the motivation in present tense ("serving and logging to the console while writing nothing to disk"). True of the start-up-failure case it describes, and the function it calls is now documented start-up-only, so no reader is misled. Not worth a round-trip.logging.rslog_dir()'s doc ("Whenfile_erroris set, NOTHING is being written there") is present-tense but conditioned onfile_error, which is now correctly scoped. Same reasoning.
Not re-litigated
Title-vs-diff, SemVer minor, dig-logging = "0.2" at latest, and the three-distinct-values test discrimination were settled at bafb54a8 and are unaffected by a docs-only delta.
Merge state at the time of this review — NOT yet mergeable
check-merge-preconditions.sh --repo DIG-Network/dig-node --pr 237 --allow-draft -> EXIT 1: four required checks SUCCESS, Test + coverage still IN_PROGRESS on 193e7119, unresolvedReviewThreads=0, draft=true. My correctness leg is clear; the orchestrator must still see that check go green before undrafting and merging.
Tracked upstream as DIG-Network/dig_ecosystem#553 (a super-repo issue, so no
Closeskeyword here -- a submodule PR cannot close it; the orchestrator closes it).Status: rebased onto current
main, squashed to one commit, awaiting its gate round. Still a DRAFT deliberately — do not merge until the gate returns.What changed, and why the title moved
The original title said "adopt dig-logging 0.2.0". That half already shipped:
origin/mainhas declareddig-logging = "0.2"since2f0f148(#395, merged while this PR sat idle), andCargo.lockalready resolvesdig-logging 0.2.0. So the dependency bump is now a no-op and the lock is untouched by this PR.What remains — and what this PR is actually for — is wiring the signal that 0.2.0 exposes.
The defect
Under
dig-logging0.1.4,dig_logging::initwas all-or-nothing. When the rolling file appender could not be built it returnedErr, and the stderr layer was never installed either, so the process ran with no tracing subscriber at all. A user runningdig-node runinteractively on a host whereC:\ProgramData\DigNetwork\logs\dig-nodebelongs to the service account got silence — which reads as a dead subsystem rather than a broken one.dig-logging0.2.0 degrades to console-only logging instead, and reports the reason throughLogGuard::file_error(). Nothing consumed that yet, so the node could serve happily while writing nothing to disk and no surface would say so.The change
logging::{health, file_error, log_dir, initialized}expose the guard state.healthis pure in its inputs, so both arms are testable without a process-global subscriber.control.statusgains aloggingobject:initialized,dir,file_logging,file_error. A node serving while writing nothing to disk no longer reports healthy file logging.tests/logging_degraded.rsproves the property end to end.SPEC.mdrecords the degraded contract (section 20.1) and corrects the sentence that still listed "unwritable dir" as aninitfailure.The fixture, and why it is the one that can fail
The nearest wrong implementation reports
file_logging: truewhenever logging initialised — precisely the lie a degraded file sink makes possible. So the unit tests carry an honest control:health(true, dir, None)must reportfile_logging: true, whilehealth(true, dir, Some(err))must reportfalse. A constant in either direction fails one of the two.The integration test's fixture is a path whose parent is a regular file.
create_dir_allcannot succeed under a file on any platform, so the fixture does not depend on running unprivileged, on ACLs, or on a read-only mount — the three things that quietly make a permission fixture pass for the wrong reason, or not fail at all under a runner elevated to Administrator. It also assertsLevelFilter::current()is notOFF, which is the assertion that would have failed on 0.1.x for the right reason.Blast radius
Checked by grep + direct read, not by gitnexus: the only index available is the superproject's, which CLAUDE.md 2.0 forbids querying from a lane, and a per-worktree
analyzewas not worth the ~10-minute bound for a diff this size. Stating the fallback per bound (2).logging::health,file_error,log_dir,initializedare all new. Their only callers arecontrol.rs:984-987and the new test. No existing caller to break.logging::init's signature is unchanged; its two callers (entrypoint.rs:1291,win_service.rs:74) are untouched. The only behaviour change inside it is 0.2.0's degradation, which is the fix.control.statusgains a field and changes none.grepfinds no consumer of aloggingkey anywhere in the tree, so the addition is purely additive on the wire.Version
0.164.0->0.165.0— minor. A compatible new capability: a new field on an existing control method, no removal, no rename, no changed default.Verification
cargo test -p dig-node-service --lib --test logging_degradedRebase note
This branch was 73 commits behind
mainand could not merge understrict=true. It was rebased, not merged. Two conflicts, both resolved by keeping both sides:control.rs(mainhad addedwallet_mtls, this branch addslogging— adjacent fields in the same object, both wanted) and the workspace version (mainat0.164.0, superseding this branch's stale0.125.0). The three original commits are squashed into one; the old third commit's message claimed a0.125.0bump that no longer exists.