Skip to content

fix(observability): repair admin Command Center (summary/trends/top/rollups) - #655

Merged
Detair merged 2 commits into
mainfrom
fix/command-center-rollup-refresh
Jul 11, 2026
Merged

fix(observability): repair admin Command Center (summary/trends/top/rollups)#655
Detair merged 2 commits into
mainfrom
fix/command-center-rollup-refresh

Conversation

@Detair

@Detair Detair commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Problem

The admin Command Center showed no data at all — reported live as empty vital signs and charts. The browser console showed the endpoints erroring, not just returning empty:

  • GET /api/admin/observability/summary500 "Database error"
  • GET /api/admin/observability/trends?...400 (body "Failed to …", not JSON)

Root causes (three distinct bugs)

  1. SUM(bigint)NUMERIC decode failure (summary 500, top-routes/top-errors 500). value_count is bigint; SUM(value_count) returns NUMERIC, which can't decode into the Rust i64 the code expects. This was invisible on an empty databaseSUM over zero rows is NULL, which decodes fine — so it only broke in production once telemetry accumulated. Fixed by casting the aggregates back to ::bigint (in summary_error_and_request_counts, query_top_routes, query_top_errors, and the rollup materialized view's total_count/error_count).

  2. Repeated metric= params couldn't be parsed (trends 400). The trends endpoint's metric: Vec<String> used axum::extract::Query (serde_urlencoded), which doesn't support repeated keys → 400 Failed to deserialize query string. The client always sends 6 metric= params, so trends never worked. Switched to axum_extra::extract::Query (serde_html_form) and enabled the axum-extra query feature.

  3. 7d/30d rollups additionally empty. The telemetry_trend_rollups materialized view could never REFRESH … CONCURRENTLY because its unique index was on an expression (COALESCE(route,'')), which PostgreSQL rejects. Recreated the view with route as a plain non-null column + a plain-column unique index; the refresh job now falls back to a blocking refresh if a concurrent one can't proceed.

Why it wasn't caught

These observability query functions had zero test coverage, and the one failure mode (SUM decode) only appears with data. Added server/tests/integration/observability_queries.rs, which seeds samples and asserts summary/top-routes/top-errors/trends all decode.

Verification

  • All fixes validated against the production database (read-only / rolled-back transaction): the SUM(...)::bigint aggregates now report type bigint; the recreated matview populates (222 rows). Applied an immediate one-off refresh on beta so 7d/30d has data now.
  • cargo build, lib clippy -D warnings, fmt --check clean; integration test compiles. (Full sqlx::test run happens in CI — local container networking is broken in this env.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01JDEzVG39gGZFozPCndbSid

Detair and others added 2 commits July 11, 2026 01:18
The `telemetry_trend_rollups` materialized view backing the admin Command
Center's 7d/30d trend charts could never be refreshed. Its unique index was
built on an expression (`COALESCE(route, '')`), and PostgreSQL only accepts a
plain-column unique index as the row-matching index for `REFRESH ...
CONCURRENTLY`. So the hourly refresh failed every cycle with "cannot refresh
materialized view concurrently", the view stayed empty since introduction, and
7d/30d charts showed no data. Live (1h/6h/24h) ranges were unaffected — they
read `telemetry_metric_samples` directly.

- Migration recreates the view with `route` as a non-null column
  (`COALESCE(labels->>'http.route', '')`) and a plain-column unique index on
  `(day, metric_name, scope, route)`, which CONCURRENTLY accepts.
- The refresh job now falls back to a blocking `REFRESH` if the concurrent
  refresh can't proceed, so rollups self-heal instead of silently going stale.

Verified against the production schema/data in a rolled-back transaction: the
recreated view populates (222 rows) and the index is plain-column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDEzVG39gGZFozPCndbSid
The Command Center showed no data once telemetry accumulated — the endpoints
errored rather than returning empty:

- summary (500) and top-routes/top-errors: their SUM(value_count) over the
  bigint column returns NUMERIC, which failed to decode into i64. Cast the
  aggregates back to ::bigint. (Invisible on an empty DB: SUM over zero rows
  is NULL, which decodes fine — so it only broke in production with data.)
- trends (400): the endpoint used axum's Query (serde_urlencoded), which
  can't parse repeated `metric=` params into a Vec. Switch to axum_extra's
  Query (serde_html_form); enable the axum-extra `query` feature.
- 7d/30d rollups: the materialized view's SUM columns are also cast to
  ::bigint so the rollup trend query decodes.

Adds an integration regression test that seeds samples and asserts the
summary/top-routes/top-errors/trends queries decode with data present — the
coverage gap that let this ship (previous tests ran against an empty DB).

Verified against production data (types now decode as bigint; migration
populates the matview).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDEzVG39gGZFozPCndbSid
@Detair Detair changed the title fix(observability): repair command-center trend rollup refresh fix(observability): repair admin Command Center (summary/trends/top/rollups) Jul 10, 2026
@Detair
Detair merged commit d44c387 into main Jul 11, 2026
18 checks passed
@Detair
Detair deleted the fix/command-center-rollup-refresh branch July 11, 2026 00:15
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.

1 participant