feat(#91): metrics gear — source-aware, no-HAProxy mode (phases 0-2) - #93
Merged
Conversation
Lay the foundation for a source-agnostic Metrics gear by introducing
the MetricSource concept end-to-end. The page now declares which
collector produces each number ("HAProxy: Sessions & Requests",
"Host: CPU Load", …) and gracefully degrades on boxes without HAProxy.
Phase 0 — Capability manifest plumbing on the dashboard:
- New per-box CapabilitiesCache (5-min TTL, 3s fetch timeout, negative
caching) in the agent client package; tested with httptest.
- Handler-level BoxCapabilities accessor + cache instance; reconnect
events (server.connected) invalidate so a restarted agent's new probe
table is reflected immediately.
- New GET /api/{boxID}/capabilities endpoint surfaces the cached
manifest to the dashboard's auth-scoped frontend.
- filterGearsByAgentCapabilities (issue #71) refactored onto the cache —
removes one synchronous agent call per Gears-page render.
Phase 1 — Source attribution in the UI:
- Chart card titles carry source-prefix badges (HAProxy / Host) so the
reader can tell which collector produced the metric at a glance.
- KPI cards render a small source-badge in the upper-right corner;
server emits source+source_label on every card.
- "Error Insights" → "HAProxy Error Insights" with explicit copy.
Phase 2 — Graceful no-HAProxy mode:
- /api/{boxID}/metrics/summary checks the haproxy capability and only
emits HAProxy KPI cards when the gear is available; host KPIs (memory,
disk, load 1m) are always emitted from system_metrics_history.
- Frontend hides HAProxy-tagged chart cards and the Error Insights
panel when capabilities lack haproxy; shows an empty-state banner.
- Fail-open everywhere — flaky capabilities don't lock the user out.
CPU%, uptime, and failed-systemd KPIs aren't persisted yet; they'll
follow when the collector starts saving them.
Refs: docs/research/metrics-source-agnostic.md
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds dashboard-side capability awareness for the Metrics gear so /history can attribute metrics to sources and degrade when HAProxy is unavailable.
Changes:
- Adds cached per-box agent capabilities and exposes them via
/api/{boxID}/capabilities. - Updates Metrics UI with source badges, HAProxy-specific hiding, and no-HAProxy messaging.
- Adds host KPI cards and helper/cache unit tests.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Ignores local config file. |
gearbox/cmd/server/main.go |
Registers the capabilities API route. |
gearbox/internal/framework/agent/capabilities_cache.go |
Adds per-box capabilities cache/accessors. |
gearbox/internal/framework/agent/capabilities_cache_test.go |
Tests capabilities cache behavior. |
gearbox/internal/framework/handler/api_capabilities.go |
Adds dashboard capabilities API handler. |
gearbox/internal/framework/handler/api_metrics_insights.go |
Adds source fields, HAProxy gating, and host KPI cards. |
gearbox/internal/framework/handler/api_metrics_insights_helpers.go |
Adds system-metrics KPI helper functions. |
gearbox/internal/framework/handler/api_metrics_insights_test.go |
Tests new system-metrics helpers. |
gearbox/internal/framework/handler/gears.go |
Reuses capabilities cache for gear filtering. |
gearbox/internal/framework/handler/handler.go |
Wires capabilities cache and reconnect invalidation. |
gearbox/internal/framework/templates/pages/history.templ |
Adds source UI badges and HAProxy capability-based hiding. |
Comments suppressed due to low confidence (4)
gearbox/internal/framework/handler/api_metrics_insights.go:367
- Using
avgPositiveSysFieldfor load average drops legitimate zero readings from idle hosts, so a window with samples like 0, 0, 1.0 is reported as 1.0 instead of ~0.33.system_metrics_historystoresload_average_1directly from the agent, where zero is a valid value, unlike the HAProxy response-time buckets this helper mirrors.
currLoad := avgPositiveSysField(curr, func(s database.SystemMetricsSnapshot) float64 { return s.LoadAverage1 })
prevLoad := avgPositiveSysField(prev, func(s database.SystemMetricsSnapshot) float64 { return s.LoadAverage1 })
gearbox/internal/framework/handler/api_metrics_insights.go:291
- The function comment says
hostKPICardsreturns a network KPI, but the implementation only builds memory, disk, and load cards. This stale documentation makes it look like a network card is missing from the code when it appears to be intentionally omitted from this phase.
// hostKPICards returns the host-level KPI cards (memory, disk, load,
// network). Computed from the dashboard's system_metrics_history table.
gearbox/internal/framework/templates/pages/history.templ:893
- Capability gating uses the same
hiddenclass thattoggleFullscreen()uses for temporary fullscreen state. On a no-HAProxy box, entering and then exiting fullscreen on a host chart will run the existing fullscreen exit path that removeshiddenfrom every other.chart-card, making the HAProxy cards visible again despite the capability result. Use a separate capability-hidden state or reapply capability filtering when leaving fullscreen.
document.querySelectorAll('[data-source="haproxy"]').forEach(function(el) {
el.classList.toggle('hidden', !haproxyAvailable);
gearbox/internal/framework/templates/pages/history.templ:214
- This copy says the Error Insights data is reported by HAProxy logs, but the panel is populated from
/metrics/error-breakdown, which aggregates thetraffic_flowstable built from HAProxy traffic/stats data rather than log parsing. That can mislead users about the data source and how to troubleshoot missing rows.
<h3 class="text-xl font-semibold text-gray-800 dark:text-gray-100">HAProxy Error Insights</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Where the 4xx/5xx responses in this window are coming from, as reported by HAProxy logs. Click any row to drill in.
- api_capabilities.go: gate /api/{boxID}/capabilities on
ComponentMetrics + PermissionView. The manifest enumerates installed
services on the host, enough for fingerprinting in multi-tenant
deploys — a user without metrics:view shouldn't enumerate the
software inventory. Mirrors APIMetricsSummaryHandler's gate.
- history.templ: no-HAProxy banner now picks copy from the actual
capability entry — not_installed / inaccessible / disabled each get
distinct guidance, and the agent's `reason` is surfaced verbatim.
Previous copy ("Install HAProxy") pointed at the wrong fix when the
binary was present but stats unreachable.
- capabilities_cache.go: cache key is now (boxID, agentURL) so an
operator editing a box's Agent URL gets fresh capabilities on the
next render rather than stale data until the 5-min TTL expires.
Invalidate() drops every entry for the boxID regardless of URL.
- haproxy_config.go: invalidate the capabilities cache on box update
and delete so Agent URL / API key edits take effect immediately.
- api_metrics_insights.go: capability gating uses the sourceHAProxy
constant instead of a duplicate "haproxy" literal — prevents future
drift between KPI source IDs and capability lookups.
New tests:
- TestCapabilitiesCacheDifferentAgentURLBypassesCache — same boxID with
different agent URLs returns each agent's actual verdict, not stale.
- TestCapabilitiesCacheInvalidateDropsAllAgentURLsForBox — Invalidate
drops every entry for a boxID across all URLs it was fetched against.
This was referenced May 14, 2026
sarg3nt
added a commit
that referenced
this pull request
May 15, 2026
) * feat(#95): agent — phase 3 source detection (nginx, Apache, Caddy, Traefik, Docker, host) Completes the remaining detection work for issue #95 on top of #96's primary-source selection. Six new probe-only gears land their verdicts in the capability manifest so the dashboard sees a complete picture of which HTTP / container sources exist on each host. No metrics collection yet — that lands in Phase 4+ per-source. The four web-server detectors declare CategoryHTTPRequests via MetricSourceGear, so the resolver from #96 now actually picks between alternatives — set GEARBOX_AGENT_HTTP_SOURCE=nginx on a host with both HAProxy and nginx and the manifest flips. Per-source env overrides for non-default surfaces: NGINX_STATUS_URL / NGINX_CONFIG_FILE APACHE_STATUS_URL / APACHE_CONFIG_FILE CADDY_ADMIN_URL TRAEFIK_METRICS_URL DOCKER_SOCKET Also drops the dead internal/framework/discovery/ package (superseded by the ProbeableGear interface from #93; nothing imported it). The discovery/docker.go detection logic was modest — binary lookup + os.Stat + systemctl is-active — and lives on in the new docker gear with the new manifest plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#95): address Copilot review findings on PR #100 Substantive: - probe.isLoopback: parse URL with net/url + check net.IP.IsLoopback so userinfo-spoofed URLs like https://localhost@evil.com/ can't trick the helper into skipping TLS verification against evil.com. - probe.HTTPGet: validate maxBody > 0; return error instead of silently returning empty body that would hide sentinel mismatches. - docker probe: capture stat's FileInfo and check Mode()&ModeSocket so a regular file or directory at the socket path is flagged Inaccessible with a reason that points at the bind-mount mismatch. - traefik probe: track the last non-matching HTTP response across the fallback URL list; the Inaccessible reason now distinguishes "wrong service on this port (200 without sentinel)" from "no listener (connection refused)" so operators debug the right cause. Comment/text alignment: - caddy/traefik/apache Info().Description: Phase 4+ → Phase 7+ to match each file's package header and the docs. - nginx wellKnownConfigPaths comment: "all four" → "all three" (slice has three entries). - nginx Probe comment: "two `nginx -V` runs" → "one `nginx -v` and one `nginx -V`" — accurate process-invocation count. - docs/source-detection.md: "six-step decision tree" → "four-step" to match the rendered numbered list. New tests: - probe_test.go: isLoopback covers userinfo spoof regression, plain loopback variants, and unparseable input. HTTPGet rejects non-positive maxBody. - docker plugin_probe_test.go: new regression test for "path exists but is not a socket". Existing fakeFileInfo now carries a Mode field. - traefik plugin_probe_test.go: new test asserts the Inaccessible reason distinguishes "200 without Traefik sentinel" from generic "unreachable". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 15, 2026
Merged
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
Lay the foundation for a source-agnostic Metrics gear by introducing the MetricSource concept end-to-end. The page now declares which collector produces each number ("HAProxy: Sessions & Requests", "Host: CPU Load", …) and gracefully degrades on boxes without HAProxy.
This PR lands phases 0-2 of #91. Each phase is independently valuable; together they ship the foundation every later phase (nginx, Apache, Caddy, Docker) builds on.
Phase 0 — Capability manifest plumbing (dashboard side)
agent.CapabilitiesCache(5-min TTL, 3s fetch timeout, negative caching) memoises per-box probe tables so gear-page handlers don't fire a fresh agent call on every render.getBoxCapabilities()+BoxCapabilitiesaccessor withHas/IsAvailable/Entryhelpers.EventTypeServerConnectedand drops the cache for that box so a restarted agent's new probe table is reflected immediately rather than at the next TTL boundary.GET /api/{boxID}/capabilitiesendpoint surfaces the cached manifest to the auth-scoped frontend.filterGearsByAgentCapabilities(issue Box gear configuration improvements #71) refactored onto the cache — removes one synchronous agent call per Gears-page render.Phase 1 — Source attribution in the UI
source+source_labelon every card.Phase 2 — Graceful no-HAProxy mode
/api/{boxID}/metrics/summarychecks the haproxy capability and only emits HAProxy KPI cards when the gear is available; host KPIs (Memory %, Disk %, Load 1m) are always emitted fromsystem_metrics_history.applyCapabilities()hides[data-source="haproxy"]chart cards + Error Insights when haproxy isn't available; shows an empty-state banner pointing to install / enable.What's intentionally not in this PR
system_metrics_historytoday. They'll follow when the collector starts saving them.Test plan
make test— 9 packages pass ongearbox; no regressions ongearbox-agent.CapabilitiesCache— TTL caching, refresh past TTL,Invalidate,InvalidateAll, negative caching, nil-safety onBoxCapabilitiesaccessors.avgPositiveSysField+sysSparkline— host-KPI counterparts of the existing stat helpers.go vet ./...clean across both modules./historyonlight-hugger(HAProxy box) shows HAProxy + Host KPIs and chart titles read "HAProxy: …" / "Host: …"./historyon a box without HAProxy (mjolnir?) shows the no-HAProxy banner, hides HAProxy cards and Error Insights, and renders just the Host KPI cards + Host chart cards.References
docs/research/metrics-source-agnostic.md.