Skip to content

feat(logs): derive default log sources from agent capabilities - #116

Merged
sarg3nt merged 2 commits into
mainfrom
fix/issue-112-log-sources-capabilities
May 17, 2026
Merged

feat(logs): derive default log sources from agent capabilities#116
sarg3nt merged 2 commits into
mainfrom
fix/issue-112-log-sources-capabilities

Conversation

@sarg3nt

@sarg3nt sarg3nt commented May 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • APILogSourcesHandler returned a hardcoded [haproxy, system] pair whenever no per-box settings existed. On a box whose agent has no logs or access-log gear (e.g. Mjolnir's distroless container agent), the Logs page would load both defaults and then fail every fetch against the agent that has nothing to serve. Combined with the plain-text 5xx responses (fixed in sibling PR fix(errors): emit JSON envelope from WriteHTTPError + harden fetch callers #114), this produced the visible Error loading logs: Unexpected token 'F' toast.
  • Drive the no-settings default off the agent's probe table:
    • haproxy offered when either access-log or logs gear is available
    • system offered only when logs gear is available
    • Empty list when the agent advertises a probe table but neither log gear is available — JS renders an empty dropdown and skips the fetch
    • Fail-open: if capabilities aren't reachable at all, fall back to legacy [haproxy, system] so deployments work through transient outages
  • All existing call sites (api_logs.go::APILogsHandler, services page, logs page) continue to work — the resolver is internal.

Phase 2 slice of #112.

Test plan

  • go build ./... and go vet ./... clean
  • go test -count=1 ./internal/framework/handler/... clean
  • On Mjolnir (agent with only access-log/host/metrics available), navigate to /logs?server=mjolnir and confirm the source dropdown shows only HAProxy (no System) and the page loads access-log content cleanly.
  • On light-hugger (logs available), confirm both HAProxy and System appear in the dropdown — backwards compatible.
  • Stop the agent temporarily, reload /logs — confirm the legacy fallback kicks in and the page degrades gracefully.

🤖 Generated with Claude Code

APILogSourcesHandler returned a hardcoded [haproxy, system] pair
whenever no explicit per-box settings existed. On a box whose agent
has no `logs` (journalctl) or `access-log` gear — e.g. mjolnir's
distroless container agent — the Logs page would still load both
defaults and then trip every fetch against the agent, which has
nothing to serve. Combined with the plain-text 5xx error responses
(see #112 / sibling PR #114) this produced the user-visible
"Error loading logs: Unexpected token 'F'" toast.

Drive the no-settings default off the agent's probe table:

  * `haproxy` is offered when either access-log OR logs gear is
    available (access-log streams directly from /var/log/haproxy; the
    logs gear can serve it via journalctl/file too).
  * `system` is offered only when the `logs` gear is available (it
    needs journalctl/tail).
  * If the agent has neither, return an empty source list — the JS
    renders an empty dropdown and skips the immediate fetch instead
    of 5xx-storming.
  * If capabilities aren't reachable at all (agent down, older agent
    that pre-dates probing) fall back to the legacy pair so existing
    deployments still work through transient outages.

Phase 2 slice of #112.

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

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

This PR updates the dashboard’s Logs API to derive the default log-source dropdown entries from the selected box’s agent capability/probe table, instead of always defaulting to the legacy haproxy + system pair when no per-box settings exist. This supports the broader “capability-driven autoconfig” effort so the Logs page doesn’t immediately issue requests for log gears the agent cannot serve.

Changes:

  • Replace the hardcoded no-settings default in APILogSourcesHandler with a capability-driven default resolver.
  • Add defaultLogSourcesForBox, which selects haproxy and/or system based on the agent’s access-log / logs gear availability, with a fail-open fallback when capabilities cannot be fetched.
Comments suppressed due to low confidence (1)

gearbox/internal/framework/handler/api_logs.go:140

  • Returning an empty sources list here assumes the frontend will preserve the empty dropdown and skip the initial log fetch, but the current Logs page JS treats sources.length === 0 as a failure and falls back to the legacy defaults, which would reintroduce the bad fetches on boxes without log gears. Either adjust the API contract so callers can distinguish "no sources available" from "error" (and update the JS accordingly), or avoid returning an empty list until the frontend handling is in place.
	if !hasAccessLog && !hasLogs {
		// Agent surfaces a probe table but neither log gear is available.
		// Returning the empty list lets the JS render an empty dropdown
		// and skip the immediate fetch — better than 5xx storming.
		return []map[string]string{}

Comment thread gearbox/internal/framework/handler/api_logs.go Outdated
…ity (#112)

Copilot pointed out that defaultLogSourcesForBox conflated two
distinct cases:
  1. The agent doesn't surface a gear name at all (older agent that
     pre-dates the gear).
  2. The agent reports the gear as unavailable.

Both used IsAvailable() and collapsed into "return empty list",
which violates the function comment's fail-open promise: a
forward-compatibility gap would silently strip the source list.

Use caps.Has() to distinguish presence from availability:
  - If the agent surfaces neither access-log nor logs by name → fail
    open to the legacy [haproxy, system] pair (older-agent path).
  - If the agent surfaces at least one and reports both unavailable →
    return empty (the only case where the JS shows an empty dropdown).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sarg3nt added a commit that referenced this pull request May 17, 2026
Two findings:

1. Services dashboard gear had no entry in dashboardGearToAgentGear,
   so the sidebar filter passed it through unconditionally — the
   exact symptom this PR was supposed to fix. Map it to "metrics"
   (the agent gear that registers /api/v1/services per
   gearbox-agent/internal/gears/metrics/plugin.go), and document that
   this is imprecise: the agent's metrics gear can be Available on a
   host where systemd isn't introspectable from the container, so a
   tighter gate (an explicit `services` capability that advertises
   systemd reachability) is part of the Phase 2 extension in #112.

2. No regression test covered the middleware path or the filter's
   fail-open contract. Add three focused tests on
   filterGearsByAgentCapabilities driven by a real httptest.Server
   returning a CapabilitiesResponse:
   - Mjolnir-shaped probe table → only available gears + dashboard-
     only gears (alerts) survive.
   - Agent unreachable → fail open, full list passes through.
   - Older agent that doesn't surface a gear name at all → that gear
     is kept (forward-compat, mirrors the #116 fix).

The tests share a slim Handler fixture (logger, capabilities cache,
static servers slice) so they exercise the production fetch path
through CapabilitiesCache without needing a DB or auth manager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sarg3nt added a commit that referenced this pull request May 17, 2026
* feat(sidebar): hide gears whose agent reports them unavailable (#112)

filterGearsByAgentCapabilities is the framework's existing
capability-aware gear filter. Today it's only used by the Gears
settings page (`/settings/gears`) — so the **sidebar** still renders
HAProxy, Logs, Services, Certificates, etc. for an active box whose
agent doesn't run any of those gears. Clicking them lands the
operator on an empty page or a JSON-envelope error.

Run the same filter on the per-box integration list inside
InjectIntegrationStatus, so the sidebar reflects what the agent can
actually serve. Failures stay fail-open (an unreachable agent does
not strip the sidebar) by reusing the existing helper's contract.

Phase 2 slice of #112.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): address Copilot review on PR 119 (#112)

Two findings:

1. Services dashboard gear had no entry in dashboardGearToAgentGear,
   so the sidebar filter passed it through unconditionally — the
   exact symptom this PR was supposed to fix. Map it to "metrics"
   (the agent gear that registers /api/v1/services per
   gearbox-agent/internal/gears/metrics/plugin.go), and document that
   this is imprecise: the agent's metrics gear can be Available on a
   host where systemd isn't introspectable from the container, so a
   tighter gate (an explicit `services` capability that advertises
   systemd reachability) is part of the Phase 2 extension in #112.

2. No regression test covered the middleware path or the filter's
   fail-open contract. Add three focused tests on
   filterGearsByAgentCapabilities driven by a real httptest.Server
   returning a CapabilitiesResponse:
   - Mjolnir-shaped probe table → only available gears + dashboard-
     only gears (alerts) survive.
   - Agent unreachable → fail open, full list passes through.
   - Older agent that doesn't surface a gear name at all → that gear
     is kept (forward-compat, mirrors the #116 fix).

The tests share a slim Handler fixture (logger, capabilities cache,
static servers slice) so they exercise the production fetch path
through CapabilitiesCache without needing a DB or auth manager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sarg3nt
sarg3nt merged commit 8edae66 into main May 17, 2026
22 checks passed
sarg3nt added a commit that referenced this pull request May 17, 2026
The agent-side companion PR adds a structured Resources field to
ProbeResult and has the access-log gear publish a `log_sources` list
(one entry per discovered web-server access log, name + display_name
+ path). This commit teaches the dashboard to read it.

  - agent.CapabilityEntry mirrors the new `resources` JSON field as
    map[string]any so per-gear shapes don't need a shared Go struct.
  - BoxCapabilities.Resource(gearName, key) — small helper to look up
    a single resource without manually chaining Entry/.Resources/map
    indexing.
  - defaultLogSourcesForBox prefers the agent's published log_sources
    when present, falling back to the existing capability heuristic
    for pre-Phase-2 agents and the legacy [haproxy, system] pair when
    capabilities aren't reachable. The path field the agent publishes
    is dropped at this layer — the dashboard doesn't expose log paths
    to the browser.

The resolution chain is now (in priority):
  1. Operator's saved per-box log-source settings (DB)
  2. Agent's structured log_sources resource (Phase 2)
  3. Capability heuristic (pre-Phase-2 fallback)
  4. Legacy [haproxy, system] (agent unreachable / forward-compat)

Tests cover the new path: JSON-decoded shape (production), Go-typed
shape (in-process tests), missing access-log gear, missing resource
key, and five malformed-payload variants (string instead of array,
array of strings, missing fields, empty array). All exercise
logSourcesFromResources directly so the helper's contract is locked
in independently of the surrounding handler.

Phase 2 of #112. Stacked on fix/issue-112-log-sources-capabilities
(PR #116); pairs with feature/issue-112-probe-resources (PR #135)
on the agent.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants