Files
felhom-controller/controller/internal/report/selftail.go
T
admin 26a43708b7 v0.116.0: observability pass — always-on debug ring + leveled sweep + agent tab + self-log pull — MinAgent: 0.81.0
Capture layer: LogBuffer always exists; logger = MultiWriter(LevelFilterWriter
(stdout, logging.level), ring) so DEBUG detail exists remotely without a config
flip while docker logs keep respecting the level. New internal/logx leveled
helpers. Report ACK gains controller_log_requested (additive); next report
ships controller_log_tail (128KB, consume-once, app-tail wire byte-compatible).
Debug page: Vezérlő|Ügynök tabs; agent tab proxies agent /debug/logs with the
pre-0.83 notice on typed 404. Sweep: netstorage_job phases, netprobe, handler
validation refusals + orphan WARN, SupportsWithSource gate line, agentapi
per-call DEBUG, migrate phase lines, tier2/offbox unswallowed persists.
Red-proofs: filter-disabled, drain-removed, dropped-phase-line all FAIL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 16:45:57 +02:00

75 lines
2.7 KiB
Go

package report
import (
"log"
"sync"
"time"
)
// Controller self-log tail (v0.116.0 observability) — the CONTROLLER's own debug
// ring riding the report channel, the exact consume-once ACK-flag shape of
// logtail.go (which stays byte-compatible; these are ADDITIVE fields): the hub
// stores a pending per-customer request; the report ACK advertises it as
// controller_log_requested; the NEXT report carries controller_log_tail; the hub
// clears the pending request on arrival. A failed push leaves the hub request
// pending — the next ACK re-arms it (fail-safe retry, no duplicate shipping).
// controllerLogMaxBytes caps the shipped tail (newest lines kept — the ring's
// Lines budget, mirrored agent-side at the same 128 KB).
const controllerLogMaxBytes = 128 * 1024
// ControllerLogTail is the report's on-demand controller ring tail.
type ControllerLogTail struct {
CollectedAt time.Time `json:"collected_at"`
Lines []string `json:"lines"`
}
var (
selfTailMu sync.Mutex
selfTailPending bool
selfTailSource func(maxBytes int) []string
)
// SetPendingControllerLog records the ACK's controller_log_requested flag. The
// hub is the source of truth: an ACK without the flag clears any stale pending.
func SetPendingControllerLog(requested bool) {
selfTailMu.Lock()
defer selfTailMu.Unlock()
selfTailPending = requested
}
// SetControllerLogSource wires the debug ring (web.LogBuffer.Lines) — set once by
// main.go at startup. Unset means a pending request is silently unfulfillable
// (drained but nothing shipped; the hub re-arms on the next ACK).
func SetControllerLogSource(src func(maxBytes int) []string) {
selfTailMu.Lock()
defer selfTailMu.Unlock()
selfTailSource = src
}
// drainPendingControllerLog takes + CLEARS the pending flag and returns the
// source (consume-once controller-side, the drainPendingLogTails shape).
func drainPendingControllerLog() (func(maxBytes int) []string, bool) {
selfTailMu.Lock()
defer selfTailMu.Unlock()
pending := selfTailPending
selfTailPending = false
return selfTailSource, pending
}
// buildControllerLogTail attaches the ring tail when a pull is pending. The INFO
// line is the CUSTOMER-VISIBLE transparency record (it lands in the ring/viewer):
// an operator pull of this box's controller log is never silent.
func buildControllerLogTail(logger *log.Logger) *ControllerLogTail {
src, pending := drainPendingControllerLog()
if !pending || src == nil {
return nil
}
// Logged BEFORE collecting, so the transparency line rides in the tail it announces.
if logger != nil {
logger.Printf("[INFO] [report] operator log pull served (controller ring)")
}
lines := src(controllerLogMaxBytes)
return &ControllerLogTail{CollectedAt: time.Now().UTC(), Lines: lines}
}