fix(reporter): an empty report says why; the first report's window ends now

Closes #407

Problem

The first checkup of a ClickHouse Managed Postgres database (UAT 2026-10-01) had 15 of 25 reports with data: {} and nothing saying why: F004, F005, H001, H002, H004, K001, K003–K008, M001–M003, N001. A reader cannot tell "nothing found" from "nothing collected" from "broken".

What we found on a real ClickHouse Postgres service:

  • postgres_ai_mon has pg_monitor and pg_read_all_stats there and sees every user's rows in pg_stat_statements. The grants were not the cause.
  • The pg_stat_statements metric records a query only after 3 calls and 1 s of total execution time (config/pgwatch-prometheus/metrics.yml). A new or idle database has almost none.
  • The reporter floors the window's end to the hour, and the first run comes soon after the stack starts, so the window ended before the box's first sample. That emptied K001, K003–K008, M001–M003 and N001 (UAT 2026-10-02).
  • D004's user: "unknown" comes from the metric, which has no user label. This MR leaves it as it is.

Change

Empty reports say why

format_report_data gives every node result without data available and a one-line reason.

  • available: true ("nothing found") needs evidence: the generator passes collected=True when the check's source metric had rows for the node and the report dropped them all. H001, H002 and H004 pass it (their rows can all be system-schema indexes).
  • Everything else empty is available: false:
    • K/M: the collection threshold.
    • N001: an idle database, or no collection yet.
    • A007: not collected (initdb always writes non-default settings).
    • F004/F005/H001/H002/H004 without rows: "none, not collected yet, or the query failed". These metrics have a row only for what they find, so no row cannot be read as an all-clear.
    • Any other check: a generic line.
  • A node result from a per-node report keeps its annotation when nodes are combined.
  • A report with data is unchanged. data is not touched.

First report window

config/scripts/postgres-reports.sh passes --use-current-time on its first cycle. Later cycles keep the hour-aligned window; USE_CURRENT_TIME=true still applies to every cycle. Noted in docker-compose.yml next to the reporter overrides.

Contract 1.1.0

The schemas accept available and reason at node level (optional). CONTRACT_VERSION is 1.1.0 in reporter/postgres_reports.py and cli/lib/checkup.ts. cli/README.md and cli/CHANGELOG.md say the fields are full-mode only (express does not emit them) and add a node-result field table.

Tests

  • tests/reporter/test_empty_reports.py: exact (available, reason) for all 30 schema check IDs, with and without evidence; unknown ID; non-dict node value; combined nodes; Prometheus unreachable through the six generators; system-schema-only rows; the first-report window (first sample 12:05, run 12:30) through K001.
  • tests/reporter/test_postgres_reports_script.py: first cycle --use-current-time, later ones not; USE_CURRENT_TIME=true on every cycle.
  • The 16 mutants from review r1/r2 are killed.
  • pytest tests/reporter: 615 passed, 14 skipped.

Evidence

Prometheus unreachable (the review's repro), before and after:

mr446_unreachable_prometheus

The real reporter CLI against a stub Prometheus for a fresh box (one database, no settings, no bloat or index rows, one H002 row for a pg_catalog index):

mr446_fresh_box_cli

What we did not check

  • The markdown rendering of these reports on the platform. The platform reads only results[host]["data"], so reason is not shown there yet.
  • Found, not fixed here: K001's hourly output (other_calls_hourly) does not validate against K001.schema.json, on main too.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GRNbWXUQJebE3vqWjAgTFh

Edited by Maya P

Merge request reports

Loading
Loading