Fail loudly on the three ways a bundle comes back empty - #36
Conversation
All three shapes were hit on one 12-node on-prem cluster: of seven bundles
collected, five described nothing, and the two that did arrived with a window
that stopped eight days before collection and an alert section that had not
evaluated at all. Each failure reported success.
Dotfiles are no longer walked as query files. Nothing downstream filters by
extension — manager.go executes whatever FindCompatibleQueries returns — so
unpacking the release on macOS and copying the folder to a server made the
collector send `._system.parts.sql` to ClickHouse and try to parse
`._replica_readonly.yaml` as a rule. That bundle reported 47 of 84 collectors
failed ("security validation failed for '._system.parts.sql'") and 15 of 15
alerts unparseable ("yaml: control characters are not allowed"), which buried
the three genuine failures and left the alert section worthless. rootDir itself
stays exempt, since "." and "./queries.onprem" are both legitimate.
A `-to` in the past now warns. Only `to < from` was validated, so a window
left over from an earlier incident truncates query_log, part_log, metric_log
and text_log at that instant while system.errors and system.parts still
describe now — the bundle looks complete and the recent hours are simply
absent. The real run used `-to 2026-09-16` on the 24th and lost two Keeper
session losses that had happened in between.
A user who can see no databases now warns before collection. With SELECT on
system but no SHOW, the object-listing tables are filtered to nothing and the
protected ones fail with 497, so the run "succeeds" and ships an archive of a
few hundred KB that describes no user data. Five nodes were collected that way
and forwarded before anyone noticed.
README: the documented grant set gains SHOW COLUMNS — without it
`system.columns` returns no rows for user databases while every other file
looks normal — and a note not to substitute `SELECT ON <db>.*`, which collects
the same bundle but hands the diagnostic user read access to customer data.
Verified on 26.2.19.43: both warnings observed end to end (the stale window
without a server, the grant preflight against a grantless user and a
system-only user on a throwaway container); the dotfile tests fail without the
finder change; gofmt, go vet and go test ./... clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved test and preflight issues can cause failures or missed warnings.
Review effort: Lite
Findings: 1
Open (3)
What changed in this PR
Improves detection of incomplete diagnostic bundles by filtering dotfiles, warning about stale windows and missing visibility, and updating grant guidance.
Changes:
- Filter dotfiles from query and alert discovery.
- Add stale-window and database-visibility warnings.
- Update grant documentation and finder tests.
| File | Summary |
|---|---|
README.md |
Documents grants; related operational references still need updating. |
internal/query/versioned_test.go |
Adds dotfile tests; one test fixture is invalid. |
internal/query/finder.go |
Skips dotfiles during discovery. |
cmd/main.go |
Adds preflight warnings; format handling, error classification, and dry-run behavior require changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "system.parts.sql": "SELECT 1", | ||
| "._system.parts.sql": "\x00\x05\x16\x07AppleDouble", | ||
| ".DS_Store": "\x00\x00\x00\x01", | ||
| "._23.5.1.0": "\x00\x05\x16\x07AppleDouble", |
There was a problem hiding this comment.
Not a conflict — the fixture uses two different names: ._23.5.1.0 is the standalone dotfile and ._24.8.1.0/system.parts.sql is the hidden directory, so both create fine. The test runs and passes in CI (test job green on every push). Leaving it as is.
| if dbErr != nil || strings.TrimSpace(dbCount) == "0" { | ||
| if dbErr != nil { | ||
| fmt.Printf("Warning: user '%s' cannot read system.databases (%v), so most collectors "+ | ||
| "will fail with Code: 497 and the bundle will be nearly empty.\n", username, dbErr) | ||
| } else { | ||
| fmt.Printf("Warning: user '%s' can see no databases outside system, so this bundle will "+ | ||
| "not describe any of your tables, parts or replicas — the files will be empty rather "+ | ||
| "than missing.\n", username) | ||
| } | ||
| fmt.Println(" Grant the diagnostic privileges and re-run:") | ||
| fmt.Println(" GRANT SHOW DATABASES, SHOW TABLES, SHOW COLUMNS ON *.* TO " + username + ";") | ||
| fmt.Println(" GRANT SELECT ON system.* TO " + username + ";") | ||
| } |
There was a problem hiding this comment.
Fixed in b9d3cba — agreed. The outcome is now classified: the server's own denial (Code: 497 / ACCESS_DENIED) keeps the privilege message and the GRANT advice; a zero count keeps the "can see no databases" message; any other error — a dial timeout, a 159, a parse error on an old server — says the check could not be made, that collection continues, and lists the grants only as "the fix if the listing files come back empty". Table-tested with the real 497 text, an i/o timeout and a 159 (cmd/preflight_test.go). Also: the probe no longer runs in dry-run, where the client answers with a synthetic empty response and the verdict would have been about the client, not the server.
| GRANT SHOW DATABASES, SHOW TABLES, SHOW COLUMNS ON *.* TO sys_read_only; | ||
| GRANT SELECT ON system.* TO sys_read_only; |
There was a problem hiding this comment.
Fixed in b9d3cba — and there were more than the two you found: running-the-tool.md (the grant block and the troubleshooting row), SKILL.md, health-checks.md's coverage row, and the README's own cloud grants box a few lines below this one had all kept the old set. All five now say SHOW DATABASES, SHOW TABLES, SHOW COLUMNS, and running-the-tool.md carries the same warning as the README against substituting GRANT SELECT ON <db>.*.
…OLUMNS everywhere Review findings on #36. The visibility probe reported every error as "cannot read system.databases, collectors will fail with 497" and printed GRANT advice. A network timeout, a server-side 159 or an old server's parse error took the same branch, and the operator would have been sent to fix privileges that were fine. The outcome is now classified: the server's own denial (Code: 497 / ACCESS_DENIED) keeps the privilege message and the grants; a zero count keeps the "can see no databases" message; any other error says the check could not be made, that collection continues, and what the grants are if the listing files come back empty. Table-tested with the real 497 text, a dial timeout and a 159. The probe also ran in dry-run, where the client answers every query with a synthetic empty response — so its verdict there was about the client, not the server. It is skipped when the client is in dry-run. SHOW COLUMNS was added to the README's grant set but not to the four other places the set is stated: the skill's running-the-tool.md (the grant block and the troubleshooting row), SKILL.md, health-checks.md's coverage row, and the README's own cloud grants box. All five now agree, and running-the-tool carries the same warning as the README against substituting GRANT SELECT ON <db>.*. Not changed: the dotfile test. The finding that "._24.8.1.0" is written as both a file and a directory misreads the fixture — the standalone dotfile is "._23.5.1.0", the hidden directory is "._24.8.1.0/", and the test passes in CI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Pushed b9d3cba on top of this branch for the review round (Camilo asked me to close it out): the visibility preflight now distinguishes a real |



Why
One on-prem cluster, 12 nodes, seven bundles collected. Five described nothing at all, and the two that did arrived with a collection window that stopped eight days early and an alert section that had never evaluated. Every one of those runs reported success, and the gaps were only found by reading the execution logs by hand a week later.
Three independent failure modes, each of which the tool could have named at the time.
1 — Dotfiles were walked as query files
FindCompatibleQuerieswalked with no dotfile filter, and nothing downstream filters by extension —manager.goexecutes whatever the walk returns. Unpacking the release on macOS and copying the folder to a server leaves an AppleDouble._namebeside every file and directory, so the collector sent._system.parts.sqlto ClickHouse and tried to parse._replica_readonly.yamlas a rule.The real bundle:
Three of those 47 failures were real (
system.crash_log,system.blob_storage_log_7_daysandsystem.zookeeper_log_errors_1_dayare absent on that version). The other 44 were noise, and the alert section was worthless.Fixed in the walk, so both the query path and the alert path (
FindVersionedFiles) are covered.rootDiritself stays exempt —.and./queries.onpremare both legitimate, and there is a test for that.2 — A
-toin the past passed silentlyOnly
to < fromwas validated. A window left over from an earlier incident truncatesquery_log,part_log,metric_logandtext_logat that instant, whilesystem.errors,system.partsand the other point-in-time tables still describe now — so the bundle looks complete and the recent hours are simply absent rather than healthy. The real run used-to 2026-09-16on the 24th and lost two Keeper session losses that had happened in between; we only found them throughzookeeper_connection.connected_time.Two hours of slack, so a normal "collect the last few hours" run stays quiet.
3 — A user who can see no databases now warns before collecting
With
SELECT ON system.*but noSHOW, the object-listing tables are filtered to nothing and the protected ones fail with 497. The run completes, and the archive is a few hundred KB whosesystem.tables,system.columnsandsystem.databasesfiles are 0 rows / 0 bytes — empty, not missing. Five nodes were collected that way and forwarded to support before anyone noticed.The
dbErr != nilbranch is defensive and not exercised by either shape I tested:system.databasesis grant-filtered rather than access-denied, so even a user with no grants at all takes the count-is-zero path.4 — README grant set
SHOW COLUMNSadded. Without itsystem.columnscomes back with no rows for user databases while every other file looks normal — the existing note coversSHOW DATABASES/SHOW TABLESbut not this.Also a warning not to substitute
GRANT SELECT ON <db>.*. It populates the listing tables and collects an identical bundle — measured on 26.2.19.43,system.tables2,system.columns5,system.databases2,system.parts1 under either grant set — but it gives the diagnostic user read access to customer data, where theSHOWgrants leave aSELECTagainst a user table refused with 497. That distinction matters for regulated customers, and this one had already been told the wrong thing.Verification
gofmt,go vet ./...,go test ./...— all clean (run ingolang:1.23.9).internal/query/versioned_test.go. Reverting onlyfinder.gomakes them fail with the exact production symptom:dotfile leaked into results: name="._system.parts.sql" path="23.5.1.0/._system.parts.sql", plus the oldSkipping directory with invalid version format: ._24.8.1.0noise.clickhouse/clickhouse-server:26.2(26.2.19.43) on a private Docker network, with both aSELECT ON system.*-only user and a user with no grants.Not in scope
query_log_detailsstill duplicates every sum per table touched (LEFT ARRAY JOIN tables). The SQL comment warns,SKILL.mdwarns, andinspect_bundle.pyhandles it correctly — but reading the file directly still multiplies counts by the table fan-out, which is how an 8× over-count reached a customer draft on this case before review caught it. Emitting thetablescardinality as a column would let any reader divide; worth a separate discussion.🤖 Generated with Claude Code