Skip to content

Darling Web: Custom Views v2 — compose-your-own metrics & dashboards (#1563)#1579

Merged
erikdarlingdata merged 7 commits into
devfrom
feature/1563-custom-views-v2
Jul 19, 2026
Merged

Darling Web: Custom Views v2 — compose-your-own metrics & dashboards (#1563)#1579
erikdarlingdata merged 7 commits into
devfrom
feature/1563-custom-views-v2

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Custom Views v2 — compose-your-own metrics & dashboards

Closes the flexibility gap in #1563. Rebuilds #1576's custom views (a picker over ~69 fixed reads, 4 viz types) into a real compose-your-own metric/dashboard model: choose the metric, filter it (incl. name patterns), group it (server/database/object), pick the aggregation, units, and chart type, and re-scope a whole dashboard at once. Reuses the entire #1574 web host + security stack and the #1576 stored-view CRUD; both panel kinds coexist (a panel is composed when it names a source, a v1 read when it names a read).

Scope was designed, hard-reviewed (a model/UX review + a security/feasibility review, both grounded in the code), and locked (custom dashboards + same-source ratios IN v1; full core pass; rollup tier deferred). Built by three subagents (backend machinery + vertical slice; full catalog fill; frontend composer); this PR is the integrated result after an orchestrator full-diff + security pass.

Backend

  • Measure catalog (Compose/MeasureCatalog.cs) — ~40 measures across the queryable collector tables; unit/archetype/dimension are a hand-authored test-pinned layer over CollectorCatalog.PayloadColumns. Four archetypes (cumulative / delta / gauge / per-event) gate legal aggregations; same-source ratios give the correct execution-weighted avg-per-execution, signal-wait %, latch avg wait, file avg latency.
  • Safe compiler (Compose/ComposeCompiler.cs) — spec → parameterized SQL; identifiers come only from the catalog, emitted schema-qualified collect.<table>; every value is bound (server_name = ANY, = ANY/LIKE, the naive-UTC window, LIMIT); date_trunc bucketing; the Queries pane: attribute Top Queries rows to a named module (procedure/function) or ad hoc — Lite + Darling viewer #1568 module join is window-bounded + server-scoped.
  • ComposeSpec.TryParsePanel — the single write-time + compile-time authority; rejects any off-catalog identifier, so a stored def always compiles and vice-versa.
  • Fleet / multi-serverserver is a universal dimension; view-level $server/$database + a global time range drive every panel; All / omitted = the whole fleet.
  • DoS controlsstatement_timeout on the viewer+mcp roles via role provisioning (NOT a migration — a role GUC has no probeable schema footprint, so a version bump would break the connect-time gate); 90-day window ceiling; window×bucket-count cap; forced bucketing; per-panel caps.
  • APIGET /api/catalog gains a compose node (data-driven composer); POST /api/compose/run compiles+runs one panel on the least-privilege viewer pool → {sql, rows}.

Frontend (vanilla ES modules, air-gapped)

  • Per-panel composer (editor.js): category-grouped measure/ratio picker, aggregate from the measure's valid set, a Shape control (over-time / ranked / single), group-by chips, filter rows (dimension + op + literal/multi/$var; LIKE only on likeable dims), a chart picker that greys viz types incoherent with the shape, grain-trap guidance, and a live WYSIWYG preview.
  • View-scope bar (server single/multi/All + time + variables) that re-runs every panel.
  • Render path (compose.js): pivots the compiler's three row shapes into line/area/stacked/bar/pie/stat/table; honest no-data / partial-fleet / error states. All untrusted text reaches the DOM via textContent.

Safety

Network-reachable surface: no arbitrary SQL (allowlisted catalog identifiers only, schema-qualified collect.*, never rides search_path); the config control-plane is excluded by the allowlist (verified by test); CSRF via the application/json requirement; window clamp + statement_timeout as the runaway backstop.

Testing

Independently verified by the orchestrator: build 0 errors; full Darling.Tests green — 2601 passed / 0 failed / 141 gated-live skipped. Includes catalog (table,column)/time-column/gauge-non-summable/ratio-scalar pins, spec validation (off-catalog + percentile-on-non-per-event + LIKE-on-non-likeable + undeclared-variable + caps), compiler safety (schema-qualified, all-bound, config-excluded, #1568-bounded), viz coherence, and explicit Flow A (proc-name LIKE on $server → line) + Flow B (top-10 DBs by CPU across the fleet → bar). Installer.Tests + DB-touching tests deliberately not run.

Deferred (documented follow-ups — honest limits, not silent holes)

Gauge-based ratios (grant used/ideal, single-use-plan %) + a Query Store execution-weighted AVG need an avg/avg per-row compiler primitive Compose doesn't have yet (QS ships executions + peak duration/CPU, which are correct); per-server measure-availability greying needs an appliesTo catalog field (no-data + partial-fleet states cover honest degradation); stacked-bar (stacked-area today), thresholds/annotations, per-panel drill-down, and notebook mode are phase-2.

🤖 Generated with Claude Code

erikdarlingdata and others added 7 commits July 18, 2026 22:30
…#1563)

The B1 backend for Custom Views v2 — a real metric/dimension/unit/filter/group/
chart composer over the collector store, alongside v1's fixed-read picker.

- MeasureCatalog: hand-authored slice (wait/cpu/procedure/query/file_io/long-query)
  proving all four archetypes (cumulative/delta/gauge/per-event) + same-table ratios +
  the #1568 derived object_name; pinned to the real collector PayloadColumns by test.
- ComposeSpec.TryParsePanel: the single write-time + compile authority — cross-checks
  every identifier-bearing field vs the catalog + declared view variables, with the
  ship-blocking DoS caps (window/bucket/topN/filter/groupBy).
- ComposeCompiler: catalog-only identifiers, schema-qualified collect.*, every value
  bound (= ANY / LIKE / LIMIT / naive-UTC window), archetype-gated aggs, percentile
  per-event only, same-table ratios, unit conversion, window-bounded #1568 module join.
- ValidateDefinition rewritten to dispatch v1 read panels vs v2 composed panels and to
  validate view-level variables + range; the v1 read-panel contract is preserved.
- /api/catalog gains a v2 "compose" section; new POST /api/compose/run compiles + runs
  one panel spec on the least-privilege viewer pool, returning rows + the compiled SQL.
- statement_timeout DoS backstop on the viewer + mcp roles (managed provisioning +
  tools/provision-roles.sql). Delivered as role provisioning, NOT a versioned migration:
  a role setting has no probeable schema footprint, so a V32 bump would break the
  viewer's connect-time schema-version gate.
- Scrubbed the stale "loopback-only" comments (DarlingManagedRoles + provision-roles.sql)
  to match the shipped any-authenticated-seat networked-edit behavior.
- DarlingComposeTests: catalog pins, spec validator, compiler safety (config-exclusion,
  bound params, #1568 bounding, archetype gating, unit conversion, DoS ceiling),
  ValidateDefinition v1/v2 dispatch, /api/catalog v2, and the provisioning backstop.

Build + full Darling.Tests green (2580 passed, 141 live-gated skipped; Installer.Tests
and DB-touching tests deliberately not run).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the two design-locked gaps on the B1 compose backend (Erik Decision 1 / §2b +
acceptance Flow B; design §4), keeping every safety invariant.

- server is now a universal virtual dimension (server_name, present on every source) —
  filterable and group-by-able on every measure (MeasureCatalog.Dimension resolves it
  for any source; ComposeSpec allows it regardless of a measure's AllowedDimensions).
- ComposeRunContext takes an OPTIONAL/multi server scope instead of a single ServerId:
  null/empty $server = the whole fleet (no server predicate); one or many names => a
  bound server_name = ANY($n), never interpolated. So "top-N databases by CPU across the
  fleet" and "top servers by CPU" (the grain-trap alternative) both compile.
- The #1568 module CTE is now fleet-aware: partitioned by (server_name, sql_handle),
  scoped to the same server set, still window-bounded; the join matches server + handle.
- /api/compose/run resolves the $server variable (a name, an array, or "All"=fleet) to
  that scope — no server-id resolution, no hardcoded single server.
- v2 viz vocabulary (line/area/bar/stacked/pie/table/stat) with viz<->mode coherence
  (line/area/stacked = time series; bar/pie = ranked; stat = scalar; table = any mode),
  so a stored def can never be un-renderable. v1 read panels keep their own KnownViz.
- /api/catalog compose section serves the v2 viz vocab + a universalDimensions:[server].
- Tests extended: fleet (no-scope / multi-server bound array / group-by-server), the
  fleet-aware #1568 CTE, server allowed on a server-grain measure, viz<->mode coherence.

Build + full Darling.Tests green (2593 passed, 141 live-gated skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two end-to-end tests proving the acceptance flows validate AND compile on the B1
backend, per the build brief:
- Flow A: avg procedure elapsed LIKE 'dbo.usp_Payment%' on $server, hourly -> line
  (schema-qualified, time-bucketed, LIKE + server bound, pattern never interpolated).
- Flow B: top-10 databases by CPU across the whole fleet -> bar (grouped by database,
  ranked with a bound LIMIT, no server predicate = fleet-wide).

Build + full Darling.Tests green (2595 passed, 141 live-gated skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author the remaining collector tables into the compose measure catalog against the same
archetype/unit/dimension model B1 proved, every (table,column) pinned to the real
collector PayloadColumns by the existing parameterized pins.

Added measures + dimensions for 23 tables:
- Cumulative+delta: latch_stats, spinlock_stats.
- Gauges: cpu_scheduler_stats, plan_cache_stats, memory_stats, memory_clerks,
  memory_grant_stats, tempdb_stats, database_size_stats, index_object_stats (sizes +
  cumulative usage exposed as current-snapshot gauges since no delta column exists),
  session_stats, session_summary_stats, waiting_tasks, query_snapshots.
- Per-event: blocked_process_reports + dmv_blocking_snapshots (count + block-duration
  percentile), deadlocks + system_health_events (count-only), default_trace_events,
  job_history.
- running_jobs (gauge duration); perfmon_stats (EAV counter value/delta; unit is a
  'count' placeholder because cntr_type is not stored — the user picks the counter).
- query_store_stats: the correct-composable subset only (executions summable; peak
  duration/CPU as MAX gauges). The pre-aggregated AVG durations are deliberately omitted
  (a plain avg-of-avgs is wrong per design; a correct execution-weighted average needs a
  compiler primitive Compose lacks) — deferred as a documented follow-up, not shipped wrong.

Deliberately EXCLUDED (not composable numeric metrics): the config snapshots
(server_config, database_config, database_scoped_config, trace_flags, server_properties
- point-in-time config, on-load only), agent_status (a boolean status snapshot), and
memory_pressure_events (ordinal 0/2/3+ indicators). server stays the universal fleet
dimension on every source.

Tests: explicit B3 coverage (source list, count-only event compile, server-grain gauge
group-by-server, per-event count + percentile) on top of the parameterized pins.

Build + full Darling.Tests green (2599 passed, 141 live-gated skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bread-and-butter derived metrics the B3 fill was missing (design §2c), each a correct
execution-weighted / rate ratio over same-source cumulative measures — SUM(a)/NULLIF(SUM(b),0),
never an avg-of-avgs:
- query/procedure avg duration + avg CPU PER EXECUTION (the correct execution-weighted form of
  "average duration") — adds query_executions as the divisor scalar (procedure_stats already had
  proc_executions).
- latch avg wait per request (ms/request); file avg read latency (ms/read) — adds file_reads scalar.

The existing pins already validate these (ratios reference real same-source scalars); added a ratio
compile test proving the SUM/NULLIF(SUM) shape.

Build + full Darling.Tests green (2600 passed, 141 live-gated skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build the B2 frontend composer on top of the v1 read-panel skeleton; both
panel kinds coexist (a stored panel is composed when it names a `source`,
read when it names a `read`).

Composer (editor.js):
- Per-panel kind toggle (Composed metric / Read); new panels default composed.
- Data-driven off /api/catalog's `compose` node (never a hardcoded measure):
  measure/ratio picker grouped by category + self-describing caption, aggregate
  from the measure's validAggregates, a Shape control (Over time / Ranked /
  Single value => timeBucket|topN), group-by chips (allowed dims + fleet),
  typeahead-free filter rows (dimension + op + literal/multi/$var; LIKE only on
  likeable dims), and a chart picker that greys out viz types incoherent with
  the shape (with a reason).
- View scope: a default time range + template variables ($database one-click);
  stored as the definition's `range` + `variables`.
- Grain-trap guidance: a measure with no per-database dimension points to
  same-category measures that have one.
- Live WYSIWYG preview via /api/compose/run; unit/percentile/per-panel-time and
  the compiled SQL fold away under Advanced.

Render path (compose.js, new): runs a composed panel against the view scope and
shapes the compiler's three row shapes into charts.js — pivots grouped time
series into multi-series, ranks into bar/pie categories, formats every value in
the panel's chosen unit. Empty rows are a clean no-data state; a group-by-server
panel notes it only lists collecting servers.

Charts (charts.js): area + stacked modes on the line chart, plus horizontal bar
and donut renderers; a wider categorical palette. All SVG stays here (one
SVG_NS occurrence for the air-gap scan).

Saved views (pages/views.js): a view-level server (single/multi/All) + time +
variable control bar that re-scopes every panel at once; composed panels
dispatch through /api/compose/run, read panels through v1 renderPanel.

views-api.js surfaces the compose catalog and extends the client-side import
validator to composed panels. compose.js is pinned in DarlingWebAssetsTests.

Build succeeded; DarlingWebAssets + air-gap self-containment tests green (26);
compose + web-endpoint contract tests green (111).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1563)

Add the [Unreleased] CHANGELOG entry for the v2 metric composer (#1579) and correct a stale ComposeCompiler param-order doc comment found in the integration review (the window is $1/$2 with an optional bound server ANY, not a leading serverId).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 7d5cca3 into dev Jul 19, 2026
2 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/1563-custom-views-v2 branch July 19, 2026 03:52
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