Skip to content

feat(#87): metrics gear — KPI band, Error Insights, drill-down drawer - #90

Merged
sarg3nt merged 3 commits into
mainfrom
feature/metrics-improvement-pass-87
May 14, 2026
Merged

feat(#87): metrics gear — KPI band, Error Insights, drill-down drawer#90
sarg3nt merged 3 commits into
mainfrom
feature/metrics-improvement-pass-87

Conversation

@sarg3nt

@sarg3nt sarg3nt commented May 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a 6-card KPI summary band (with sparklines + Δ vs prior window), an Error Insights panel (top backends / source IPs / countries by 4xx+5xx), and a slide-in drill-down drawer with per-backend mini-charts, top sources, and recent 5xx HAProxy log lines parsed live from the agent.
  • All new endpoints sit on top of existing stats_history and traffic_flows tables — no new agent collection required.
  • Crosshair tooltips and a gradient fill on the 5xx chart for the visual polish the issue asked for.
  • Also includes a planning doc (docs/research/metrics-source-agnostic.md) outlining a 9-phase plan to make the gear source-agnostic so it works on plain Linux boxes and any detected web server / proxy. That doc is a follow-up — this PR doesn't implement it.

What changed

New backend (gearbox/internal/framework/handler/, gearbox/internal/framework/database/):

  • GET /api/{id}/metrics/summary?range=… — KPI cards
  • GET /api/{id}/metrics/error-breakdown?range=… — top backends/sources/countries
  • GET /api/{id}/metrics/backend/{name}/details?range=… — drill-down data
  • GET /api/{id}/metrics/log-errors?status_min=500&backend=… — recent parsed HAProxy log lines
  • DB queries in a new metrics_insights.go file
  • 22 new unit tests for KPI math, sparkline downsampling, log-line parsing, and range parsing

Dashboard UI (history.templ):

  • KPI band at top, replacing the previous "scroll down to see anything" layout
  • Error Insights panel replacing "Recent Incidents" (incidents are still queryable; we just don't surface them here anymore)
  • Drill-down drawer (slides in from right) with Esc/backdrop close
  • Crosshair tooltips and gradient fill applied to existing charts via the shared option helpers

Docs:

  • gearbox/internal/gears/metrics/README.md rewritten to describe the new layout, endpoints, and data sources
  • docs/research/metrics-source-agnostic.md — the follow-up plan
  • docs/research/README.md — links the new plan

Issue answered

Closes #87.

The original issue asked three things, addressed as follows:

Issue ask How it's addressed
"Errors skyrocketing — but where?" Error Insights panel shows top backends by 5xx + 4xx, ranked. Click a row → drawer with per-backend mini-charts + top source IPs + recent 5xx log lines.
"Requests going up — who/what/where?" KPI band shows requests/min with prior-window delta + sparkline. Drill-down drawer lists top source IPs hitting any backend.
"Traffic gear says 29% error rate to light-hugger — find it" Clicking the relevant backend row in Error Insights opens the drawer with the live request-vs-error chart, status-code mix, top sources, and 5xx log lines.

Test plan

  • make templ-generate && make build — clean build (verified locally)
  • go vet ./... — clean (verified locally)
  • go test ./... — full suite passes, including 22 new tests (verified locally)
  • Visit /history on mjolnir with HAProxy data present — KPI band populates, Error Insights shows backends, click-through opens drawer with log lines
  • Hover any chart — crosshair tooltip shows all series at the same x
  • Click 5xx chart fullscreen icon — chart expands, gradient fill visible
  • Drawer behaviour: Esc closes; backdrop click closes; close (×) button closes; opens cleanly on second/third use without leftover charts
  • No regression on existing 7 charts or the time-range/resolution controls

🤖 Generated with Claude Code

sarg3nt and others added 2 commits May 14, 2026 10:05
The Metrics page used to be seven Chart.js panels and a "Recent Incidents"
list. When the 5xx chart spiked the user had no way to find out *which*
backend, *what* path, or *which* source IP was responsible — they had to
hunt through the Logs gear's many sources by hand.

Reworked the page into four stacked surfaces:

1. KPI summary band — six stat cards with sparklines and delta vs. the
   previous window: Requests/min, Avg Response, Error Rate %, 5xx Errors,
   Active Sessions, Healthy Backends N/M. Cards colour by health and
   delta arrows invert so "up" is red for errors, green for traffic.
2. Charts grid — same seven charts, now with crosshair tooltips
   (index-mode hover) and a gradient fill on the 5xx Errors chart so
   spikes pop visually.
3. Error Insights panel (replaces Recent Incidents) — three columns of
   top backends / source IPs / countries by 4xx+5xx count, each row
   clickable. Renders an empty-state pill when the window is quiet.
4. Drill-down drawer — slides in from the right with a per-backend
   summary, requests+errors mini-chart, status-code doughnut, top
   sources hitting the backend, and recent 5xx HAProxy log lines
   parsed live from the agent's haproxy log.

Backend additions (no new agent collection — everything sits on top of
the existing stats_history and traffic_flows tables):

- /api/{id}/metrics/summary?range=…
- /api/{id}/metrics/error-breakdown?range=…
- /api/{id}/metrics/backend/{name}/details?range=…
- /api/{id}/metrics/log-errors?status_min=500&lines=…&backend=…

The log-errors endpoint parses HAProxy access-log lines with a small
regex set and returns structured records (status / source IP / backend /
method / path). It degrades gracefully when the agent's logs aren't
reachable — the drawer shows a "logs unavailable" hint instead of
breaking the rest of the page.

22 new unit tests cover the helpers and the HAProxy log parser.

Closes #87

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the first-pass implementation, the deeper question is whether the
gear is HAProxy-only by accident. Audited the agent side and confirmed:
the bones are in place (gear.ProbeResult schema exists, discovery/
package exists with HAProxy/Docker/systemd detectors), but the
capability manifest endpoint that manager.go references is unshipped
and discovery isn't wired into the probe phase.

The new doc lays out a 9-phase plan that's strictly additive at each
step:

  0. Ship /api/v1/system/capabilities (the "upcoming" endpoint the
     codebase already references).
  1. Source-attribute every chart/KPI in the UI (wording only).
  2. Graceful no-HAProxy mode — show host metrics on plain boxes.
  3. discovery/{nginx,apache,caddy,traefik}.go probes.
  4. nginx metrics gear — first non-HAProxy source, proves the model.
  5. Promote access-log parsing agent-side with format profiles.
  6. Error Insights panel becomes multi-source.
  7. Apache / Caddy / Docker / Traefik.
  8. Optional cross-source aggregates.

Phases 0–2 are recommended as the next PR — they make the page honest
on non-HAProxy boxes and set up the capability manifest abstraction
that every later phase depends on.

A condensed version of the plan was posted as a comment on #87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 14, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new "insights" surface to the Metrics gear: a 6-card KPI band, an Error Insights panel (top backends/IPs/countries by 4xx+5xx), and a slide-in drill-down drawer with per-backend mini-charts, top sources, and live-parsed HAProxy 5xx log lines. Backed by four new dashboard endpoints that aggregate over existing stats_history and traffic_flows tables (no agent changes). Also includes a research doc outlining a future source-agnostic plan.

Changes:

  • New backend handlers (api_metrics_insights.go + helpers + DB queries) for KPI summary, error breakdown, backend details, and HAProxy log-error parsing, registered under /api/{boxID}/metrics/*.
  • Major rework of history.templ: KPI band with sparklines, Error Insights replacing Recent Incidents, slide-in drill-down drawer, gradient fills + crosshair tooltips on existing charts.
  • README rewrite for the metrics gear and a new 9-phase research doc on going source-agnostic.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
gearbox/cmd/server/main.go Registers four new /metrics/* routes.
gearbox/internal/framework/handler/api_metrics_insights.go KPI/error-breakdown/details/log-errors handlers + HAProxy log line parser.
gearbox/internal/framework/handler/api_metrics_insights_helpers.go KPI math, sparkline downsampling, error-totals helpers.
gearbox/internal/framework/handler/api_metrics_insights_test.go Unit tests for range parsing, KPI math, log parsing, downsampling.
gearbox/internal/framework/database/metrics_insights.go New traffic_flows aggregation queries (top backends/sources/countries, per-backend timeseries, error-rate buckets).
gearbox/internal/framework/templates/pages/history.templ KPI band, Error Insights panel, drill-down drawer markup, JS, and CSS; chart gradient/crosshair upgrades.
gearbox/internal/gears/metrics/README.md Documents the new layout, endpoints, and data sources.
docs/research/metrics-source-agnostic.md Phased plan for evolving Metrics into a source-agnostic surface.
docs/research/README.md Index entry for the new research doc.
Comments suppressed due to low confidence (1)

gearbox/internal/framework/handler/api_metrics_insights.go:121

  • TotalRequests in stats_history is the cumulative HAProxy request counter (lifetime since process start), not a per-bucket count. maxStatField(curr, ...) returns the largest value of that counter in the window, and perMinute(currReqMax, since, until) then divides that absolute counter by the window length. On a long-running HAProxy with, say, 100M lifetime requests, a 24h window will report ~70 000 requests/min regardless of how many requests actually arrived during that day. To get the actual rate, compute (max - min) of the counter in the window (handling counter resets if HAProxy restarted) and divide by the window minutes. The same issue applies to the TotalRequests sparkline — the current sparkline plots the cumulative counter, which is an almost-flat ramp rather than a meaningful "requests per bucket" trend. The previously-existing total_5xx_errors-based KPI has the same shape.
	currReqMax := maxStatField(curr, func(s database.StatsSnapshot) float64 { return float64(s.TotalRequests) })
	prevReqMax := maxStatField(prev, func(s database.StatsSnapshot) float64 { return float64(s.TotalRequests) })
	currReqRate := perMinute(currReqMax, since, until)
	prevReqRate := perMinute(prevReqMax, prevSince, since)
	cards = append(cards, kpiCard{
		Key:         "requests",
		Label:       "Requests / min",
		Value:       currReqRate,
		Unit:        "/min",
		Decimals:    1,
		PrevValue:   prevReqRate,
		DeltaPct:    pctDelta(currReqRate, prevReqRate),
		Status:      "good",
		Sparkline:   statSparkline(curr, func(s database.StatsSnapshot) float64 { return float64(s.TotalRequests) }, 30),
		Description: "Total HTTP requests served per minute, averaged across the window.",
	})

Comment thread gearbox/internal/framework/handler/api_metrics_insights.go Outdated
Comment thread gearbox/internal/framework/handler/api_metrics_insights.go
Comment thread gearbox/internal/framework/templates/pages/history.templ
Three findings, all valid:

1. The prev-window stats slice was un-trimmed. GetStatsHistory takes
   a lower bound only, so prevRaw spanned both windows; the `prevOnly`
   index list I computed was promptly discarded with `_ = prevOnly`,
   so every prev-window aggregate was actually a union aggregate and
   the resulting delta_pct was ~0 % regardless of actual change.
   Fixed by collapsing prevRaw to entries strictly before `since`.

2. APIMetricsLogErrorsHandler returned HTTP 403 when the user lacked
   `logs:view`, but the README and the frontend's loadLogErrors() both
   expected the structured {available:false, reason:…} envelope, so
   users without log access saw the generic "Failed to load log lines"
   error instead of the documented hint. Switched to the envelope.

3. escapeJSArg only handled the JS string context (\\ and '), but the
   value is interpolated into a double-quoted HTML onclick attribute,
   so an embedded " could break out and a < could start a new tag.
   Hardened to also escape &<>" — defence in depth on the source-IP
   path where values flow from agent-collected data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sarg3nt
sarg3nt merged commit 502bd46 into main May 14, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metrics gear: Improvement pass

2 participants