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
This commit is contained in:
2026-07-11 16:45:57 +02:00
parent 647116480a
commit 26a43708b7
25 changed files with 797 additions and 31 deletions
+41
View File
@@ -103,6 +103,47 @@ func (lb *LogBuffer) Entries(minLevel string, limit int, after time.Time) ([]Log
return result, total
}
// Lines renders the held entries as plain text lines (chronological), dropping
// from the HEAD (oldest) to honor maxBytes so the newest lines survive — the
// report-channel controller_log_tail budget (maxBytes ≤ 0 → no byte cap).
// Format mirrors the agent ring's: "<RFC3339> [<LEVEL>] <message>".
func (lb *LogBuffer) Lines(maxBytes int) []string {
lb.mu.RLock()
total := lb.size
start := 0
if !lb.full {
total = lb.pos
} else {
start = lb.pos
}
entries := make([]LogEntry, 0, total)
for i := 0; i < total; i++ {
entries = append(entries, lb.entries[(start+i)%lb.size])
}
lb.mu.RUnlock()
lines := make([]string, len(entries))
for i, e := range entries {
src := ""
if e.Source != "" {
src = e.Source + ": "
}
lines[i] = e.Timestamp.Format(time.RFC3339) + " [" + e.Level + "] " + src + e.Message
}
if maxBytes <= 0 {
return lines
}
budget := 0
keepFrom := len(lines)
for i := len(lines) - 1; i >= 0; i-- {
budget += len(lines[i]) + 1 // +1 for the newline it represents
if budget > maxBytes {
break
}
keepFrom = i
}
return lines[keepFrom:]
}
// parseLine parses a single log line into a LogEntry.
func parseLine(line string) LogEntry {
entry := LogEntry{