feat(#87): metrics gear — KPI band, Error Insights, drill-down drawer - #90
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
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
TotalRequestsinstats_historyis 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, andperMinute(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 theTotalRequestssparkline — the current sparkline plots the cumulative counter, which is an almost-flat ramp rather than a meaningful "requests per bucket" trend. The previously-existingtotal_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.",
})
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>
38 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
stats_historyandtraffic_flowstables — no new agent collection required.What changed
New backend (
gearbox/internal/framework/handler/,gearbox/internal/framework/database/):GET /api/{id}/metrics/summary?range=…— KPI cardsGET /api/{id}/metrics/error-breakdown?range=…— top backends/sources/countriesGET /api/{id}/metrics/backend/{name}/details?range=…— drill-down dataGET /api/{id}/metrics/log-errors?status_min=500&backend=…— recent parsed HAProxy log linesmetrics_insights.gofileDashboard UI (
history.templ):Esc/backdrop closeDocs:
gearbox/internal/gears/metrics/README.mdrewritten to describe the new layout, endpoints, and data sourcesdocs/research/metrics-source-agnostic.md— the follow-up plandocs/research/README.md— links the new planIssue answered
Closes #87.
The original issue asked three things, addressed as follows:
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)/historyon mjolnir with HAProxy data present — KPI band populates, Error Insights shows backends, click-through opens drawer with log lines🤖 Generated with Claude Code