v0.83.0: observability pass — always-DEBUG capture ring + GET /debug/logs + heartbeat log-pull + gap-fill sweep

Capture layer: applog.New returns (logger, Ring) — slog fan-out, stderr at the
configured level, ~1000-entry ring fixed at LevelDebug (remote diagnostics
without a config flip). GET /debug/logs (token-authed, ?raw=1) + request-level
DEBUG middleware. Heartbeat log-pull mirrors the report logtail pattern:
envelope log_tail_requested -> next heartbeat carries log_tail (128KB cap,
consume-once, failed-push retry proven). Gap-fill sweep over netverify/
netstorage/netmount/signedjobs/selfupdate/disks/controller-swap/desired/loop.
Red-proofs: ring-at-emit-level FAILs capture test; drain removed FAILs
consume-once; dropped phase line FAILs the S7 log-sequence smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-11 16:24:07 +02:00
parent 461eaf42c1
commit cb692f8788
20 changed files with 902 additions and 29 deletions
+14 -3
View File
@@ -16,6 +16,7 @@ import (
"gitea.dooplex.hu/admin/felhom-agent/internal/escrow"
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
applog "gitea.dooplex.hu/admin/felhom-agent/internal/log"
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
)
@@ -133,7 +134,10 @@ type Options struct {
// Supports() compares this against a per-feature MinAgent table instead of route-probing
// (v0.82.0; the probe stays as the fallback for header-less agents). Optional.
AgentVersion string
Logger *slog.Logger
// LogRing is the agent's always-DEBUG capture ring (v0.83.0 observability), served by
// GET /debug/logs. OPTIONAL — when nil the endpoint reports "not configured".
LogRing *applog.Ring
Logger *slog.Logger
}
// defaultBackupCadence is the fallback /backup/due window when none is configured.
@@ -191,6 +195,7 @@ type Server struct {
hostMetrics HostMetricsProvider // slice 9 (optional)
hostID string // slice 10B: for the data-bearing-format pending-op hint
agentVersion string // v0.82.0: the X-Felhom-Agent-Version response header value
logRing *applog.Ring // v0.83.0: GET /debug/logs source (optional)
// reresolveWipe performs the [AGENT-001] anti-retarget re-resolution before an
// inline customer-confirmed wipe (durable id → current device, re-derive+match,
@@ -281,6 +286,7 @@ func NewServer(o Options) (*Server, error) {
hostMetrics: o.HostMetrics,
hostID: o.HostID,
agentVersion: o.AgentVersion,
logRing: o.LogRing,
jobs: map[int]*backupJob{},
swapInFlight: map[int]bool{},
}
@@ -344,16 +350,21 @@ func (s *Server) Handler() http.Handler {
// fork-4 hygiene: wipe the staged secret once escrowed (controller calls this on confirm). Idempotent.
mux.HandleFunc("DELETE /escrow/stage-secret", s.withGuest(s.handleWipeStagedEscrowSecret))
// v0.83.0 observability: the agent's always-DEBUG capture ring, for the controller's
// Debug page agent tab (same auth/self-scoping wrap as every sibling route).
mux.HandleFunc("GET /debug/logs", s.withGuest(s.handleDebugLogs))
// v0.82.0 version channel: EVERY response (any route, any status — including auth failures)
// carries X-Felhom-Agent-Version, so the controller learns the agent version passively from its
// ordinary traffic and can capability-gate by comparison instead of route-probing. Header-less
// (pre-0.82) agents keep working — the controller falls back to the probe.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// v0.83.0: wrapped in the request-level DEBUG log middleware (method/path/status/duration).
return s.logRequests(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.agentVersion != "" {
w.Header().Set("X-Felhom-Agent-Version", s.agentVersion)
}
mux.ServeHTTP(w, r)
})
}))
}
// Run binds the bridge socket, serves TLS, and shuts down gracefully on ctx cancellation. It